pax_global_header00006660000000000000000000000064152067414650014524gustar00rootroot0000000000000052 comment=872e0c840c7e15f297487dcbf49bb0779f188a82 golang-github-go-rod-rod-0.116.2/000077500000000000000000000000001520674146500163715ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/.eslintrc.yml000066400000000000000000000001661520674146500210200ustar00rootroot00000000000000extends: - eslint:recommended env: browser: true es6: true parserOptions: ecmaVersion: 2018 plugins: - html golang-github-go-rod-rod-0.116.2/.github/000077500000000000000000000000001520674146500177315ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/.github/CODE_OF_CONDUCT.md000066400000000000000000000064071520674146500225370ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq golang-github-go-rod-rod-0.116.2/.github/CONTRIBUTING.md000066400000000000000000000122531520674146500221650ustar00rootroot00000000000000# Contributing Anyone has contributed code to the project can become a member of the project and have the write permission to issues and doc repositories. At the early stage of this project, we will use a simple model to promote members to maintainers. Maintainers will have all the permissions of this project, only the first 2 maintainers are granted by the owner, the standard is whether the member is good enough to review others' code, then we will start to elect new maintainers by voting in the public issue. If no one votes down and 2/3 votes up then an election passes. ## Contribute Doc Check [here](https://github.com/go-rod/go-rod.github.io/blob/main/contribute-doc.md). ## Terminology When we talk about type in the doc we use [gopls](https://github.com/golang/tools/tree/master/gopls) symbol query syntax. For example, when we say `rod.Page.PDF`, you can run the below to locate the file and line of it: ```bash gopls workspace_symbol -matcher=fuzzy rod.Page.PDF$ ``` - `cdp`: It's short for Chrome Devtools Protocol ## How it works Here's the common start process of rod: 1. Try to connect to a Devtools endpoint (WebSocket), if not found try to launch a local browser, if still not found try to download one, then connect again. The lib to handle it is [launcher](lib/launcher). 1. Use the JSON-RPC to talk to the Devtools endpoint to control the browser. The lib handles it is [cdp](lib/cdp). 1. Use the type definitions of the JSON-RPC to perform high-level actions. The lib handles it is [proto](lib/proto). Object model: ![object model](../fixtures/object-model.svg) ## Run tests First, launch a test shell for rod: ```bash go run ./lib/utils/shell ``` Then, no magic, just `go test`. Read the test template [rod_test.go](../rod_test.go) to get started. The entry point of tests is [setup_test.go](../setup_test.go). All the test helpers are defined in it. The `cdp` requests of each test will be recorded and output to folder `tmp/cdp-log`, the CI will store them as [artifacts](https://docs.github.com/en/actions/guides/storing-workflow-data-as-artifacts) so that we can download them for debugging. Usually, you only need to run the tests that you are working on, for example: ```bash go test -run=^TestClick$ ``` The above will only run TestClick. ### Disable headless mode ```bash go test -rod=show ``` Check [defaults](../lib/defaults/defaults.go) for other available options. ### Lint project You can run all commands inside Docker so that you don't have to install all the development dependencies. Check [Use Docker for development](#use-docker-for-development) for more info. ```bash go generate # only required for first time go run ./lib/utils/lint ``` ### Code Coverage If the code coverage is less than 100%, the CI will fail. Learn the [basics](https://blog.golang.org/cover) first. To visually see the coverage report you can run something like this: ```bash go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out ``` It will open a web page to tell you which line is not covered. To cover the error branch of the code we usually intercept cdp calls. There are several helper functions for it: - `rod_test.MockClient.stubCounter` - `rod_test.MockClient.stub` - `rod_test.MockClient.stubErr` ### Use Docker for development 1. Build the test image: `docker build -t rod -f lib/docker/dev.Dockerfile .` 1. Run a container with and mount the cache volume to it: `docker run -v $(pwd):/t --name rod -it rod bash` 1. Open another terminal, copy your global git-ignore file to the container: `docker cp ~/.gitignore_global rod:/root/` 1. Run lint in the container: `go run ./lib/utils/lint` 1. Run tests in the container: `go test` 1. After you exit the container with `exit`, you can restart it by: `docker start -i rod` ### Deployment of docker images We use `.github/workflows/docker.yml` to automate it. ### Detect goroutine leak Because parallel execution will pollution the global goroutine stack, by default, the goroutine leak detection for each test will be disabled, but the detection for the whole test program will still work as well. To enable detection for each test, just use `go test -parallel=1`. ### Debug dependency libs Run `go mod vendor` to create a local mirror of dependencies. The Golang compiler will use the libs under `vendor` folder as a priority. For example, we can modify file `./vendor/github.com/ysmood/goob/goob.go` to debug, such as add some extra logs. ## Comments All conversations in Github issues, PRs, etc. should be summarized into code comments so that this project is not deep coupled with Github service. ## Convention of the git commit message The commit message follows the rules [here](https://github.com/torvalds/subsurface-for-dirk/blame/a48494d2fbed58c751e9b7e8fbff88582f9b2d02/README#L88). We don't use rules like [Conventional Commits](https://www.conventionalcommits.org/) because it's hard for beginners to write correct commit messages. It will encourage reviewers to spend more time on high-level problems, not the details. We also want to reduce the overhead when reading the git-blame, for example, `fix: correct minor typos in code` is the same as `fix minor typos in code`, there's no need to repeat content in the title line. golang-github-go-rod-rod-0.116.2/.github/FUNDING.yml000066400000000000000000000000211520674146500215370ustar00rootroot00000000000000github: [ysmood] golang-github-go-rod-rod-0.116.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001520674146500221145ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000001561520674146500256430ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: enhance assignees: '' --- golang-github-go-rod-rod-0.116.2/.github/ISSUE_TEMPLATE/question.md000066400000000000000000000016401520674146500243060ustar00rootroot00000000000000--- name: Question about: Title of your question. title: '' labels: question assignees: '' --- Rod Version: v0.0.0 ## The code to demonstrate your question 1. Clone Rod to your local and cd to the repository: ```bash git clone https://github.com/go-rod/rod cd rod ``` 1. Use your code to replace the content of function `TestRod` in file `rod_test.go`. 1. Test your code with: `go test -run TestRod`, make sure it fails as expected. 1. Replace ALL THE CONTENT under "The code to demonstrate your question" with your `TestRod` function, like below: ```go func TestRod(t *testing.T) { g := setup(t) g.Eq(1, 2) // the test should fail, here 1 doesn't equal 2 } ``` ## What you got Such as what error you see. ## What you expect to see Such as what you want to do. ## What have you tried to solve the question Such as after modifying some source code of Rod you are able to get rid of the problem. golang-github-go-rod-rod-0.116.2/.github/pull_request_template.md000066400000000000000000000002611520674146500246710ustar00rootroot00000000000000# Development guide [Link](https://github.com/go-rod/rod/blob/main/.github/CONTRIBUTING.md) ## Test on local before making the PR ```bash go run ./lib/utils/simple-check ``` golang-github-go-rod-rod-0.116.2/.github/workflows/000077500000000000000000000000001520674146500217665ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/.github/workflows/check-examples.yml000066400000000000000000000005551520674146500254070ustar00rootroot00000000000000name: Check Examples on: schedule: - cron: '23 3 * * *' jobs: run: runs-on: ubuntu-latest steps: - uses: actions/setup-go@v5 with: go-version: 1.22 - uses: actions/checkout@v4 - run: | go run ./lib/utils/get-browser go test -run Example ./... go test ./lib/examples/e2e-testing golang-github-go-rod-rod-0.116.2/.github/workflows/check-issues.yml000066400000000000000000000006071520674146500251020ustar00rootroot00000000000000name: Check Issues on: issues: types: [opened, edited] permissions: issues: write jobs: run: runs-on: ubuntu-latest steps: - uses: actions/setup-go@v5 with: go-version: 1.22 - uses: actions/checkout@v4 - name: check env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: cd lib/utils/check-issue && go run . golang-github-go-rod-rod-0.116.2/.github/workflows/check-revision.yml000066400000000000000000000004721520674146500254250ustar00rootroot00000000000000name: Check Revision on: schedule: - cron: '0 0 1 * *' # monthly jobs: run: runs-on: ubuntu-latest steps: - uses: actions/setup-go@v5 with: go-version: 1.22 - uses: actions/checkout@v4 - run: | go run ./lib/launcher/revision go generate golang-github-go-rod-rod-0.116.2/.github/workflows/docker.yml000066400000000000000000000026711520674146500237660ustar00rootroot00000000000000# When git main branch changes it will build a image based on the main branch, the tag of the image will be latest. # When a git semver tag is pushed it will build a image based on it, the tag will be the same as git's. # It will do nothing on other git events. # For the usage of the image, check lib/examples/launch-managed . name: Release docker image on: [push, pull_request] permissions: packages: write jobs: # TODO: we should merge job docker-amd and job docker-arm once the github actions fix their issue with cross-platform building. docker-amd: runs-on: ubuntu-22.04 steps: - uses: actions/setup-go@v5 with: go-version: 1.22 - uses: docker/setup-qemu-action@v3 - uses: actions/checkout@v4 - run: go run ./lib/docker $GITHUB_REF env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - uses: actions/upload-artifact@v4 with: name: review-fonts-docker path: tmp/fonts.pdf - uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: cdp-log-docker path: tmp/cdp-log docker-arm: runs-on: ubuntu-22.04 steps: - uses: actions/setup-go@v5 with: go-version: 1.22 - uses: docker/setup-qemu-action@v3 - uses: actions/checkout@v4 - run: go run ./lib/docker $GITHUB_REF env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} ARCH: arm golang-github-go-rod-rod-0.116.2/.github/workflows/test-linux.yml000066400000000000000000000013321520674146500246240ustar00rootroot00000000000000name: Test Linux on: push: branches: - '**' pull_request: schedule: - cron: '17 5 * * *' jobs: test-linux: runs-on: ubuntu-latest steps: - uses: actions/setup-go@v5 with: go-version: 1.22 - uses: actions/checkout@v4 - run: go generate - run: go run ./lib/utils/ci-test -race -coverprofile=coverage.out -run=^Test . ./lib/utils ./lib/proto ./lib/cdp ./lib/defaults ./lib/devices ./lib/launcher ./lib/input - run: go run github.com/ysmood/got/cmd/check-cov@latest - uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: cdp-log-linux path: | tmp/cdp-log coverage.out golang-github-go-rod-rod-0.116.2/.github/workflows/test-other-platforms.yml000066400000000000000000000021371520674146500266170ustar00rootroot00000000000000name: Test Other Platforms on: push: branches: - '**' pull_request: jobs: test-mac: runs-on: macos-latest steps: - uses: actions/setup-go@v5 with: go-version: 1.22 - uses: actions/checkout@v4 - run: go run ./lib/utils/ci-test -timeout-each=2m -run=^Test - uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: cdp-log-macos path: tmp/cdp-log test-windows: runs-on: windows-latest steps: - uses: actions/setup-go@v5 with: go-version: 1.22 - uses: actions/checkout@v4 - run: go run ./lib/utils/ci-test -timeout-each=2m -run=^Test - uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: cdp-log-windows path: tmp/cdp-log test-old-go: runs-on: ubuntu-latest steps: - uses: actions/setup-go@v5 with: go-version: 1.18 - uses: actions/checkout@v4 # As long as the build works we don't have to run tests. - run: go build ./lib/examples/translator golang-github-go-rod-rod-0.116.2/.gitignore000066400000000000000000000001031520674146500203530ustar00rootroot00000000000000vendor/ node_modules/ tmp/ .git .dockerignore *.out *.test *.json golang-github-go-rod-rod-0.116.2/.golangci.yml000066400000000000000000000033641520674146500207630ustar00rootroot00000000000000linters: enable-all: true disable: - gochecknoinits - paralleltest - wrapcheck - gosec - gochecknoglobals - musttag - varnamelen - wsl - nonamedreturns - tagliatelle - nlreturn - nakedret - gomnd - mnd - err113 - exhaustruct - godox - depguard - testpackage - exhaustive - containedctx - prealloc - perfsprint - ireturn - contextcheck - canonicalheader - copyloopvar - intrange # Deprecated ones: - execinquery - structcheck - interfacer - deadcode - varcheck - ifshort - exhaustivestruct - golint - maligned - nosnakecase - scopelint linters-settings: cyclop: max-complexity: 15 gocyclo: min-complexity: 15 nestif: min-complexity: 6 funlen: lines: 120 issues: exclude-use-default: false exclude-rules: - path: _test.go$ linters: - lll - funlen - dupword - goconst - contextcheck - errorlint - testableexamples - forcetypeassert # Generated code - path: lib/proto/ linters: - lll - gocritic - dupword - forcetypeassert - path: lib/devices/list.go linters: - lll - path: lib/js/helper.go linters: - lll - path: /fixtures/ linters: - forbidigo - path: lib/examples/ linters: - forbidigo - noctx - gocritic - path: examples?_test.go$ linters: - forbidigo - noctx - gocritic - path: main.go$ linters: - forbidigo - noctx - forcetypeassert - lll - path: lib/assets/ linters: - lll golang-github-go-rod-rod-0.116.2/.prettierrc.yml000066400000000000000000000000621520674146500213530ustar00rootroot00000000000000semi: false singleQuote: true trailingComma: none golang-github-go-rod-rod-0.116.2/LICENSE000066400000000000000000000020511520674146500173740ustar00rootroot00000000000000The MIT License Copyright 2019 Yad Smood Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.golang-github-go-rod-rod-0.116.2/README.md000066400000000000000000000056111520674146500176530ustar00rootroot00000000000000# Overview [![Go Reference](https://pkg.go.dev/badge/github.com/go-rod/rod.svg)](https://pkg.go.dev/github.com/go-rod/rod) [![Discord Chat](https://img.shields.io/discord/719933559456006165.svg)][discord room] ## [Documentation](https://go-rod.github.io/) | [API reference](https://pkg.go.dev/github.com/go-rod/rod?tab=doc) | [FAQ](https://go-rod.github.io/#/faq/README) Rod is a high-level driver directly based on [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol). It's designed for web automation and scraping for both high-level and low-level use, senior developers can use the low-level packages and functions to easily customize or build up their own version of Rod, the high-level functions are just examples to build a default version of Rod. [中文 API 文档](https://pkg.go.dev/github.com/go-rod/go-rod-chinese) ## Features - Chained context design, intuitive to timeout or cancel the long-running task - Auto-wait elements to be ready - Debugging friendly, auto input tracing, remote monitoring headless browser - Thread-safe for all operations - Automatically find or download [browser](lib/launcher) - High-level helpers like WaitStable, WaitRequestIdle, HijackRequests, WaitDownload, etc - Two-step WaitEvent design, never miss an event ([how it works](https://github.com/ysmood/goob)) - Correctly handles nested iframes or shadow DOMs - No zombie browser process after the crash ([how it works](https://github.com/ysmood/leakless)) - [CI](https://github.com/go-rod/rod/actions) enforced 100% test coverage ## Examples Please check the [examples_test.go](examples_test.go) file first, then check the [examples](lib/examples) folder. For more detailed examples, please search the unit tests. Such as the usage of method `HandleAuth`, you can search all the `*_test.go` files that contain `HandleAuth`, for example, use Github online [search in repository](https://github.com/go-rod/rod/search?q=HandleAuth&unscoped_q=HandleAuth). You can also search the GitHub [issues](https://github.com/go-rod/rod/issues) or [discussions](https://github.com/go-rod/rod/discussions), a lot of usage examples are recorded there. [Here](lib/examples/compare-chromedp) is a comparison of the examples between rod and Chromedp. If you have questions, please raise an [issues](https://github.com/go-rod/rod/issues)/[discussions](https://github.com/go-rod/rod/discussions) or join the [chat room][discord room]. ## Join us Your help is more than welcome! Even just open an issue to ask a question may greatly help others. Please read [How To Ask Questions The Smart Way](http://www.catb.org/~esr/faqs/smart-questions.html) before you ask questions. We use Github Projects to manage tasks, you can see the priority and progress of the issues [here](https://github.com/go-rod/rod/projects). If you want to contribute please read the [Contributor Guide](.github/CONTRIBUTING.md). [discord room]: https://discord.gg/CpevuvY golang-github-go-rod-rod-0.116.2/browser.go000066400000000000000000000337721520674146500204170ustar00rootroot00000000000000//go:generate go run ./lib/utils/setup //go:generate go run ./lib/proto/generate //go:generate go run ./lib/js/generate //go:generate go run ./lib/assets/generate //go:generate go run ./lib/utils/lint // Package rod is a high-level driver directly based on DevTools Protocol. package rod import ( "context" "reflect" "strings" "sync" "time" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/defaults" "github.com/go-rod/rod/lib/devices" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/goob" ) // Browser implements these interfaces. var ( _ proto.Client = &Browser{} _ proto.Contextable = &Browser{} ) // Browser represents the browser. // It doesn't depends on file system, it should work with remote browser seamlessly. // To check the env var you can use to quickly enable options from CLI, check here: // https://pkg.go.dev/github.com/go-rod/rod/lib/defaults type Browser struct { // BrowserContextID is the id for incognito window BrowserContextID proto.BrowserBrowserContextID e eFunc ctx context.Context sleeper func() utils.Sleeper logger utils.Logger slowMotion time.Duration // see defaults.slow trace bool // see defaults.Trace monitor string defaultDevice devices.Device controlURL string client CDPClient event *goob.Observable // all the browser events from cdp client targetsLock *sync.Mutex // stores all the previous cdp call of same type. Browser doesn't have enough API // for us to retrieve all its internal states. This is an workaround to map them to local. // For example you can't use cdp API to get the current position of mouse. states *sync.Map } // New creates a controller. // DefaultDevice to emulate is set to [devices.LaptopWithMDPIScreen].Landscape(), it will change the default // user-agent and can make the actual view area smaller than the browser window on headful mode, // you can use [Browser.NoDefaultDevice] to disable it. func New() *Browser { return (&Browser{ ctx: context.Background(), sleeper: DefaultSleeper, controlURL: defaults.URL, slowMotion: defaults.Slow, trace: defaults.Trace, monitor: defaults.Monitor, logger: DefaultLogger, defaultDevice: devices.LaptopWithMDPIScreen.Landscape(), targetsLock: &sync.Mutex{}, states: &sync.Map{}, }).WithPanic(utils.Panic) } // Incognito creates a new incognito browser. func (b *Browser) Incognito() (*Browser, error) { res, err := proto.TargetCreateBrowserContext{}.Call(b) if err != nil { return nil, err } incognito := *b incognito.BrowserContextID = res.BrowserContextID return &incognito, nil } // ControlURL set the url to remote control browser. func (b *Browser) ControlURL(url string) *Browser { b.controlURL = url return b } // SlowMotion set the delay for each control action, such as the simulation of the human inputs. func (b *Browser) SlowMotion(delay time.Duration) *Browser { b.slowMotion = delay return b } // Trace enables/disables the visual tracing of the input actions on the page. func (b *Browser) Trace(enable bool) *Browser { b.trace = enable return b } // Monitor address to listen if not empty. Shortcut for [Browser.ServeMonitor]. func (b *Browser) Monitor(url string) *Browser { b.monitor = url return b } // Logger overrides the default log functions for tracing. func (b *Browser) Logger(l utils.Logger) *Browser { b.logger = l return b } // Client set the cdp client. func (b *Browser) Client(c CDPClient) *Browser { b.client = c return b } // DefaultDevice sets the default device for new page to emulate in the future. // Default is [devices.LaptopWithMDPIScreen]. // Set it to [devices.Clear] to disable it. func (b *Browser) DefaultDevice(d devices.Device) *Browser { b.defaultDevice = d return b } // NoDefaultDevice is the same as [Browser.DefaultDevice](devices.Clear). func (b *Browser) NoDefaultDevice() *Browser { return b.DefaultDevice(devices.Clear) } // Connect to the browser and start to control it. // If fails to connect, try to launch a local browser, if local browser not found try to download one. func (b *Browser) Connect() error { if b.client == nil { u := b.controlURL if u == "" { var err error u, err = launcher.New().Context(b.ctx).Launch() if err != nil { return err } } c, err := cdp.StartWithURL(b.ctx, u, nil) if err != nil { return err } b.client = c } else if b.controlURL != "" { panic("Browser.Client and Browser.ControlURL can't be set at the same time") } b.initEvents() if b.monitor != "" { launcher.Open(b.ServeMonitor(b.monitor)) } return proto.TargetSetDiscoverTargets{Discover: true}.Call(b) } // Close the browser. func (b *Browser) Close() error { if b.BrowserContextID == "" { return proto.BrowserClose{}.Call(b) } return proto.TargetDisposeBrowserContext{BrowserContextID: b.BrowserContextID}.Call(b) } // Page creates a new browser tab. If opts.URL is empty, the default target will be "about:blank". func (b *Browser) Page(opts proto.TargetCreateTarget) (p *Page, err error) { req := opts req.BrowserContextID = b.BrowserContextID req.URL = "about:blank" target, err := req.Call(b) if err != nil { return nil, err } defer func() { // If Navigate or PageFromTarget fails we should close the target to prevent leak if err != nil { _, _ = proto.TargetCloseTarget{TargetID: target.TargetID}.Call(b) } }() p, err = b.PageFromTarget(target.TargetID) if err != nil { return } if opts.URL == "" { return } err = p.Navigate(opts.URL) return } // Pages retrieves all visible pages. func (b *Browser) Pages() (Pages, error) { list, err := proto.TargetGetTargets{}.Call(b) if err != nil { return nil, err } pageList := Pages{} for _, target := range list.TargetInfos { if target.Type != proto.TargetTargetInfoTypePage { continue } page, err := b.PageFromTarget(target.TargetID) if err != nil { return nil, err } pageList = append(pageList, page) } return pageList, nil } // Call implements the [proto.Client] to call raw cdp interface directly. func (b *Browser) Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) { res, err = b.client.Call(ctx, sessionID, methodName, params) if err != nil { return nil, err } b.set(proto.TargetSessionID(sessionID), methodName, params) return } // PageFromSession is used for low-level debugging. func (b *Browser) PageFromSession(sessionID proto.TargetSessionID) *Page { sessionCtx, cancel := context.WithCancel(b.ctx) return &Page{ e: b.e, ctx: sessionCtx, sessionCancel: cancel, sleeper: b.sleeper, browser: b, SessionID: sessionID, } } // PageFromTarget gets or creates a Page instance. func (b *Browser) PageFromTarget(targetID proto.TargetTargetID) (*Page, error) { b.targetsLock.Lock() defer b.targetsLock.Unlock() page := b.loadCachedPage(targetID) if page != nil { return page, nil } session, err := proto.TargetAttachToTarget{ TargetID: targetID, Flatten: true, // if it's not set no response will return }.Call(b) if err != nil { return nil, err } sessionCtx, cancel := context.WithCancel(b.ctx) page = &Page{ e: b.e, ctx: sessionCtx, sessionCancel: cancel, sleeper: b.sleeper, browser: b, TargetID: targetID, SessionID: session.SessionID, FrameID: proto.PageFrameID(targetID), jsCtxLock: &sync.Mutex{}, jsCtxID: new(proto.RuntimeRemoteObjectID), helpersLock: &sync.Mutex{}, } page.root = page page.newKeyboard().newMouse().newTouch() if !b.defaultDevice.IsClear() { err = page.Emulate(b.defaultDevice) if err != nil { return nil, err } } b.cachePage(page) page.initEvents() // If we don't enable it, it will cause a lot of unexpected browser behavior. // Such as proto.PageAddScriptToEvaluateOnNewDocument won't work. page.EnableDomain(&proto.PageEnable{}) return page, nil } // EachEvent is similar to [Page.EachEvent], but catches events of the entire browser. func (b *Browser) EachEvent(callbacks ...interface{}) (wait func()) { return b.eachEvent("", callbacks...) } // WaitEvent waits for the next event for one time. It will also load the data into the event object. func (b *Browser) WaitEvent(e proto.Event) (wait func()) { return b.waitEvent("", e) } // waits for the next event for one time. It will also load the data into the event object. func (b *Browser) waitEvent(sessionID proto.TargetSessionID, e proto.Event) (wait func()) { valE := reflect.ValueOf(e) valTrue := reflect.ValueOf(true) if valE.Kind() != reflect.Ptr { valE = reflect.New(valE.Type()) } // dynamically creates a function on runtime: // // func(ee proto.Event) bool { // *e = *ee // return true // } fnType := reflect.FuncOf([]reflect.Type{valE.Type()}, []reflect.Type{valTrue.Type()}, false) fnVal := reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value { valE.Elem().Set(args[0].Elem()) return []reflect.Value{valTrue} }) return b.eachEvent(sessionID, fnVal.Interface()) } // If the any callback returns true the event loop will stop. // It will enable the related domains if not enabled, and restore them after wait ends. func (b *Browser) eachEvent(sessionID proto.TargetSessionID, callbacks ...interface{}) (wait func()) { cbMap := map[string]reflect.Value{} restores := []func(){} for _, cb := range callbacks { cbVal := reflect.ValueOf(cb) eType := cbVal.Type().In(0) name := reflect.New(eType.Elem()).Interface().(proto.Event).ProtoEvent() //nolint: forcetypeassert cbMap[name] = cbVal // Only enabled domains will emit events to cdp client. // We enable the domains for the event types if it's not enabled. // We restore the domains to their previous states after the wait ends. domain, _ := proto.ParseMethodName(name) if req := proto.GetType(domain + ".enable"); req != nil { enable := reflect.New(req).Interface().(proto.Request) //nolint: forcetypeassert restores = append(restores, b.EnableDomain(sessionID, enable)) } } b, cancel := b.WithCancel() messages := b.Event() return func() { if messages == nil { panic("can't use wait function twice") } defer func() { cancel() messages = nil for _, restore := range restores { restore() } }() for msg := range messages { if !(sessionID == "" || msg.SessionID == sessionID) { continue } if cbVal, has := cbMap[msg.Method]; has { e := reflect.New(proto.GetType(msg.Method)) msg.Load(e.Interface().(proto.Event)) //nolint: forcetypeassert args := []reflect.Value{e} if cbVal.Type().NumIn() == 2 { args = append(args, reflect.ValueOf(msg.SessionID)) } res := cbVal.Call(args) if len(res) > 0 { if res[0].Bool() { return } } } } } } // Event of the browser. func (b *Browser) Event() <-chan *Message { src := b.event.Subscribe(b.ctx) dst := make(chan *Message) go func() { defer close(dst) for { select { case <-b.ctx.Done(): return case e, ok := <-src: if !ok { return } select { case <-b.ctx.Done(): return case dst <- e.(*Message): //nolint: forcetypeassert } } } }() return dst } func (b *Browser) initEvents() { ctx, cancel := context.WithCancel(b.ctx) b.event = goob.New(ctx) event := b.client.Event() go func() { defer cancel() for e := range event { b.event.Publish(&Message{ SessionID: proto.TargetSessionID(e.SessionID), Method: e.Method, lock: &sync.Mutex{}, data: e.Params, }) } }() } func (b *Browser) pageInfo(id proto.TargetTargetID) (*proto.TargetTargetInfo, error) { res, err := proto.TargetGetTargetInfo{TargetID: id}.Call(b) if err != nil { return nil, err } return res.TargetInfo, nil } func (b *Browser) isHeadless() (enabled bool) { res, _ := proto.BrowserGetBrowserCommandLine{}.Call(b) for _, v := range res.Arguments { if strings.Contains(v, "headless") { return true } } return false } // IgnoreCertErrors switch. If enabled, all certificate errors will be ignored. func (b *Browser) IgnoreCertErrors(enable bool) error { return proto.SecuritySetIgnoreCertificateErrors{Ignore: enable}.Call(b) } // GetCookies from the browser. func (b *Browser) GetCookies() ([]*proto.NetworkCookie, error) { res, err := proto.StorageGetCookies{BrowserContextID: b.BrowserContextID}.Call(b) if err != nil { return nil, err } return res.Cookies, nil } // SetCookies to the browser. If the cookies is nil it will clear all the cookies. func (b *Browser) SetCookies(cookies []*proto.NetworkCookieParam) error { if cookies == nil { return proto.StorageClearCookies{BrowserContextID: b.BrowserContextID}.Call(b) } return proto.StorageSetCookies{ Cookies: cookies, BrowserContextID: b.BrowserContextID, }.Call(b) } // WaitDownload returns a helper to get the next download file. // The file path will be: // // filepath.Join(dir, info.GUID) func (b *Browser) WaitDownload(dir string) func() (info *proto.PageDownloadWillBegin) { var oldDownloadBehavior proto.BrowserSetDownloadBehavior has := b.LoadState("", &oldDownloadBehavior) _ = proto.BrowserSetDownloadBehavior{ Behavior: proto.BrowserSetDownloadBehaviorBehaviorAllowAndName, BrowserContextID: b.BrowserContextID, DownloadPath: dir, }.Call(b) var start *proto.PageDownloadWillBegin waitProgress := b.EachEvent(func(e *proto.PageDownloadWillBegin) { start = e }, func(e *proto.PageDownloadProgress) bool { return start != nil && start.GUID == e.GUID && e.State == proto.PageDownloadProgressStateCompleted }) return func() *proto.PageDownloadWillBegin { defer func() { if has { _ = oldDownloadBehavior.Call(b) } else { _ = proto.BrowserSetDownloadBehavior{ Behavior: proto.BrowserSetDownloadBehaviorBehaviorDefault, BrowserContextID: b.BrowserContextID, }.Call(b) } }() waitProgress() return start } } // Version info of the browser. func (b *Browser) Version() (*proto.BrowserGetVersionResult, error) { return proto.BrowserGetVersion{}.Call(b) } golang-github-go-rod-rod-0.116.2/browser_test.go000066400000000000000000000234221520674146500214450ustar00rootroot00000000000000package rod_test import ( "errors" "fmt" "net/http" "os" "os/exec" "runtime" "testing" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/devices" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/got" "github.com/ysmood/gson" ) func TestIncognito(t *testing.T) { g := setup(t) k := g.RandStr(16) b := g.browser.MustIncognito().Sleeper(rod.DefaultSleeper) defer b.MustClose() page := b.MustPage(g.blank()) defer page.MustClose() page.MustEval(`k => localStorage[k] = 1`, k) g.True(g.page.MustNavigate(g.blank()).MustEval(`k => localStorage[k]`, k).Nil()) g.Eq(page.MustEval(`k => localStorage[k]`, k).Str(), "1") // localStorage can only store string g.Panic(func() { g.mc.stubErr(1, proto.TargetCreateBrowserContext{}) g.browser.MustIncognito() }) } func TestBrowserResetControlURL(_ *testing.T) { rod.New().ControlURL("test").ControlURL("") } func TestDefaultDevice(t *testing.T) { g := setup(t) ua := "" s := g.Serve() s.Mux.HandleFunc("/t", func(_ http.ResponseWriter, r *http.Request) { ua = r.Header.Get("User-Agent") }) // TODO: https://github.com/golang/go/issues/51459 b := *g.browser b.DefaultDevice(devices.IPhoneX) b.MustPage(s.URL("/t")).MustClose() g.Eq(ua, devices.IPhoneX.UserAgentEmulation().UserAgent) b.NoDefaultDevice() b.MustPage(s.URL("/t")).MustClose() g.Neq(ua, devices.IPhoneX.UserAgentEmulation().UserAgent) } func TestPageErr(t *testing.T) { g := setup(t) g.Panic(func() { g.mc.stubErr(1, proto.TargetAttachToTarget{}) g.browser.MustPage() }) } func TestPageFromTarget(t *testing.T) { g := setup(t) g.Panic(func() { res, err := proto.TargetCreateTarget{URL: "about:blank"}.Call(g.browser) g.E(err) defer func() { g.browser.MustPageFromTargetID(res.TargetID).MustClose() }() g.mc.stubErr(1, proto.EmulationSetDeviceMetricsOverride{}) g.browser.MustPageFromTargetID(res.TargetID) }) } func TestBrowserPages(t *testing.T) { g := setup(t) b := g.browser pages := b.MustPages() g.Gte(len(pages), 1) { g.mc.stub(1, proto.TargetGetTargets{}, func(send StubSend) (gson.JSON, error) { d, _ := send() return *d.Set("targetInfos.0.type", "iframe"), nil }) b.MustPages() } g.Panic(func() { g.mc.stubErr(1, proto.TargetCreateTarget{}) b.MustPage() }) g.Panic(func() { g.mc.stubErr(1, proto.TargetGetTargets{}) b.MustPages() }) g.Panic(func() { _, err := proto.TargetCreateTarget{URL: "about:blank"}.Call(b) g.E(err) g.mc.stubErr(1, proto.TargetAttachToTarget{}) b.MustPages() }) } func TestBrowserClearStates(t *testing.T) { g := setup(t) g.E(proto.EmulationClearGeolocationOverride{}.Call(g.page)) } func TestBrowserEvent(t *testing.T) { g := setup(t) messages := g.browser.Context(g.Context()).Event() p := g.newPage() wait := make(chan struct{}) for msg := range messages { e := proto.TargetAttachedToTarget{} if msg.Load(&e) { g.Eq(e.TargetInfo.TargetID, p.TargetID) close(wait) break } } <-wait } func TestBrowserWaitEvent(t *testing.T) { g := setup(t) g.NotNil(g.browser.Context(g.Context()).Event()) wait := g.page.WaitEvent(proto.PageFrameNavigated{}) g.page.MustNavigate(g.blank()) wait() wait = g.browser.EachEvent(func(_ *proto.PageFrameNavigated, _ proto.TargetSessionID) bool { return true }) g.page.MustNavigate(g.blank()) wait() } func TestBrowserCrash(t *testing.T) { g := setup(t) browser := rod.New().Context(g.Context()).MustConnect() page := browser.MustPage() js := `() => new Promise(r => setTimeout(r, 10000))` go g.Panic(func() { page.MustEval(js) }) utils.Sleep(0.2) _ = proto.BrowserCrash{}.Call(browser) utils.Sleep(0.3) _, err := page.Eval(js) g.Has(err.Error(), "use of closed network connection") } func TestBrowserCall(t *testing.T) { g := setup(t) v, err := proto.BrowserGetVersion{}.Call(g.browser) g.E(err) g.Regex("1.3", v.ProtocolVersion) } func TestBlockingNavigation(t *testing.T) { g := setup(t) /* Navigate can take forever if a page doesn't response. If one page is blocked, other pages should still work. */ s := g.Serve() pause := g.Context() s.Mux.HandleFunc("/a", func(_ http.ResponseWriter, _ *http.Request) { <-pause.Done() }) s.Route("/b", ".html", `ok`) blocked := g.newPage() go func() { g.Panic(func() { blocked.MustNavigate(s.URL("/a")) }) }() utils.Sleep(0.3) g.newPage(s.URL("/b")) } func TestResolveBlocking(t *testing.T) { g := setup(t) s := g.Serve() pause := g.Context() s.Mux.HandleFunc("/", func(_ http.ResponseWriter, _ *http.Request) { <-pause.Done() }) p := g.newPage() go func() { utils.Sleep(0.1) p.MustStopLoading() }() g.Panic(func() { p.MustNavigate(s.URL()) }) } func TestTestTry(t *testing.T) { g := setup(t) g.Nil(rod.Try(func() {})) err := rod.Try(func() { panic(1) }) var errVal *rod.TryError g.True(errors.As(err, &errVal)) g.Is(err, &rod.TryError{}) g.Eq(errVal.Unwrap().Error(), "1") g.Eq(1, errVal.Value) g.Has(errVal.Error(), "error value: 1\ngoroutine") errVal = rod.Try(func() { panic(errors.New("t")) }).(*rod.TryError) g.Eq(errVal.Unwrap().Error(), "t") } func TestBrowserOthers(t *testing.T) { g := setup(t) g.browser.Timeout(time.Second).CancelTimeout().MustGetCookies() } func TestBinarySize(t *testing.T) { g := setup(t) if runtime.GOOS == "windows" || utils.InContainer { g.SkipNow() } cmd := exec.Command("go", "build", "-trimpath", "-ldflags", "-w -s", "-o", "tmp/translator", "./lib/examples/translator") cmd.Env = append(os.Environ(), "GOOS=linux") g.Nil(cmd.Run()) stat, err := os.Stat("tmp/translator") g.E(err) g.Lte(float64(stat.Size())/1024/1024, 11) // mb } func TestBrowserCookies(t *testing.T) { g := setup(t) b := g.browser.MustIncognito() defer b.MustClose() b.MustSetCookies(&proto.NetworkCookie{ Name: "a", Value: "val", Domain: "test.com", }) cookies := b.MustGetCookies() g.Len(cookies, 1) g.Eq(cookies[0].Name, "a") g.Eq(cookies[0].Value, "val") { b.MustSetCookies() cookies := b.MustGetCookies() g.Len(cookies, 0) } g.mc.stubErr(1, proto.StorageGetCookies{}) g.Err(b.GetCookies()) } func TestWaitDownload(t *testing.T) { g := setup(t) s := g.Serve() content := "test content" s.Route("/d", ".bin", []byte(content)) s.Route("/page", ".html", fmt.Sprintf(`click`, s.URL())) page := g.page.MustNavigate(s.URL("/page")) wait := g.browser.MustWaitDownload() page.MustElement("a").MustClick() data := wait() g.Eq(content, string(data)) } func TestWaitDownloadDataURI(t *testing.T) { g := setup(t) s := g.Serve() s.Route("/", ".html", ` click click `, ) page := g.page.MustNavigate(s.URL()) wait1 := g.browser.MustWaitDownload() page.MustElement("#a").MustClick() data := wait1() g.Eq("test data", string(data)) wait2 := g.browser.MustWaitDownload() page.MustElement("#b").MustClick() data = wait2() g.Eq("test blob", string(data)) } func TestWaitDownloadCancel(t *testing.T) { g := setup(t) wait := g.browser.Context(g.Timeout(0)).WaitDownload(os.TempDir()) g.Eq(wait(), (*proto.PageDownloadWillBegin)(nil)) } func TestWaitDownloadFromNewPage(t *testing.T) { g := setup(t) s := g.Serve() content := "test content" s.Route("/d", ".bin", content) s.Route("/page", ".html", fmt.Sprintf( `click`, s.URL()), ) page := g.page.MustNavigate(s.URL("/page")) wait := g.browser.MustWaitDownload() page.MustElement("a").MustClick() data := wait() g.Eq(content, string(data)) } func TestBrowserConnectErr(t *testing.T) { g := setup(t) g.Panic(func() { rod.New().ControlURL(g.RandStr(16)).MustConnect() }) } func TestStreamReader(t *testing.T) { g := setup(t) r := rod.NewStreamReader(g.page, "") g.mc.stub(1, proto.IORead{}, func(_ StubSend) (gson.JSON, error) { return gson.New(proto.IOReadResult{ Data: "test", }), nil }) b := make([]byte, 4) _, _ = r.Read(b) g.Eq("test", string(b)) g.mc.stubErr(1, proto.IORead{}) _, err := r.Read(nil) g.Err(err) g.mc.stub(1, proto.IORead{}, func(_ StubSend) (gson.JSON, error) { return gson.New(proto.IOReadResult{ Base64Encoded: true, Data: "@", }), nil }) _, err = r.Read(nil) g.Err(err) } func TestBrowserConnectFailure(t *testing.T) { g := setup(t) c := g.Context() c.Cancel() err := rod.New().Context(c).Connect() if err == nil { g.Fatal("expected an error on connect failure") } } func TestBrowserPool(t *testing.T) { g := got.T(t) pool := rod.NewBrowserPool(3) b, err := pool.Get(func() (*rod.Browser, error) { browser := rod.New() return browser, browser.Connect() }) g.E(err) pool.Put(b) b = pool.MustGet(func() *rod.Browser { return rod.New().MustConnect() }) pool.Put(b) pool.Cleanup(func(p *rod.Browser) { p.MustClose() }) } func TestOldBrowser(t *testing.T) { t.Skip() g := setup(t) u := launcher.New().Revision(686378).MustLaunch() b := rod.New().ControlURL(u).MustConnect() g.Cleanup(b.MustClose) res, err := proto.BrowserGetVersion{}.Call(b) g.E(err) g.Eq(res.Revision, "@19d4547535ab5aba70b4730443f84e8153052174") } func TestBrowserLostConnection(t *testing.T) { g := setup(t) l := launcher.New() p := rod.New().ControlURL(l.MustLaunch()).MustConnect().MustPage(g.blank()) go func() { utils.Sleep(1) l.Kill() }() _, err := p.Eval(`() => new Promise(r => {})`) g.Err(err) } func TestBrowserConnectConflict(t *testing.T) { g := setup(t) g.Panic(func() { rod.New().Client(&cdp.Client{}).ControlURL("test").MustConnect() }) } golang-github-go-rod-rod-0.116.2/context.go000066400000000000000000000076351520674146500204170ustar00rootroot00000000000000package rod import ( "context" "time" "github.com/go-rod/rod/lib/utils" ) type ( timeoutContextKey struct{} timeoutContextVal struct { parent context.Context cancel context.CancelFunc } ) // Context returns a clone with the specified ctx for chained sub-operations. func (b *Browser) Context(ctx context.Context) *Browser { newObj := *b newObj.ctx = ctx return &newObj } // GetContext of current instance. func (b *Browser) GetContext() context.Context { return b.ctx } // Timeout returns a clone with the specified total timeout of all chained sub-operations. func (b *Browser) Timeout(d time.Duration) *Browser { ctx, cancel := context.WithTimeout(b.ctx, d) return b.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{b.ctx, cancel})) } // CancelTimeout cancels the current timeout context and returns a clone with the parent context. func (b *Browser) CancelTimeout() *Browser { val := b.ctx.Value(timeoutContextKey{}).(*timeoutContextVal) //nolint:forcetypeassert val.cancel() return b.Context(val.parent) } // WithCancel returns a clone with a context cancel function. func (b *Browser) WithCancel() (*Browser, func()) { ctx, cancel := context.WithCancel(b.ctx) return b.Context(ctx), cancel } // Sleeper returns a clone with the specified sleeper for chained sub-operations. func (b *Browser) Sleeper(sleeper func() utils.Sleeper) *Browser { newObj := *b newObj.sleeper = sleeper return &newObj } // Context returns a clone with the specified ctx for chained sub-operations. func (p *Page) Context(ctx context.Context) *Page { p.helpersLock.Lock() newObj := *p p.helpersLock.Unlock() newObj.ctx = ctx return &newObj } // GetContext of current instance. func (p *Page) GetContext() context.Context { return p.ctx } // Timeout returns a clone with the specified total timeout of all chained sub-operations. func (p *Page) Timeout(d time.Duration) *Page { ctx, cancel := context.WithTimeout(p.ctx, d) return p.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{p.ctx, cancel})) } // CancelTimeout cancels the current timeout context and returns a clone with the parent context. func (p *Page) CancelTimeout() *Page { val := p.ctx.Value(timeoutContextKey{}).(*timeoutContextVal) //nolint: forcetypeassert val.cancel() return p.Context(val.parent) } // WithCancel returns a clone with a context cancel function. func (p *Page) WithCancel() (*Page, func()) { ctx, cancel := context.WithCancel(p.ctx) return p.Context(ctx), cancel } // Sleeper returns a clone with the specified sleeper for chained sub-operations. func (p *Page) Sleeper(sleeper func() utils.Sleeper) *Page { newObj := *p newObj.sleeper = sleeper return &newObj } // Context returns a clone with the specified ctx for chained sub-operations. func (el *Element) Context(ctx context.Context) *Element { newObj := *el newObj.ctx = ctx return &newObj } // GetContext of current instance. func (el *Element) GetContext() context.Context { return el.ctx } // Timeout returns a clone with the specified total timeout of all chained sub-operations. func (el *Element) Timeout(d time.Duration) *Element { ctx, cancel := context.WithTimeout(el.ctx, d) return el.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{el.ctx, cancel})) } // CancelTimeout cancels the current timeout context and returns a clone with the parent context. func (el *Element) CancelTimeout() *Element { val := el.ctx.Value(timeoutContextKey{}).(*timeoutContextVal) //nolint: forcetypeassert val.cancel() return el.Context(val.parent) } // WithCancel returns a clone with a context cancel function. func (el *Element) WithCancel() (*Element, func()) { ctx, cancel := context.WithCancel(el.ctx) return el.Context(ctx), cancel } // Sleeper returns a clone with the specified sleeper for chained sub-operations. func (el *Element) Sleeper(sleeper func() utils.Sleeper) *Element { newObj := *el newObj.sleeper = sleeper return &newObj } golang-github-go-rod-rod-0.116.2/cspell.json000066400000000000000000000047031520674146500205520ustar00rootroot00000000000000// cSpell Settings { // Version of the setting file. Always 0.2 "version": "0.2", // language - current active spelling language "language": "en", "ignorePaths": [ "**/*.{out,sketch,svg}", "fixtures/fonts.html", "**/tmp/**", "lib/devices/list.go", "lib/js/helper.go", "lib/proto/!(a_*)", "**/go.{mod,sum}", ".golangci.yml" ], // words - list of words to be always considered correct "words": [ "APPDATA", "Arraybuffer", "backgrounding", "backoff", "Backquote", "beforeunload", "bodyclose", "breakpad", "Chromedp", "codesearch", "commandline", "COMSPEC", "containerenv", "contenteditable", "Contentful", "Contextable", "contextcheck", "coverprofile", "Dataview", "datetime", "dockerenv", "dropzone", "duckduckgo", "enctype", "errcheck", "evenodd", "excludesfile", "fetchup", "fontconfig", "forbidigo", "forcetypeassert", "Fullscreen", "Geolocation", "getent", "gobwas", "gocognit", "gocyclo", "GODEBUG", "gofmt", "gofumpt", "goimports", "golangci", "goob", "gopls", "goproxy", "gotrace", "gson", "headful", "iframe", "iframes", "Interactable", "ioutil", "keychain", "KHTML", "ldflags", "leakless", "libasound", "libcairo", "libgbm", "libgobject", "libgtk", "libnss", "libxss", "libxtst", "Lmsgprefix", "loglevel", "MDPI", "MITM", "mitmproxy", "mvdan", "nilnil", "noctx", "nolint", "Noto", "Numpad", "onbeforeunload", "onclick", "onmouseenter", "onmouseout", "OOPIF", "opencontainers", "osversion", "progresser", "proto", "proxyauth", "Rects", "repost", "sattributes", "schildren", "Sessionable", "Smood", "Socketable", "spki", "spkis", "srgb", "staticcheck", "stdlib", "termux", "tlid", "touchend", "touchstart", "tparallel", "tracebackancestors", "trimpath", "Typedarray", "tzdata", "Unserializable", "Wasmvalue", "Weakmap", "Weakset", "Webassemblymemory", "wsutil", "xlink", "XVFB", "ysmood" ], // flagWords - list of words to be always considered incorrect // This is useful for offensive words and common spelling errors. // For example "hte" should be "the" "flagWords": [] } golang-github-go-rod-rod-0.116.2/dev_helpers.go000066400000000000000000000142631520674146500212260ustar00rootroot00000000000000// This file defines the helpers to develop automation. // Such as when running automation we can use trace to visually // see where the mouse going to click. package rod import ( "encoding/json" "fmt" "html" "net" "net/http" "strings" "time" "github.com/go-rod/rod/lib/assets" "github.com/go-rod/rod/lib/js" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" ) // TraceType for logger. type TraceType string // String interface. func (t TraceType) String() string { return fmt.Sprintf("[%s]", string(t)) } const ( // TraceTypeWaitRequestsIdle type. TraceTypeWaitRequestsIdle TraceType = "wait requests idle" // TraceTypeWaitRequests type. TraceTypeWaitRequests TraceType = "wait requests" // TraceTypeQuery type. TraceTypeQuery TraceType = "query" // TraceTypeWait type. TraceTypeWait TraceType = "wait" // TraceTypeInput type. TraceTypeInput TraceType = "input" ) // ServeMonitor starts the monitor server. // The reason why not to use "chrome://inspect/#devices" is one target cannot be driven by multiple controllers. func (b *Browser) ServeMonitor(host string) string { u, mux, closeSvr := serve(host) go func() { <-b.ctx.Done() utils.E(closeSvr()) }() mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { httHTML(w, assets.Monitor) }) mux.HandleFunc("/api/pages", func(w http.ResponseWriter, _ *http.Request) { res, err := proto.TargetGetTargets{}.Call(b) //nolint: contextcheck utils.E(err) list := []*proto.TargetTargetInfo{} for _, info := range res.TargetInfos { if info.Type == proto.TargetTargetInfoTypePage { list = append(list, info) } } w.WriteHeader(http.StatusOK) utils.E(w.Write(utils.MustToJSONBytes(list))) }) mux.HandleFunc("/page/", func(w http.ResponseWriter, _ *http.Request) { httHTML(w, assets.MonitorPage) }) mux.HandleFunc("/api/page/", func(w http.ResponseWriter, r *http.Request) { id := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] info, err := b.pageInfo(proto.TargetTargetID(id)) //nolint: contextcheck utils.E(err) w.WriteHeader(http.StatusOK) utils.E(w.Write(utils.MustToJSONBytes(info))) }) mux.HandleFunc("/screenshot/", func(w http.ResponseWriter, r *http.Request) { id := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] target := proto.TargetTargetID(id) p := b.MustPageFromTargetID(target) w.Header().Add("Content-Type", "image/png;") utils.E(w.Write(p.MustScreenshot())) //nolint: contextcheck }) return u } // check method and sleep if needed. func (b *Browser) trySlowMotion() { if b.slowMotion == 0 { return } time.Sleep(b.slowMotion) } // ExposeHelpers helper functions to page's js context so that we can use the Devtools' console to debug them. func (p *Page) ExposeHelpers(list ...*js.Function) { p.MustEvaluate(evalHelper(&js.Function{ Name: "_" + utils.RandString(8), // use a random name so it won't hit the cache Definition: "() => { window.rod = functions }", Dependencies: list, })) } // Overlay a rectangle on the main frame with specified message. func (p *Page) Overlay(left, top, width, height float64, msg string) (remove func()) { id := utils.RandString(8) _, _ = p.root.Evaluate(evalHelper(js.Overlay, id, left, top, width, height, msg, ).ByPromise()) remove = func() { _, _ = p.root.Evaluate(evalHelper(js.RemoveOverlay, id)) } return } func (p *Page) tryTrace(typ TraceType, msg ...interface{}) func() { if !p.browser.trace { return func() {} } msg = append([]interface{}{typ}, msg...) msg = append(msg, p) p.browser.logger.Println(msg...) return p.Overlay(0, 0, 500, 0, fmt.Sprint(msg)) } func (p *Page) tryTraceQuery(opts *EvalOptions) func() { if !p.browser.trace { return func() {} } p.browser.logger.Println(TraceTypeQuery, opts, p) msg := fmt.Sprintf("%s", html.EscapeString(opts.String())) return p.Overlay(0, 0, 500, 0, msg) } func (p *Page) tryTraceReq(includes, excludes []string) func(map[proto.NetworkRequestID]string) { if !p.browser.trace { return func(map[proto.NetworkRequestID]string) {} } msg := map[string][]string{ "includes": includes, "excludes": excludes, } p.browser.logger.Println(TraceTypeWaitRequestsIdle, msg, p) cleanup := p.Overlay(0, 0, 500, 0, utils.MustToJSON(msg)) ch := make(chan map[string]string) update := func(list map[proto.NetworkRequestID]string) { clone := map[string]string{} for k, v := range list { clone[string(k)] = v } ch <- clone } go func() { var waitList map[string]string t := time.NewTicker(time.Second) for { select { case <-p.ctx.Done(): t.Stop() cleanup() return case waitList = <-ch: case <-t.C: p.browser.logger.Println(TraceTypeWaitRequests, p, waitList) } } }() return update } // Overlay msg on the element. func (el *Element) Overlay(msg string) (removeOverlay func()) { id := utils.RandString(8) _, _ = el.Evaluate(evalHelper(js.ElementOverlay, id, msg, ).ByPromise()) removeOverlay = func() { _, _ = el.Evaluate(evalHelper(js.RemoveOverlay, id)) } return } func (el *Element) tryTrace(typ TraceType, msg ...interface{}) func() { if !el.page.browser.trace { return func() {} } msg = append([]interface{}{typ}, msg...) msg = append(msg, el) el.page.browser.logger.Println(msg...) return el.Overlay(fmt.Sprint(msg)) } func (m *Mouse) initMouseTracer() { _, _ = m.page.Evaluate(evalHelper(js.InitMouseTracer, m.id, assets.MousePointer).ByPromise()) } func (m *Mouse) updateMouseTracer() bool { res, err := m.page.Evaluate(evalHelper(js.UpdateMouseTracer, m.id, m.pos.X, m.pos.Y)) if err != nil { return true } return res.Value.Bool() } // Serve a port, if host is empty a random port will be used. func serve(host string) (string, *http.ServeMux, func() error) { if host == "" { host = "127.0.0.1:0" } mux := http.NewServeMux() srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { w.WriteHeader(http.StatusBadRequest) utils.E(json.NewEncoder(w).Encode(err)) } }() mux.ServeHTTP(w, r) })} l, err := net.Listen("tcp", host) utils.E(err) go func() { _ = srv.Serve(l) }() url := "http://" + l.Addr().String() return url, mux, srv.Close } golang-github-go-rod-rod-0.116.2/dev_helpers_test.go000066400000000000000000000047041520674146500222640ustar00rootroot00000000000000package rod_test import ( "testing" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/defaults" "github.com/go-rod/rod/lib/js" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/gson" ) func TestMonitor(t *testing.T) { g := setup(t) b := rod.New().MustConnect() defer b.MustClose() p := b.MustPage(g.blank()).MustWaitLoad() b, cancel := b.WithCancel() defer cancel() host := b.Context(g.Context()).ServeMonitor("") page := g.page.MustNavigate(host) g.Has(page.MustElement("#targets a").MustParent().MustHTML(), string(p.TargetID)) page.MustNavigate(host + "/page/" + string(p.TargetID)) page.MustWait(`(id) => document.title.includes(id)`, p.TargetID) img := g.Req("", host+"/screenshot").Bytes() g.Gt(img.Len(), 10) res := g.Req("", host+"/api/page/test") g.Eq(400, res.StatusCode) g.Eq(-32602, gson.New(res.Body).Get("code").Int()) } func TestMonitorErr(t *testing.T) { g := setup(t) l := launcher.New() u := l.MustLaunch() defer l.Kill() g.Panic(func() { rod.New().Monitor("abc").ControlURL(u).MustConnect() }) } func TestTrace(t *testing.T) { g := setup(t) g.Eq(rod.TraceTypeInput.String(), "[input]") var msg []interface{} g.browser.Logger(utils.Log(func(list ...interface{}) { msg = list })) g.browser.Trace(true).SlowMotion(time.Microsecond) defer func() { g.browser.Logger(rod.DefaultLogger) g.browser.Trace(defaults.Trace).SlowMotion(defaults.Slow) }() p := g.page.MustNavigate(g.srcFile("fixtures/click.html")).MustWaitLoad() g.Eq(rod.TraceTypeWait, msg[0]) g.Eq("load", msg[1]) g.Eq(p, msg[2]) el := p.MustElement("button") el.MustClick() g.Eq(rod.TraceTypeInput, msg[0]) g.Eq("left click", msg[1]) g.Eq(el, msg[2]) g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) _ = p.Mouse.MoveTo(proto.NewPoint(10, 10)) } func TestTraceLogs(t *testing.T) { g := setup(t) g.browser.Logger(utils.LoggerQuiet) g.browser.Trace(true) defer func() { g.browser.Logger(rod.DefaultLogger) g.browser.Trace(defaults.Trace) }() p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) el := p.MustElement("button") el.MustClick() g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) p.Overlay(0, 0, 100, 30, "") } func TestExposeHelpers(t *testing.T) { g := setup(t) p := g.newPage(g.srcFile("fixtures/click.html")) p.ExposeHelpers(js.ElementR) g.Eq(p.MustElementByJS(`() => rod.elementR('button', 'click me')`).MustText(), "click me") } golang-github-go-rod-rod-0.116.2/element.go000066400000000000000000000465351520674146500203660ustar00rootroot00000000000000package rod import ( "context" "errors" "fmt" "reflect" "strings" "time" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/input" "github.com/go-rod/rod/lib/js" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/gson" ) // Element implements these interfaces. var ( _ proto.Client = &Element{} _ proto.Contextable = &Element{} _ proto.Sessionable = &Element{} ) // Element represents the DOM element. type Element struct { Object *proto.RuntimeRemoteObject e eFunc ctx context.Context sleeper func() utils.Sleeper page *Page } // GetSessionID interface. func (el *Element) GetSessionID() proto.TargetSessionID { return el.page.SessionID } // String interface. func (el *Element) String() string { return fmt.Sprintf("<%s>", el.Object.Description) } // Page of the element. func (el *Element) Page() *Page { return el.page } // Focus sets focus on the specified element. // Before the action, it will try to scroll to the element. func (el *Element) Focus() error { err := el.ScrollIntoView() if err != nil { return err } _, err = el.Evaluate(Eval(`() => this.focus()`).ByUser()) return err } // ScrollIntoView scrolls the current element into the visible area of the browser // window if it's not already within the visible area. func (el *Element) ScrollIntoView() error { defer el.tryTrace(TraceTypeInput, "scroll into view")() el.page.browser.trySlowMotion() err := el.WaitStableRAF() if err != nil { return err } return proto.DOMScrollIntoViewIfNeeded{ObjectID: el.id()}.Call(el) } // Hover the mouse over the center of the element. // Before the action, it will try to scroll to the element and wait until it's interactable. func (el *Element) Hover() error { pt, err := el.WaitInteractable() if err != nil { return err } return el.page.Context(el.ctx).Mouse.MoveTo(*pt) } // MoveMouseOut of the current element. func (el *Element) MoveMouseOut() error { shape, err := el.Shape() if err != nil { return err } box := shape.Box() return el.page.Mouse.MoveTo(proto.NewPoint(box.X+box.Width, box.Y)) } // Click will press then release the button just like a human. // Before the action, it will try to scroll to the element, hover the mouse over it, // wait until the it's interactable and enabled. func (el *Element) Click(button proto.InputMouseButton, clickCount int) error { err := el.Hover() if err != nil { return err } err = el.WaitEnabled() if err != nil { return err } defer el.tryTrace(TraceTypeInput, string(button)+" click")() return el.page.Context(el.ctx).Mouse.Click(button, clickCount) } // Tap will scroll to the button and tap it just like a human. // Before the action, it will try to scroll to the element and wait until it's interactable and enabled. func (el *Element) Tap() error { err := el.ScrollIntoView() if err != nil { return err } err = el.WaitEnabled() if err != nil { return err } pt, err := el.WaitInteractable() if err != nil { return err } defer el.tryTrace(TraceTypeInput, "tap")() return el.page.Context(el.ctx).Touch.Tap(pt.X, pt.Y) } // Interactable checks if the element is interactable with cursor. // The cursor can be mouse, finger, stylus, etc. // If not interactable err will be ErrNotInteractable, such as when covered by a modal,. func (el *Element) Interactable() (pt *proto.Point, err error) { noPointerEvents, err := el.Eval(`() => getComputedStyle(this).pointerEvents === 'none'`) if err != nil { return nil, err } if noPointerEvents.Value.Bool() { return nil, &NoPointerEventsError{el} } shape, err := el.Shape() if err != nil { return nil, err } pt = shape.OnePointInside() if pt == nil { err = &InvisibleShapeError{el} return } scroll, err := el.page.root.Context(el.ctx).Eval(`() => ({ x: window.scrollX, y: window.scrollY })`) if err != nil { return } elAtPoint, err := el.page.Context(el.ctx).ElementFromPoint( int(pt.X)+scroll.Value.Get("x").Int(), int(pt.Y)+scroll.Value.Get("y").Int(), ) if err != nil { if errors.Is(err, cdp.ErrNodeNotFoundAtPos) { err = &InvisibleShapeError{el} } return } isParent, err := el.ContainsElement(elAtPoint) if err != nil { return } if !isParent { err = &CoveredError{elAtPoint} } return } // Shape of the DOM element content. The shape is a group of 4-sides polygons. // A 4-sides polygon is not necessary a rectangle. 4-sides polygons can be apart from each other. // For example, we use 2 4-sides polygons to describe the shape below: // // ____________ ____________ // / ___/ = /___________/ + _________ // /________/ /________/ func (el *Element) Shape() (*proto.DOMGetContentQuadsResult, error) { return proto.DOMGetContentQuads{ObjectID: el.id()}.Call(el) } // Type is similar with Keyboard.Type. // Before the action, it will try to scroll to the element and focus on it. func (el *Element) Type(keys ...input.Key) error { err := el.Focus() if err != nil { return err } return el.page.Context(el.ctx).Keyboard.Type(keys...) } // KeyActions is similar with Page.KeyActions. // Before the action, it will try to scroll to the element and focus on it. func (el *Element) KeyActions() (*KeyActions, error) { err := el.Focus() if err != nil { return nil, err } return el.page.Context(el.ctx).KeyActions(), nil } // SelectText selects the text that matches the regular expression. // Before the action, it will try to scroll to the element and focus on it. func (el *Element) SelectText(regex string) error { err := el.Focus() if err != nil { return err } defer el.tryTrace(TraceTypeInput, "select text: "+regex)() el.page.browser.trySlowMotion() _, err = el.Evaluate(evalHelper(js.SelectText, regex).ByUser()) return err } // SelectAllText selects all text // Before the action, it will try to scroll to the element and focus on it. func (el *Element) SelectAllText() error { err := el.Focus() if err != nil { return err } defer el.tryTrace(TraceTypeInput, "select all text")() el.page.browser.trySlowMotion() _, err = el.Evaluate(evalHelper(js.SelectAllText).ByUser()) return err } // Input focuses on the element and input text to it. // Before the action, it will scroll to the element, wait until it's visible, enabled and writable. // To empty the input you can use something like // // el.SelectAllText().MustInput("") func (el *Element) Input(text string) error { err := el.Focus() if err != nil { return err } err = el.WaitEnabled() if err != nil { return err } err = el.WaitWritable() if err != nil { return err } err = el.page.Context(el.ctx).InsertText(text) _, _ = el.Evaluate(evalHelper(js.InputEvent).ByUser()) return err } // InputTime focuses on the element and input time to it. // Before the action, it will scroll to the element, wait until it's visible, enabled and writable. // It will wait until the element is visible, enabled and writable. func (el *Element) InputTime(t time.Time) error { err := el.Focus() if err != nil { return err } err = el.WaitEnabled() if err != nil { return err } err = el.WaitWritable() if err != nil { return err } defer el.tryTrace(TraceTypeInput, "input "+t.String())() _, err = el.Evaluate(evalHelper(js.InputTime, t.UnixNano()/1e6).ByUser()) return err } // InputColor focuses on the element and inputs a color string to it. // Before the action, it will scroll to the element, wait until it's visible, enabled and writable. func (el *Element) InputColor(color string) error { err := el.Focus() if err != nil { return err } err = el.WaitEnabled() if err != nil { return err } err = el.WaitWritable() if err != nil { return err } defer el.tryTrace(TraceTypeInput, "input "+color)() _, err = el.Evaluate(evalHelper(js.InputColor, color)) return err } // Blur removes focus from the element. func (el *Element) Blur() error { _, err := el.Evaluate(Eval("() => this.blur()").ByUser()) return err } // Select the children option elements that match the selectors. // Before the action, it will scroll to the element, wait until it's visible. // If no option matches the selectors, it will return [ErrElementNotFound]. func (el *Element) Select(selectors []string, selected bool, t SelectorType) error { err := el.Focus() if err != nil { return err } defer el.tryTrace(TraceTypeInput, fmt.Sprintf(`select "%s"`, strings.Join(selectors, "; ")))() el.page.browser.trySlowMotion() res, err := el.Evaluate(evalHelper(js.Select, selectors, selected, t).ByUser()) if err != nil { return err } if !res.Value.Bool() { return &ElementNotFoundError{} } return nil } // Matches checks if the element can be selected by the css selector. func (el *Element) Matches(selector string) (bool, error) { res, err := el.Eval(`s => this.matches(s)`, selector) if err != nil { return false, err } return res.Value.Bool(), nil } // Attribute of the DOM object. // Attribute vs Property: // https://stackoverflow.com/questions/6003819/what-is-the-difference-between-properties-and-attributes-in-html func (el *Element) Attribute(name string) (*string, error) { attr, err := el.Eval("(n) => this.getAttribute(n)", name) if err != nil { return nil, err } if attr.Value.Nil() { return nil, nil //nolint: nilnil } s := attr.Value.Str() return &s, nil } // Property of the DOM object. // Property vs Attribute: // https://stackoverflow.com/questions/6003819/what-is-the-difference-between-properties-and-attributes-in-html func (el *Element) Property(name string) (gson.JSON, error) { prop, err := el.Eval("(n) => this[n]", name) if err != nil { return gson.New(nil), err } return prop.Value, nil } // Disabled checks if the element is disabled. func (el *Element) Disabled() (bool, error) { prop, err := el.Property("disabled") if err != nil { return false, err } return prop.Bool(), nil } // SetFiles of the current file input element. func (el *Element) SetFiles(paths []string) error { absPaths := utils.AbsolutePaths(paths) defer el.tryTrace(TraceTypeInput, fmt.Sprintf("set files: %v", absPaths))() el.page.browser.trySlowMotion() err := proto.DOMSetFileInputFiles{ Files: absPaths, ObjectID: el.id(), }.Call(el) return err } // Describe the current element. The depth is the maximum depth at which children should be retrieved, defaults to 1, // use -1 for the entire subtree or provide an integer larger than 0. // The pierce decides whether or not iframes and shadow roots should be traversed when returning the subtree. // The returned [proto.DOMNode.NodeID] will always be empty, // because NodeID is not stable (when [proto.DOMDocumentUpdated] // is fired all NodeID on the page will be reassigned to another value) // we don't recommend using the NodeID, instead, use the [proto.DOMBackendNodeID] to identify the element. func (el *Element) Describe(depth int, pierce bool) (*proto.DOMNode, error) { val, err := proto.DOMDescribeNode{ObjectID: el.id(), Depth: gson.Int(depth), Pierce: pierce}.Call(el) if err != nil { return nil, err } return val.Node, nil } // ShadowRoot returns the shadow root of this element. func (el *Element) ShadowRoot() (*Element, error) { node, err := el.Describe(1, false) if err != nil { return nil, err } // though now it's an array, w3c changed the spec of it to be a single. if len(node.ShadowRoots) == 0 { return nil, &NoShadowRootError{el} } id := node.ShadowRoots[0].BackendNodeID shadowNode, err := proto.DOMResolveNode{BackendNodeID: id}.Call(el) if err != nil { return nil, err } return el.page.Context(el.ctx).ElementFromObject(shadowNode.Object) } // Frame creates a page instance that represents the iframe. func (el *Element) Frame() (*Page, error) { node, err := el.Describe(1, false) if err != nil { return nil, err } clone := *el.page clone.FrameID = node.FrameID clone.jsCtxID = new(proto.RuntimeRemoteObjectID) clone.element = el clone.sleeper = el.sleeper return &clone, nil } // ContainsElement check if the target is equal or inside the element. func (el *Element) ContainsElement(target *Element) (bool, error) { res, err := el.Evaluate(evalHelper(js.ContainsElement, target.Object)) if err != nil { return false, err } return res.Value.Bool(), nil } // Text that the element displays. func (el *Element) Text() (string, error) { str, err := el.Evaluate(evalHelper(js.Text)) if err != nil { return "", err } return str.Value.String(), nil } // HTML of the element. func (el *Element) HTML() (string, error) { res, err := proto.DOMGetOuterHTML{ObjectID: el.Object.ObjectID}.Call(el) if err != nil { return "", err } return res.OuterHTML, nil } // Visible returns true if the element is visible on the page. func (el *Element) Visible() (bool, error) { res, err := el.Evaluate(evalHelper(js.Visible)) if err != nil { return false, err } return res.Value.Bool(), nil } // WaitLoad for element like . func (el *Element) WaitLoad() error { defer el.tryTrace(TraceTypeWait, "load")() _, err := el.Evaluate(evalHelper(js.WaitLoad).ByPromise()) return err } // WaitStable waits until no shape or position change for d duration. // Be careful, d is not the max wait timeout, it's the least stable time. // If you want to set a timeout you can use the [Element.Timeout] function. func (el *Element) WaitStable(d time.Duration) error { err := el.WaitVisible() if err != nil { return err } defer el.tryTrace(TraceTypeWait, "stable")() shape, err := el.Shape() if err != nil { return err } t := time.NewTicker(d) defer t.Stop() for { select { case <-t.C: case <-el.ctx.Done(): return el.ctx.Err() } current, err := el.Shape() if err != nil { return err } if reflect.DeepEqual(shape, current) { break } shape = current } return nil } // WaitStableRAF waits until no shape or position change for 2 consecutive animation frames. // If you want to wait animation that is triggered by JS not CSS, you'd better use [Element.WaitStable]. // About animation frame: https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame func (el *Element) WaitStableRAF() error { err := el.WaitVisible() if err != nil { return err } defer el.tryTrace(TraceTypeWait, "stable RAF")() var shape *proto.DOMGetContentQuadsResult page := el.page.Context(el.ctx) for { err = page.WaitRepaint() if err != nil { return err } current, err := el.Shape() if err != nil { return err } if reflect.DeepEqual(shape, current) { break } shape = current } return nil } // WaitInteractable waits for the element to be interactable. // It will try to scroll to the element on each try. func (el *Element) WaitInteractable() (pt *proto.Point, err error) { defer el.tryTrace(TraceTypeWait, "interactable")() err = utils.Retry(el.ctx, el.sleeper(), func() (bool, error) { // For lazy loading page the element can be outside of the viewport. // If we don't scroll to it, it will never be available. err := el.ScrollIntoView() if err != nil { return true, err } pt, err = el.Interactable() if errors.Is(err, &CoveredError{}) { return false, nil } return true, err }) return } // Wait until the js returns true. func (el *Element) Wait(opts *EvalOptions) error { return el.page.Context(el.ctx).Sleeper(el.sleeper).Wait(opts.This(el.Object)) } // WaitVisible until the element is visible. func (el *Element) WaitVisible() error { defer el.tryTrace(TraceTypeWait, "visible")() return el.Wait(evalHelper(js.Visible)) } // WaitEnabled until the element is not disabled. // Doc for readonly: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly func (el *Element) WaitEnabled() error { defer el.tryTrace(TraceTypeWait, "enabled")() return el.Wait(Eval(`() => !this.disabled`)) } // WaitWritable until the element is not readonly. // Doc for disabled: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled func (el *Element) WaitWritable() error { defer el.tryTrace(TraceTypeWait, "writable")() return el.Wait(Eval(`() => !this.readonly`)) } // WaitInvisible until the element invisible. func (el *Element) WaitInvisible() error { defer el.tryTrace(TraceTypeWait, "invisible")() return el.Wait(evalHelper(js.Invisible)) } // CanvasToImage get image data of a canvas. // The default format is image/png. // The default quality is 0.92. // doc: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL func (el *Element) CanvasToImage(format string, quality float64) ([]byte, error) { res, err := el.Eval(`(format, quality) => this.toDataURL(format, quality)`, format, quality) if err != nil { return nil, err } _, bin := parseDataURI(res.Value.Str()) return bin, nil } // Resource returns the "src" content of current element. Such as the jpg of . func (el *Element) Resource() ([]byte, error) { src, err := el.Evaluate(evalHelper(js.Resource).ByPromise()) if err != nil { return nil, err } return el.page.Context(el.ctx).GetResource(src.Value.String()) } // BackgroundImage returns the css background-image of the element. func (el *Element) BackgroundImage() ([]byte, error) { res, err := el.Eval(`() => window.getComputedStyle(this).backgroundImage.replace(/^url\("/, '').replace(/"\)$/, '')`) if err != nil { return nil, err } u := res.Value.Str() return el.page.Context(el.ctx).GetResource(u) } // Screenshot of the area of the element. func (el *Element) Screenshot(format proto.PageCaptureScreenshotFormat, quality int) ([]byte, error) { err := el.ScrollIntoView() if err != nil { return nil, err } opts := &proto.PageCaptureScreenshot{ Quality: gson.Int(quality), Format: format, } bin, err := el.page.Context(el.ctx).Screenshot(false, opts) if err != nil { return nil, err } // so that it won't clip the css-transformed element shape, err := el.Shape() if err != nil { return nil, err } box := shape.Box() // TODO: proto.PageCaptureScreenshot has a Clip option, but it's buggy, so now we do in Go. return utils.CropImage(bin, quality, int(box.X), int(box.Y), int(box.Width), int(box.Height), ) } // Release is a shortcut for [Page.Release] current element. func (el *Element) Release() error { return el.page.Context(el.ctx).Release(el.Object) } // Remove the element from the page. func (el *Element) Remove() error { _, err := el.Eval(`() => this.remove()`) if err != nil { return err } return el.Release() } // Call implements the [proto.Client]. func (el *Element) Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) { return el.page.Call(ctx, sessionID, methodName, params) } // Eval is a shortcut for [Element.Evaluate] with AwaitPromise, ByValue and AutoExp set to true. func (el *Element) Eval(js string, params ...interface{}) (*proto.RuntimeRemoteObject, error) { return el.Evaluate(Eval(js, params...).ByPromise()) } // Evaluate is just a shortcut of [Page.Evaluate] with This set to current element. func (el *Element) Evaluate(opts *EvalOptions) (*proto.RuntimeRemoteObject, error) { return el.page.Context(el.ctx).Evaluate(opts.This(el.Object)) } // Equal checks if the two elements are equal. func (el *Element) Equal(elm *Element) (bool, error) { res, err := el.Eval(`elm => this === elm`, elm.Object) return res.Value.Bool(), err } func (el *Element) id() proto.RuntimeRemoteObjectID { return el.Object.ObjectID } // GetXPath returns the xpath of the element. func (el *Element) GetXPath(optimized bool) (string, error) { str, err := el.Evaluate(evalHelper(js.GetXPath, optimized)) if err != nil { return "", err } return str.Value.String(), nil } golang-github-go-rod-rod-0.116.2/element_test.go000066400000000000000000000567001520674146500214200ustar00rootroot00000000000000package rod_test import ( "bytes" "encoding/json" "errors" "fmt" "image/color" "image/png" "net" "os" "path/filepath" "testing" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/devices" "github.com/go-rod/rod/lib/input" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/gson" ) func TestGetElementPage(t *testing.T) { g := setup(t) el := g.page.MustNavigate(g.blank()).MustElement("html") g.Eq(el.Page().SessionID, g.page.SessionID) } func TestClick(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) el := p.MustElement("button") el.MustClick() g.True(p.MustHas("[a=ok]")) g.Panic(func() { g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) el.MustClick() }) g.Panic(func() { g.mc.stubErr(8, proto.RuntimeCallFunctionOn{}) el.MustClick() }) } func TestClickWrapped(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click-wrapped.html")).MustWaitLoad() el := p.MustElement("#target") shape := el.MustShape() g.Len(shape.Quads, 2) el.MustClick() g.True(p.MustHas("[a=ok]")) } func TestTap(t *testing.T) { g := setup(t) page := g.newPage() page.MustEmulate(devices.IPad). MustNavigate(g.srcFile("fixtures/touch.html")). MustWaitLoad() el := page.MustElement("button") el.MustTap() g.True(page.MustHas("[tapped=true]")) g.Panic(func() { g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) el.MustTap() }) g.Panic(func() { g.mc.stubErr(1, proto.DOMScrollIntoViewIfNeeded{}) el.MustTap() }) g.Panic(func() { g.mc.stubErr(4, proto.RuntimeCallFunctionOn{}) el.MustTap() }) g.Panic(func() { g.mc.stubErr(7, proto.RuntimeCallFunctionOn{}) el.MustTap() }) } func TestInteractable(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) el := p.MustElement("button") g.True(el.MustInteractable()) g.mc.stubErr(4, proto.RuntimeCallFunctionOn{}) g.Err(el.Interactable()) } func TestNotInteractable(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) el := p.MustElement("button") // cover the button with a green div p.MustWaitLoad().MustEval(`() => { let div = document.createElement('div') div.style = 'position: absolute; left: 0; top: 0; width: 500px; height: 500px;' document.body.append(div) }`) _, err := el.Interactable() g.Has(err.Error(), "element covered by:
") g.Is(err, &rod.NotInteractableError{}) g.Is(err, &rod.CoveredError{}) g.False(el.MustInteractable()) var ee *rod.NotInteractableError g.True(errors.As(err, &ee)) g.Eq(ee.Error(), "element is not cursor interactable") p.MustElement("div").MustRemove() g.mc.stubErr(1, proto.DOMGetContentQuads{}) _, err = el.Interactable() g.Err(err) g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) g.Err(el.Interactable()) g.mc.stubErr(1, proto.DOMDescribeNode{}) g.Err(el.Interactable()) g.mc.stubErr(2, proto.RuntimeCallFunctionOn{}) g.Err(el.Interactable()) } func TestInteractableWithNoShape(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/interactable.html")) el := p.MustElement("#no-shape") _, err := el.Interactable() g.Is(err, &rod.InvisibleShapeError{}) g.Is(err, &rod.NotInteractableError{}) g.Eq(err.Error(), "element has no visible shape or outside the viewport: ") el = p.MustElement("#outside") _, err = el.Interactable() g.Is(err, &rod.InvisibleShapeError{}) el = p.MustElement("#invisible") _, err = el.Interactable() g.Is(err, &rod.InvisibleShapeError{}) } func TestNotInteractableWithNoPointerEvents(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/interactable.html")) _, err := p.MustElementR("#no-pointer-events", "click me").Interactable() g.Is(err, &rod.NoPointerEventsError{}) g.Is(err, &rod.NotInteractableError{}) g.Eq(err.Error(), "element's pointer-events is none: ") } func TestWaitInteractable(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) el := p.MustElement("button") start := time.Now() // cover the button with a green div for 1sec p.MustWaitLoad().MustEval(`() => { let div = document.createElement('div') div.style = 'position: absolute; left: 0; top: 0; width: 500px; height: 500px;' document.body.append(div) setTimeout(() => div.remove(), 1000) }`) el.MustWaitInteractable() g.Gt(time.Since(start), time.Second) g.mc.stubErr(1, proto.DOMScrollIntoViewIfNeeded{}) g.Err(el.WaitInteractable()) } func TestHover(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) el := p.MustElement("button") el.MustEval(`() => this.onmouseenter = () => this.dataset['a'] = 1`) el.MustHover() g.Eq("1", el.MustEval(`() => this.dataset['a']`).String()) g.mc.stubErr(1, proto.DOMScrollIntoViewIfNeeded{}) g.Err(el.Hover()) g.mc.stubErr(1, proto.DOMGetContentQuads{}) g.Err(el.Hover()) g.mc.stubErr(3, proto.DOMGetContentQuads{}) g.Err(el.Hover()) g.mc.stubErr(1, proto.InputDispatchMouseEvent{}) g.Err(el.Hover()) } func TestElementMoveMouseOut(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) btn := p.MustElement("button") btn.MustEval(`() => this.onmouseout = () => this.setAttribute('name', 'mouse moved.')`) g.Eq("mouse moved.", *btn.MustHover().MustMoveMouseOut().MustAttribute("name")) g.mc.stubErr(1, proto.DOMGetContentQuads{}) g.Err(btn.MoveMouseOut()) } func TestElementContext(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) el := p.MustElement("button").Timeout(time.Hour).CancelTimeout() el, cancel := el.WithCancel() defer cancel() el.Sleeper(rod.DefaultSleeper).MustClick() } func TestElementCancelContext(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) el := p.Timeout(time.Second).MustElement("button") el = el.CancelTimeout() utils.Sleep(1.1) el.MustClick() } func TestIframes(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click-iframes.html")) frame01 := p.MustElement("iframe").MustFrame() frame02 := frame01.MustElement("iframe").MustFrame() el := frame02.MustElement("button") el.MustClick() g.Eq(frame01.MustEval(`() => testIsolation()`).Str(), "ok") g.True(frame02.MustHas("[a=ok]")) } func TestIframeCrossDomains(t *testing.T) { g := setup(t) r1 := g.Serve() r2 := g.Serve() // Same domain name with different ports won't trigger OOPIF (out-of-process iframes) // To check the page OOPIF status, you can use chrome://process-internals tab in the browser. host1 := net.JoinHostPort("localhost", r1.HostURL.Port()) host2 := net.JoinHostPort("127.0.0.1", r2.HostURL.Port()) u1 := fmt.Sprintf("http://%s/iframe", host1) u2 := fmt.Sprintf("http://%s/page", host2) r1.Route("/iframe", ".html", `
a
`) r2.Route("/page", ".html", ` `) u := launcher.New().HeadlessNew(true).NoSandbox(true).MustLaunch() browser := rod.New().ControlURL(u).NoDefaultDevice().MustConnect() defer browser.MustClose() page := browser.MustPage(u2) g.Eq(page.MustElement("iframe").MustFrame().MustElement("#a").MustText(), "a") } func TestContains(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) a := p.MustElement("button") b := p.MustElementFromNode(a.MustDescribe()) g.True(a.MustContainsElement(b)) pt := a.MustShape().OnePointInside() el := p.MustElementFromPoint(int(pt.X), int(pt.Y)) g.True(a.MustContainsElement(el)) g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) g.Err(a.ContainsElement(el)) } func TestShadowDOM(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/shadow-dom.html")).MustWaitLoad() el := p.MustElement("#container") g.Eq("inside", el.MustShadowRoot().MustElement("p").MustText()) g.Panic(func() { g.mc.stubErr(1, proto.DOMDescribeNode{}) el.MustShadowRoot() }) g.Panic(func() { g.mc.stubErr(1, proto.DOMResolveNode{}) el.MustShadowRoot() }) elNoShadow := p.MustElement("script") _, err := elNoShadow.ShadowRoot() g.True((&rod.NoShadowRootError{}).Is(err)) g.Has(err.Error(), "element has no shadow root:") } func TestInputTime(t *testing.T) { g := setup(t) now := time.Date(2006, 1, 2, 3, 4, 5, 0, time.Local) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) var el *rod.Element { el = p.MustElement("[type=date]") el.MustInputTime(now) g.Eq(el.MustText(), now.Format("2006-01-02")) g.True(p.MustHas("[event=input-date-change]")) } { el = p.MustElement("[type=datetime-local]") el.MustInputTime(now) g.Eq(el.MustText(), now.Format("2006-01-02T15:04")) g.True(p.MustHas("[event=input-datetime-local-change]")) } { el = p.MustElement("[type=time]") el.MustInputTime(now) g.Eq(el.MustText(), fmt.Sprintf("%02d:%02d", now.Hour(), now.Minute())) g.True(p.MustHas("[event=input-time-change]")) } { el = p.MustElement("[type=month]") el.MustInputTime(now) g.Eq(el.MustText(), now.Format("2006-01")) g.True(p.MustHas("[event=input-month-change]")) } g.Panic(func() { g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) el.MustInputTime(now) }) g.Panic(func() { g.mc.stubErr(5, proto.RuntimeCallFunctionOn{}) el.MustInputTime(now) }) g.Panic(func() { g.mc.stubErr(6, proto.RuntimeCallFunctionOn{}) el.MustInputTime(now) }) g.Panic(func() { g.mc.stubErr(7, proto.RuntimeCallFunctionOn{}) el.MustInputTime(now) }) } func TestInputColor(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) var el *rod.Element { el = p.MustElement("[type=color]") el.MustInputColor("#ff6f00") g.Eq(el.MustText(), "#ff6f00") g.True(p.MustHas("[event=input-color-change]")) } g.Panic(func() { g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) el.MustInputColor("#ff6f00") }) g.Panic(func() { g.mc.stubErr(5, proto.RuntimeCallFunctionOn{}) el.MustInputColor("#ff6f00") }) g.Panic(func() { g.mc.stubErr(6, proto.RuntimeCallFunctionOn{}) el.MustInputColor("#ff6f00") }) } func TestElementInputDate(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) p.MustElement("[type=date]").MustInput("12") } func TestCheckbox(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement("[type=checkbox]") g.True(el.MustClick().MustProperty("checked").Bool()) } func TestSelectText(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement("textarea") el.MustInput("test") el.MustSelectAllText() el.MustInput("test") g.Eq("test", el.MustText()) el.MustSelectText(`es`) el.MustInput("__") g.Eq("t__t", el.MustText()) g.Panic(func() { g.mc.stubErr(1, proto.DOMScrollIntoViewIfNeeded{}) el.MustSelectText("") }) g.Panic(func() { g.mc.stubErr(1, proto.DOMScrollIntoViewIfNeeded{}) el.MustSelectAllText() }) g.Panic(func() { g.mc.stubErr(1, proto.DOMScrollIntoViewIfNeeded{}) el.MustInput("") }) g.Panic(func() { g.mc.stubErr(1, proto.InputInsertText{}) el.MustInput("") }) } func TestBlur(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement("#blur").MustInput("test").MustBlur() g.Eq("ok", *el.MustAttribute("a")) } func TestSelectQuery(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement("select") err := el.Select([]string{`[value="c"]`}, true, rod.SelectorTypeCSSSector) g.E(err) g.Eq(2, el.MustEval("() => this.selectedIndex").Int()) } func TestSelectOptions(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement("select") el.MustSelect("B", "C") g.Eq("B,C", el.MustText()) g.Eq(1, el.MustProperty("selectedIndex").Int()) // unselect with regex err := el.Select([]string{`^B$`}, false, rod.SelectorTypeRegex) g.E(err) g.Eq("C", el.MustText()) // unselect with css selector err = el.Select([]string{`[value="c"]`}, false, rod.SelectorTypeCSSSector) g.E(err) g.Eq("", el.MustText()) // option not found error g.Is(el.Select([]string{"not-exists"}, true, rod.SelectorTypeCSSSector), &rod.ElementNotFoundError{}) { g.mc.stubErr(5, proto.RuntimeCallFunctionOn{}) g.Err(el.Select([]string{"B"}, true, rod.SelectorTypeText)) } } func TestMatches(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement("textarea") g.True(el.MustMatches(`[cols="30"]`)) g.Panic(func() { g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) el.MustMatches("") }) } func TestAttribute(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement("textarea") cols := el.MustAttribute("cols") rows := el.MustAttribute("rows") g.Eq("30", *cols) g.Eq("10", *rows) p = g.page.MustNavigate(g.srcFile("fixtures/click.html")) el = p.MustElement("button").MustClick() g.Eq("ok", *el.MustAttribute("a")) g.Nil(el.MustAttribute("b")) g.Panic(func() { g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) el.MustAttribute("") }) } func TestProperty(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement("textarea") cols := el.MustProperty("cols") rows := el.MustProperty("rows") g.Eq(float64(30), cols.Num()) g.Eq(float64(10), rows.Num()) p = g.page.MustNavigate(g.srcFile("fixtures/open-page.html")) el = p.MustElement("a") g.Eq("link", el.MustProperty("id").Str()) g.Eq("_blank", el.MustProperty("target").Str()) g.True(el.MustProperty("test").Nil()) g.Panic(func() { g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) el.MustProperty("") }) } func TestDisabled(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) g.False(p.MustElement("#EnabledButton").MustDisabled()) g.True(p.MustElement("#DisabledButton").MustDisabled()) g.Panic(func() { el := p.MustElement("#EnabledButton") g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) el.MustDisabled() }) } func TestSetFiles(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement(`[type=file]`) el.MustSetFiles( slash("fixtures/click.html"), slash("fixtures/alert.html"), ) list := el.MustEval("() => Array.from(this.files).map(f => f.name)").Arr() g.Len(list, 2) g.Eq("alert.html", list[1].String()) } func TestEnter(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement("[type=submit]") el.MustType(input.Enter) g.True(p.MustHas("[event=submit]")) } func TestWaitInvisible(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) h4 := p.MustElement("h4") btn := p.MustElement("button") g.True(h4.MustVisible()) h4.MustWaitVisible() go func() { utils.Sleep(0.03) h4.MustEval(`() => this.remove()`) utils.Sleep(0.03) btn.MustEval(`() => this.style.visibility = 'hidden'`) }() h4.MustWaitInvisible() btn.MustWaitInvisible() g.False(p.MustHas("h4")) } func TestWaitEnabled(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) p.MustElement("button").MustWaitEnabled() } func TestWaitWritable(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) p.MustElement("input").MustWaitWritable() } func TestWaitStable(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/wait-stable.html")) el := p.MustElement("button") go func() { utils.Sleep(1) el.MustEval(`() => this.classList.remove("play")`) }() start := time.Now() el.MustWaitStable() g.Gt(time.Since(start), time.Second) ctx := g.Context() g.mc.stub(1, proto.DOMGetContentQuads{}, func(send StubSend) (gson.JSON, error) { go func() { utils.Sleep(0.1) ctx.Cancel() }() return send() }) g.Err(el.Context(ctx).WaitStable(time.Minute)) g.Panic(func() { g.mc.stubErr(1, proto.DOMGetContentQuads{}) el.MustWaitStable() }) g.Panic(func() { g.mc.stubErr(2, proto.DOMGetContentQuads{}) el.MustWaitStable() }) } func TestWaitStableRAP(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/wait-stable.html")) el := p.MustElement("button") go func() { utils.Sleep(1) el.MustEval(`() => this.classList.remove("play")`) }() start := time.Now() g.E(el.WaitStableRAF()) g.Gt(time.Since(start), time.Second) g.mc.stubErr(2, proto.RuntimeCallFunctionOn{}) g.Err(el.WaitStableRAF()) g.mc.stubErr(1, proto.DOMGetContentQuads{}) g.Err(el.WaitStableRAF()) } func TestCanvasToImage(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/canvas.html")) src, err := png.Decode(bytes.NewBuffer(p.MustElement("#canvas").MustCanvasToImage())) g.E(err) g.Eq(src.At(50, 50), color.NRGBA{0xFF, 0x00, 0x00, 0xFF}) } func TestElementWaitLoad(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/resource.html")) p.MustElement("img").MustWaitLoad() } func TestResource(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/resource.html")) el := p.MustElement("img") g.Eq(len(el.MustResource()), 22661) g.mc.stub(1, proto.PageGetResourceContent{}, func(_ StubSend) (gson.JSON, error) { return gson.New(proto.PageGetResourceContentResult{ Content: "ok", Base64Encoded: false, }), nil }) g.Eq([]byte("ok"), el.MustResource()) g.Panic(func() { g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) el.MustResource() }) g.Panic(func() { g.mc.stubErr(1, proto.PageGetResourceContent{}) el.MustResource() }) } func TestBackgroundImage(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/resource.html")).MustWaitStable() el := p.MustElement("div") g.Eq(len(el.MustBackgroundImage()), 22661) { g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) g.Err(el.BackgroundImage()) } } func TestElementScreenshot(t *testing.T) { g := setup(t) f := filepath.Join("tmp", "screenshots", g.RandStr(16)+".png") p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) el := p.MustElement("h4") data := el.MustScreenshot(f) img, err := png.Decode(bytes.NewBuffer(data)) g.E(err) g.Eq(200, img.Bounds().Dx()) g.Eq(30, img.Bounds().Dy()) g.Nil(os.Stat(f)) g.Panic(func() { g.mc.stubErr(1, proto.DOMScrollIntoViewIfNeeded{}) el.MustScreenshot() }) g.Panic(func() { g.mc.stubErr(1, proto.PageCaptureScreenshot{}) el.MustScreenshot() }) g.Panic(func() { g.mc.stubErr(3, proto.DOMGetContentQuads{}) el.MustScreenshot() }) } func TestUseReleasedElement(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) btn := p.MustElement("button") btn.MustRelease() g.Err(btn.Click("left", 1)) btn = p.MustElement("button") g.E(proto.RuntimeReleaseObject{ObjectID: btn.Object.ObjectID}.Call(p)) g.Is(btn.Click("left", 1), cdp.ErrObjNotFound) } func TestElementRemove(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) btn := p.MustElement("button") g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) g.Err(btn.Remove()) } func TestElementMultipleTimes(t *testing.T) { g := setup(t) // To see whether chrome will reuse the remote object ID or not. // Seems like it will not. page := g.page.MustNavigate(g.srcFile("fixtures/click.html")) btn01 := page.MustElement("button") btn02 := page.MustElement("button") g.Eq(btn01.MustText(), btn02.MustText()) g.Neq(btn01.Object, btn02.Object) } func TestFnErr(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) el := p.MustElement("button") _, err := el.Eval("foo()") g.Err(err) g.Has(err.Error(), "ReferenceError: foo is not defined") var e *rod.EvalError g.True(errors.As(err, &e)) g.Eq(proto.RuntimeRemoteObjectSubtypeError, e.Exception.Subtype) _, err = el.ElementByJS(rod.Eval("() => foo()")) g.Err(err) g.Has(err.Error(), "ReferenceError: foo is not defined") g.True(errors.Is(err, &rod.EvalError{})) } func TestElementEWithDepth(t *testing.T) { g := setup(t) checkStr := `green tea` p := g.page.MustNavigate(g.srcFile("fixtures/describe.html")) ulDOMNode, err := p.MustElement(`ul`).Describe(-1, true) g.Nil(errors.Unwrap(err)) data, err := json.Marshal(ulDOMNode) g.Nil(errors.Unwrap(err)) // The depth is -1, should contain checkStr g.Has(string(data), checkStr) } func TestElementOthers(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement("form") el.MustFocus() el.MustScrollIntoView() g.Eq("submit", el.MustElement("[type=submit]").MustText()) g.Eq("", el.MustElement("[type=submit]").MustHTML()) el.MustWait(`() => true`) g.Eq("form", el.MustElementByJS(`() => this`).MustDescribe().LocalName) g.Len(el.MustElementsByJS(`() => []`), 0) } func TestElementEqual(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/describe.html")) el1 := p.MustElement("body > ul") el2 := p.MustElement("html > body > ul") g.True(el1.MustEqual(el2)) el3 := p.MustElement("ul ul") g.False(el1.MustEqual(el3)) } func TestElementWait(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/describe.html")) e1 := p.MustElement("body > ul > li") g.Eq(e1.MustText(), "coffee") params := []interface{}{1, 3, 4} go func() { utils.Sleep(0.3) e1.MustEval(`(a, b, c) => this.innerText = 'x'.repeat(a + b + c)`, params...) }() e1.MustWait(`(a, b, c) => this.innerText.length === (a + b + c)`, params...) g.Eq(e1.MustText(), "xxxxxxxx") } func TestShapeInIframe(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click-iframe.html")) pt := p.MustElement("iframe").MustFrame().MustElement("button").MustShape().OnePointInside() g.InDelta(pt.X, 238, 1) g.InDelta(pt.Y, 287, 1) } func TestElementFromPointErr(t *testing.T) { g := setup(t) g.mc.stubErr(1, proto.DOMGetNodeForLocation{}) g.Err(g.page.ElementFromPoint(10, 10)) } func TestElementFromNodeErr(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) el := p.MustElementX("//button/text()") g.mc.stubErr(3, proto.RuntimeCallFunctionOn{}) g.Err(p.ElementFromNode(el.MustDescribe())) } func TestElementErrors(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement("form") ctx := g.Timeout(0) _, err := el.Context(ctx).Describe(-1, true) g.Err(err) _, err = el.Context(ctx).Frame() g.Err(err) err = el.Context(ctx).Focus() g.Err(err) _, err = el.Context(ctx).KeyActions() g.Err(err) err = el.Context(ctx).Input("a") g.Err(err) err = el.Context(ctx).Select([]string{"a"}, true, rod.SelectorTypeText) g.Err(err) err = el.Context(ctx).WaitStable(0) g.Err(err) _, err = el.Context(ctx).Resource() g.Err(err) err = el.Context(ctx).Input("a") g.Err(err) err = el.Context(ctx).Input("a") g.Err(err) _, err = el.Context(ctx).HTML() g.Err(err) _, err = el.Context(ctx).Visible() g.Err(err) _, err = el.Context(ctx).CanvasToImage("", 0) g.Err(err) err = el.Context(ctx).Release() g.Err(err) } func TestElementGetXPath(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement("textarea") xpath := el.MustGetXPath(true) g.Eq(xpath, "/html/body/form/textarea") xpath = el.MustGetXPath(false) g.Eq(xpath, "/html/body/form/textarea") g.Panic(func() { g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) el.MustGetXPath(true) }) } golang-github-go-rod-rod-0.116.2/error.go000066400000000000000000000107121520674146500200520ustar00rootroot00000000000000package rod import ( "context" "fmt" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" ) // TryError error. type TryError struct { Value interface{} Stack string } func (e *TryError) Error() string { return fmt.Sprintf("error value: %#v\n%s", e.Value, e.Stack) } // Is interface. func (e *TryError) Is(err error) bool { _, ok := err.(*TryError); return ok } // Unwrap stdlib interface. func (e *TryError) Unwrap() error { if err, ok := e.Value.(error); ok { return err } return fmt.Errorf("%v", e.Value) } // ExpectElementError error. type ExpectElementError struct { *proto.RuntimeRemoteObject } func (e *ExpectElementError) Error() string { return fmt.Sprintf("expect js to return an element, but got: %s", utils.MustToJSON(e)) } // Is interface. func (e *ExpectElementError) Is(err error) bool { _, ok := err.(*ExpectElementError); return ok } // ExpectElementsError error. type ExpectElementsError struct { *proto.RuntimeRemoteObject } func (e *ExpectElementsError) Error() string { return fmt.Sprintf("expect js to return an array of elements, but got: %s", utils.MustToJSON(e)) } // Is interface. func (e *ExpectElementsError) Is(err error) bool { _, ok := err.(*ExpectElementsError); return ok } // ElementNotFoundError error. type ElementNotFoundError struct{} func (e *ElementNotFoundError) Error() string { return "cannot find element" } // NotFoundSleeper returns ErrElementNotFound on the first call. func NotFoundSleeper() utils.Sleeper { return func(context.Context) error { return &ElementNotFoundError{} } } // ObjectNotFoundError error. type ObjectNotFoundError struct { *proto.RuntimeRemoteObject } func (e *ObjectNotFoundError) Error() string { return fmt.Sprintf("cannot find object: %s", utils.MustToJSON(e)) } // Is interface. func (e *ObjectNotFoundError) Is(err error) bool { _, ok := err.(*ObjectNotFoundError); return ok } // EvalError error. type EvalError struct { *proto.RuntimeExceptionDetails } func (e *EvalError) Error() string { exp := e.Exception return fmt.Sprintf("eval js error: %s %s", exp.Description, exp.Value) } // Is interface. func (e *EvalError) Is(err error) bool { _, ok := err.(*EvalError); return ok } // NavigationError error. type NavigationError struct { Reason string } func (e *NavigationError) Error() string { return "navigation failed: " + e.Reason } // Is interface. func (e *NavigationError) Is(err error) bool { _, ok := err.(*NavigationError); return ok } // PageCloseCanceledError error. type PageCloseCanceledError struct{} func (e *PageCloseCanceledError) Error() string { return "page close canceled" } // NotInteractableError error. Check the doc of Element.Interactable for details. type NotInteractableError struct{} func (e *NotInteractableError) Error() string { return "element is not cursor interactable" } // InvisibleShapeError error. type InvisibleShapeError struct { *Element } // Error ... func (e *InvisibleShapeError) Error() string { return fmt.Sprintf("element has no visible shape or outside the viewport: %s", e.String()) } // Is interface. func (e *InvisibleShapeError) Is(err error) bool { _, ok := err.(*InvisibleShapeError); return ok } // Unwrap ... func (e *InvisibleShapeError) Unwrap() error { return &NotInteractableError{} } // CoveredError error. type CoveredError struct { *Element } // Error ... func (e *CoveredError) Error() string { return fmt.Sprintf("element covered by: %s", e.String()) } // Unwrap ... func (e *CoveredError) Unwrap() error { return &NotInteractableError{} } // Is interface. func (e *CoveredError) Is(err error) bool { _, ok := err.(*CoveredError); return ok } // NoPointerEventsError error. type NoPointerEventsError struct { *Element } // Error ... func (e *NoPointerEventsError) Error() string { return fmt.Sprintf("element's pointer-events is none: %s", e.String()) } // Unwrap ... func (e *NoPointerEventsError) Unwrap() error { return &NotInteractableError{} } // Is interface. func (e *NoPointerEventsError) Is(err error) bool { _, ok := err.(*NoPointerEventsError); return ok } // PageNotFoundError error. type PageNotFoundError struct{} func (e *PageNotFoundError) Error() string { return "cannot find page" } // NoShadowRootError error. type NoShadowRootError struct { *Element } // Error ... func (e *NoShadowRootError) Error() string { return fmt.Sprintf("element has no shadow root: %s", e.String()) } // Is interface. func (e *NoShadowRootError) Is(err error) bool { _, ok := err.(*NoShadowRootError); return ok } golang-github-go-rod-rod-0.116.2/examples_test.go000066400000000000000000000473671520674146500216160ustar00rootroot00000000000000package rod_test import ( "context" "errors" "fmt" "math/rand" "net/http" "path/filepath" "sync" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/input" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/gson" ) // This example opens https://github.com/, searches for "git", // and then gets the header element which gives the description for Git. func Example_basic() { // Launch a new browser with default options, and connect to it. browser := rod.New().MustConnect() // Even you forget to close, rod will close it after main process ends. defer browser.MustClose() // Create a new page page := browser.MustPage("https://github.com").MustWaitStable() // Trigger the search input with hotkey "/" page.Keyboard.MustType(input.Slash) // We use css selector to get the search input element and input "git" page.MustElement("#query-builder-test").MustInput("git").MustType(input.Enter) // Wait until css selector get the element then get the text content of it. text := page.MustElementR("span", "most widely used").MustText() fmt.Println(text) // Get all input elements. Rod supports query elements by css selector, xpath, and regex. // For more detailed usage, check the query_test.go file. fmt.Println("Found", len(page.MustElements("input")), "input elements") // Eval js on the page page.MustEval(`() => console.log("hello world")`) // Pass parameters as json objects to the js function. This MustEval will result 3 fmt.Println("1 + 2 =", page.MustEval(`(a, b) => a + b`, 1, 2).Int()) // When eval on an element, "this" in the js is the current DOM element. fmt.Println(page.MustElement("title").MustEval(`() => this.innerText`).String()) // Output: // Git is the most widely used version control system. // Found 10 input elements // 1 + 2 = 3 // Repository search results · GitHub } // Shows how to disable headless mode and debug. // Rod provides a lot of debug options, you can set them with setter methods or use environment variables. // Doc for environment variables: https://pkg.go.dev/github.com/go-rod/rod/lib/defaults func Example_disable_headless_to_debug() { // Headless runs the browser on foreground, you can also use flag "-rod=show" // Devtools opens the tab in each new tab opened automatically l := launcher.New(). Headless(false). Devtools(true) defer l.Cleanup() url := l.MustLaunch() // Trace shows verbose debug information for each action executed // SlowMotion is a debug related function that waits 2 seconds between // each action, making it easier to inspect what your code is doing. browser := rod.New(). ControlURL(url). Trace(true). SlowMotion(2 * time.Second). MustConnect() // ServeMonitor plays screenshots of each tab. This feature is extremely // useful when debugging with headless mode. // You can also enable it with flag "-rod=monitor" launcher.Open(browser.ServeMonitor("")) defer browser.MustClose() page := browser.MustPage("https://github.com/") page.MustElement("input").MustInput("git").MustType(input.Enter) text := page.MustElement(".codesearch-results p").MustText() fmt.Println(text) utils.Pause() // pause goroutine } // Rod use https://golang.org/pkg/context to handle cancellations for IO blocking operations, most times it's timeout. // Context will be recursively passed to all sub-methods. // For example, methods like Page.Context(ctx) will return a clone of the page with the ctx, // all the methods of the returned page will use the ctx if they have IO blocking operations. // [Page.Timeout] or [Page.WithCancel] is just a shortcut for Page.Context. // Of course, Browser or Element works the same way. func Example_context_and_timeout() { page := rod.New().MustConnect().MustPage("https://github.com") page. // Set a 5-second timeout for all chained methods Timeout(5 * time.Second). // The total time for MustWaitLoad and MustElement must be less than 5 seconds MustWaitLoad(). MustElement("title"). // Methods after CancelTimeout won't be affected by the 5-second timeout CancelTimeout(). // Set a 10-second timeout for all chained methods Timeout(10 * time.Second). // Panics if it takes more than 10 seconds MustText() // The two code blocks below are basically the same: { page.Timeout(5 * time.Second).MustElement("a").CancelTimeout() } { // Use this way you can customize your own way to cancel long-running task page, cancel := page.WithCancel() go func() { time.Sleep(time.Duration(rand.Int())) // cancel after randomly time cancel() }() page.MustElement("a") } } func Example_context_and_EachEvent() { browser := rod.New().MustConnect() defer browser.MustClose() page := browser.MustPage("https://github.com").MustWaitLoad() page, cancel := page.WithCancel() go func() { time.Sleep(time.Second) cancel() }() // It's a blocking method, it will wait until the context is cancelled page.EachEvent(func(_ *proto.PageLifecycleEvent) {})() if page.GetContext().Err() == context.Canceled { fmt.Println("cancelled") } } // We use "Must" prefixed functions to write example code. But in production you may want to use // the no-prefix version of them. // About why we use "Must" as the prefix, it's similar to https://golang.org/pkg/regexp/#MustCompile func Example_error_handling() { page := rod.New().MustConnect().MustPage("https://mdn.dev") // We use Go's standard way to check error types, no magic. check := func(err error) { var evalErr *rod.EvalError if errors.Is(err, context.DeadlineExceeded) { // timeout error fmt.Println("timeout err") } else if errors.As(err, &evalErr) { // eval error fmt.Println(evalErr.LineNumber) } else if err != nil { fmt.Println("can't handle", err) } } // The two code blocks below are doing the same thing in two styles: // The block below is better for debugging or quick scripting. We use panic to short-circuit logics. // So that we can take advantage of fluent interface (https://en.wikipedia.org/wiki/Fluent_interface) // and fail-fast (https://en.wikipedia.org/wiki/Fail-fast). // This style will reduce code, but it may also catch extra errors (less consistent and precise). { err := rod.Try(func() { fmt.Println(page.MustElement("a").MustHTML()) // use "Must" prefixed functions }) check(err) } // The block below is better for production code. It's the standard way to handle errors. // Usually, this style is more consistent and precise. { el, err := page.Element("a") if err != nil { check(err) return } html, err := el.HTML() if err != nil { check(err) return } fmt.Println(html) } } // Example_search shows how to use Search to get element inside nested iframes or shadow DOMs. // It works the same as https://developers.google.com/web/tools/chrome-devtools/dom#search func Example_search() { browser := rod.New().MustConnect() defer browser.MustClose() page := browser.MustPage("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe") // Click the zoom-in button of the OpenStreetMap page.MustSearch(".leaflet-control-zoom-in").MustClick() fmt.Println("done") // Output: done } func Example_page_screenshot() { page := rod.New().MustConnect().MustPage("https://github.com").MustWaitLoad() // simple version page.MustScreenshot("my.png") // customization version img, _ := page.Screenshot(true, &proto.PageCaptureScreenshot{ Format: proto.PageCaptureScreenshotFormatJpeg, Quality: gson.Int(90), Clip: &proto.PageViewport{ X: 0, Y: 0, Width: 300, Height: 200, Scale: 1, }, FromSurface: true, }) _ = utils.OutputFile("my.jpg", img) } func Example_page_scroll_screenshot() { browser := rod.New().MustConnect() // capture entire browser viewport, returning jpg with quality=90 img, err := browser.MustPage("https://desktop.github.com/").MustWaitStable().ScrollScreenshot(&rod.ScrollScreenshotOptions{ Format: proto.PageCaptureScreenshotFormatJpeg, Quality: gson.Int(90), }) if err != nil { panic(err) } _ = utils.OutputFile("my.jpg", img) } func Example_page_pdf() { page := rod.New().MustConnect().MustPage("https://github.com").MustWaitLoad() // simple version page.MustPDF("my.pdf") // customized version pdf, _ := page.PDF(&proto.PagePrintToPDF{ PaperWidth: gson.Num(8.5), PaperHeight: gson.Num(11), PageRanges: "1-3", }) _ = utils.OutputFile("my.pdf", pdf) } // Show how to handle multiple results of an action. // Such as when you login a page, the result can be success or wrong password. func Example_race_selectors() { const username = "" const password = "" browser := rod.New().MustConnect() page := browser.MustPage("https://leetcode.com/accounts/login/") page.MustElement("#id_login").MustInput(username) page.MustElement("#id_password").MustInput(password).MustType(input.Enter) // It will keep retrying until one selector has found a match elm := page.Race().Element(".nav-user-icon-base").MustHandle(func(e *rod.Element) { // print the username after successful login fmt.Println(*e.MustAttribute("title")) }).Element("[data-cy=sign-in-error]").MustDo() if elm.MustMatches("[data-cy=sign-in-error]") { // when wrong username or password panic(elm.MustText()) } } // Rod uses mouse cursor to simulate clicks, so if a button is moving because of animation, the click may not work as expected. // We usually use WaitStable to make sure the target isn't changing anymore. func Example_wait_for_animation() { browser := rod.New().MustConnect() defer browser.MustClose() page := browser.MustPage("https://getbootstrap.com/docs/4.0/components/modal/") page.MustWaitLoad().MustElement("[data-target='#exampleModalLive']").MustClick() saveBtn := page.MustElementR("#exampleModalLive button", "Close") // Here, WaitStable will wait until the button's position and size become stable. saveBtn.MustWaitStable().MustClick().MustWaitInvisible() fmt.Println("done") // Output: done } // When you want to wait for an ajax request to complete, this example will be useful. func Example_wait_for_request() { browser := rod.New().MustConnect() defer browser.MustClose() page := browser.MustPage("https://www.wikipedia.org/").MustWaitLoad() // Start to analyze request events wait := page.MustWaitRequestIdle() // This will trigger the search ajax request page.MustElement("#searchInput").MustClick().MustInput("lisp") // Wait until there's no active requests wait() // We want to make sure that after waiting, there are some autocomplete // suggestions available. fmt.Println(len(page.MustElements(".suggestion-link")) > 0) // Output: true } // Shows how to change the retry/polling options that is used to query elements. // This is useful when you want to customize the element query retry logic. func Example_customize_retry_strategy() { browser := rod.New().MustConnect() defer browser.MustClose() page := browser.MustPage("https://github.com") // sleep for 0.5 seconds before every retry sleeper := func() utils.Sleeper { return func(context.Context) error { time.Sleep(time.Second / 2) return nil } } el, _ := page.Sleeper(sleeper).Element("input") fmt.Println(el.MustProperty("name")) // If sleeper is nil page.ElementE will query without retrying. // If nothing found it will return an error. el, err := page.Sleeper(rod.NotFoundSleeper).Element("input") if errors.Is(err, &rod.ElementNotFoundError{}) { fmt.Println("element not found") } else if err != nil { panic(err) } fmt.Println(el.MustProperty("name")) // Output: // type // type } // Shows how we can further customize the browser with the launcher library. // Usually you use launcher lib to set the browser's command line flags (switches). // Doc for flags: https://peter.sh/experiments/chromium-command-line-switches func Example_customize_browser_launch() { url := launcher.New(). Proxy("127.0.0.1:8080"). // set flag "--proxy-server=127.0.0.1:8080" Delete("use-mock-keychain"). // delete flag "--use-mock-keychain" MustLaunch() browser := rod.New().ControlURL(url).MustConnect() defer browser.MustClose() // So that we don't have to self issue certs for MITM browser.MustIgnoreCertErrors(true) // Adding authentication to the proxy, for the next auth request. // We use CLI tool "mitmproxy --proxyauth user:pass" as an example. go browser.MustHandleAuth("user", "pass")() // mitmproxy needs a cert config to support https. We use http here instead, // for example fmt.Println(browser.MustPage("https://mdn.dev/").MustElement("title").MustText()) } // When rod doesn't have a feature that you need. You can easily call the cdp to achieve it. // List of cdp API: https://github.com/go-rod/rod/tree/main/lib/proto func Example_direct_cdp() { page := rod.New().MustConnect().MustPage() // Rod doesn't have a method to enable AD blocking, // but you can call cdp interface directly to achieve it. // The two code blocks below are equal to enable AD blocking { _ = proto.PageSetAdBlockingEnabled{ Enabled: true, }.Call(page) } { // Interact with the cdp JSON API directly _, _ = page.Call(context.TODO(), "", "Page.setAdBlockingEnabled", map[string]bool{ "enabled": true, }) } } // Shows how to listen for events. func Example_handle_events() { browser := rod.New().MustConnect() defer browser.MustClose() page := browser.MustPage() done := make(chan struct{}) // Listen for all events of console output. go page.EachEvent(func(e *proto.RuntimeConsoleAPICalled) { if e.Type == proto.RuntimeConsoleAPICalledTypeLog { fmt.Println(page.MustObjectsToJSON(e.Args)) close(done) } })() wait := page.WaitEvent(&proto.PageLoadEventFired{}) page.MustNavigate("https://mdn.dev") wait() // EachEvent allows us to achieve the same functionality as above. if false { // Subscribe events before they happen, run the "wait()" to start consuming // the events. We can return an optional stop signal to unsubscribe events. wait := page.EachEvent(func(_ *proto.PageLoadEventFired) (stop bool) { return true }) page.MustNavigate("https://mdn.dev") wait() } // Or the for-loop style to handle events to do the same thing above. if false { page.MustNavigate("https://mdn.dev") for msg := range page.Event() { e := proto.PageLoadEventFired{} if msg.Load(&e) { break } } } page.MustEval(`() => console.log("hello", "world")`) <-done // Output: // [hello world] } func Example_download_file() { browser := rod.New().MustConnect() page := browser.MustPage("https://file-examples.com/index.php/sample-documents-download/sample-pdf-download/") wait := browser.MustWaitDownload() page.MustElementR("a", "DOWNLOAD SAMPLE PDF FILE").MustClick() _ = utils.OutputFile("t.pdf", wait()) } // Shows how to intercept requests and modify // both the request and the response. // The entire process of hijacking one request: // // browser --req-> rod ---> server ---> rod --res-> browser // // The --req-> and --res-> are the parts that can be modified. func Example_hijack_requests() { browser := rod.New().MustConnect() defer browser.MustClose() router := browser.HijackRequests() defer router.MustStop() router.MustAdd("*.js", func(ctx *rod.Hijack) { // Here we update the request's header. Rod gives functionality to // change or update all parts of the request. Refer to the documentation // for more information. ctx.Request.Req().Header.Set("My-Header", "test") // LoadResponse runs the default request to the destination of the request. // Not calling this will require you to mock the entire response. // This can be done with the SetXxx (Status, Header, Body) functions on the // ctx.Response struct. _ = ctx.LoadResponse(http.DefaultClient, true) // Here we append some code to every js file. // The code will update the document title to "hi" ctx.Response.SetBody(ctx.Response.Body() + "\n document.title = 'hi' ") }) go router.Run() browser.MustPage("https://go-rod.github.io").MustWait(`() => document.title === 'hi'`) fmt.Println("done") // Output: done } // Shows how to share a remote object reference between two Eval. func Example_eval_reuse_remote_object() { page := rod.New().MustConnect().MustPage() fn := page.MustEvaluate(rod.Eval(`() => Math.random`).ByObject()) res := page.MustEval(`f => f()`, fn) // print a random number fmt.Println(res.Num()) } // Shows how to update the state of the current page. // In this example we enable the network domain. func Example_states() { browser := rod.New().MustConnect() defer browser.MustClose() page := browser.MustPage() // LoadState detects whether the network domain is enabled or not. fmt.Println(page.LoadState(&proto.NetworkEnable{})) _ = proto.NetworkEnable{}.Call(page) // Check if the network domain is successfully enabled. fmt.Println(page.LoadState(&proto.NetworkEnable{})) // Output: // false // true } // We can use [rod.PagePool] to concurrently control and reuse pages. func ExamplePage_pool() { browser := rod.New().MustConnect() defer browser.MustClose() // We create a pool that will hold at most 3 pages which means the max concurrency is 3 pool := rod.NewPagePool(3) // Create a page if needed create := func() *rod.Page { // We use MustIncognito to isolate pages with each other return browser.MustIncognito().MustPage() } yourJob := func() { page := pool.MustGet(create) // Put the instance back to the pool after we're done, // so the instance can be reused by other goroutines. defer pool.Put(page) page.MustNavigate("http://mdn.dev").MustWaitLoad() fmt.Println(page.MustInfo().Title) } // Run jobs concurrently wg := sync.WaitGroup{} for range "...." { wg.Add(1) go func() { defer wg.Done() yourJob() }() } wg.Wait() // cleanup pool pool.Cleanup(func(p *rod.Page) { p.MustClose() }) // Output: // MDN Web Docs // MDN Web Docs // MDN Web Docs // MDN Web Docs } // We can use [rod.BrowserPool] to concurrently control and reuse browsers. func ExampleBrowser_pool() { // Create a new browser pool with a limit of 3 pool := rod.NewBrowserPool(3) // Create a function that returns a new browser instance create := func() *rod.Browser { browser := rod.New().MustConnect() return browser } // Use the browser instances in separate goroutines var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func() { defer wg.Done() // Get a browser instance from the pool browser := pool.MustGet(create) // Put the instance back to the pool after we're done, // so the instance can be reused by other goroutines. defer pool.Put(browser) // Use the browser instance page := browser.MustPage("https://www.google.com") fmt.Println(page.MustInfo().Title) }() } // Wait for all the goroutines to finish wg.Wait() // Cleanup the pool by closing all the browser instances pool.Cleanup(func(p *rod.Browser) { p.MustClose() }) } func Example_load_extension() { extPath, _ := filepath.Abs("fixtures/chrome-extension") u := launcher.New(). // Must use abs path for an extension Set("load-extension", extPath). // Headless mode doesn't support extension yet. // Reason: https://bugs.chromium.org/p/chromium/issues/detail?id=706008#c5 // You can use XVFB to get rid of it: https://github.com/go-rod/rod/blob/main/lib/examples/launch-managed/main.go Headless(false). MustLaunch() page := rod.New().ControlURL(u).MustConnect().MustPage("http://mdn.dev") page.MustWait(`() => document.title === 'test-extension'`) fmt.Println("ok") // Skip // Output: ok } func Example_log_cdp_traffic() { cdp := cdp.New(). // Here we can customize how to log the requests, responses, and events transferred between Rod and the browser. Logger(utils.Log(func(args ...interface{}) { switch v := args[0].(type) { case *cdp.Request: fmt.Printf("id: %d", v.ID) } })). Start(cdp.MustConnectWS(launcher.New().MustLaunch())) rod.New().Client(cdp).MustConnect().MustPage("http://mdn.dev") } golang-github-go-rod-rod-0.116.2/fixtures/000077500000000000000000000000001520674146500202425ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/fixtures/README.md000066400000000000000000000001641520674146500215220ustar00rootroot00000000000000# Overview The source code for all images under this folder are all come from file [design.sketch](design.sketch). golang-github-go-rod-rod-0.116.2/fixtures/add-script-tag.js000066400000000000000000000000731520674146500234030ustar00rootroot00000000000000window.n = 0 window.count = () => { return window.n++ } golang-github-go-rod-rod-0.116.2/fixtures/add-style-tag.css000066400000000000000000000000251520674146500234100ustar00rootroot00000000000000h4 { color: red; } golang-github-go-rod-rod-0.116.2/fixtures/alert.html000066400000000000000000000001301520674146500222310ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/blank.html000066400000000000000000000000161520674146500222140ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/canvas.html000066400000000000000000000004201520674146500223770ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/chrome-extension/000077500000000000000000000000001520674146500235315ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/fixtures/chrome-extension/main.js000066400000000000000000000000511520674146500250070ustar00rootroot00000000000000window.document.title = 'test-extension' golang-github-go-rod-rod-0.116.2/fixtures/chrome-extension/manifest.json000066400000000000000000000003071520674146500262320ustar00rootroot00000000000000{ "manifest_version": 2, "name": "test", "description": "Test extension", "version": "1.0", "content_scripts": [ { "js": ["main.js"], "matches": [""] } ] } golang-github-go-rod-rod-0.116.2/fixtures/click-iframe.html000066400000000000000000000004401520674146500234540ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/click-iframes.html000066400000000000000000000002701520674146500236400ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/click-wrapped.html000066400000000000000000000011071520674146500236540ustar00rootroot00000000000000
long-text-content-to-wrap
golang-github-go-rod-rod-0.116.2/fixtures/click.html000066400000000000000000000003561520674146500222210ustar00rootroot00000000000000

Title

golang-github-go-rod-rod-0.116.2/fixtures/describe.html000066400000000000000000000003251520674146500227100ustar00rootroot00000000000000
  • coffee
  • tea
    • red tea
    • green tea
  • milk
golang-github-go-rod-rod-0.116.2/fixtures/design.sketch000066400000000000000000001234571520674146500227320ustar00rootroot00000000000000PK%iu@ document.jsonXYO#G+Jv7+o{'c5'֌ fI@B:6j5Mz/zSOj%der{"S"=fqKHIYxM?U}4lR^of#_cK XWV#AlEb9eC }_wH׎|]gE_>sdEɱ1/ Y,=RA;uQg{T'X>d r6-֍7Ή\a5e*EU>=%lzSD1HBx<1]T l4f!M َ̑d@˲?W.T]ty]l;)z({̈́o-,t[\W]opߓ YJc9&2G(VR6O9]e]>KfEw>:+f󺾃ϕ&X&,UTeh5xO荮> .l4V:\'zה E:OpE6VtRiI@Ii4sOIԩ?z Vx.Cl޶ES t< D5'uIq|GylXQ) ]ACK5#B:S#4S{Qz h)%F:4#)MgrticFS8( uJ _PÍz㼀YkeLy1BGإ F=|;)p;)% { L*H~}/E3 7P:$,#.1 CwO>?n-ߜ+h\g>}fPKR/pages/DCC20775-E17B-4B1B-9A1F-1B18B7813C36.jsonW[oF+&ΛM(GְmֻhY8e;U[UNռ80ܾf35qeԨ(W :a$b;6 {eڱadGk)FI™qe34+{@Kͅ4mpq#3 IO8<)6hA@kX5( sZ{7|*pC+)amM>Fxi*"=ݢ)=ia9muG~f|v/Ɗ g(` 'ɇ> >7U켐BVk_7WuTHoyQ&95rKNh)¢ \ POaV"X ǸUr$ a8U"״:Ғ!Z9fp=Y~pnU=PAڦd;LI^5(xl}AY$xzHTg: $C9\tj|y/kpї]xUOCUfXJHb)P]INΨ5ʉ  },1, 4-!%Ywnͦ*z臣( (^$.Rی}oez+ܳsǵc/ẍV n8m%ț{-LIVc ds)Aߧ;t mױEnO HT[O A%RRBhZ ~?;P5YGQl-2}[r97 TGxib:3/N뽎ZM54?C,Vε,KԌvDz"?]c!huT U FR5νl|/2x` eA&o'Wt撛QsfX@@]=Qs:ߵߛ֥^h8Su3\@_1pB.]Asm".Jt3j]}'Nַ!|@? *}eW+}/ʃZą:m*CV-rF( wٖ3fۺ <ovs|{vaϣYEj"VƸBτ+vtI`'5K(o/5hj\4)h:!GZ3'QVrR =s+Jԧ.wqQ\N 0eX&\:bVQ7C?'waVf$KB?2d:̞}eF&CZ\ Y˷iUE`At1%LfUY/URW4_x&2Ls.'[y4u$|TYeXO?Z.lm#Ie+[U S{NvS=sS.úYܕ;J8P0m|{_UiJtH4y>F` w&nÑ#US6D+ه/s,ʦ‘>RQ`6awayim~M. Nޜi~Y٭$oU9S#H VsP3>7Xü1括B֫ Tamwq`dqX%vwHsu\ e|#uLfJt]lh|8r(ED}Vu^j^c}?("IۛcŞR~jޥY.06;IXo$Y-MU@fU=λrU[wCܙjE1Xxb+Op0"BtSO9Q6}J`,_.όC`#LuuQb҇}D/̀#iCk+袲:M++DSVk6fatoǞMmj ?+pS6[O'my+-z}1oȞg{qzt T)$\FLt %tX7 0hTx>4>ly@ vn( ŘwIXh8MQ) ;%qS{@P]Jc}.fsCd3L ̅"3E\4wXѡhYyWu}MYT:_t+r @ Pզ f5 Ti_oف` ͠ Z7%7iu^5QѦɼe7iG쮏\f/][c*]֞yo0@oC|Z9B*Г>*o0K #))نPgp.Sm҆j!5n`/Ѫ|@s_pJ(&'hpucv'ipsuB;5.`z#ĕ2AD%~Hho޸S%bPTr qe^|| K0$<~i!(. RK P{!G;]OU>>g 8QOz$+SnJr`%:W*{DOQ |*%zJ\)z|@g.W=;Ō4ˆB.|'~*7.AK)  $g~zߏ"Eg~Oϔ Kα{r\2rP59%Qjn%ŒobӕhNOU>>^$TBK %#;j7 gG`Mp#ѧ+ʿr Ne:Uu[K!Kȑd: irIVEf:Iz*dpRpN\Xg:Y|bPKmVa/pages/C416C58F-D490-48FB-96FA-C8733AAEFC12.json][s+.=n]FpnI>gN%F&U$'IQ,օ(]e3h|_σ?oox1IK U T#x[$r!j?z3ϧ`1N~`4?G ߟNg~tx_b0߇pO÷ãK>ތ?,fgxa8i4_?NvR;:rUb ^THmA.PK3cprr0{;mûѢb0[\hr:YJ7V@ɠӅ'? YcݴYײ08g糠5Xc$Kr[gʣddV+Vs:ٴx:k2Mۍon ̺DLkNVo=E?Egӓ<]~Ejz:8->nh<^?h2^ G_>' omfM[ywl8͡tw,ŭk3JAcDhS6Q.VcUe%<}}I#St|k MM ~?>،S{W'wHWzT>_M[6ʕR[zqpOQzb0>=͕'O OaEU\lrM g@bMFJj͹c_/=\3sV˜L}-5*k3;ԚnlM|֜4wvi3h_ e[8qԮ;Dn؅=of8Y\`&Olmw}~f\_0_LOo^S|Q wohF]wqiloj2|>е .M巵,L'|cCnu1̗tpȃ{=Z[ %l7duykucwJ8]$h)h뵨1VQr?ũ꽇U.(!&@zMjܫH#r?^)Xhj)P6 Y16wᖗxڑFN&^F*+m$gXٗs_6=R_6=R_6=R_6}k_6З'/Lƈ"@,ƁՖ,5+3VƊެ 02*JV!΂i˪ͤ5䳶je33_Zڟ/٫OhQX,f7g{/_mj&ǵoslf3y8?N+uy9l<͗A4PuMk˘뛻9X]3& &vΡa)T xi5o'z^9m&N ,ߥ\V |u~El@Y^~1mL/xȥ.Y,XbږC <[[g.ܬGm_}̦f#hqû7 pR 55M1 U{ ʀ6+ɮ+d7EԊ22FBNo LQB_:.vf 6C1$Fߓ߲XJbˮ-{ޱ(mO D[|6*L~@Qh1}׽(< >"Qy ɁC"CA"ZP G{AqJ HS$W@(SkEeb66d, r}/mYQ%aLe).3Ob;B29&ة'gY#6{"g*=%o]7]f.յY$̒ (򇡂9};sbFw[^O!+ukk/rzh6@:K֐}˪n!3^LvgA)E F5fjj k#&1Lz\FE;;D=i:D)3rJPR"&ŵ*  $uę\YYC$H( ^ {);'%`j3.zi<<H:-"cLʼ'ɕIʙ\+͆"ŔHwo@9CoPƥ 64oh8CQQ #ǰdeD+| lh0bC ~64`C jo6gt/@VUʾ.bo)thU.Ĩ2V1@^A!KbDa+Ls.\Z3TS,ud Ud.Vtm A uRidoPbC] 4?&eMB.DSHJNi/*|o,j0fQ1uIt&]$/Ќ6g$:D=LoqLsqN%qJmhvt$ E NiR=w0DTRTPKuH- .pΩ')iYքt;*\*Y;2DȮjQ5uVX9V¢,z|5 "U 1aLĘh8-ߖn4IDր B6gfkZ[;"(蜕IŞg`eRj?g}drD KmVKѠSH*Vir6M14 v^-D9xmJ,K2hMck! 8ӉZS[=Ai0(G[gv-n1wnEi lNCkNi8rẑbi\M&`MLɀ9dL^Y .&ז6os,cyBBu'k.ƧR<>JBL8{ajٷRe<|2T]u-Ԏ+c8 \2P $1HzS2X@Gj TC(Iι%%!Xd,J!bPNXEq,YYA\ƞ@*VD ޅDYs\ pd e Bhf8c|@W"Lw2EY"\T;zMQC;dz2sz64 _Ё>D/g`C 64j h:JCU apTϥXUI23P9ؓ3MZc!&@$>lf0 9zŒ?(c2H9fWkOs]AՃu+t4}l!_~Lgepw"èas/sK6%HZy`e NWKv4)F1s߉1UjEMlku>%~ιttI\:].5!d@Qfk'{$&G^7&av2U*jL Q`mُ~Φtך{@w@|NhBtH%ȔHOhE50]hm<b^!IL%fչ܄E6,ak3,%V6ys6P1ZsIEbRn5omFM,L^qnBMȹ %\!|3+t |I b5"2O5Սy ^;NK(I 3B&D #B5 _2M`]{W3>эZ){nnEaTml, ʥ 04t 4FJ^(F_UJEG_;ouwdZ'& 5 8Qy0 Խq6>;mxxl `w/J|uɳHABHr X< 3o˹"9 hoK*0Wat i`yςaԭamEzW *x,UteUܿ*$3i?B1]&P%:f*6d 2fbĘ1VՑ"˰i"Yo`O6&->?PK2gtext-previews/text-previews.pdf0%PDF-1.3 % 4 0 obj << /Length 5 0 R /Filter /FlateDecode >> stream xmM; 0=EEZZg BJ|HAD>A ewIl[$X2vHQ8Kpf S?3_z/" endstream endobj 5 0 obj 103 endobj 2 0 obj << /Type /Page /Parent 3 0 R /Resources 6 0 R /Contents 4 0 R /MediaBox [0 0 517 304] >> endobj 6 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /TT2 9 0 R >> >> endobj 10 0 obj << /Length 11 0 R /N 3 /Alternate /DeviceRGB /Filter /FlateDecode >> stream xwTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf endstream endobj 11 0 obj 2612 endobj 7 0 obj [ /ICCBased 10 0 R ] endobj 3 0 obj << /Type /Pages /MediaBox [0 0 1000 1000] /Count 1 /Kids [ 2 0 R ] >> endobj 12 0 obj << /Type /Catalog /Pages 3 0 R >> endobj 9 0 obj << /Type /Font /Subtype /TrueType /BaseFont /XFIUKS+Roboto-Thin /FontDescriptor 13 0 R /ToUnicode 14 0 R /FirstChar 33 /LastChar 35 /Widths [ 654 550 547 ] >> endobj 14 0 obj << /Length 15 0 R /Filter /FlateDecode >> stream x]N0 y aJ(iVho!qm~^;2LFm% 89Ϛ3j:2Np-Ώd% v/6 Pjobr~ױ~gS ,ERmO(5  &'dR%O'd(Y$Sl[$!bdId_,.6kJ~Q9GKva endstream endobj 15 0 obj 242 endobj 13 0 obj << /Type /FontDescriptor /FontName /XFIUKS+Roboto-Thin /Flags 4 /FontBBox [-732 -271 1127 1056] /ItalicAngle 0 /Ascent 928 /Descent -244 /CapHeight 711 /StemV 0 /XHeight 528 /AvgWidth 543 /MaxWidth 1144 /FontFile2 16 0 R >> endobj 16 0 obj << /Length 17 0 R /Length1 1944 /Filter /FlateDecode >> stream x]T]LW>wfvg˂"3 ]QV%D.!Th3C-?\ ZyK7UCcҦy%m}iSZ4jSt;M;;瞻9s390 Le?pq+ 3օ YM C*a @ 5騅B;;A^IU|[nO;̐Rr#_gǵ.XϚnI=h@!S.OrM  S3MG8&6lrt=#*W2!2URMT~e [7O',¥R:Ù: ݕT 2ε`Mg+ȃ6KvD}ڲQ-;#L$p,I % ]e9 RAJC*kZ%{E'׈Z^[_3L_!ps?nIKSy[_xo=#7" Eaˊ^~T 2y{2]H\ !c $VI"\YS+Z(j%\EOx̩%#քfFN]^;GOzin%ቶ;6l"Dp^n%wla7\a 7b:=Xh|һ(hҵ0J`El:ϷS.CC^dݼ4L»_'y;ۯ])zغ {<Ͻ@D}c3xgʍz endstream endobj 17 0 obj 1390 endobj 18 0 obj (38B0E570-514E-4EE9-B6C4-9E84D72B2AF5) endobj 1 0 obj << /Producer (macOS Version 10.15.7 \(Build 19H2\) Quartz PDFContext) /CreationDate (D:20201023034741Z00'00') /ModDate (D:20201023034741Z00'00') /Keywords 18 0 R >> endobj xref 0 19 0000000000 65535 f 0000005634 00000 n 0000000218 00000 n 0000003191 00000 n 0000000022 00000 n 0000000199 00000 n 0000000322 00000 n 0000003155 00000 n 0000000000 00000 n 0000003326 00000 n 0000000419 00000 n 0000003134 00000 n 0000003276 00000 n 0000003838 00000 n 0000003500 00000 n 0000003818 00000 n 0000004078 00000 n 0000005558 00000 n 0000005579 00000 n trailer << /Size 19 /Root 12 0 R /Info 1 0 R /ID [ ] >> startxref 5814 %%EOF PKXث)text-previews/text-previews-metadata.jsonY{"0":{"_class":"MSAttributedStringKey","size":"{517, 304}","string":{"_class":"attributedString","string":"Rod","attributes":[{"_class":"stringAttribute","location":0,"length":3,"attributes":{"MSAttributedStringFontAttribute":{"_class":"fontDescriptor","attributes":{"name":"Roboto-Thin","size":260}},"MSAttributedStringColorAttribute":{"_class":"color","alpha":1,"blue":0.1764705882352941,"green":0.1764705882352941,"red":0.1764705882352941},"paragraphStyle":{"_class":"paragraphStyle"},"kerning":20.3125}}]},"styleID":"ECC172B2-9400-4C5D-BBDB-A20085DAFD9F"},"frames":{"38B0E570-514E-4EE9-B6C4-9E84D72B2AF5":{"_class":"NSValue","type":"rect","value":"{-592, -318, 517, 304}"}}}PK  user.jsonm=0 h6D}e'n8C.E# 8q8@ Ck%`PGe $rȖN)%` $V(RO*CIG7al[=blKV9 x Q'!pkP>I,!%.|!z0*Z,JJֈn+DIZ:1`vBU!kd*L%Vָ"ϵ^o0`&u |eUL(1+ 41),$y op$eKF9^-3*kC U>'8/n<(,Hfhgt.;3¿u!dFtwsB>||} S ܆a7~d^\X1z]LL=o|󅸚xGd)%>C@i[?0/?6Rz=PK0;sXXpreviews/preview.pngXzPNG  IHDR #sRGBDeXIfMM*i@IDATx%E0i E$ H$HP  DA@d$z$$Q9眓WVO̽3}:gvzwnJ @@coLK5 @% t# @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @ @x+K,D:#9]#uv @@"@tW@]_g'@TN@X.Q! @@wuv @@"@tW@]_g'@TN@X.Q! @@wuv @@ dnK;b.?D'8I&N:i?fq4 35_\4D@8믧kQO8iiN3Owy[VԌ/ ~5׿w:߳…^8-䒭f< @>r+*3Ϥ /U3fLZjZ_r}Cj @|A1z|p iKzZc5\sՁ;UOyWgM{lZkz뭗N>k!@+ t+Z[oM{wZyG^yZW @@(P/}Kiww㥗^J/rc=}юT_LrH:l)#v k x㍗.)ջU6ܛn)x㍭oPFya?>m喭`p  @zcm[ :[nW4wIwuW+0|{@(H̏~4  @^r51ZA:xҹ瞛N=V08QG믿~zSD"@u=܀M=i 7lӲ.Xn=UJ @po9蠃馛˦Xp:G#@=) nSfe|']|i=L ,@i曭9{GUM x#I'Z1jԨp駧/*&'M#AO,L|ͭȱD$ SojKᡇz;d[{MlIz'-`zvT`NwE<@+|-`/:vE`YgMG}taR,JM74tE `^SM5Ukf)۰| ʽn#&#|p*+0o~3buta 0X`|1p}IR8<DzI@K.r˥6,{_iw @UVWԩR[nec9ue]͗A& ZO&p´{s9+Wo"@9O''@Z(mٖvm+ @UV7ԥ[ouzꩳu4 AVCTN:ihge @믿~5jTsN6O*==!C^|gOE%  @R`KO,_qg=3<)~1oLb:%03 oSq8$9uw[o5Ŋcn 8%(oOgMW_}u;}cin;oK%}> R#Ht /d2=ܓ;:ovWd߈Nx OSꪫ]wF=7xcBFJ-\k%X" d]ܾ (`0Nnz]i*3Ϥ?/FF:s1i9h M'z瞛N9/;Kg>/:S"\qg>4v"S.@54Ӥ78ziI'ZUէ.:u3=oN8!m-+wJ}~_ 6 m&9 @Lկ~N;J1?➋6(/ T?Ɲi# Ub.駟V[mtAu59|O+߇>>5#hM_ UP}QoЊGnCI\xwWR#oƖ?uc5q0ƶMq<0xi-Hkn馴.zhW-\b>h|Ž7sDZセ_<vű{uXi<scZBw[>7L{T}1eX@`R|MFLO}Swe+(]K$F`3&H,{w:cwM@<6_@K-BieIK/VO2$)~>c-ދoO~/~1?h3XxA=' .S N@ tϾ?pWYe`oq .`+&Gz[ceO>9*//q7tւq2)`OuVQ >[|Ŝ/(bR~2?ʜs8 #ex+c$G_zrZtNy/ ~sW@*ѿv*=#0e_߶꫷F0"NbO~tI't#oGp=#ӌyr߻?Z575WK1 a0+ۇ\Xwv%0,*57\8.x[_u88F\Gڂq_cxībDm^[l~GrGgZV14\\!;W +9V}X;ﴭ-K&죥%~@ś \1Tf.٫z$Ҷx3FUfHƱ&c#R^hl\4[g?/ŭ]r◎ϴc+ ^oW^m/͓x[RANm[ԦNņo־v=&ܹ=+u\ksx{͚kٶH, _j ?jWaKFs29)/ZS J['oeQW΁?L ,@ۏr)NV: 5!`r?LcxT$-6/ؾc5Xj.JwǢNzqWwnXnUo<#' 9{Wa . տrs'#׿WKؚ(ţ9/1c-6qX8ct\[n%vi9rw8󲵷R5Kz$t ' 3f?p G\s͕/ne]2M<#SA%4s1G.wbn;.{]c#, p7,u]m+>z[C7`,(z$4$F&thtb:ñ(E؂)FDO>,lx;[@{na뗷/Rk GԠ>l=ҤK.$]wu>Y,0sޖRT*6h._)-ɥmݶ smx{COoUiUU+JOGOvm)yWv w%cGlQ>ϧ/}KEE򗿜VYe҇zhv$/%"5i4#\jb2d I6\G o4@"7Gj~GV {2,m`>òubet.m橿w.>¿݂&TzUNkI^ziz5U[~x&?n; hٛmYoceo)Fj ?jWc=6;R+._ԴZx'өT<2 xԖ%r;PxbQHR|ҔSNV_}\%"b-JTd$#=!poqfiz-]O?=(S._l#uOSTZa /P~3c+2[X<o& <a?-춰ob%.{0ojg3:odetN w.s9^exlGTvc$蝹 V@8X9k{瞶mG1/߶<3O?tw)^#%u_ 64._MHRlR4g29 :3ݠU8YgoFozF* HuBވ1|⽿7pC"_y2.OH'E@8,.k{쑭vlv+>`vDL/ڣYSO]HK_rOS+l.4 yNQ)I積7?-IlWb ) w]wu ?]Q><̅>2.oR5Aj 'niZVnx|K/xv_z0\7L1KK/t.!J` vש{K;hL+r)ޱJ+W FY c4rEb˞.l*+r-)VҒK.r|VVKv i{K/KdM3C֧ݒuޞ8Z.zWu^q[[x=T轌?|L<<(#o)7uK'wŠ +A=x,.?_~~DW}݅@"EcO뮻nj1?͓Av <i 6Hߢ2ˤi &(*&EG҂ .Ȫs9g}ٳ5ꪫyދ@MqM~o/ԈI/J, bD:EQyEb[曯:&I@8L.3C`o{o_-c<}e}@lQ4Ϫ(`8E۞{'Ju銱g.ŖD8^g`{WZ=XqFmb+oq:,V(j"loTKoCF,Weid\ĦwpwmN8!]pf1[@,{x/p.E`ss{yDQ8,8 km`^`/vi 쭷J>hVYfi*jK/SXSOJ^sӣL38mv@SM隵3>O^^s~~qj>j(kSc10Tj"0 ONf)h=790F%?C=4NfT-޴K8S>dc=zOzL}S@8֪v}~>Zky䑶:餓ꫯ̽-ϰ|ײfi*QOcQ_F C+0> U]v%[xM՞{Y'avrNT`QOW&Ȉt.<(կ"zk~͗}@@T^QvW@]_g@+O>d( L6dJrXv! @].!tt/7sOnm 1fetWhp'ŝ#u, cD&~}uM .`Ɨ\rI˲2'P4({'F񝼃k s`vKЇQ_=/;EQwE`ovZzM@k=;e7`?9lFc, :]Nk^뱆wJ34SV!V~|(zӂ{wE#ݸp(xW/NUUj+#;sۼ8ط7`Ty#/e\do)=]v.rieͶ[nI9`N5kg[:SO=UVkuļ(3:ݝ{Y{G@;}c <)R\g}rَ 0@bx׸D&~=V3]4ʢ9~9z_v L9iv^筷J{gJlÇ)0Aw2u, = =! nRVYeKf|O@Q1]E#E",jpڻv\`]wMEހO?tǯQ5*85 P`M:R3RG?b,E-kb\n$Lfy摪ZǮor:Lu'KUW]5G}4va| ?.p 7dY4rP3◓@c v* pX] ;cMsO~t?>g ]uU<#'P/#W_h5vLWPnveOc ,Rcf?(kq͞?FuI褀Uy^;}oLzj6_F^`(|5\a?^{׍ǦZ([m#Y5ּ57^'`g:(=3cDzgE=ҌHFQ/I Q?O9^Ͼh {׾/[c`^z饳_zVg3_ӡ_qfH34SDF42j, qjZ^`-,\x+29mbXK/bke96>`۳WFu]}|'`cZC[;؇!4LV_/8쳳5jTaf?XOSkz:/ 켩3.VXalm-2 =ͯl6lGcy睗/~2F1P"$`z[[~&lq7`D,Lpl=rgfet_ b> -rټ^Θwy;颋.; Luq[omR&k=W ܘc938bN3:E+x\/'1W"$`z[[ ikcN;-/c\~<]q|xӥ^kuME_`V@ؖ& ?{.x~8pPhb6hpcMofJQzSO׹絭@'^Ҏ .]wl^z饴~ee_`'ORlC@çrJ̋-X=zt617҅^r@R 6l~?'d?Ќ:F[~_6Tfd ^}Ygl^2{ -@wNwjXV@XVJ L1~a;kGhnX\s͕]tl/lxW~ng`Uwcۡ:'v! LuXi W p`]$l`'TzP'pBWҚk r~Vc,͠2g}:{ B&dl͏;t=deO F=t7&?ps:9裏>:{XQ>(jo33o:s"g鸀N:kַm4dģ}106#>7x#{if1c%H6[ۦ#C9m7x̆p8]g6hsrnH~z.bo}'<'Sߟ͛oy6_WnlrF9T)Hɻnb?a;QcC/N˱A9_=ナ'r-ӔSNYX11'`?kӇ]oyX"ͧ Z+Wm",R ߇iHtP;njFtpnWkM_4pl|}bXb&~7M;jǣk&/o}v%Jł>A \veO,|G)N8Y@i`fFp$]gj}?O X(̏|#i 7,,V@](f̩ E)ކQJg_FAc9S&w-sў] R(^K?p:sَ% b\і]v{s@ Kў1/5\V[lEtIm:CyFifv3j~MXٮQ* ĈJnxcM{o\ɺĻX.ygS0vmjXA?L)R^۾=r)6Ҷ?1j[s\pd]og=[xg͗?m٦=Q2yŮbݬl/ߎ;>ݣH=&nOtM7F~nm,FX@8#wu ̘+Fx~9W+h-( Nb_z=#-ذ|j\})/pwVk?ݿKOPWH/8Z4rU?ܲo+:*WV1;*q'?YTj1?u'V&{<7bv)6wye9V`:AzK VP:SaXtE}Wp%F^8OؠQqg=۽7AlM?b.;?A۹xl7ss#M7ݴnͮU{NX5DgQy5+brHGCsm=v{W ){^{~0FsO1':1IDAT駟'|=Sm ǣbASP\H2b5.eV jط-70Klx sQcyQ^<79/Yf%K#l}=_M>h[S1l:m:}&=&\Zw}lǘ;*uP&~ؘ"?L3Mw)m◑ۄ"6^R,ZhvَUL@XQ>6hlv< b}7j~ LnBz뭗gml曯5j<@x|߿xU\̯puN7c;0O$vjWXp'Fj;y>\ "XR5I_Rs#; $\hҿZkw}8PMxTn-[Ps ~w4GU+rG /.O퉑7:uh:X+*+pgF^އy|,XwuSqSla?.b߆nbUp12ڥ&姝L o\x`!OeF~mc?f̘/pVŶ;[lP`݌͒1lwk`|]y啃Z[|9x#Pl ]6ſ[t饗Np($\0; ,@ދFM Oy8g"lߚk@6Hhh-^GU96M^p=ztCI7M7 )x6_,SN9l^}VPt1Aǩ38Dbˠ;3q7`RElMQB {U\@=ztEx_f{2{ݵcin;[uLbhZ7|sSL1E}[۩"#bT?˭Ѡ:xɖ[nbT-Zo8sZ (b)|ދ1{1_|b{kqߍ8ARslʾ;x-O.]K.i=ʝ>^Pu9?bTw]I_>mv Bg[% +cXf/+BkkD2 Ο*Λ:#g/:ψ?01rҋ)~?5dhbŜxOѿ(67j>-TWWJ tI 'U8s[+9s+,=LW\QXV0u~3>"J1:X b׉x,5G@؜aeYZsbP.V ;^N Gs/_߱Qj/G̱Z}=3lNu][#1SOMr[ 7_k2F*[us0px]H12x-,:lꪓUX+c%",;W<-bX 6ؕmj<^lZA_:~z k( aj cp(~{}_tbrRb-bt0rSFʴi'n&:k2=v 2U7ojM@ڈGDZcxm X"0{,}_q-am3ϜbA@`%e @5B:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@98_tIENDB`PK%iu@ document.jsonPKR/pages/DCC20775-E17B-4B1B-9A1F-1B18B7813C36.jsonPK!0*/ pages/9824D426-ED06-4B4A-90C7-F8A988882129.jsonPKmVa/`pages/C416C58F-D490-48FB-96FA-C8733AAEFC12.jsonPK2gW,text-previews/text-previews.pdfPKXث)hEtext-previews/text-previews-metadata.jsonPK  ZHuser.jsonPKt"} Jmeta.jsonPK0;sXXKpreviews/preview.pngPK sgolang-github-go-rod-rod-0.116.2/fixtures/double-click.html000066400000000000000000000001451520674146500234650ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/drag.html000066400000000000000000000055241520674146500220530ustar00rootroot00000000000000
This div is draggable
golang-github-go-rod-rod-0.116.2/fixtures/fetch.html000066400000000000000000000011051520674146500222160ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/fonts.html000066400000000000000000000251211520674146500222620ustar00rootroot00000000000000

This file is generated by "fixtures/gen-fonts"

Test smileys: 😀 😁 😂 🤣 😃 😄 😅 😆 😉 😊 😋 😎 😍 😘 🥰 😗 😙 😚

Afrikaans Toets die google translate.
Albanian Testo përkthimin google.
Amharic የጉግል ትርጉምን ይሞክሩ።
Arabic اختبر ترجمة جوجل.
Armenian Փորձեք google- ի թարգմանությունը:
Azerbaijani Google translate test edin.
Basque Probatu google itzulpena.
Belarusian Праверце пераклад Google.
Bengali গুগল অনুবাদ পরীক্ষা করুন।
Bosnian Testirajte Google Translate.
Bulgarian Тествайте google translate.
Catalan Proveu el Google Translate.
Cebuano Sulayi ang paghubad sa google.
Chichewa Yesani kutanthauzira kwa google.
Chinese (Simplified) 测试谷歌翻译。
Chinese (Traditional) 測試谷歌翻譯。
Corsican Pruvate u google translate.
Croatian Testirajte Google Translate.
Czech Vyzkoušejte překladač Google.
Danish Test google translate.
Dutch Test de google translate.
English Test the google translate.
Esperanto Provu la google-tradukon.
Estonian Testige Google'i tõlget.
Filipino Subukan ang isalin sa google.
Finnish Testaa Google-käännös.
French Testez la traduction de Google.
Frisian Test de google translate.
Galician Proba o google translate.
Georgian შეამოწმეთ google translate.
German Testen Sie die Google Übersetzer.
Greek Δοκιμάστε τη μετάφραση google.
Gujarati ગૂગલ ટ્રાન્સલેટનું પરીક્ષણ કરો.
Haitian Creole Teste google translate la.
Hausa Gwada fassarar google.
Hawaiian Hoʻāʻo i ka unuhi google.
Hebrew בדוק את תרגום Google.
Hindi Google अनुवाद का परीक्षण करें।
Hmong Sim google txhais lus.
Hungarian Tesztelje a google fordítást.
Icelandic Prófaðu google translate.
Igbo Nwaleenụ google ịsụgharị.
Indonesian Uji google translate.
Irish Tástáil an google translate.
Italian Prova Google Translate.
Japanese グーグル翻訳をテストします。
Javanese Tes nerjemahake google.
Kannada Google ಅನುವಾದವನ್ನು ಪರೀಕ್ಷಿಸಿ.
Kazakh Google translate бағдарламасын тексеріп көріңіз.
Khmer សាកល្បងកម្មវិធីបកប្រែហ្គូហ្គល។
Kinyarwanda Gerageza ibisobanuro bya google.
Korean Google 번역을 테스트하십시오.
Kurdish (Kurmanji) Wergera google-ê biceribînin.
Kyrgyz Google котормосун сынап көрүңүз.
Lao ທົດສອບການແປພາສາ google.
Latin Ad temptare translate.
Latvian Pārbaudiet Google tulkošanu.
Lithuanian Išbandykite „Google“ vertimą.
Luxembourgish Test de Google Iwwersetzen.
Macedonian Тестирајте го преводот на Google.
Malagasy Andramo ny fandikanteny google.
Malay Uji terjemahan google.
Malayalam Google വിവർത്തനം പരിശോധിക്കുക.
Maltese Ittestja l-google translate.
Maori Whakamatauhia te whakamaori google.
Marathi गूगल ट्रान्सलेशनची चाचणी घ्या.
Mongolian Google орчуулгыг туршиж үзээрэй.
Myanmar (Burmese) Google Translate ကိုစမ်းသပ်ပါ။
Nepali गुगल अनुवाद परीक्षण गर्नुहोस्।
Norwegian Test google translate.
Odia (Oriya) ଗୁଗୁଲ୍ ଅନୁବାଦ ପରୀକ୍ଷା କରନ୍ତୁ |
Pashto د ګوګل ژباړې ازمول.
Persian گوگل ترجمه را امتحان کنید.
Polish Przetestuj tłumacza Google.
Portuguese Teste o google tradutor.
Punjabi ਗੂਗਲ ਅਨੁਵਾਦ ਦੀ ਜਾਂਚ ਕਰੋ.
Romanian Testați traducerea google.
Russian Протестируйте гугл переводчик.
Samoan Tofotofo le faʻaliliu google.
Scots Gaelic Dèan deuchainn air an eadar-theangachadh google.
Serbian Тестирајте Гоогле Транслате.
Sesotho Leka phetolelo ea google.
Shona Edza iyo google translate.
Sindhi گوگل ٽرانسليٽر ٽيسٽ ڪريو.
Sinhala ගූගල් පරිවර්තනය පරීක්ෂා කරන්න.
Slovak Vyskúšajte prekladač google.
Slovenian Preizkusite google translate.
Somali Tijaabi tarjumaadda google.
Spanish Prueba el traductor de Google.
Sundanese Nguji tarjamahan google.
Swahili Jaribu kutafsiri kwa google.
Swedish Testa google translate.
Tajik Google translate -ро санҷед.
Tamil Google மொழிபெயர்ப்பை சோதிக்கவும்.
Tatar Google тәрҗемәсен сынап кара.
Telugu గూగుల్ అనువాదాన్ని పరీక్షించండి.
Thai ทดสอบ Google Translate
Turkish Google çeviriyi test edin.
Turkmen Google terjimesini synap görüň.
Ukrainian Перевірте перекладач Google.
Urdu گوگل ترجمہ کی جانچ کریں۔
Uyghur Google تەرجىمىسىنى سىناپ بېقىڭ.
Uzbek Google translate-ni sinab ko'ring.
Vietnamese Kiểm tra google dịch.
Welsh Profwch y cyfieithiad google.
Xhosa Vavanya ukuguqulelwa kukagoogle.
Yiddish טעסט די Google איבערזעצן.
Yoruba Ṣe idanwo itumọ google.
Zulu Hlola ukuhumusha kwe-google.
golang-github-go-rod-rod-0.116.2/fixtures/gen-fonts/000077500000000000000000000000001520674146500221425ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/fixtures/gen-fonts/main.go000066400000000000000000000031231520674146500234140ustar00rootroot00000000000000// generates the fixtures/fonts.html for testing the fonts in docker. // Use the google translate to translate "test" into all the languages, print the result into a html page. // By reviewing the generated pdf we can find out what font is missing for a specific language. // Package main ... package main import ( "fmt" "log" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/utils" ) func main() { url := launcher.New().MustLaunch() b := rod.New().ControlURL(url).MustConnect() defer b.MustClose() p := b.MustPage("https://translate.google.com/") p.MustElement("#source").MustInput("Test the google translate.") if p.MustHas(".tlid-dismiss-button") { p.MustElement(".tlid-dismiss-button").MustClick() } showList := p.MustElement(".tlid-open-target-language-list") list := p.MustElements(".language-list:nth-child(2) .language_list_section:nth-child(2) .language_list_item_language_name") html := "" for _, lang := range list { showList.MustClick() wait := p.MustWaitRequestIdle() lang.MustClick() wait() name := lang.MustText() result := p.MustElement(".tlid-translation").MustText() log.Println(name, result) html += fmt.Sprintf("%s%s\n", name, result) } html = fmt.Sprintf(`

This file is generated by "fixtures/gen-fonts"

Test smileys: 😀 😁 😂 🤣 😃 😄 😅 😆 😉 😊 😋 😎 😍 😘 🥰 😗 😙 😚

%s
`, html, ) utils.E(utils.OutputFile("fixtures/fonts.html", html)) } golang-github-go-rod-rod-0.116.2/fixtures/icon.png000066400000000000000000000542051520674146500217060ustar00rootroot00000000000000PNG  IHDR #sRGBDeXIfMM*i@IDATx%E0i E$ H$HP  DA@d$z$$Q9眓WVO̽3}:gvzwnJ @@coLK5 @% t# @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @=@ha @{ 0`:\s  @@ @au @ @@ p%@ @ K @ 65 t @& lXk. @@ @4L@ذ\ @ @x+K,D:#9]#uv @@"@tW@]_g'@TN@X.Q! @@wuv @@"@tW@]_g'@TN@X.Q! @@wuv @@ dnK;b.?D'8I&N:i?fq4 35_\4D@8믧kQO8iiN3Owy[VԌ/ ~5׿w:߳…^8-䒭f< @>r+*3Ϥ /U3fLZjZ_r}Cj @|A1z|p iKzZc5\sՁ;UOyWgM{lZkz뭗N>k!@+ t+Z[oM{wZyG^yZW @@(P/}Kiww㥗^J/rc=}юT_LrH:l)#v k x㍗.)ջU6ܛn)x㍭oPFya?>m喭`p  @zcm[ :[nW4wIwuW+0|{@(H̏~4  @^r51ZA:xҹ瞛N=V08QG믿~zSD"@u=܀M=i 7lӲ.Xn=UJ @po9蠃馛˦Xp:G#@=) nSfe|']|i=L ,@i曭9{GUM x#I'Z1jԨp駧/*&'M#AO,L|ͭȱD$ SojKᡇz;d[{MlIz'-`zvT`NwE<@+|-`/:vE`YgMG}taR,JM74tE `^SM5Ukf)۰| ʽn#&#|p*+0o~3buta 0X`|1p}IR8<DzI@K.r˥6,{_iw @UVWԩR[nec9ue]͗A& ZO&p´{s9+Wo"@9O''@Z(mٖvm+ @UV7ԥ[ouzꩳu4 AVCTN:ihge @믿~5jTsN6O*==!C^|gOE%  @R`KO,_qg=3<)~1oLb:%03 oSq8$9uw[o5Ŋcn 8%(oOgMW_}u;}cin;oK%}> R#Ht /d2=ܓ;:ovWd߈Nx OSꪫ]wF=7xcBFJ-\k%X" d]ܾ (`0Nnz]i*3Ϥ?/FF:s1i9h M'z瞛N9/;Kg>/:S"\qg>4v"S.@54Ӥ78ziI'ZUէ.:u3=oN8!m-+wJ}~_ 6 m&9 @Lկ~N;J1?➋6(/ T?Ɲi# Ub.駟V[mtAu59|O+߇>>5#hM_ UP}QoЊGnCI\xwWR#oƖ?uc5q0ƶMq<0xi-Hkn馴.zhW-\b>h|Ž7sDZセ_<vű{uXi<scZBw[>7L{T}1eX@`R|MFLO}Swe+(]K$F`3&H,{w:cwM@<6_@K-BieIK/VO2$)~>c-ދoO~/~1?h3XxA=' .S N@ tϾ?pWYe`oq .`+&Gz[ceO>9*//q7tւq2)`OuVQ >[|Ŝ/(bR~2?ʜs8 #ex+c$G_zrZtNy/ ~sW@*ѿv*=#0e_߶꫷F0"NbO~tI't#oGp=#ӌyr߻?Z575WK1 a0+ۇ\Xwv%0,*57\8.x[_u88F\Gڂq_cxībDm^[l~GrGgZV14\\!;W +9V}X;ﴭ-K&죥%~@ś \1Tf.٫z$Ҷx3FUfHƱ&c#R^hl\4[g?/ŭ]r◎ϴc+ ^oW^m/͓x[RANm[ԦNņo־v=&ܹ=+u\ksx{͚kٶH, _j ?jWaKFs29)/ZS J['oeQW΁?L ,@ۏr)NV: 5!`r?LcxT$-6/ؾc5Xj.JwǢNzqWwnXnUo<#' 9{Wa . տrs'#׿WKؚ(ţ9/1c-6qX8ct\[n%vi9rw8󲵷R5Kz$t ' 3f?p G\s͕/ne]2M<#SA%4s1G.wbn;.{]c#, p7,u]m+>z[C7`,(z$4$F&thtb:ñ(E؂)FDO>,lx;[@{na뗷/Rk GԠ>l=ҤK.$]wu>Y,0sޖRT*6h._)-ɥmݶ smx{COoUiUU+JOGOvm)yWv w%cGlQ>ϧ/}KEE򗿜VYe҇zhv$/%"5i4#\jb2d I6\G o4@"7Gj~GV {2,m`>òubet.m橿w.>¿݂&TzUNkI^ziz5U[~x&?n; hٛmYoceo)Fj ?jWc=6;R+._ԴZx'өT<2 xԖ%r;PxbQHR|ҔSNV_}\%"b-JTd$#=!poqfiz-]O?=(S._l#uOSTZa /P~3c+2[X<o& <a?-춰ob%.{0ojg3:odetN w.s9^exlGTvc$蝹 V@8X9k{瞶mG1/߶<3O?tw)^#%u_ 64._MHRlR4g29 :3ݠU8YgoFozF* HuBވ1|⽿7pC"_y2.OH'E@8,.k{쑭vlv+>`vDL/ڣYSO]HK_rOS+l.4 yNQ)I積7?-IlWb ) w]wu ?]Q><̅>2.oR5Aj 'niZVnx|K/xv_z0\7L1KK/t.!J` vש{K;hL+r)ޱJ+W FY c4rEb˞.l*+r-)VҒK.r|VVKv i{K/KdM3C֧ݒuޞ8Z.zWu^q[[x=T轌?|L<<(#o)7uK'wŠ +A=x,.?_~~DW}݅@"EcO뮻nj1?͓Av <i 6Hߢ2ˤi &(*&EG҂ .Ȫs9g}ٳ5ꪫyދ@MqM~o/ԈI/J, bD:EQyEb[曯:&I@8L.3C`o{o_-c<}e}@lQ4Ϫ(`8E۞{'Ju銱g.ŖD8^g`{WZ=XqFmb+oq:,V(j"loTKoCF,Weid\ĦwpwmN8!]pf1[@,{x/p.E`ss{yDQ8,8 km`^`/vi 쭷J>hVYfi*jK/SXSOJ^sӣL38mv@SM隵3>O^^s~~qj>j(kSc10Tj"0 ONf)h=790F%?C=4NfT-޴K8S>dc=zOzL}S@8֪v}~>Zky䑶:餓ꫯ̽-ϰ|ײfi*QOcQ_F C+0> U]v%[xM՞{Y'avrNT`QOW&Ȉt.<(կ"zk~͗}@@T^QvW@]_g@+O>d( L6dJrXv! @].!tt/7sOnm 1fetWhp'ŝ#u, cD&~}uM .`Ɨ\rI˲2'P4({'F񝼃k s`vKЇQ_=/;EQwE`ovZzM@k=;e7`?9lFc, :]Nk^뱆wJ34SV!V~|(zӂ{wE#ݸp(xW/NUUj+#;sۼ8ط7`Ty#/e\do)=]v.rieͶ[nI9`N5kg[:SO=UVkuļ(3:ݝ{Y{G@;}c <)R\g}rَ 0@bx׸D&~=V3]4ʢ9~9z_v L9iv^筷J{gJlÇ)0Aw2u, = =! nRVYeKf|O@Q1]E#E",jpڻv\`]wMEހO?tǯQ5*85 P`M:R3RG?b,E-kb\n$Lfy摪ZǮor:Lu'KUW]5G}4va| ?.p 7dY4rP3◓@c v* pX] ;cMsO~t?>g ]uU<#'P/#W_h5vLWPnveOc ,Rcf?(kq͞?FuI褀Uy^;}oLzj6_F^`(|5\a?^{׍ǦZ([m#Y5ּ57^'`g:(=3cDzgE=ҌHFQ/I Q?O9^Ͼh {׾/[c`^z饳_zVg3_ӡ_qfH34SDF42j, qjZ^`-,\x+29mbXK/bke96>`۳WFu]}|'`cZC[;؇!4LV_/8쳳5jTaf?XOSkz:/ 켩3.VXalm-2 =ͯl6lGcy睗/~2F1P"$`z[[~&lq7`D,Lpl=rgfet_ b> -rټ^Θwy;颋.; Luq[omR&k=W ܘc938bN3:E+x\/'1W"$`z[[ ikcN;-/c\~<]q|xӥ^kuME_`V@ؖ& ?{.x~8pPhb6hpcMofJQzSO׹絭@'^Ҏ .]wl^z饴~ee_`'ORlC@çrJ̋-X=zt617҅^r@R 6l~?'d?Ќ:F[~_6Tfd ^}Ygl^2{ -@wNwjXV@XVJ L1~a;kGhnX\s͕]tl/lxW~ng`Uwcۡ:'v! LuXi W p`]$l`'TzP'pBWҚk r~Vc,͠2g}:{ B&dl͏;t=deO F=t7&?ps:9裏>:{XQ>(jo33o:s"g鸀N:kַm4dģ}106#>7x#{if1c%H6[ۦ#C9m7x̆p8]g6hsrnH~z.bo}'<'Sߟ͛oy6_WnlrF9T)Hɻnb?a;QcC/N˱A9_=ナ'r-ӔSNYX11'`?kӇ]oyX"ͧ Z+Wm",R ߇iHtP;njFtpnWkM_4pl|}bXb&~7M;jǣk&/o}v%Jł>A \veO,|G)N8Y@i`fFp$]gj}?O X(̏|#i 7,,V@](f̩ E)ކQJg_FAc9S&w-sў] R(^K?p:sَ% b\і]v{s@ Kў1/5\V[lEtIm:CyFifv3j~MXٮQ* ĈJnxcM{o\ɺĻX.ygS0vmjXA?L)R^۾=r)6Ҷ?1j[s\pd]og=[xg͗?m٦=Q2yŮbݬl/ߎ;>ݣH=&nOtM7F~nm,FX@8#wu ̘+Fx~9W+h-( Nb_z=#-ذ|j\})/pwVk?ݿKOPWH/8Z4rU?ܲo+:*WV1;*q'?YTj1?u'V&{<7bv)6wye9V`:AzK VP:SaXtE}Wp%F^8OؠQqg=۽7AlM?b.;?A۹xl7ss#M7ݴnͮU{NX5DgQy5+brHGCsm=v{W ){^{~0FsO1':1IDAT駟'|=Sm ǣbASP\H2b5.eV jط-70Klx sQcyQ^<79/Yf%K#l}=_M>h[S1l:m:}&=&\Zw}lǘ;*uP&~ؘ"?L3Mw)m◑ۄ"6^R,ZhvَUL@XQ>6hlv< b}7j~ LnBz뭗gml曯5j<@x|߿xU\̯puN7c;0O$vjWXp'Fj;y>\ "XR5I_Rs#; $\hҿZkw}8PMxTn-[Ps ~w4GU+rG /.O퉑7:uh:X+*+pgF^އy|,XwuSqSla?.b߆nbUp12ڥ&姝L o\x`!OeF~mc?f̘/pVŶ;[lP`݌͒1lwk`|]y啃Z[|9x#Pl ]6ſ[t饗Np($\0; ,@ދFM Oy8g"lߚk@6Hhh-^GU96M^p=ztCI7M7 )x6_,SN9l^}VPt1Aǩ38Dbˠ;3q7`RElMQB {U\@=ztEx_f{2{ݵcin;[uLbhZ7|sSL1E}[۩"#bT?˭Ѡ:xɖ[nbT-Zo8sZ (b)|ދ1{1_|b{kqߍ8ARslʾ;x-O.]K.i=ʝ>^Pu9?bTw]I_>mv Bg[% +cXf/+BkkD2 Ο*Λ:#g/:ψ?01rҋ)~?5dhbŜxOѿ(67j>-TWWJ tI 'U8s[+9s+,=LW\QXV0u~3>"J1:X b׉x,5G@؜aeYZsbP.V ;^N Gs/_߱Qj/G̱Z}=3lNu][#1SOMr[ 7_k2F*[us0px]H12x-,:lꪓUX+c%",;W<-bX 6ؕmj<^lZA_:~z k( aj cp(~{}_tbrRb-bt0rSFʴi'n&:k2=v 2U7ojM@ڈGDZcxm X"0{,}_q-am3ϜbA@`%e @5B:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@`%e @5֨35 PF@XFI @@5LM!@QR P#`:SS @ee!@H@X @@98_tIENDB`golang-github-go-rod-rod-0.116.2/fixtures/iframe.html000066400000000000000000000001161520674146500223710ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/input.html000066400000000000000000000035151520674146500222730ustar00rootroot00000000000000













golang-github-go-rod-rod-0.116.2/fixtures/interactable.html000066400000000000000000000006641520674146500235730ustar00rootroot00000000000000
golang-github-go-rod-rod-0.116.2/fixtures/keys.html000066400000000000000000000007751520674146500221140ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/mouse-move.html000066400000000000000000000004331520674146500232240ustar00rootroot00000000000000
golang-github-go-rod-rod-0.116.2/fixtures/mouse-pointer.svg000066400000000000000000000027561520674146500236030ustar00rootroot00000000000000 mouse-pointer Created with Sketch. golang-github-go-rod-rod-0.116.2/fixtures/object-model.svg000066400000000000000000000255371520674146500233430ustar00rootroot00000000000000 object-model Created with Sketch. rod.Browser window incognito window rod.Page tab iframe web worker rod.Element DOM element shadow DOM proto.* Browser launcher.Launcher cdp.Client use proto lib to talk with cdp.Client use websocket to talk with browser launch golang-github-go-rod-rod-0.116.2/fixtures/open-page-subpage.html000066400000000000000000000001721520674146500244270ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/open-page.html000066400000000000000000000001611520674146500230010ustar00rootroot00000000000000 open page golang-github-go-rod-rod-0.116.2/fixtures/page-wait-stable.html000066400000000000000000000006031520674146500242550ustar00rootroot00000000000000
golang-github-go-rod-rod-0.116.2/fixtures/prevent-close.html000066400000000000000000000001671520674146500237220ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/resource.html000066400000000000000000000002531520674146500227570ustar00rootroot00000000000000 img
golang-github-go-rod-rod-0.116.2/fixtures/scroll-y.html000066400000000000000000000002201520674146500226660ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/scroll.html000066400000000000000000000002531520674146500224260ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/selector.html000066400000000000000000000002571520674146500227540ustar00rootroot00000000000000 01
golang-github-go-rod-rod-0.116.2/fixtures/shadow-dom.html000066400000000000000000000003771520674146500232010ustar00rootroot00000000000000
golang-github-go-rod-rod-0.116.2/fixtures/slow-render.html000066400000000000000000000003261520674146500233720ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/touch.html000066400000000000000000000017171520674146500222600ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/wait-stable.html000066400000000000000000000004301520674146500233410ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/wait_elements.html000066400000000000000000000005611520674146500237720ustar00rootroot00000000000000
  • list 1
  • list 2
  • list 3
  • list 4
  • list 5
golang-github-go-rod-rod-0.116.2/fixtures/worker.html000066400000000000000000000002211520674146500224340ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/fixtures/worker.js000066400000000000000000000000671520674146500221140ustar00rootroot00000000000000// echo message onmessage = (e) => postMessage(e.data) golang-github-go-rod-rod-0.116.2/go.mod000066400000000000000000000004511520674146500174770ustar00rootroot00000000000000module github.com/go-rod/rod go 1.21 require ( github.com/ysmood/fetchup v0.2.3 github.com/ysmood/goob v0.4.0 github.com/ysmood/got v0.40.0 github.com/ysmood/gotrace v0.6.0 github.com/ysmood/gson v0.7.3 github.com/ysmood/leakless v0.9.0 ) require github.com/ysmood/gop v0.2.0 // indirect golang-github-go-rod-rod-0.116.2/go.sum000066400000000000000000000022071520674146500175250ustar00rootroot00000000000000github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= golang-github-go-rod-rod-0.116.2/go.work000066400000000000000000000001521520674146500177000ustar00rootroot00000000000000go 1.22 use ( . ./lib/examples/custom-websocket ./lib/examples/e2e-testing ./lib/utils/check-issue ) golang-github-go-rod-rod-0.116.2/go.work.sum000066400000000000000000000005141520674146500205050ustar00rootroot00000000000000github.com/ysmood/fetchup v0.2.1 h1:n/NgIx92KOXFiKAhK3d+LlKpl8JuSjh5U27ULmHKtag= github.com/ysmood/fetchup v0.2.1/go.mod h1:94ROLWpn5fmCD4LPlcZ+LOE/iE/kRTU3kL+0ue/V+Os= github.com/ysmood/got v0.33.2 h1:mz0PaCMzR//YBtDDkDf6z0O09SfotXBHzw3zLrrS2sw= github.com/ysmood/got v0.33.2/go.mod h1:P3C/Wwttv4uq/tpovaH+c8ANmHePyFPxEbNzdxcEGDU= golang-github-go-rod-rod-0.116.2/hijack.go000066400000000000000000000241501520674146500201530ustar00rootroot00000000000000package rod import ( "bytes" "context" "io" "net/http" "net/url" "regexp" "strings" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/gson" ) // HijackRequests same as Page.HijackRequests, but can intercept requests of the entire browser. func (b *Browser) HijackRequests() *HijackRouter { return newHijackRouter(b, b).initEvents() } // HijackRequests creates a new router instance for requests hijacking. // When use Fetch domain outside the router should be stopped. Enabling hijacking disables page caching, // but such as 304 Not Modified will still work as expected. // The entire process of hijacking one request: // // browser --req-> rod ---> server ---> rod --res-> browser // // The --req-> and --res-> are the parts that can be modified. func (p *Page) HijackRequests() *HijackRouter { return newHijackRouter(p.browser, p).initEvents() } // HijackRouter context. type HijackRouter struct { run func() stop func() handlers []*hijackHandler enable *proto.FetchEnable client proto.Client browser *Browser } func newHijackRouter(browser *Browser, client proto.Client) *HijackRouter { return &HijackRouter{ enable: &proto.FetchEnable{}, browser: browser, client: client, handlers: []*hijackHandler{}, } } func (r *HijackRouter) initEvents() *HijackRouter { //nolint: gocognit ctx := r.browser.ctx if cta, ok := r.client.(proto.Contextable); ok { ctx = cta.GetContext() } var sessionID proto.TargetSessionID if tsa, ok := r.client.(proto.Sessionable); ok { sessionID = tsa.GetSessionID() } eventCtx, cancel := context.WithCancel(ctx) r.stop = cancel _ = r.enable.Call(r.client) r.run = r.browser.Context(eventCtx).eachEvent(sessionID, func(e *proto.FetchRequestPaused) bool { go func() { ctx := r.new(eventCtx, e) for _, h := range r.handlers { if !h.regexp.MatchString(e.Request.URL) { continue } h.handler(ctx) if ctx.continueRequest != nil { ctx.continueRequest.RequestID = e.RequestID err := ctx.continueRequest.Call(r.client) if err != nil { ctx.OnError(err) } return } if ctx.Skip { continue } if ctx.Response.fail.ErrorReason != "" { err := ctx.Response.fail.Call(r.client) if err != nil { ctx.OnError(err) } return } err := ctx.Response.payload.Call(r.client) if err != nil { ctx.OnError(err) return } } }() return false }) return r } // Add a hijack handler to router, the doc of the pattern is the same as "proto.FetchRequestPattern.URLPattern". func (r *HijackRouter) Add(pattern string, resourceType proto.NetworkResourceType, handler func(*Hijack)) error { r.enable.Patterns = append(r.enable.Patterns, &proto.FetchRequestPattern{ URLPattern: pattern, ResourceType: resourceType, }) reg := regexp.MustCompile(proto.PatternToReg(pattern)) r.handlers = append(r.handlers, &hijackHandler{ pattern: pattern, regexp: reg, handler: handler, }) return r.enable.Call(r.client) } // Remove handler via the pattern. func (r *HijackRouter) Remove(pattern string) error { patterns := []*proto.FetchRequestPattern{} handlers := []*hijackHandler{} for _, h := range r.handlers { if h.pattern != pattern { patterns = append(patterns, &proto.FetchRequestPattern{URLPattern: h.pattern}) handlers = append(handlers, h) } } r.enable.Patterns = patterns r.handlers = handlers return r.enable.Call(r.client) } // new context. func (r *HijackRouter) new(ctx context.Context, e *proto.FetchRequestPaused) *Hijack { headers := http.Header{} for k, v := range e.Request.Headers { headers[k] = []string{v.String()} } u, _ := url.Parse(e.Request.URL) req := &http.Request{ Method: e.Request.Method, URL: u, Body: io.NopCloser(strings.NewReader(e.Request.PostData)), Header: headers, } return &Hijack{ Request: &HijackRequest{ event: e, req: req.WithContext(ctx), }, Response: &HijackResponse{ payload: &proto.FetchFulfillRequest{ ResponseCode: 200, RequestID: e.RequestID, }, fail: &proto.FetchFailRequest{ RequestID: e.RequestID, }, }, OnError: func(_ error) {}, browser: r.browser, } } // Run the router, after you call it, you shouldn't add new handler to it. func (r *HijackRouter) Run() { r.run() } // Stop the router. func (r *HijackRouter) Stop() error { r.stop() return proto.FetchDisable{}.Call(r.client) } // hijackHandler to handle each request that match the regexp. type hijackHandler struct { pattern string regexp *regexp.Regexp handler func(*Hijack) } // Hijack context. type Hijack struct { Request *HijackRequest Response *HijackResponse OnError func(error) // Skip to next handler Skip bool continueRequest *proto.FetchContinueRequest // CustomState is used to store things for this context CustomState interface{} browser *Browser } // ContinueRequest without hijacking. The RequestID will be set by the router, you don't have to set it. func (h *Hijack) ContinueRequest(cq *proto.FetchContinueRequest) { h.continueRequest = cq } // LoadResponse will send request to the real destination and load the response as default response to override. func (h *Hijack) LoadResponse(client *http.Client, loadBody bool) error { res, err := client.Do(h.Request.req) if err != nil { return err } defer func() { _ = res.Body.Close() }() h.Response.payload.ResponseCode = res.StatusCode h.Response.RawResponse = res for k, vs := range res.Header { for _, v := range vs { h.Response.SetHeader(k, v) } } if loadBody { b, err := io.ReadAll(res.Body) if err != nil { return err } h.Response.payload.Body = b } return nil } // HijackRequest context. type HijackRequest struct { event *proto.FetchRequestPaused req *http.Request } // Type of the resource. func (ctx *HijackRequest) Type() proto.NetworkResourceType { return ctx.event.ResourceType } // Method of the request. func (ctx *HijackRequest) Method() string { return ctx.event.Request.Method } // URL of the request. func (ctx *HijackRequest) URL() *url.URL { u, _ := url.Parse(ctx.event.Request.URL) return u } // Header via a key. func (ctx *HijackRequest) Header(key string) string { return ctx.event.Request.Headers[key].String() } // Headers of request. func (ctx *HijackRequest) Headers() proto.NetworkHeaders { return ctx.event.Request.Headers } // Body of the request, devtools API doesn't support binary data yet, only string can be captured. func (ctx *HijackRequest) Body() string { return ctx.event.Request.PostData } // JSONBody of the request. func (ctx *HijackRequest) JSONBody() gson.JSON { return gson.NewFrom(ctx.Body()) } // Req returns the underlying http.Request instance that will be used to send the request. func (ctx *HijackRequest) Req() *http.Request { return ctx.req } // SetContext of the underlying http.Request instance. func (ctx *HijackRequest) SetContext(c context.Context) *HijackRequest { ctx.req = ctx.req.WithContext(c) return ctx } // SetBody of the request, if obj is []byte or string, raw body will be used, else it will be encoded as json. func (ctx *HijackRequest) SetBody(obj interface{}) *HijackRequest { var b []byte switch body := obj.(type) { case []byte: b = body case string: b = []byte(body) default: b = utils.MustToJSONBytes(body) } ctx.req.Body = io.NopCloser(bytes.NewBuffer(b)) return ctx } // IsNavigation determines whether the request is a navigation request. func (ctx *HijackRequest) IsNavigation() bool { return ctx.Type() == proto.NetworkResourceTypeDocument } // HijackResponse context. type HijackResponse struct { payload *proto.FetchFulfillRequest RawResponse *http.Response fail *proto.FetchFailRequest } // Payload to respond the request from the browser. func (ctx *HijackResponse) Payload() *proto.FetchFulfillRequest { return ctx.payload } // Body of the payload. func (ctx *HijackResponse) Body() string { return string(ctx.payload.Body) } // Headers returns the clone of response headers. // If you want to modify the response headers use HijackResponse.SetHeader . func (ctx *HijackResponse) Headers() http.Header { header := http.Header{} for _, h := range ctx.payload.ResponseHeaders { header.Add(h.Name, h.Value) } return header } // SetHeader of the payload via key-value pairs. func (ctx *HijackResponse) SetHeader(pairs ...string) *HijackResponse { for i := 0; i < len(pairs); i += 2 { ctx.payload.ResponseHeaders = append(ctx.payload.ResponseHeaders, &proto.FetchHeaderEntry{ Name: pairs[i], Value: pairs[i+1], }) } return ctx } // SetBody of the payload, if obj is []byte or string, raw body will be used, else it will be encoded as json. func (ctx *HijackResponse) SetBody(obj interface{}) *HijackResponse { switch body := obj.(type) { case []byte: ctx.payload.Body = body case string: ctx.payload.Body = []byte(body) default: ctx.payload.Body = utils.MustToJSONBytes(body) } return ctx } // Fail request. func (ctx *HijackResponse) Fail(reason proto.NetworkErrorReason) *HijackResponse { ctx.fail.ErrorReason = reason return ctx } // HandleAuth for the next basic HTTP authentication. // It will prevent the popup that requires user to input user name and password. // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication func (b *Browser) HandleAuth(username, password string) func() error { enable := b.DisableDomain("", &proto.FetchEnable{}) disable := b.EnableDomain("", &proto.FetchEnable{ HandleAuthRequests: true, }) paused := &proto.FetchRequestPaused{} auth := &proto.FetchAuthRequired{} ctx, cancel := context.WithCancel(b.ctx) waitPaused := b.Context(ctx).WaitEvent(paused) waitAuth := b.Context(ctx).WaitEvent(auth) return func() (err error) { defer enable() defer disable() defer cancel() waitPaused() err = proto.FetchContinueRequest{ RequestID: paused.RequestID, }.Call(b) if err != nil { return } waitAuth() err = proto.FetchContinueWithAuth{ RequestID: auth.RequestID, AuthChallengeResponse: &proto.FetchAuthChallengeResponse{ Response: proto.FetchAuthChallengeResponseResponseProvideCredentials, Username: username, Password: password, }, }.Call(b) return } } golang-github-go-rod-rod-0.116.2/hijack_test.go000066400000000000000000000203001520674146500212030ustar00rootroot00000000000000package rod_test import ( "context" "errors" "io" "mime" "net/http" "sync" "testing" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/gson" ) func TestHijack(t *testing.T) { g := setup(t) s := g.Serve() // to simulate a backend server s.Route("/", slash("fixtures/fetch.html")) s.Mux.HandleFunc("/a", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { panic("wrong http method") } g.Eq("header", r.Header.Get("Test")) b, err := io.ReadAll(r.Body) g.E(err) g.Eq("a", string(b)) g.HandleHTTP(".html", "test")(w, r) }) s.Route("/b", "", "b") router := g.page.HijackRequests() defer router.MustStop() router.MustAdd(s.URL("/a"), func(ctx *rod.Hijack) { r := ctx.Request.SetContext(g.Context()) r.Req().Header.Set("Test", "header") // override request header r.SetBody([]byte("test")) // override request body r.SetBody(123) // override request body r.SetBody(r.Body()) // override request body type MyState struct { Val int } ctx.CustomState = &MyState{10} g.Eq(http.MethodPost, r.Method()) g.Eq(s.URL("/a"), r.URL().String()) g.Eq(proto.NetworkResourceTypeXHR, ctx.Request.Type()) g.Is(ctx.Request.IsNavigation(), false) g.Has(s.URL(), ctx.Request.Header("Origin")) g.Len(ctx.Request.Headers(), 6) g.True(ctx.Request.JSONBody().Nil()) // send request load response from real destination as the default value to hijack ctx.MustLoadResponse() g.Eq(200, ctx.Response.Payload().ResponseCode) // override status code ctx.Response.Payload().ResponseCode = http.StatusCreated g.Eq("4", ctx.Response.Headers().Get("Content-Length")) g.Has(ctx.Response.Headers().Get("Content-Type"), "text/html; charset=utf-8") // override response header ctx.Response.SetHeader("Set-Cookie", "key=val") // override response body ctx.Response.SetBody([]byte("test")) ctx.Response.SetBody("test") ctx.Response.SetBody(map[string]string{ "text": "test", }) g.Eq("{\"text\":\"test\"}", ctx.Response.Body()) }) router.MustAdd(s.URL("/b"), func(_ *rod.Hijack) { panic("should not come to here") }) router.MustRemove(s.URL("/b")) router.MustAdd(s.URL("/b"), func(ctx *rod.Hijack) { // transparent proxy ctx.MustLoadResponse() }) go router.Run() g.page.MustNavigate(s.URL()) g.Eq("201 test key=val", g.page.MustElement("#a").MustText()) g.Eq("b", g.page.MustElement("#b").MustText()) } func TestHijackContinue(t *testing.T) { g := setup(t) s := g.Serve().Route("/", ".html", `ok`) router := g.page.HijackRequests() defer router.MustStop() wg := &sync.WaitGroup{} wg.Add(1) router.MustAdd(s.URL("/a"), func(ctx *rod.Hijack) { ctx.ContinueRequest(&proto.FetchContinueRequest{}) wg.Done() }) go router.Run() g.page.MustNavigate(s.URL("/a")) g.Eq("ok", g.page.MustElement("body").MustText()) wg.Wait() } func TestHijackMockWholeResponseEmptyBody(t *testing.T) { g := setup(t) router := g.page.HijackRequests() defer router.MustStop() router.MustAdd("*", func(ctx *rod.Hijack) { ctx.Response.SetBody("") }) go router.Run() // needs to timeout or will hang when "omitempty" does not get removed from body in fulfillRequest timed := g.page.Timeout(time.Second) timed.MustNavigate(g.Serve().Route("/", ".txt", "OK").URL()) g.Eq("", g.page.MustElement("body").MustText()) } func TestHijackMockWholeResponseNoBody(t *testing.T) { // TODO: remove the skip t.Skip("Because of flaky test result") g := setup(t) router := g.page.HijackRequests() defer router.MustStop() // intercept and reply without setting a body router.MustAdd("*", func(_ *rod.Hijack) { // we don't set any body here }) go router.Run() // has to timeout as it will lock up the browser reading the reply. err := g.page.Timeout(time.Second).Navigate(g.Serve().Route("/", "").URL()) g.Is(err, context.DeadlineExceeded) } func TestHijackMockWholeResponse(t *testing.T) { g := setup(t) router := g.page.HijackRequests() defer router.MustStop() router.MustAdd("*", func(ctx *rod.Hijack) { ctx.Response.SetHeader("Content-Type", mime.TypeByExtension(".html")) ctx.Response.SetBody("ok") }) go router.Run() g.page.MustNavigate("http://localhost") g.Eq("ok", g.page.MustElement("body").MustText()) } func TestHijackSkip(t *testing.T) { g := setup(t) s := g.Serve() router := g.page.HijackRequests() defer router.MustStop() wg := &sync.WaitGroup{} wg.Add(2) router.MustAdd(s.URL("/a"), func(ctx *rod.Hijack) { ctx.Skip = true wg.Done() }) router.MustAdd(s.URL("/a"), func(ctx *rod.Hijack) { ctx.ContinueRequest(&proto.FetchContinueRequest{}) wg.Done() }) go router.Run() g.page.MustNavigate(s.URL("/a")) wg.Wait() } func TestHijackOnErrorLog(t *testing.T) { g := setup(t) s := g.Serve().Route("/", ".html", `ok`) router := g.page.HijackRequests() defer router.MustStop() wg := &sync.WaitGroup{} wg.Add(1) var err error router.MustAdd(s.URL("/a"), func(ctx *rod.Hijack) { ctx.OnError = func(e error) { err = e wg.Done() } ctx.ContinueRequest(&proto.FetchContinueRequest{}) }) go router.Run() g.mc.stub(1, proto.FetchContinueRequest{}, func(_ StubSend) (gson.JSON, error) { return gson.New(nil), errors.New("err") }) go func() { _ = g.page.Context(g.Context()).Navigate(s.URL("/a")) }() wg.Wait() g.Eq(err.Error(), "err") } func TestHijackFailRequest(t *testing.T) { g := setup(t) s := g.Serve().Route("/page", ".html", ` `) router := g.browser.HijackRequests() defer router.MustStop() router.MustAdd(s.URL("/a"), func(ctx *rod.Hijack) { ctx.Response.Fail(proto.NetworkErrorReasonAborted) }) go router.Run() g.page.MustNavigate(s.URL("/page")).MustWaitLoad() g.page.MustWait(`() => document.title === 'Failed to fetch'`) { // test error log g.mc.stub(1, proto.FetchFailRequest{}, func(send StubSend) (gson.JSON, error) { _, _ = send() return gson.JSON{}, errors.New("err") }) _ = g.page.Navigate(s.URL("/a")) } } func TestHijackLoadResponseErr(t *testing.T) { g := setup(t) p := g.newPage().Context(g.Context()) router := p.HijackRequests() defer router.MustStop() wg := &sync.WaitGroup{} wg.Add(1) router.MustAdd("http://localhost/a", func(ctx *rod.Hijack) { g.Err(ctx.LoadResponse(&http.Client{ Transport: &MockRoundTripper{err: errors.New("err")}, }, true)) g.Err(ctx.LoadResponse(&http.Client{ Transport: &MockRoundTripper{res: &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(&MockReader{err: errors.New("err")}), }}, }, true)) wg.Done() ctx.Response.Fail(proto.NetworkErrorReasonAborted) }) go router.Run() _ = p.Navigate("http://localhost/a") wg.Wait() } func TestHijackResponseErr(t *testing.T) { g := setup(t) s := g.Serve().Route("/", ".html", `ok`) p := g.newPage().Context(g.Context()) router := p.HijackRequests() defer router.MustStop() wg := &sync.WaitGroup{} wg.Add(1) router.MustAdd(s.URL("/a"), func(ctx *rod.Hijack) { // to ignore favicon ctx.OnError = func(err error) { g.Err(err) wg.Done() } ctx.MustLoadResponse() g.mc.stub(1, proto.FetchFulfillRequest{}, func(send StubSend) (gson.JSON, error) { res, _ := send() return res, errors.New("err") }) }) go router.Run() p.MustNavigate(s.URL("/a")) wg.Wait() } func TestHandleAuth(t *testing.T) { g := setup(t) s := g.Serve() // mock the server s.Mux.HandleFunc("/a", func(w http.ResponseWriter, r *http.Request) { u, p, ok := r.BasicAuth() if !ok { w.Header().Add("WWW-Authenticate", `Basic realm="web"`) w.WriteHeader(http.StatusUnauthorized) return } g.Eq("a", u) g.Eq("b", p) g.HandleHTTP(".html", `

ok

`)(w, r) }) s.Route("/err", ".html", "err page") go g.browser.MustHandleAuth("a", "b")() page := g.newPage(s.URL("/a")) page.MustElementR("p", "ok") wait := g.browser.HandleAuth("a", "b") var page2 *rod.Page wait2 := utils.All(func() { page2, _ = g.browser.Page(proto.TargetCreateTarget{URL: s.URL("/err")}) }) g.mc.stubErr(1, proto.FetchContinueRequest{}) g.Err(wait()) wait2() page2.MustClose() } golang-github-go-rod-rod-0.116.2/input.go000066400000000000000000000243371520674146500200700ustar00rootroot00000000000000package rod import ( "fmt" "sync" "github.com/go-rod/rod/lib/input" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/gson" ) // Keyboard represents the keyboard on a page, it's always related the main frame. type Keyboard struct { sync.Mutex page *Page // pressed keys must be released before it can be pressed again pressed map[input.Key]struct{} } func (p *Page) newKeyboard() *Page { p.Keyboard = &Keyboard{page: p, pressed: map[input.Key]struct{}{}} return p } func (k *Keyboard) getModifiers() int { k.Lock() defer k.Unlock() return k.modifiers() } func (k *Keyboard) modifiers() int { ms := 0 for key := range k.pressed { ms |= key.Modifier() } return ms } // Press the key down. // To input characters that are not on the keyboard, such as Chinese or Japanese, you should // use method like [Page.InsertText]. func (k *Keyboard) Press(key input.Key) error { defer k.page.tryTrace(TraceTypeInput, "press key: "+key.Info().Code)() k.page.browser.trySlowMotion() k.Lock() defer k.Unlock() k.pressed[key] = struct{}{} return key.Encode(proto.InputDispatchKeyEventTypeKeyDown, k.modifiers()).Call(k.page) } // Release the key. func (k *Keyboard) Release(key input.Key) error { defer k.page.tryTrace(TraceTypeInput, "release key: "+key.Info().Code)() k.Lock() defer k.Unlock() if _, has := k.pressed[key]; !has { return nil } delete(k.pressed, key) return key.Encode(proto.InputDispatchKeyEventTypeKeyUp, k.modifiers()).Call(k.page) } // Type releases the key after the press. func (k *Keyboard) Type(keys ...input.Key) (err error) { for _, key := range keys { err = k.Press(key) if err != nil { return } err = k.Release(key) if err != nil { return } } return } // KeyActionType enum. type KeyActionType int // KeyActionTypes. const ( KeyActionPress KeyActionType = iota KeyActionRelease KeyActionTypeKey ) // KeyAction to perform. type KeyAction struct { Type KeyActionType Key input.Key } // KeyActions to simulate. type KeyActions struct { keyboard *Keyboard Actions []KeyAction } // KeyActions simulates the type actions on a physical keyboard. // Useful when input shortcuts like ctrl+enter . func (p *Page) KeyActions() *KeyActions { return &KeyActions{keyboard: p.Keyboard} } // Press keys is guaranteed to have a release at the end of actions. func (ka *KeyActions) Press(keys ...input.Key) *KeyActions { for _, key := range keys { ka.Actions = append(ka.Actions, KeyAction{KeyActionPress, key}) } return ka } // Release keys. func (ka *KeyActions) Release(keys ...input.Key) *KeyActions { for _, key := range keys { ka.Actions = append(ka.Actions, KeyAction{KeyActionRelease, key}) } return ka } // Type will release the key immediately after the pressing. func (ka *KeyActions) Type(keys ...input.Key) *KeyActions { for _, key := range keys { ka.Actions = append(ka.Actions, KeyAction{KeyActionTypeKey, key}) } return ka } // Do the actions. func (ka *KeyActions) Do() (err error) { for _, a := range ka.balance() { switch a.Type { case KeyActionPress: err = ka.keyboard.Press(a.Key) case KeyActionRelease: err = ka.keyboard.Release(a.Key) case KeyActionTypeKey: err = ka.keyboard.Type(a.Key) } if err != nil { return } } return } // Make sure there's at least one release after the presses, such as: // // p1,p2,p1,r1 => p1,p2,p1,r1,r2 func (ka *KeyActions) balance() []KeyAction { actions := ka.Actions h := map[input.Key]bool{} for _, a := range actions { switch a.Type { case KeyActionPress: h[a.Key] = true case KeyActionRelease, KeyActionTypeKey: h[a.Key] = false } } for key, needRelease := range h { if needRelease { actions = append(actions, KeyAction{KeyActionRelease, key}) } } return actions } // InsertText is like pasting text into the page. func (p *Page) InsertText(text string) error { defer p.tryTrace(TraceTypeInput, "insert text "+text)() p.browser.trySlowMotion() err := proto.InputInsertText{Text: text}.Call(p) return err } // Mouse represents the mouse on a page, it's always related the main frame. type Mouse struct { sync.Mutex page *Page id string // mouse svg dom element id pos proto.Point // the buttons is currently being pressed, reflects the press order buttons []proto.InputMouseButton } func (p *Page) newMouse() *Page { p.Mouse = &Mouse{page: p, id: utils.RandString(8)} return p } // Position of current cursor. func (m *Mouse) Position() proto.Point { m.Lock() defer m.Unlock() return m.pos } // MoveTo the absolute position. func (m *Mouse) MoveTo(p proto.Point) error { m.Lock() defer m.Unlock() button, buttons := input.EncodeMouseButton(m.buttons) m.page.browser.trySlowMotion() err := proto.InputDispatchMouseEvent{ Type: proto.InputDispatchMouseEventTypeMouseMoved, X: p.X, Y: p.Y, Button: button, Buttons: gson.Int(buttons), Modifiers: m.page.Keyboard.getModifiers(), }.Call(m.page) if err != nil { return err } // to make sure set only when call is successful m.pos = p if m.page.browser.trace { if !m.updateMouseTracer() { m.initMouseTracer() m.updateMouseTracer() } } return nil } // MoveAlong the guide function. // Every time the guide function is called it should return the next mouse position, return true to stop. // Read the source code of [Mouse.MoveLinear] as an example to use this method. func (m *Mouse) MoveAlong(guide func() (proto.Point, bool)) error { for { p, stop := guide() if stop { return m.MoveTo(p) } err := m.MoveTo(p) if err != nil { return err } } } // MoveLinear to the absolute position with the given steps. // Such as move from (0,0) to (6,6) with 3 steps, the mouse will first move to (2,2) then (4,4) then (6,6). func (m *Mouse) MoveLinear(to proto.Point, steps int) error { p := m.Position() step := to.Minus(p).Scale(1 / float64(steps)) count := 0 return m.MoveAlong(func() (proto.Point, bool) { count++ if count == steps { return to, true } p = p.Add(step) return p, false }) } // Scroll the relative offset with specified steps. func (m *Mouse) Scroll(offsetX, offsetY float64, steps int) error { m.Lock() defer m.Unlock() defer m.page.tryTrace(TraceTypeInput, fmt.Sprintf("scroll (%.2f, %.2f)", offsetX, offsetY))() m.page.browser.trySlowMotion() if steps < 1 { steps = 1 } button, buttons := input.EncodeMouseButton(m.buttons) stepX := offsetX / float64(steps) stepY := offsetY / float64(steps) for i := 0; i < steps; i++ { err := proto.InputDispatchMouseEvent{ Type: proto.InputDispatchMouseEventTypeMouseWheel, Button: button, Buttons: gson.Int(buttons), Modifiers: m.page.Keyboard.getModifiers(), DeltaX: stepX, DeltaY: stepY, X: m.pos.X, Y: m.pos.Y, }.Call(m.page) if err != nil { return err } } return nil } // Down holds the button down. func (m *Mouse) Down(button proto.InputMouseButton, clickCount int) error { m.Lock() defer m.Unlock() toButtons := append(append([]proto.InputMouseButton{}, m.buttons...), button) _, buttons := input.EncodeMouseButton(toButtons) err := proto.InputDispatchMouseEvent{ Type: proto.InputDispatchMouseEventTypeMousePressed, Button: button, Buttons: gson.Int(buttons), ClickCount: clickCount, Modifiers: m.page.Keyboard.getModifiers(), X: m.pos.X, Y: m.pos.Y, }.Call(m.page) if err != nil { return err } m.buttons = toButtons return nil } // Up releases the button. func (m *Mouse) Up(button proto.InputMouseButton, clickCount int) error { m.Lock() defer m.Unlock() toButtons := []proto.InputMouseButton{} for _, btn := range m.buttons { if btn == button { continue } toButtons = append(toButtons, btn) } _, buttons := input.EncodeMouseButton(toButtons) err := proto.InputDispatchMouseEvent{ Type: proto.InputDispatchMouseEventTypeMouseReleased, Button: button, Buttons: gson.Int(buttons), ClickCount: clickCount, Modifiers: m.page.Keyboard.getModifiers(), X: m.pos.X, Y: m.pos.Y, }.Call(m.page) if err != nil { return err } m.buttons = toButtons return nil } // Click the button. It's the combination of [Mouse.Down] and [Mouse.Up]. func (m *Mouse) Click(button proto.InputMouseButton, clickCount int) error { m.page.browser.trySlowMotion() err := m.Down(button, clickCount) if err != nil { return err } return m.Up(button, clickCount) } // Touch presents a touch device, such as a hand with fingers, each finger is a [proto.InputTouchPoint]. // Touch events is stateless, we use the struct here only as a namespace to make the API style unified. type Touch struct { page *Page } func (p *Page) newTouch() *Page { p.Touch = &Touch{page: p} return p } // Start a touch action. func (t *Touch) Start(points ...*proto.InputTouchPoint) error { // TODO: https://crbug.com/613219 _ = t.page.WaitRepaint() _ = t.page.WaitRepaint() return proto.InputDispatchTouchEvent{ Type: proto.InputDispatchTouchEventTypeTouchStart, TouchPoints: points, Modifiers: t.page.Keyboard.getModifiers(), }.Call(t.page) } // Move touch points. Use the [proto.InputTouchPoint.ID] (Touch.identifier) to track points. // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Touch_events func (t *Touch) Move(points ...*proto.InputTouchPoint) error { return proto.InputDispatchTouchEvent{ Type: proto.InputDispatchTouchEventTypeTouchMove, TouchPoints: points, Modifiers: t.page.Keyboard.getModifiers(), }.Call(t.page) } // End touch action. func (t *Touch) End() error { return proto.InputDispatchTouchEvent{ Type: proto.InputDispatchTouchEventTypeTouchEnd, TouchPoints: []*proto.InputTouchPoint{}, Modifiers: t.page.Keyboard.getModifiers(), }.Call(t.page) } // Cancel touch action. func (t *Touch) Cancel() error { return proto.InputDispatchTouchEvent{ Type: proto.InputDispatchTouchEventTypeTouchCancel, TouchPoints: []*proto.InputTouchPoint{}, Modifiers: t.page.Keyboard.getModifiers(), }.Call(t.page) } // Tap dispatches a touchstart and touchend event. func (t *Touch) Tap(x, y float64) error { defer t.page.tryTrace(TraceTypeInput, "touch")() t.page.browser.trySlowMotion() p := &proto.InputTouchPoint{X: x, Y: y} err := t.Start(p) if err != nil { return err } return t.End() } golang-github-go-rod-rod-0.116.2/input_test.go000066400000000000000000000153501520674146500211220ustar00rootroot00000000000000package rod_test import ( "testing" "github.com/go-rod/rod/lib/devices" "github.com/go-rod/rod/lib/input" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" ) func TestKeyActions(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/keys.html")) body := p.MustElement("body") p.KeyActions().Press(input.ControlLeft).Type(input.Enter).MustDo() g.Eq(body.MustText(), `↓ "Control" ControlLeft 17 modifiers(ctrl) ↓ "Enter" Enter 13 modifiers(ctrl) ↑ "Enter" Enter 13 modifiers(ctrl) ↑ "Control" ControlLeft 17 modifiers() `) body.MustEval("() => this.innerText = ''") body.MustKeyActions(). Press(input.ShiftLeft).Type('A', 'X').Release(input.ShiftLeft). Type('a').MustDo() g.Eq(body.MustText(), `↓ "Shift" ShiftLeft 16 modifiers(shift) ↓ "A" KeyA 65 modifiers(shift) ↑ "A" KeyA 65 modifiers(shift) ↓ "X" KeyX 88 modifiers(shift) ↑ "X" KeyX 88 modifiers(shift) ↑ "Shift" ShiftLeft 16 modifiers() ↓ "a" KeyA 65 modifiers() ↑ "a" KeyA 65 modifiers() `) g.Nil(p.Keyboard.Release('a')) } func TestKeyType(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) el := p.MustElement("[type=text]") el.MustKeyActions().Type('1', '2', input.Backspace, ' ').MustDo() el.MustKeyActions().Type('A', ' ', 'b').MustDo() p.MustInsertText(" test") p.Keyboard.MustType(input.Tab) g.Eq("1 A b test", el.MustText()) } func TestKeyTypeErr(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/keys.html")) body := p.MustElement("body") g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) g.Err(body.Type('a')) g.mc.stubErr(1, proto.InputDispatchKeyEvent{}) g.Err(p.Keyboard.Type('a')) g.mc.stubErr(2, proto.InputDispatchKeyEvent{}) g.Err(p.Keyboard.Type('a')) g.mc.stubErr(1, proto.InputDispatchKeyEvent{}) g.Err(p.KeyActions().Press('a').Do()) } func TestInput(t *testing.T) { g := setup(t) text := "雲の上は\nいつも晴れ" p := g.page.MustNavigate(g.srcFile("fixtures/input.html")) { el := p.MustElement("[contenteditable=true]").MustInput(text) g.Eq(text, el.MustText()) } el := p.MustElement("textarea") el.MustInput(text) g.Eq(text, el.MustText()) g.True(p.MustHas("[event=textarea-change]")) g.Panic(func() { g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) el.MustText() }) g.Panic(func() { g.mc.stubErr(4, proto.RuntimeCallFunctionOn{}) el.MustInput("") }) g.Panic(func() { g.mc.stubErr(5, proto.RuntimeCallFunctionOn{}) el.MustInput("") }) g.Panic(func() { g.mc.stubErr(6, proto.RuntimeCallFunctionOn{}) el.MustInput("") }) g.Panic(func() { g.mc.stubErr(1, proto.InputInsertText{}) el.MustInput("") }) } func TestMouse(t *testing.T) { g := setup(t) page := g.page.MustNavigate(g.srcFile("fixtures/click.html")) page.MustElement("button") mouse := page.Mouse mouse.MustScroll(0, 10) mouse.MustMoveTo(140, 160) mouse.MustDown("left") mouse.MustUp("left") g.True(page.MustHas("[a=ok]")) g.Panic(func() { g.mc.stubErr(1, proto.InputDispatchMouseEvent{}) mouse.MustScroll(0, 10) }) g.Panic(func() { g.mc.stubErr(1, proto.InputDispatchMouseEvent{}) mouse.MustDown(proto.InputMouseButtonLeft) }) g.Panic(func() { g.mc.stubErr(1, proto.InputDispatchMouseEvent{}) mouse.MustUp(proto.InputMouseButtonLeft) }) g.Panic(func() { g.mc.stubErr(1, proto.InputDispatchMouseEvent{}) mouse.MustClick(proto.InputMouseButtonLeft) }) } func TestMouseHoldMultiple(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.blank()) p.Mouse.MustDown("left") defer p.Mouse.MustUp("left") p.Mouse.MustDown("right") defer p.Mouse.MustUp("right") } func TestMouseClick(t *testing.T) { g := setup(t) g.browser.SlowMotion(1) defer func() { g.browser.SlowMotion(0) }() page := g.page.MustNavigate(g.srcFile("fixtures/click.html")) page.MustElement("button") mouse := page.Mouse mouse.MustMoveTo(140, 160) mouse.MustClick("left") g.True(page.MustHas("[a=ok]")) } func TestMouseDoubleClick(t *testing.T) { g := setup(t) g.browser.SlowMotion(1) defer func() { g.browser.SlowMotion(0) }() page := g.page.MustNavigate(g.srcFile("fixtures/double-click.html")) el := page.MustElement("button") el.MustDoubleClick() g.Eq(el.MustText(), "ok") } func TestMouseDrag(t *testing.T) { g := setup(t) page := g.newPage().MustNavigate(g.srcFile("fixtures/drag.html")).MustWaitLoad() mouse := page.Mouse mouse.MustMoveTo(3, 3) mouse.MustDown("left") g.E(mouse.MoveLinear(proto.NewPoint(60, 80), 3)) mouse.MustUp("left") utils.Sleep(0.3) g.Eq(page.MustEval(`() => dragTrack`).Str(), " move 3 3 down 3 3 move 22 28 move 41 54 move 60 80 up 60 80") } func TestMouseScroll(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/scroll.html")).MustWaitLoad() p.Mouse.MustMoveTo(30, 30) p.Mouse.MustClick(proto.InputMouseButtonLeft) p.Mouse.MustScroll(0, 10) p.Mouse.MustScroll(100, 190) g.E(p.Mouse.Scroll(200, 300, 5)) p.MustWait(`() => pageXOffset > 200 && pageYOffset > 300`) } func TestMouseMoveLinear(t *testing.T) { g := setup(t) page := g.newPage().MustNavigate(g.srcFile("fixtures/mouse-move.html")).MustWaitLoad() mouse := page.Mouse mouse.MustMoveTo(1, 2) g.E(mouse.MoveLinear(proto.NewPoint(3, 4), 3)) utils.Sleep(0.3) g.Eq(page.MustEval(`() => moveTrack`).Str(), " move 1 2 move 1 2 move 2 3 move 3 4") } func TestMouseMoveErr(t *testing.T) { g := setup(t) p := g.page.MustNavigate(g.srcFile("fixtures/click.html")) g.mc.stubErr(2, proto.InputDispatchMouseEvent{}) g.Err(p.Mouse.MoveLinear(proto.NewPoint(10, 10), 3)) } func TestNativeDrag(t *testing.T) { // devtools doesn't support to use mouse event to simulate it for now t.Skip() g := setup(t) page := g.page.MustNavigate(g.srcFile("fixtures/drag.html")) mouse := page.Mouse pt := page.MustElement("#draggable").MustShape().OnePointInside() toY := page.MustElement(".dropzone:nth-child(2)").MustShape().OnePointInside().Y page.Overlay(pt.X, pt.Y, 10, 10, "from") page.Overlay(pt.X, toY, 10, 10, "to") mouse.MustMoveTo(pt.X, pt.Y) mouse.MustDown("left") g.E(mouse.MoveLinear(proto.NewPoint(pt.X, toY), 5)) page.MustScreenshot("") mouse.MustUp("left") page.MustElement(".dropzone:nth-child(2) #draggable") } func TestTouch(t *testing.T) { g := setup(t) page := g.newPage().MustEmulate(devices.IPad) wait := page.WaitNavigation(proto.PageLifecycleEventNameLoad) page.MustNavigate(g.srcFile("fixtures/touch.html")) wait() touch := page.Touch touch.MustTap(10, 20) p := &proto.InputTouchPoint{X: 30, Y: 40} touch.MustStart(p).MustEnd() touch.MustStart(p) p.MoveTo(50, 60) touch.MustMove(p).MustCancel() page.MustWait(`() => touchTrack == ' start 10 20 end start 30 40 end start 30 40 move 50 60 cancel'`) g.Panic(func() { g.mc.stubErr(1, proto.InputDispatchTouchEvent{}) touch.MustTap(1, 2) }) } golang-github-go-rod-rod-0.116.2/lib/000077500000000000000000000000001520674146500171375ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/assets/000077500000000000000000000000001520674146500204415ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/assets/README.md000066400000000000000000000000471520674146500217210ustar00rootroot00000000000000# Assets Static files for the project golang-github-go-rod-rod-0.116.2/lib/assets/assets.go000066400000000000000000000123651520674146500223010ustar00rootroot00000000000000// Package assets is generated by "lib/assets/generate" package assets // MousePointer for rod. const MousePointer = ` mouse-pointer Created with Sketch. ` // Monitor for rod. const Monitor = ` Rod Monitor - Pages

Choose a Page to Monitor

` // MonitorPage for rod. const MonitorPage = `

    
  
  

`
golang-github-go-rod-rod-0.116.2/lib/assets/generate/000077500000000000000000000000001520674146500222335ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/assets/generate/main.go000066400000000000000000000013721520674146500235110ustar00rootroot00000000000000// Package main ...
package main

import (
	"path/filepath"

	"github.com/go-rod/rod/lib/utils"
)

var slash = filepath.FromSlash

func main() {
	build := utils.S(`// Package assets is generated by "lib/assets/generate"
package assets

// MousePointer for rod
const MousePointer = {{.mousePointer}}

// Monitor for rod
const Monitor = {{.monitor}}

// MonitorPage for rod
const MonitorPage = {{.monitorPage}}
`,
		"mousePointer", get("../../fixtures/mouse-pointer.svg"),
		"monitor", get("monitor.html"),
		"monitorPage", get("monitor-page.html"),
	)

	utils.E(utils.OutputFile(slash("lib/assets/assets.go"), build))
}

func get(path string) string {
	code, err := utils.ReadString(slash("lib/assets/" + path))
	utils.E(err)
	return utils.EscapeGoString(code)
}
golang-github-go-rod-rod-0.116.2/lib/assets/monitor-page.html000066400000000000000000000046321520674146500237350ustar00rootroot00000000000000
  
    
  
  
    
    

    
  
  

golang-github-go-rod-rod-0.116.2/lib/assets/monitor.html000066400000000000000000000021141520674146500230140ustar00rootroot00000000000000
  
    Rod Monitor - Pages
    
  
  
    

Choose a Page to Monitor

golang-github-go-rod-rod-0.116.2/lib/benchmark/000077500000000000000000000000001520674146500210715ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/benchmark/basic_test.go000066400000000000000000000012501520674146500235360ustar00rootroot00000000000000// Example run: // go test -bench . ./lib/benchmark package main_test import ( "path/filepath" "testing" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/got" ) func BenchmarkCleanup(b *testing.B) { u := got.New(b).Serve().Route("/", "", "page body").URL("/") b.RunParallel(func(pb *testing.PB) { for pb.Next() { launch := launcher.New().UserDataDir(filepath.Join("tmp", "cleanup", utils.RandString(8))) b.Cleanup(launch.Cleanup) url := launch.MustLaunch() browser := rod.New().ControlURL(url).MustConnect() b.Cleanup(browser.MustClose) browser.MustPage(u).MustClose() } }) } golang-github-go-rod-rod-0.116.2/lib/cdp/000077500000000000000000000000001520674146500177055ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/cdp/README.md000066400000000000000000000005241520674146500211650ustar00rootroot00000000000000# Overview This client is directly based on this [doc](https://chromedevtools.github.io/devtools-protocol/). You can treat it as a minimal example of how to use the DevTools Protocol, no complex abstraction. It's thread-safe, and context first. For basic usage, check this [file](example_test.go). For more info, check the unit tests. golang-github-go-rod-rod-0.116.2/lib/cdp/client.go000066400000000000000000000074421520674146500215210ustar00rootroot00000000000000// Package cdp for application layer communication with browser. package cdp import ( "context" "encoding/json" "sync" "sync/atomic" "github.com/go-rod/rod/lib/defaults" "github.com/go-rod/rod/lib/utils" ) // Request to send to browser. type Request struct { ID int `json:"id"` SessionID string `json:"sessionId,omitempty"` Method string `json:"method"` Params interface{} `json:"params,omitempty"` } // Response from browser. type Response struct { ID int `json:"id"` Result json.RawMessage `json:"result,omitempty"` Error *Error `json:"error,omitempty"` } // Event from browser. type Event struct { SessionID string `json:"sessionId,omitempty"` Method string `json:"method"` Params json.RawMessage `json:"params,omitempty"` } // WebSocketable enables you to choose the websocket lib you want to use. // Such as you can easily wrap gorilla/websocket and use it as the transport layer. type WebSocketable interface { // Send text message only Send(data []byte) error // Read returns text message only Read() ([]byte, error) } // Client is a devtools protocol connection instance. type Client struct { count uint64 ws WebSocketable pending sync.Map // pending requests event chan *Event // events from browser logger utils.Logger } // New creates a cdp connection, all messages from Client.Event must be received or they will block the client. func New() *Client { return &Client{ event: make(chan *Event), logger: defaults.CDP, } } // Logger sets the logger to log all the requests, responses, and events transferred between Rod and the browser. // The default format for each type is in file format.go. func (cdp *Client) Logger(l utils.Logger) *Client { cdp.logger = l return cdp } // Start to browser. func (cdp *Client) Start(ws WebSocketable) *Client { cdp.ws = ws go cdp.consumeMessages() return cdp } type result struct { msg json.RawMessage err error } // Call a method and wait for its response. func (cdp *Client) Call(ctx context.Context, sessionID, method string, params interface{}) ([]byte, error) { req := &Request{ ID: int(atomic.AddUint64(&cdp.count, 1)), SessionID: sessionID, Method: method, Params: params, } cdp.logger.Println(req) data, err := json.Marshal(req) utils.E(err) done := make(chan result) once := sync.Once{} cdp.pending.Store(req.ID, func(res result) { once.Do(func() { select { case <-ctx.Done(): case done <- res: } }) }) defer cdp.pending.Delete(req.ID) err = cdp.ws.Send(data) if err != nil { return nil, err } select { case <-ctx.Done(): return nil, ctx.Err() case res := <-done: return res.msg, res.err } } // Event returns a channel that will emit browser devtools protocol events. Must be consumed or will block producer. func (cdp *Client) Event() <-chan *Event { return cdp.event } // Consume messages coming from the browser via the websocket. func (cdp *Client) consumeMessages() { defer close(cdp.event) for { data, err := cdp.ws.Read() if err != nil { cdp.pending.Range(func(_, val interface{}) bool { val.(func(result))(result{err: err}) //nolint: forcetypeassert return true }) return } var id struct { ID int `json:"id"` } err = json.Unmarshal(data, &id) utils.E(err) if id.ID == 0 { var evt Event err := json.Unmarshal(data, &evt) utils.E(err) cdp.logger.Println(&evt) cdp.event <- &evt continue } var res Response err = json.Unmarshal(data, &res) utils.E(err) cdp.logger.Println(&res) val, ok := cdp.pending.Load(id.ID) if !ok { continue } if res.Error == nil { val.(func(result))(result{res.Result, nil}) //nolint: forcetypeassert } else { val.(func(result))(result{nil, res.Error}) //nolint: forcetypeassert } } } golang-github-go-rod-rod-0.116.2/lib/cdp/client_test.go000066400000000000000000000165661520674146500225670ustar00rootroot00000000000000package cdp_test import ( "context" "encoding/json" "fmt" "io" "path/filepath" "testing" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/defaults" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/got" "github.com/ysmood/gotrace" "github.com/ysmood/gson" ) var setup = got.Setup(nil) func TestBasic(t *testing.T) { g := setup(t) ctx := g.Context() client := cdp.New().Logger(defaults.CDP).Start(cdp.MustConnectWS(launcher.New().MustLaunch())) defer func() { _, _ = client.Call(ctx, "", "Browser.close", nil) }() go func() { for range client.Event() { utils.Noop() } }() file, err := filepath.Abs(filepath.FromSlash("fixtures/iframe.html")) g.E(err) res, err := client.Call(ctx, "", "Target.createTarget", map[string]string{ "url": "file://" + file, }) g.E(err) targetID := gson.New(res).Get("targetId").String() res, err = client.Call(ctx, "", "Target.attachToTarget", map[string]interface{}{ "targetId": targetID, "flatten": true, // if it's not set no response will return }) g.E(err) sessionID := gson.New(res).Get("sessionId").String() _, err = client.Call(ctx, sessionID, "Page.enable", nil) g.E(err) _, err = client.Call(ctx, "", "Target.attachToTarget", map[string]interface{}{ "targetId": "abc", }) g.Err(err) timeout := g.Context() sleeper := func() utils.Sleeper { return utils.BackoffSleeper(30*time.Millisecond, 3*time.Second, nil) } // cancel call tmpCtx, tmpCancel := context.WithCancel(ctx) tmpCancel() _, err = client.Call(tmpCtx, sessionID, "Runtime.evaluate", map[string]interface{}{ "expression": `10`, }) g.Eq(err.Error(), context.Canceled.Error()) g.E(utils.Retry(timeout, sleeper(), func() (bool, error) { res, err = client.Call(ctx, sessionID, "Runtime.evaluate", map[string]interface{}{ "expression": `document.querySelector('iframe')`, }) return err == nil && gson.New(res).Get("result.subtype").String() != "null", nil })) res, err = client.Call(ctx, sessionID, "DOM.describeNode", map[string]interface{}{ "objectId": gson.New(res).Get("result.objectId").String(), }) g.E(err) frameID := gson.New(res).Get("node.frameId").String() timeout = g.Context() g.E(utils.Retry(timeout, sleeper(), func() (bool, error) { // we might need to recreate the world because world can be // destroyed after the frame is reloaded res, err = client.Call(ctx, sessionID, "Page.createIsolatedWorld", map[string]interface{}{ "frameId": frameID, }) g.E(err) res, err = client.Call(ctx, sessionID, "Runtime.evaluate", map[string]interface{}{ "contextId": gson.New(res).Get("executionContextId").Int(), "expression": `document.querySelector('h4')`, }) return err == nil && gson.New(res).Get("result.subtype").String() != "null", nil })) res, err = client.Call(ctx, sessionID, "DOM.getOuterHTML", map[string]interface{}{ "objectId": gson.New(res).Get("result.objectId").String(), }) g.E(err) g.Eq("

it works

", gson.New(res).Get("outerHTML").String()) } func TestError(t *testing.T) { g := setup(t) cdpErr := cdp.Error{10, "err", "data"} g.Eq(cdpErr.Error(), "{10 err data}") g.True(cdpErr.Is(&cdpErr)) g.Panic(func() { cdp.MustStartWithURL(context.Background(), "", nil) }) } func TestCrash(t *testing.T) { g := setup(t) ctx := g.Context() client := cdp.MustStartWithURL(ctx, launcher.New().MustLaunch(), nil) go func() { for range client.Event() { utils.Noop() } }() file, err := filepath.Abs(filepath.FromSlash("fixtures/iframe.html")) g.E(err) res, err := client.Call(ctx, "", "Target.createTarget", map[string]interface{}{ "url": "file://" + file, }) g.E(err) targetID := gson.New(res).Get("targetId").String() res, err = client.Call(ctx, "", "Target.attachToTarget", map[string]interface{}{ "targetId": targetID, "flatten": true, }) g.E(err) sessionID := gson.New(res).Get("sessionId").String() _, err = client.Call(ctx, sessionID, "Page.enable", nil) g.E(err) go func() { utils.Sleep(1) _, err := client.Call(ctx, sessionID, "Browser.crash", nil) g.Eq(err, io.EOF) }() _, err = client.Call(ctx, sessionID, "Runtime.evaluate", map[string]interface{}{ "expression": `new Promise(() => {})`, "awaitPromise": true, }) g.Eq(err, io.EOF) _, err = client.Call(ctx, sessionID, "Runtime.evaluate", map[string]interface{}{ "expression": `10`, }) g.Has(err.Error(), "use of closed network connection") } func TestFormat(t *testing.T) { g := setup(t) g.Eq(cdp.Request{ ID: 123, SessionID: "000000001234", Method: "test", Params: 1, }.String(), `=> #123 @00000000 test 1`) g.Eq(cdp.Response{ ID: 0, Result: []byte("11"), }.String(), "<= #0 11") g.Eq(cdp.Response{Error: &cdp.Error{}}.String(), `<= #0 error: {"code":0,"message":"","data":""}`) g.Eq(cdp.Event{ Method: "event", Params: []byte("11"), }.String(), `<- @00000000 event 11`) } func TestSlowSend(t *testing.T) { g := setup(t) gotrace.CheckLeak(g, 0) id := 0 wait := make(chan int) ws := &MockWebSocket{ send: func([]byte) error { close(wait) utils.Sleep(0.3) return nil }, read: func() ([]byte, error) { if id > 0 { return nil, io.EOF } id++ <-wait return json.Marshal(cdp.Response{ ID: id, Result: json.RawMessage("1"), Error: nil, }) }, } c := cdp.New().Start(ws) _, err := c.Call(g.Context(), "1234567890", "method", 1) g.E(err) } func TestCancelCallLeak(t *testing.T) { g := setup(t) gotrace.CheckLeak(g, 0) for i := 0; i < 30; i++ { id := 0 wait := make(chan int) ws := &MockWebSocket{ send: func([]byte) error { close(wait) utils.Sleep(0.01) return nil }, read: func() ([]byte, error) { if id > 0 { return nil, io.EOF } id++ <-wait return json.Marshal(cdp.Response{ ID: id, Result: json.RawMessage("1"), Error: nil, }) }, } c := cdp.New().Start(ws) ctx := g.Context() ctx.Cancel() _, _ = c.Call(ctx, "1234567890", "method", 1) } } func TestConcurrentCall(t *testing.T) { g := setup(t) gotrace.CheckLeak(g, 0) req := make(chan []byte, 30) t.Cleanup(func() { close(req) }) ws := &MockWebSocket{ send: func(data []byte) error { req <- data return nil }, read: func() ([]byte, error) { data, ok := <-req if !ok { return nil, io.EOF } var req cdp.Request err := json.Unmarshal(data, &req) if err != nil { return nil, err } return json.Marshal(cdp.Response{ ID: req.ID, Result: json.RawMessage(gson.New(req.Params).JSON("", "")), Error: nil, }) }, } c := cdp.New().Start(ws) for i := 0; i < 1000; i++ { i := i t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { g := setup(t) g.Parallel() res, err := c.Call(g.Context(), "1234567890", "method", i) g.E(err) g.Eq(gson.New(res).Int(), i) }) } } func TestMassBrowserClose(t *testing.T) { //nolint: tparallel t.Skip() g := setup(t) s := g.Serve() for i := 0; i < 50; i++ { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { t.Parallel() browser := rod.New().MustConnect() browser.MustPage(s.URL()).MustWaitLoad().MustClose() browser.MustClose() }) } } type MockWebSocket struct { send func(data []byte) error read func() ([]byte, error) } func (c *MockWebSocket) Send(data []byte) error { return c.send(data) } func (c *MockWebSocket) Read() ([]byte, error) { return c.read() } golang-github-go-rod-rod-0.116.2/lib/cdp/error.go000066400000000000000000000024241520674146500213670ustar00rootroot00000000000000package cdp import ( "fmt" ) // Error of the Response. type Error struct { Code int `json:"code"` Message string `json:"message"` Data string `json:"data"` } // Error stdlib interface. func (e *Error) Error() string { return fmt.Sprintf("%v", *e) } // Is stdlib interface. func (e Error) Is(target error) bool { err, ok := target.(*Error) return ok && e == *err } // ErrCtxNotFound type. var ErrCtxNotFound = &Error{ Code: -32000, Message: "Cannot find context with specified id", } // ErrSessionNotFound type. var ErrSessionNotFound = &Error{ Code: -32001, Message: "Session with given id not found.", } // ErrSearchSessionNotFound type. var ErrSearchSessionNotFound = &Error{ Code: -32000, Message: "No search session with given id found", } // ErrCtxDestroyed type. var ErrCtxDestroyed = &Error{ Code: -32000, Message: "Execution context was destroyed.", } // ErrObjNotFound type. var ErrObjNotFound = &Error{ Code: -32000, Message: "Could not find object with given id", } // ErrNodeNotFoundAtPos type. var ErrNodeNotFoundAtPos = &Error{ Code: -32000, Message: "No node found at given location", } // ErrNotAttachedToActivePage type. var ErrNotAttachedToActivePage = &Error{ Code: -32000, Message: "Not attached to an active page", } golang-github-go-rod-rod-0.116.2/lib/cdp/example_test.go000066400000000000000000000023351520674146500227310ustar00rootroot00000000000000package cdp_test import ( "context" "fmt" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/gson" ) func ExampleClient() { ctx := context.Background() // launch a browser url := launcher.New().MustLaunch() // create a controller client := cdp.New().Start(cdp.MustConnectWS(url)) go func() { for range client.Event() { // you must consume the events utils.Noop() } }() // Such as call this endpoint on the api doc: // https://chromedevtools.github.io/devtools-protocol/tot/Page#method-navigate // This will create a new tab and navigate to the test.com res, err := client.Call(ctx, "", "Target.createTarget", map[string]string{ "url": "http://test.com", }) utils.E(err) fmt.Println(len(gson.New(res).Get("targetId").Str())) // close browser by using the proto lib to encode json _ = proto.BrowserClose{}.Call(client) // Output: 32 } func Example_customize_cdp_log() { ws := cdp.MustConnectWS(launcher.New().MustLaunch()) cdp.New(). Logger(utils.Log(func(args ...interface{}) { switch v := args[0].(type) { case *cdp.Request: fmt.Printf("id: %d", v.ID) } })). Start(ws) } golang-github-go-rod-rod-0.116.2/lib/cdp/fixtures/000077500000000000000000000000001520674146500215565ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/cdp/fixtures/basic.html000066400000000000000000000000701520674146500235220ustar00rootroot00000000000000

it works

golang-github-go-rod-rod-0.116.2/lib/cdp/fixtures/iframe.html000066400000000000000000000001131520674146500237020ustar00rootroot00000000000000 golang-github-go-rod-rod-0.116.2/lib/cdp/format.go000066400000000000000000000013431520674146500215250ustar00rootroot00000000000000package cdp import ( "fmt" "github.com/go-rod/rod/lib/utils" ) func (req Request) String() string { return fmt.Sprintf( "=> #%d %s %s %s", req.ID, fSessionID(req.SessionID), req.Method, dump(req.Params), ) } func (res Response) String() string { if res.Error != nil { return fmt.Sprintf( "<= #%d error: %s", res.ID, dump(res.Error), ) } return fmt.Sprintf( "<= #%d %s", res.ID, dump(res.Result), ) } func (e Event) String() string { return fmt.Sprintf( "<- %s %s %s", fSessionID(e.SessionID), e.Method, dump(e.Params), ) } func fSessionID(s string) string { if s == "" { s = "00000000" } s = s[:8] return "@" + s } func dump(v interface{}) string { return utils.MustToJSON(v) } golang-github-go-rod-rod-0.116.2/lib/cdp/utils.go000066400000000000000000000021671520674146500214020ustar00rootroot00000000000000package cdp import ( "context" "crypto/tls" "net" "net/http" "github.com/go-rod/rod/lib/utils" ) // Dialer interface for WebSocket connection. type Dialer interface { DialContext(ctx context.Context, network, address string) (net.Conn, error) } // TODO: replace it with tls.Dialer once golang v1.15 is widely used. type tlsDialer struct{} func (d *tlsDialer) DialContext(_ context.Context, network, address string) (net.Conn, error) { return tls.Dial(network, address, nil) } // MustConnectWS helper to make a websocket connection. func MustConnectWS(wsURL string) WebSocketable { ws := &WebSocket{} utils.E(ws.Connect(context.Background(), wsURL, nil)) return ws } // MustStartWithURL helper for ConnectURL. func MustStartWithURL(ctx context.Context, u string, h http.Header) *Client { c, err := StartWithURL(ctx, u, h) utils.E(err) return c } // StartWithURL helper to connect to the u with the default websocket lib. func StartWithURL(ctx context.Context, u string, h http.Header) (*Client, error) { ws := &WebSocket{} err := ws.Connect(ctx, u, h) if err != nil { return nil, err } return New().Start(ws), nil } golang-github-go-rod-rod-0.116.2/lib/cdp/websocket.go000066400000000000000000000110171520674146500222220ustar00rootroot00000000000000package cdp import ( "bufio" "context" "crypto/sha1" "encoding/base64" "fmt" "io" "net" "net/http" "net/url" "sync" ) var _ WebSocketable = &WebSocket{} // WebSocket client for chromium. It only implements a subset of WebSocket protocol. // Both the Read and Write are thread-safe. // Limitation: https://bugs.chromium.org/p/chromium/issues/detail?id=1069431 // Ref: https://tools.ietf.org/html/rfc6455 type WebSocket struct { // Dialer is usually used for proxy Dialer Dialer lock sync.Mutex conn net.Conn r *bufio.Reader } // Connect to browser. func (ws *WebSocket) Connect(ctx context.Context, wsURL string, header http.Header) error { if ws.conn != nil { panic("duplicated connection: " + wsURL) } u, err := url.Parse(wsURL) if err != nil { return err } ws.initDialer(u) conn, err := ws.Dialer.DialContext(ctx, "tcp", u.Host) if err != nil { return err } ws.conn = conn ws.r = bufio.NewReader(conn) return ws.handshake(ctx, u, header) } // Close the underlying connection. func (ws *WebSocket) Close() error { return ws.conn.Close() } func (ws *WebSocket) initDialer(u *url.URL) { if ws.Dialer != nil { return } if u.Scheme == "wss" { ws.Dialer = &tlsDialer{} if u.Port() == "" { u.Host += ":443" } } else { ws.Dialer = &net.Dialer{} } } // Send a message to browser. // Because we use zero-copy design, it will modify the content of the msg. // It won't allocate new memory. func (ws *WebSocket) Send(msg []byte) error { err := ws.send(msg) if err != nil { _ = ws.Close() } return err } func (ws *WebSocket) send(msg []byte) error { // FIN is alway true, Opcode is always text frame. header := [18]byte{0b1000_0001, 0b1000_0000} mask := []byte{0, 1, 2, 3} size := len(msg) fieldLen := 0 switch { case size <= 125: header[1] |= byte(size) case size < 65536: header[1] |= 126 fieldLen = 2 default: header[1] |= 127 fieldLen = 8 } var i int for i = 0; i < fieldLen; i++ { digit := (fieldLen - i - 1) * 8 header[i+2] = byte((size >> digit) & 0xff) } copy(header[i+2:], mask) for i := range msg { msg[i] ^= mask[i%4] } data := make([]byte, i+6+len(msg)) copy(data, header[:i+6]) copy(data[i+6:], msg) _, err := ws.conn.Write(data) return err } // Read a message from browser. func (ws *WebSocket) Read() ([]byte, error) { b, err := ws.read() if err != nil { _ = ws.Close() return nil, err } return b, nil } func (ws *WebSocket) read() ([]byte, error) { ws.lock.Lock() defer ws.lock.Unlock() _, err := ws.r.ReadByte() if err != nil { return nil, err } b, err := ws.r.ReadByte() if err != nil { return nil, err } size := 0 fieldLen := 0 b &= 0x7f switch { case b <= 125: size = int(b) case b == 126: fieldLen = 2 case b == 127: fieldLen = 8 } for i := 0; i < fieldLen; i++ { b, err := ws.r.ReadByte() if err != nil { return nil, err } size = size<<8 + int(b) } data := make([]byte, size) _, err = io.ReadFull(ws.r, data) return data, err } // BadHandshakeError type. type BadHandshakeError struct { Status string Body string } func (e *BadHandshakeError) Error() string { return fmt.Sprintf( "websocket bad handshake: %s. %s", e.Status, e.Body, ) } func verifyWebSocketAccept(responseHeaders http.Header, websocketKey string) bool { expectedKey := websocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" hash := sha1.New() hash.Write([]byte(expectedKey)) expectedAccept := base64.StdEncoding.EncodeToString(hash.Sum(nil)) return responseHeaders.Get("Sec-WebSocket-Accept") == expectedAccept } func (ws *WebSocket) handshake(ctx context.Context, u *url.URL, header http.Header) error { defaultSecKey := "nil" req := (&http.Request{Method: http.MethodGet, URL: u, Header: http.Header{ "Upgrade": {"websocket"}, "Connection": {"Upgrade"}, "Sec-WebSocket-Key": {defaultSecKey}, "Sec-WebSocket-Version": {"13"}, }}).WithContext(ctx) secKey := defaultSecKey for k, vs := range header { switch { case k == "Host" && len(vs) > 0: req.Host = vs[0] case k == "Sec-WebSocket-Key" && len(vs) > 0: secKey = vs[0] req.Header[k] = vs default: req.Header[k] = vs } } err := req.Write(ws.conn) if err != nil { return err } res, err := http.ReadResponse(ws.r, req) if err != nil { return err } defer func() { _ = res.Body.Close() }() if res.StatusCode != http.StatusSwitchingProtocols || !verifyWebSocketAccept(res.Header, secKey) { body, _ := io.ReadAll(res.Body) return &BadHandshakeError{ Status: res.Status, Body: string(body), } } return nil } golang-github-go-rod-rod-0.116.2/lib/cdp/websocket_private_test.go000066400000000000000000000033041520674146500250130ustar00rootroot00000000000000package cdp import ( "bufio" "context" "errors" "net" "net/url" "sync" "testing" "time" "github.com/ysmood/got" ) var setup = got.Setup(nil) func TestWebSocketErr(t *testing.T) { g := setup(t) ws := WebSocket{} g.Err(ws.Connect(g.Context(), "://", nil)) ws.Dialer = &net.Dialer{} ws.initDialer(nil) u, err := url.Parse("wss://no-exist") g.E(err) ws.Dialer = nil ws.initDialer(u) mc := &MockConn{} ws.conn = mc g.Err(ws.Send([]byte("test"))) mc.errOnCount = 1 mc.frame = []byte{0, 127, 1} ws.r = bufio.NewReader(mc) g.Err(ws.Read()) mc.errOnCount = 1 mc.frame = []byte{0} ws.r = bufio.NewReader(mc) g.Err(ws.Read()) g.Err(ws.handshake(g.Timeout(0), nil, nil)) mc.errOnCount = 1 g.Err(ws.handshake(g.Context(), u, nil)) tls := &tlsDialer{} g.Err(tls.DialContext(context.Background(), "", "")) } type MockConn struct { sync.Mutex errOnCount int frame []byte } func (c *MockConn) checkErr(d int) error { c.Lock() defer c.Unlock() if c.errOnCount == 0 { return errors.New("err") } c.errOnCount += d return nil } func (c *MockConn) Read(b []byte) (int, error) { if err := c.checkErr(-1); err != nil { return 0, err } return copy(b, c.frame), nil } func (c *MockConn) Write(b []byte) (int, error) { if err := c.checkErr(-1); err != nil { return 0, err } return len(b), nil } func (c *MockConn) Close() error { return c.checkErr(0) } func (c *MockConn) LocalAddr() net.Addr { return nil } func (c *MockConn) RemoteAddr() net.Addr { return nil } func (c *MockConn) SetDeadline(_ time.Time) error { return nil } func (c *MockConn) SetReadDeadline(_ time.Time) error { return nil } func (c *MockConn) SetWriteDeadline(_ time.Time) error { return nil } golang-github-go-rod-rod-0.116.2/lib/cdp/websocket_test.go000066400000000000000000000050551520674146500232660ustar00rootroot00000000000000package cdp_test import ( "context" "fmt" "net/http" "path/filepath" "strings" "sync" "testing" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/got" "github.com/ysmood/gson" ) func TestWebSocketLargePayload(t *testing.T) { g := setup(t) ctx := g.Context() client, id := newPage(ctx, g) const size = 2 * 1024 * 1024 res, err := client.Call(ctx, id, "Runtime.evaluate", map[string]interface{}{ "expression": fmt.Sprintf(`"%s"`, strings.Repeat("a", size)), "returnByValue": true, }) g.E(err) g.Gt(len(res), size) // 2MB } func ConcurrentCall(t *testing.T) { t.Helper() g := setup(t) ctx := g.Context() client, id := newPage(ctx, g) wg := sync.WaitGroup{} for i := 0; i < 30; i++ { wg.Add(1) go func() { res, err := client.Call(ctx, id, "Runtime.evaluate", map[string]interface{}{ "expression": `10`, }) g.Nil(err) g.Eq(string(res), "{\"result\":{\"type\":\"number\",\"value\":10,\"description\":\"10\"}}") wg.Done() }() } wg.Wait() } func TestWebSocketHeader(t *testing.T) { g := setup(t) s := g.Serve() wait := make(chan struct{}) s.Mux.HandleFunc("/a", func(_ http.ResponseWriter, r *http.Request) { g.Eq(r.Header.Get("Test"), "header") g.Eq(r.Host, "test.com") g.Eq(r.URL.Query().Get("q"), "ok") close(wait) }) ws := cdp.WebSocket{} err := ws.Connect(g.Context(), s.URL("/a?q=ok"), http.Header{ "Host": {"test.com"}, "Test": {"header"}, "Sec-WebSocket-Key": {"key"}, }) <-wait g.Eq(err.Error(), "websocket bad handshake: 200 OK. ") } func newPage(ctx context.Context, g got.G) (*cdp.Client, string) { l := launcher.New() g.Cleanup(l.Kill) client := cdp.New().Start(cdp.MustConnectWS(l.MustLaunch())) go func() { for range client.Event() { utils.Noop() } }() file, err := filepath.Abs(filepath.FromSlash("fixtures/basic.html")) g.E(err) res, err := client.Call(ctx, "", "Target.createTarget", map[string]interface{}{ "url": "file://" + file, }) g.E(err) targetID := gson.New(res).Get("targetId").String() res, err = client.Call(ctx, "", "Target.attachToTarget", map[string]interface{}{ "targetId": targetID, "flatten": true, }) g.E(err) sessionID := gson.New(res).Get("sessionId").String() return client, sessionID } func TestDuplicatedConnectErr(t *testing.T) { g := setup(t) l := launcher.New() g.Cleanup(l.Kill) u := l.MustLaunch() ws := &cdp.WebSocket{} g.E(ws.Connect(g.Context(), u, nil)) g.Panic(func() { _ = ws.Connect(g.Context(), u, nil) }) } golang-github-go-rod-rod-0.116.2/lib/defaults/000077500000000000000000000000001520674146500207465ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/defaults/defaults.go000066400000000000000000000102161520674146500231040ustar00rootroot00000000000000// Package defaults of commonly used options parsed from environment. // Check ResetWith for details. package defaults import ( "flag" "log" "os" "regexp" "strconv" "strings" "time" "github.com/go-rod/rod/lib/utils" ) // Trace is the default of rod.Browser.Trace . // Option name is "trace". var Trace bool // Slow is the default of rod.Browser.SlowMotion . // The format is same as https://golang.org/pkg/time/#ParseDuration // Option name is "slow". var Slow time.Duration // Monitor is the default of rod.Browser.ServeMonitor . // Option name is "monitor". var Monitor string // Show is the default of launcher.Launcher.Headless . // Option name is "show". var Show bool // Devtools is the default of launcher.Launcher.Devtools . // Option name is "devtools". var Devtools bool // Dir is the default of launcher.Launcher.UserDataDir . // Option name is "dir". var Dir string // Port is the default of launcher.Launcher.RemoteDebuggingPort . // Option name is "port". var Port string // Bin is the default of launcher.Launcher.Bin . // Option name is "bin". var Bin string // Proxy is the default of launcher.Launcher.Proxy // Option name is "proxy". var Proxy string // LockPort is the default of launcher.Browser.LockPort // Option name is "lock". var LockPort int // URL is the default websocket url for remote control a browser. // Option name is "url". var URL string // CDP is the default of cdp.Client.Logger // Option name is "cdp". var CDP utils.Logger // Reset all flags to their init values. func Reset() { Trace = false Slow = 0 Monitor = "" Show = false Devtools = false Dir = "" Port = "0" Bin = "" Proxy = "" LockPort = 2978 URL = "" CDP = utils.LoggerQuiet } var envParsers = map[string]func(string){ "trace": func(string) { Trace = true }, "slow": func(v string) { var err error Slow, err = time.ParseDuration(v) if err != nil { msg := "invalid value for \"slow\": " + err.Error() + " (learn format from https://golang.org/pkg/time/#ParseDuration)" panic(msg) } }, "monitor": func(v string) { Monitor = ":0" if v != "" { Monitor = v } }, "show": func(string) { Show = true }, "devtools": func(string) { Devtools = true }, "dir": func(v string) { Dir = v }, "port": func(v string) { Port = v }, "bin": func(v string) { Bin = v }, "proxy": func(v string) { Proxy = v }, "lock": func(v string) { i, err := strconv.ParseInt(v, 10, 32) if err == nil { LockPort = int(i) } }, "url": func(v string) { URL = v }, "cdp": func(_ string) { CDP = log.New(log.Writer(), "[cdp] ", log.LstdFlags) }, } // Parse the flags. func init() { ResetWith("") } // ResetWith options and "-rod" command line flag. // It will be called in an init() , so you don't have to call it manually. // It will try to load the cli flag "-rod" and then the options, the later override the former. // If you want to disable the global cli argument flag, set env DISABLE_ROD_FLAG. // Values are separated by commas, key and value are separated by "=". For example: // // go run main.go -rod=show // go run main.go -rod show,trace,slow=1s,monitor // go run main.go --rod="slow=1s,dir=path/has /space,monitor=:9223" func ResetWith(options string) { Reset() if _, has := os.LookupEnv("DISABLE_ROD_FLAG"); !has { if !flag.Parsed() && flag.Lookup("rod") == nil { flag.String("rod", "", `Set the default value of options used by rod.`) } parseFlag(os.Args) } parse(options) } func parseFlag(args []string) { reg := regexp.MustCompile(`^--?rod$`) regEq := regexp.MustCompile(`^--?rod=(.*)$`) opts := "" for i, arg := range args { if reg.MatchString(arg) && i+1 < len(args) { opts = args[i+1] } else if m := regEq.FindStringSubmatch(arg); len(m) == 2 { opts = m[1] } } parse(opts) } // parse options and set them globally. func parse(options string) { if options == "" { return } reg := regexp.MustCompile(`[,\r\n]`) for _, str := range reg.Split(options, -1) { kv := strings.SplitN(str, "=", 2) v := "" if len(kv) == 2 { v = kv[1] } n := strings.TrimSpace(kv[0]) if n == "" { continue } f := envParsers[n] if f == nil { panic("unknown rod env option: " + n) } f(v) } } golang-github-go-rod-rod-0.116.2/lib/defaults/defaults_test.go000066400000000000000000000024341520674146500241460ustar00rootroot00000000000000package defaults import ( "testing" "time" "github.com/ysmood/got" ) func TestBasic(t *testing.T) { g := got.T(t) Show = true Devtools = true URL = "test" Monitor = "test" ResetWith("") parse("") g.False(Show) g.False(Devtools) g.Eq("", Monitor) g.Eq("", URL) g.Eq(2978, LockPort) parse("show,devtools,trace,slow=2s,port=8080,dir=tmp," + "url=http://test.com,cdp,monitor,bin=/path/to/chrome," + "proxy=localhost:8080,lock=9981,", ) g.True(Show) g.True(Devtools) g.True(Trace) g.Eq(2*time.Second, Slow) g.Eq("8080", Port) g.Eq("/path/to/chrome", Bin) g.Eq("tmp", Dir) g.Eq("http://test.com", URL) g.NotNil(CDP.Println) g.Eq(":0", Monitor) g.Eq("localhost:8080", Proxy) g.Eq(9981, LockPort) parse("monitor=:1234") g.Eq(":1234", Monitor) g.Panic(func() { parse("a") }) 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)") } func try(fn func()) (err interface{}) { defer func() { err = recover() }() fn() return err } func TestParseFlag(t *testing.T) { g := got.T(t) Reset() parseFlag([]string{"-rod"}) g.False(Show) parseFlag([]string{"-rod=show"}) g.True(Show) Reset() parseFlag([]string{"-rod", "show"}) g.True(Show) } golang-github-go-rod-rod-0.116.2/lib/devices/000077500000000000000000000000001520674146500205615ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/devices/device.go000066400000000000000000000043731520674146500223560ustar00rootroot00000000000000// Package devices ... package devices import ( "github.com/go-rod/rod/lib/proto" "github.com/ysmood/gson" ) // Device represents a emulated device. type Device struct { Capabilities []string UserAgent string AcceptLanguage string Screen Screen Title string landscape bool clear bool } // Screen represents the screen of a device. type Screen struct { DevicePixelRatio float64 Horizontal ScreenSize Vertical ScreenSize } // ScreenSize represents the size of the screen. type ScreenSize struct { Width int Height int } // Landscape clones the device and set it to landscape mode. func (device Device) Landscape() Device { d := device d.landscape = true return d } // MetricsEmulation config. func (device Device) MetricsEmulation() *proto.EmulationSetDeviceMetricsOverride { if device.IsClear() { return nil } var screen ScreenSize var orientation *proto.EmulationScreenOrientation if device.landscape { screen = device.Screen.Horizontal orientation = &proto.EmulationScreenOrientation{ Angle: 90, Type: proto.EmulationScreenOrientationTypeLandscapePrimary, } } else { screen = device.Screen.Vertical orientation = &proto.EmulationScreenOrientation{ Angle: 0, Type: proto.EmulationScreenOrientationTypePortraitPrimary, } } return &proto.EmulationSetDeviceMetricsOverride{ Width: screen.Width, Height: screen.Height, DeviceScaleFactor: device.Screen.DevicePixelRatio, ScreenOrientation: orientation, Mobile: has(device.Capabilities, "mobile"), } } // TouchEmulation config. func (device Device) TouchEmulation() *proto.EmulationSetTouchEmulationEnabled { if device.IsClear() { return &proto.EmulationSetTouchEmulationEnabled{ Enabled: false, } } return &proto.EmulationSetTouchEmulationEnabled{ Enabled: has(device.Capabilities, "touch"), MaxTouchPoints: gson.Int(5), } } // UserAgentEmulation config. func (device Device) UserAgentEmulation() *proto.NetworkSetUserAgentOverride { if device.IsClear() { return nil } return &proto.NetworkSetUserAgentOverride{ UserAgent: device.UserAgent, AcceptLanguage: device.AcceptLanguage, } } // IsClear type. func (device Device) IsClear() bool { return device.clear } golang-github-go-rod-rod-0.116.2/lib/devices/generate/000077500000000000000000000000001520674146500223535ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/devices/generate/main.go000066400000000000000000000054451520674146500236360ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "io" "net/http" "path/filepath" "strings" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/gson" ) func main() { devices := getDeviceList() code := `` for _, d := range devices.Arr() { d = d.Get("device") name := d.Get("title").String() code += utils.S(` // {{.name}} device {{.name}} = Device{ Title: "{{.title}}", Capabilities: {{.capabilities}}, UserAgent: "{{.userAgent}}", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: {{.devicePixelRatio}}, Horizontal: ScreenSize{ Width: {{.horizontalWidth}}, Height: {{.horizontalHeight}}, }, Vertical: ScreenSize{ Width: {{.verticalWidth}}, Height: {{.verticalHeight}}, }, }, }`, "name", normalizeName(name), "title", name, "capabilities", toGoArr(d.Get("capabilities")), "userAgent", getUserAgent(d), "devicePixelRatio", d.Get("screen.device-pixel-ratio").Int(), "horizontalWidth", d.Get("screen.horizontal.width").Int(), "horizontalHeight", d.Get("screen.horizontal.height").Int(), "verticalWidth", d.Get("screen.vertical.width").Int(), "verticalHeight", d.Get("screen.vertical.height").Int(), ) } code = utils.S(`// generated by "lib/devices/generate" package devices var ( {{.code}} ) `, "code", code) path := "./lib/devices/list.go" utils.E(utils.OutputFile(path, code)) utils.Exec("gofumpt -w", path) utils.Exec( "go run github.com/ysmood/golangci-lint@latest -- "+ "run --fix", filepath.Dir(path), ) } func getDeviceList() gson.JSON { // we use the list from the web UI of devtools // TODO: We should keep update with their latest list, using hash id is a temp solution res, err := http.Get( "https://raw.githubusercontent.com/ChromeDevTools/devtools-frontend/c4e2fefe3327aa9fe5f4398a1baddb8726c230d5/front_end/emulated_devices/module.json", ) utils.E(err) defer func() { _ = res.Body.Close() }() data, err := io.ReadAll(res.Body) utils.E(err) return gson.New(data).Get("extensions") } func normalizeName(name string) string { name = strings.ReplaceAll(name, "/", "or") list := []string{} for _, s := range strings.Split(name, " ") { if len(s) > 1 { list = append(list, strings.ToUpper(s[0:1])+s[1:]) } else { list = append(list, strings.ToUpper(s)) } } return strings.Join(list, "") } func getUserAgent(val gson.JSON) string { ua := val.Get("user-agent").String() if ua == "" { return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" } ua = strings.ReplaceAll(ua, "%s", "114.0.0.0") return ua } func toGoArr(val gson.JSON) string { list := []string{} for _, s := range val.Arr() { list = append(list, s.String()) } return fmt.Sprintf("%#v", list) } golang-github-go-rod-rod-0.116.2/lib/devices/list.go000066400000000000000000000423141520674146500220670ustar00rootroot00000000000000// generated by "lib/devices/generate" package devices var ( // IPhone4 device. IPhone4 = Device{ Title: "iPhone 4", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 480, Height: 320, }, Vertical: ScreenSize{ Width: 320, Height: 480, }, }, } // IPhone5orSE device. IPhone5orSE = Device{ Title: "iPhone 5/SE", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 568, Height: 320, }, Vertical: ScreenSize{ Width: 320, Height: 568, }, }, } // IPhone6or7or8 device. IPhone6or7or8 = Device{ Title: "iPhone 6/7/8", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 667, Height: 375, }, Vertical: ScreenSize{ Width: 375, Height: 667, }, }, } // IPhone6or7or8Plus device. IPhone6or7or8Plus = Device{ Title: "iPhone 6/7/8 Plus", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 3, Horizontal: ScreenSize{ Width: 736, Height: 414, }, Vertical: ScreenSize{ Width: 414, Height: 736, }, }, } // IPhoneX device. IPhoneX = Device{ Title: "iPhone X", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 3, Horizontal: ScreenSize{ Width: 812, Height: 375, }, Vertical: ScreenSize{ Width: 375, Height: 812, }, }, } // BlackBerryZ30 device. BlackBerryZ30 = Device{ Title: "BlackBerry Z30", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 640, Height: 360, }, Vertical: ScreenSize{ Width: 360, Height: 640, }, }, } // Nexus4 device. Nexus4 = Device{ Title: "Nexus 4", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 640, Height: 384, }, Vertical: ScreenSize{ Width: 384, Height: 640, }, }, } // Nexus5 device. Nexus5 = Device{ Title: "Nexus 5", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 3, Horizontal: ScreenSize{ Width: 640, Height: 360, }, Vertical: ScreenSize{ Width: 360, Height: 640, }, }, } // Nexus5X device. Nexus5X = Device{ Title: "Nexus 5X", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 732, Height: 412, }, Vertical: ScreenSize{ Width: 412, Height: 732, }, }, } // Nexus6 device. Nexus6 = Device{ Title: "Nexus 6", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 3, Horizontal: ScreenSize{ Width: 732, Height: 412, }, Vertical: ScreenSize{ Width: 412, Height: 732, }, }, } // Nexus6P device. Nexus6P = Device{ Title: "Nexus 6P", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 3, Horizontal: ScreenSize{ Width: 732, Height: 412, }, Vertical: ScreenSize{ Width: 412, Height: 732, }, }, } // Pixel2 device. Pixel2 = Device{ Title: "Pixel 2", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 731, Height: 411, }, Vertical: ScreenSize{ Width: 411, Height: 731, }, }, } // Pixel2XL device. Pixel2XL = Device{ Title: "Pixel 2 XL", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 3, Horizontal: ScreenSize{ Width: 823, Height: 411, }, Vertical: ScreenSize{ Width: 411, Height: 823, }, }, } // LGOptimusL70 device. LGOptimusL70 = Device{ Title: "LG Optimus L70", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/114.0.0.0 Mobile Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 1, Horizontal: ScreenSize{ Width: 640, Height: 384, }, Vertical: ScreenSize{ Width: 384, Height: 640, }, }, } // NokiaN9 device. NokiaN9 = Device{ Title: "Nokia N9", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 1, Horizontal: ScreenSize{ Width: 854, Height: 480, }, Vertical: ScreenSize{ Width: 480, Height: 854, }, }, } // NokiaLumia520 device. NokiaLumia520 = Device{ Title: "Nokia Lumia 520", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 1, Horizontal: ScreenSize{ Width: 533, Height: 320, }, Vertical: ScreenSize{ Width: 320, Height: 533, }, }, } // MicrosoftLumia550 device. MicrosoftLumia550 = Device{ Title: "Microsoft Lumia 550", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 640, Height: 360, }, Vertical: ScreenSize{ Width: 640, Height: 360, }, }, } // MicrosoftLumia950 device. MicrosoftLumia950 = Device{ Title: "Microsoft Lumia 950", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 4, Horizontal: ScreenSize{ Width: 640, Height: 360, }, Vertical: ScreenSize{ Width: 360, Height: 640, }, }, } // GalaxySIII device. GalaxySIII = Device{ Title: "Galaxy S III", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 640, Height: 360, }, Vertical: ScreenSize{ Width: 360, Height: 640, }, }, } // GalaxyS5 device. GalaxyS5 = Device{ Title: "Galaxy S5", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 3, Horizontal: ScreenSize{ Width: 640, Height: 360, }, Vertical: ScreenSize{ Width: 360, Height: 640, }, }, } // JioPhone2 device. JioPhone2 = Device{ Title: "JioPhone 2", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 1, Horizontal: ScreenSize{ Width: 320, Height: 240, }, Vertical: ScreenSize{ Width: 240, Height: 320, }, }, } // KindleFireHDX device. KindleFireHDX = Device{ Title: "Kindle Fire HDX", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 1280, Height: 800, }, Vertical: ScreenSize{ Width: 800, Height: 1280, }, }, } // IPadMini device. IPadMini = Device{ Title: "iPad Mini", Capabilities: []string{"touch", "mobile"}, UserAgent: "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", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 1024, Height: 768, }, Vertical: ScreenSize{ Width: 768, Height: 1024, }, }, } // IPad device. IPad = Device{ Title: "iPad", Capabilities: []string{"touch", "mobile"}, UserAgent: "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", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 1024, Height: 768, }, Vertical: ScreenSize{ Width: 768, Height: 1024, }, }, } // IPadPro device. IPadPro = Device{ Title: "iPad Pro", Capabilities: []string{"touch", "mobile"}, UserAgent: "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", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 1366, Height: 1024, }, Vertical: ScreenSize{ Width: 1024, Height: 1366, }, }, } // BlackberryPlayBook device. BlackberryPlayBook = Device{ Title: "Blackberry PlayBook", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 1, Horizontal: ScreenSize{ Width: 1024, Height: 600, }, Vertical: ScreenSize{ Width: 600, Height: 1024, }, }, } // Nexus10 device. Nexus10 = Device{ Title: "Nexus 10", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 1280, Height: 800, }, Vertical: ScreenSize{ Width: 800, Height: 1280, }, }, } // Nexus7 device. Nexus7 = Device{ Title: "Nexus 7", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 960, Height: 600, }, Vertical: ScreenSize{ Width: 600, Height: 960, }, }, } // GalaxyNote3 device. GalaxyNote3 = Device{ Title: "Galaxy Note 3", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 3, Horizontal: ScreenSize{ Width: 640, Height: 360, }, Vertical: ScreenSize{ Width: 360, Height: 640, }, }, } // GalaxyNoteII device. GalaxyNoteII = Device{ Title: "Galaxy Note II", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 640, Height: 360, }, Vertical: ScreenSize{ Width: 360, Height: 640, }, }, } // LaptopWithTouch device. LaptopWithTouch = Device{ Title: "Laptop with touch", Capabilities: []string{"touch"}, UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 1, Horizontal: ScreenSize{ Width: 1280, Height: 950, }, Vertical: ScreenSize{ Width: 950, Height: 1280, }, }, } // LaptopWithHiDPIScreen device. LaptopWithHiDPIScreen = Device{ Title: "Laptop with HiDPI screen", Capabilities: []string{}, UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 1440, Height: 900, }, Vertical: ScreenSize{ Width: 900, Height: 1440, }, }, } // LaptopWithMDPIScreen device. LaptopWithMDPIScreen = Device{ Title: "Laptop with MDPI screen", Capabilities: []string{}, UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 1, Horizontal: ScreenSize{ Width: 1280, Height: 800, }, Vertical: ScreenSize{ Width: 800, Height: 1280, }, }, } // MotoG4 device. MotoG4 = Device{ Title: "Moto G4", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; Android 6.0.1; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 3, Horizontal: ScreenSize{ Width: 640, Height: 360, }, Vertical: ScreenSize{ Width: 360, Height: 640, }, }, } // SurfaceDuo device. SurfaceDuo = Device{ Title: "Surface Duo", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 2, Horizontal: ScreenSize{ Width: 720, Height: 540, }, Vertical: ScreenSize{ Width: 540, Height: 720, }, }, } // GalaxyFold device. GalaxyFold = Device{ Title: "Galaxy Fold", Capabilities: []string{"touch", "mobile"}, UserAgent: "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", AcceptLanguage: "en", Screen: Screen{ DevicePixelRatio: 3, Horizontal: ScreenSize{ Width: 653, Height: 280, }, Vertical: ScreenSize{ Width: 280, Height: 653, }, }, } ) golang-github-go-rod-rod-0.116.2/lib/devices/utils.go000066400000000000000000000003261520674146500222510ustar00rootroot00000000000000package devices // Clear is used to clear overrides. var Clear = Device{clear: true} func has(arr []string, str string) bool { for _, item := range arr { if item == str { return true } } return false } golang-github-go-rod-rod-0.116.2/lib/devices/utils_test.go000066400000000000000000000016631520674146500233150ustar00rootroot00000000000000package devices_test import ( "testing" "github.com/go-rod/rod/lib/devices" "github.com/ysmood/got" ) func TestErr(t *testing.T) { as := got.New(t) v := devices.IPad.MetricsEmulation() touch := devices.IPad.TouchEmulation() as.Eq(768, v.Width) as.Eq(1024, v.Height) as.Eq(2, v.DeviceScaleFactor) as.Eq(0, v.ScreenOrientation.Angle) as.True(v.Mobile) as.True(touch.Enabled) v = devices.LaptopWithMDPIScreen.Landscape().MetricsEmulation() touch = devices.LaptopWithMDPIScreen.TouchEmulation() as.Eq(1280, v.Width) as.Eq(90, v.ScreenOrientation.Angle) as.False(v.Mobile) as.False(touch.Enabled) u := devices.IPad.UserAgentEmulation() 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) as.Nil(devices.Clear.MetricsEmulation()) as.False(devices.Clear.TouchEmulation().Enabled) as.Nil(devices.Clear.UserAgentEmulation()) } golang-github-go-rod-rod-0.116.2/lib/docker/000077500000000000000000000000001520674146500204065ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/docker/Dockerfile000066400000000000000000000023701520674146500224020ustar00rootroot00000000000000# To build the image: # docker build -t ghcr.io/go-rod/rod -f lib/docker/Dockerfile . # # build rod-manager FROM golang as go ARG goproxy="https://proxy.golang.org,direct" COPY . /rod WORKDIR /rod RUN go env -w GOPROXY=$goproxy RUN go build ./lib/launcher/rod-manager RUN go run ./lib/utils/get-browser FROM ubuntu:noble COPY --from=go /root/.cache/rod /root/.cache/rod RUN ln -s /root/.cache/rod/browser/$(ls /root/.cache/rod/browser)/chrome /usr/bin/chrome RUN touch /.dockerenv COPY --from=go /rod/rod-manager /usr/bin/ ARG apt_sources="http://archive.ubuntu.com" RUN sed -i "s|http://archive.ubuntu.com|$apt_sources|g" /etc/apt/sources.list && \ apt-get update > /dev/null && \ apt-get install --no-install-recommends -y \ # chromium dependencies libnss3 \ libxss1 \ libasound2t64 \ libxtst6 \ libgtk-3-0 \ libgbm1 \ ca-certificates \ # fonts fonts-liberation fonts-noto-color-emoji fonts-noto-cjk \ # timezone tzdata \ # process reaper dumb-init \ # headful mode support, for example: $ xvfb-run chromium-browser --remote-debugging-port=9222 xvfb \ > /dev/null && \ # cleanup rm -rf /var/lib/apt/lists/* # process reaper ENTRYPOINT ["dumb-init", "--"] CMD rod-manager golang-github-go-rod-rod-0.116.2/lib/docker/dev.Dockerfile000066400000000000000000000016511520674146500231600ustar00rootroot00000000000000# A docker image for rod development. # To build the image: # docker build -t ghcr.io/go-rod/rod:dev -f lib/docker/dev.Dockerfile . FROM ghcr.io/go-rod/rod ARG nodejs ARG golang ARG apt_sources="http://archive.ubuntu.com" RUN sed -i "s|http://archive.ubuntu.com|$apt_sources|g" /etc/apt/sources.list && \ apt-get update > /dev/null && \ apt-get install --no-install-recommends -y git curl xz-utils build-essential > /dev/null && \ rm -rf /var/lib/apt/lists/* # install nodejs RUN curl -L $nodejs > node.tar.xz && \ tar -xf node.tar.xz && \ mv node-* /usr/local/lib/.node && \ rm node.tar.xz # install golang RUN curl -L $golang > golang.tar.gz && \ tar -xf golang.tar.gz && \ mv go /usr/local/lib/go && \ rm golang.tar.gz ENV PATH="/usr/local/lib/.node/bin:/usr/local/lib/go/bin:/root/go/bin/:${PATH}" # setup global git ignore RUN git config --global core.excludesfile ~/.gitignore_global golang-github-go-rod-rod-0.116.2/lib/docker/fonts-local.conf000066400000000000000000000013701520674146500234770ustar00rootroot00000000000000 serif Noto Serif Noto Color Emoji sans-serif Noto Sans Noto Color Emoji sans Noto Sans Noto Color Emoji monospace Noto Sans Mono Noto Color Emoji golang-github-go-rod-rod-0.116.2/lib/docker/main.go000066400000000000000000000072151520674146500216660ustar00rootroot00000000000000// The .github/workflows/docker.yml uses it as an github action // and run it like this: // // GITHUB_TOKEN=$TOKEN go run ./lib/utils/docker $GITHUB_REF package main import ( "fmt" "os" "os/exec" "regexp" "strings" "github.com/go-rod/rod/lib/utils" ) func main() { event := os.Args[1] fmt.Println("Event:", event) isMain := regexp.MustCompile(`^refs/heads/main$`).MatchString(event) m := regexp.MustCompile(`^refs/tags/(v[0-9]+\.[0-9]+\.[0-9]+)$`).FindStringSubmatch(event) ver := "" if len(m) > 1 { ver = m[1] } at := getArchType() switch { case isMain: releaseLatest(at) case ver != "": releaseWithVer(ver) default: test(at) } } func releaseLatest(at archType) { login() test(at) utils.Exec("docker push", at.tagDev()) utils.Exec("docker push", at.tag()) } func releaseWithVer(ver string) { login() verImageDev := registry + ":" + ver + "-dev" utils.Exec("docker manifest create", verImageDev, archAmd.tagDev(), archArm.tagDev()) utils.Exec("docker manifest push", verImageDev) verImage := registry + ":" + ver utils.Exec("docker manifest create", verImage, archAmd.tag(), archArm.tag()) utils.Exec("docker manifest push", verImage) registryDev := registry + ":dev" utils.Exec("docker manifest create", registryDev, archAmd.tagDev(), archArm.tagDev()) utils.Exec("docker manifest push", registryDev) utils.Exec("docker manifest create", registry, archAmd.tag(), archArm.tag()) utils.Exec("docker manifest push", registry) } func test(at archType) { utils.Exec("docker build -f=lib/docker/Dockerfile", "--platform", at.platform(), "-t", at.tag(), description(false), ".") utils.Exec("docker build -f=lib/docker/dev.Dockerfile", "--platform", at.platform(), "--build-arg", "golang="+at.golang(), "--build-arg", "nodejs="+at.nodejs(), "-t", at.tagDev(), description(true), ".", ) utils.Exec("docker run", at.tag(), "rod-manager", "-h") // TODO: arm cross execution for chromium doesn't work well on github actions. if at != archArm { wd, err := os.Getwd() utils.E(err) utils.Exec("docker run -w=/t -v", fmt.Sprintf("%s:/t", wd), at.tagDev(), "go", "run", "./lib/utils/ci-test") } } func login() { cmd := exec.Command("docker", "login", "-u=rod-robot", "-p", os.Getenv("GITHUB_TOKEN"), registry) out, err := cmd.CombinedOutput() utils.E(err) utils.E(os.Stdout.Write(out)) } var headSha = strings.TrimSpace(utils.ExecLine(false, "git", "rev-parse", "HEAD")) func description(dev bool) string { f := "Dockerfile" if dev { f = "dev." + f } return `--label=org.opencontainers.image.description=https://github.com/go-rod/rod/blob/` + headSha + "/lib/docker/" + f } const registry = "ghcr.io/go-rod/rod" type archType int const ( archAmd archType = iota archArm ) func getArchType() archType { arch := os.Getenv("ARCH") switch arch { case "arm": return archArm default: return archAmd } } func (at archType) platform() string { switch at { case archArm: return "linux/arm64" default: return "linux/amd64" } } func (at archType) tag() string { switch at { case archArm: return registry + ":arm" default: return registry + ":amd" } } func (at archType) tagDev() string { switch at { case archArm: return registry + ":arm-dev" default: return registry + ":amd-dev" } } func (at archType) golang() string { switch at { case archArm: return "https://go.dev/dl/go1.19.1.linux-arm64.tar.gz" default: return "https://go.dev/dl/go1.19.1.linux-amd64.tar.gz" } } func (at archType) nodejs() string { switch at { case archArm: return "https://nodejs.org/dist/v16.17.0/node-v16.17.0-linux-arm64.tar.xz" default: return "https://nodejs.org/dist/v16.17.0/node-v16.17.0-linux-x64.tar.xz" } } golang-github-go-rod-rod-0.116.2/lib/examples/000077500000000000000000000000001520674146500207555ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/anti-bot-detection/000077500000000000000000000000001520674146500244465ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/anti-bot-detection/README.md000066400000000000000000000001171520674146500257240ustar00rootroot00000000000000# Anti-bot-detection Check this [project](https://github.com/go-rod/stealth). golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/000077500000000000000000000000001520674146500242025ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/README.md000066400000000000000000000107171520674146500254670ustar00rootroot00000000000000# Rod comparison with chromedp chromedp is one of the most popular drivers for Devtools Protocol. To help developers who are familiar with chromedp to understand rod better we created side by side examples between rod and chromedp. To run an example: 1. clone rod 2. cd to the folder of an example, such as `cd lib/examples/compare-chromedp/click` 3. run `go run .` | rod | chromedp | Description | | ---------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | [click](./click) | [click](https://github.com/chromedp/examples/blob/master/click) | use a selector to click on an element | | [cookie](./cookie) | [cookie](https://github.com/chromedp/examples/blob/master/cookie) | set a HTTP cookie on requests | | [download_file](./download_file) | [download_file](https://github.com/chromedp/examples/tree/master/download_file) | do headless file downloads | | [download_image](./download_image) | [download_image](https://github.com/chromedp/examples/tree/master/download_image) | do headless image downloads | | [emulate](./emulate) | [emulate](https://github.com/chromedp/examples/blob/master/emulate) | emulate a specific device such as an iPhone | | [eval](./eval) | [eval](https://github.com/chromedp/examples/blob/master/eval) | evaluate javascript and retrieve the result | | [headers](./headers) | [headers](https://github.com/chromedp/examples/blob/master/headers) | set a HTTP header on requests | | [keys](./keys) | [keys](https://github.com/chromedp/examples/blob/master/keys) | send key events to an element | | [logic](./logic) | [logic](https://github.com/chromedp/examples/blob/master/logic) | more complex logic beyond simple actions | | [pdf](./pdf) | [pdf](https://github.com/chromedp/examples/tree/master/pdf) | capture a pdf of a page | | [proxy](./proxy) | [proxy](https://github.com/chromedp/examples/tree/master/proxy) | authenticate a proxy server which requires authentication | | [remote](./remote) | [remote](https://github.com/chromedp/examples/blob/master/remote) | connect to an existing DevTools instance using a remote WebSocket URL | | [screenshot](./screenshot) | [screenshot](https://github.com/chromedp/examples/blob/master/screenshot) | take a screenshot of a specific element and of the entire browser viewport | | [submit](./submit) | [submit](https://github.com/chromedp/examples/blob/master/submit) | fill out and submit a form | | [subtree](./subtree) | [subtree](https://github.com/chromedp/examples/tree/master/subtree) | populate and travel a subtree of the DOM | | [text](./text) | [text](https://github.com/chromedp/examples/blob/master/text) | extract text from a specific element | | [upload](./upload) | [upload](https://github.com/chromedp/examples/blob/master/upload) | upload a file on a form | | [visible](./visible) | [visible](https://github.com/chromedp/examples/blob/master/visible) | wait until an element is visible | Occasionally, some of these examples may break if the specific websites these examples use get updated. We suggest you create an [issue](https://github.com/go-rod/rod/issues/new/choose). golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/click/000077500000000000000000000000001520674146500252675ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/click/main.go000066400000000000000000000012751520674146500265470ustar00rootroot00000000000000// Package main ... package main import ( "log" "time" "github.com/go-rod/rod" ) // This example demonstrates how to use a selector to click on an element. func main() { page := rod.New(). MustConnect(). Trace(true). // log useful info about what rod is doing Timeout(15 * time.Second). MustPage("https://pkg.go.dev/time/") // wait for footer element is visible (ie, page is loaded) page.MustElement(`body > footer`).MustWaitVisible() // find and click "Expand All" link page.MustElement(`#pkg-examples`).MustClick() // retrieve the value of the textarea example := page.MustElement(`#example-After textarea`).MustText() log.Printf("Go's time.After example:\n%s", example) } golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/cookie/000077500000000000000000000000001520674146500254535ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/cookie/main.go000066400000000000000000000032501520674146500267260ustar00rootroot00000000000000// Package main ... package main import ( "encoding/json" "fmt" "log" "net" "net/http" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) // This example demonstrates how we can modify the cookies on a web page. func main() { expr := proto.TimeSinceEpoch(time.Now().Add(180 * 24 * time.Hour).Unix()) page := rod.New().MustConnect().MustPage() page.MustSetCookies(&proto.NetworkCookieParam{ Name: "cookie1", Value: "value1", Domain: "127.0.0.1", HTTPOnly: true, Expires: expr, }, &proto.NetworkCookieParam{ Name: "cookie2", Value: "value2", Domain: "127.0.0.1", HTTPOnly: true, Expires: expr, }) page.MustNavigate(cookieServer()) // read network values for i, cookie := range page.MustCookies() { log.Printf("chrome cookie %d: %+v", i, cookie) } // chrome received cookies log.Printf("chrome received cookies: %s", page.MustElement(`#result`).MustText()) } // cookieServer creates a simple HTTP server that logs any passed cookies. func cookieServer() string { l, _ := net.Listen("tcp4", "127.0.0.1:0") go func() { _ = http.Serve(l, http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { cookies := req.Cookies() for i, cookie := range cookies { log.Printf("from %s, server received cookie %d: %v", req.RemoteAddr, i, cookie) } buf, err := json.MarshalIndent(req.Cookies(), "", " ") if err != nil { http.Error(res, err.Error(), http.StatusInternalServerError) return } _, _ = fmt.Fprintf(res, indexHTML, string(buf)) })) }() return "http://" + l.Addr().String() } const ( indexHTML = `
%s
` ) golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/download_file/000077500000000000000000000000001520674146500270105ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/download_file/main.go000066400000000000000000000015741520674146500302720ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "log" "os" "path/filepath" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) func main() { // get working directory wd, err := os.Getwd() if err != nil { log.Fatal(err) } browser := rod.New().MustConnect() page := browser.MustPage("https://github.com/chromedp/examples") page.MustElementR("summary", "Code").MustClick() wait := page.Browser().WaitDownload(wd) go browser.EachEvent(func(e *proto.PageDownloadProgress) bool { completed := "(unknown)" if e.TotalBytes != 0 { completed = fmt.Sprintf("%0.2f%%", e.ReceivedBytes/e.TotalBytes*100.0) } log.Printf("state: %s, completed: %s\n", e.State, completed) return e.State == proto.PageDownloadProgressStateCompleted })() page.MustElementR("a", "Download ZIP").MustClick() res := wait() log.Printf("wrote %s", filepath.Join(wd, res.GUID)) } golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/download_image/000077500000000000000000000000001520674146500271535ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/download_image/main.go000066400000000000000000000006531520674146500304320ustar00rootroot00000000000000// Package main ... package main import ( "log" "os" "github.com/go-rod/rod" ) func main() { u := "https://avatars.githubusercontent.com/u/33149672" browser := rod.New().MustConnect() page := browser.MustPage(u).MustWaitLoad() b, err := page.GetResource(u) if err != nil { log.Fatal(err) } if err := os.WriteFile("download.png", b, 0o644); err != nil { log.Fatal(err) } log.Print("wrote download.png") } golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/emulate/000077500000000000000000000000001520674146500256365ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/emulate/main.go000066400000000000000000000010611520674146500271070ustar00rootroot00000000000000// Package main ... package main import ( "github.com/go-rod/rod" "github.com/go-rod/rod/lib/devices" ) func main() { page := rod.New().MustConnect().MustPage() // emulate iPhone 7 landscape err := page.Emulate(devices.IPhone6or7or8.Landscape()) if err != nil { panic(err) } page.MustNavigate("https://www.whatsmyua.info/") page.MustScreenshot("screenshot1.png") // reset page.MustEmulate(devices.Clear) page.MustSetViewport(1920, 2000, 1, false) page.MustNavigate("https://www.whatsmyua.info/?a") page.MustScreenshot("screenshot2.png") } golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/eval/000077500000000000000000000000001520674146500251315ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/eval/main.go000066400000000000000000000007551520674146500264130ustar00rootroot00000000000000// Package main ... package main import ( "log" "github.com/go-rod/rod" ) // This example shows how we can use Eval to run scripts in the page. // Note: `this` in the eval function will refer to the element that Eval is // called on. This can be useful for things such as blurring elements. func main() { res := rod.New().MustConnect(). MustPage("https://www.google.com/"). MustElement(`input`). MustEval("() => Object.keys(window)") log.Printf("window object keys: %v", res) } golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/headers/000077500000000000000000000000001520674146500256155ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/headers/main.go000066400000000000000000000020511520674146500270660ustar00rootroot00000000000000// Package main ... package main import ( "encoding/json" "fmt" "log" "net" "net/http" "github.com/go-rod/rod" ) // This example demonstrates how to set a HTTP header on requests. func main() { host := headerServer() page := rod.New().MustConnect().MustPage(host) page.MustSetExtraHeaders("X-Header", "my request header") page.MustNavigate(host) res := page.MustElement("#result").MustText() log.Printf("received headers: %s", res) } // headerServer is a simple HTTP server that displays the passed headers in the html. func headerServer() string { l, _ := net.Listen("tcp4", "127.0.0.1:0") go func() { _ = http.Serve(l, http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { buf, err := json.MarshalIndent(req.Header, "", " ") if err != nil { http.Error(res, err.Error(), http.StatusInternalServerError) return } _, _ = fmt.Fprintf(res, indexHTML, string(buf)) })) }() return "http://" + l.Addr().String() } const indexHTML = `
%s
` golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/keys/000077500000000000000000000000001520674146500251555ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/keys/main.go000066400000000000000000000034551520674146500264370ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "log" "net" "net/http" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/input" ) // This example demonstrates how to send key events to an element. func main() { page := rod.New().MustConnect().MustPage(testServer()) val1 := page.MustElement("#input1").MustText() // cSpell:ignore naoeu val2 := page.MustElement("#textarea1").MustInput("\b\b\n\naoeu\n\ntest1\n\nblah2\n\n\t\t\t\b\bother box!\t\ntest4").MustText() val3 := page.MustElement("#input2").MustInput("test3").MustText() val4 := page.MustElement("#select1").MustType(input.ArrowDown, input.ArrowDown).MustProperty("value").Str() log.Printf("#input1 value: %s", val1) log.Printf("#textarea1 value: %s", val2) log.Printf("#input2 value: %s", val3) log.Printf("#select1 value: %s", val4) } // testServer is a simple HTTP server that displays elements and inputs. func testServer() string { l, _ := net.Listen("tcp4", "127.0.0.1:0") go func() { _ = http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprint(w, indexHTML) })) }() return "http://" + l.Addr().String() } const indexHTML = ` example

box3

box4 text



` golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/logic/000077500000000000000000000000001520674146500252775ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/logic/main.go000066400000000000000000000012621520674146500265530ustar00rootroot00000000000000// Package main ... package main import ( "log" "time" "github.com/go-rod/rod" ) // On awesome-go page, finding the specified section sect, // and retrieving the associated projects from the page. func main() { page := rod.New().MustConnect().Timeout(time.Second * 15).MustPage("https://github.com/avelino/awesome-go") section := page.MustElementR("p", "Selenium and browser control tools").MustNext() // query children elements of an element projects := section.MustElements("li") for _, project := range projects { link := project.MustElement("a") log.Printf( "project %s (%s): '%s'", link.MustText(), link.MustProperty("href"), project.MustText(), ) } } golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/pdf/000077500000000000000000000000001520674146500247535ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/pdf/main.go000066400000000000000000000003421520674146500262250ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "github.com/go-rod/rod" ) func main() { rod.New().MustConnect().MustPage("https://www.google.com/").MustWaitLoad().MustPDF("sample.pdf") fmt.Println("wrote sample.pdf") } golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/proxy/000077500000000000000000000000001520674146500253635ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/proxy/main.go000066400000000000000000000034001520674146500266330ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "log" "net/http" "net/http/httptest" "net/http/httputil" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" ) func main() { p := httptest.NewServer(newProxy()) defer p.Close() s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprint(w, "test") })) defer s.Close() url := launcher.New().Proxy(p.URL).Set("proxy-bypass-list", "<-loopback>").MustLaunch() browser := rod.New().ControlURL(url).MustConnect() defer browser.MustClose() browser.MustIgnoreCertErrors(true) go browser.MustHandleAuth("u", "p")() page := browser.MustPage(s.URL) page.MustNavigate(s.URL + "/tab") } // newProxy creates a proxy that requires authentication. func newProxy() *httputil.ReverseProxy { return &httputil.ReverseProxy{ Director: func(r *http.Request) { if dump, err := httputil.DumpRequest(r, true); err == nil { log.Printf("%s", dump) } // hardcode username/password "u:p" (base64 encoded: dTpw ) to make it simple if auth := r.Header.Get("Proxy-Authorization"); auth != "Basic dTpw" { r.Header.Set("X-Failed", "407") } }, Transport: &transport{http.DefaultTransport}, ErrorHandler: func(w http.ResponseWriter, _ *http.Request, err error) { if err.Error() == "407" { log.Println("proxy: not authorized") w.Header().Add("Proxy-Authenticate", `Basic realm="Proxy Authorization"`) w.WriteHeader(http.StatusProxyAuthRequired) } else { w.WriteHeader(http.StatusBadGateway) } }, } } type transport struct { http.RoundTripper } func (t *transport) RoundTrip(r *http.Request) (*http.Response, error) { if h := r.Header.Get("X-Failed"); h != "" { return nil, fmt.Errorf(h) } return t.RoundTripper.RoundTrip(r) } golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/remote/000077500000000000000000000000001520674146500254755ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/remote/main.go000066400000000000000000000012061520674146500267470ustar00rootroot00000000000000// Package main ... package main import ( "flag" "log" "github.com/go-rod/rod" ) var flagDevToolWsURL = flag.String("devtools-ws-url", "", "DevTools WebSocket URL") // This example demonstrates how to connect to an existing Chrome DevTools // instance using a remote WebSocket URL. func main() { flag.Parse() if *flagDevToolWsURL == "" { log.Fatal("must specify -devtools-ws-url") } page := rod.New().ControlURL(*flagDevToolWsURL).MustConnect().MustPage("https://duckduckgo.com") page.MustElement("#logo_homepage_link").MustWaitVisible() log.Println("Body of duckduckgo.com starts with:") log.Println(page.MustHTML()[0:100]) } golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/screenshot/000077500000000000000000000000001520674146500263575ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/screenshot/main.go000066400000000000000000000015411520674146500276330ustar00rootroot00000000000000// Package main ... package main import ( "os" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" "github.com/ysmood/gson" ) // This example demonstrates how to take a screenshot of a specific element and // of the entire browser viewport, as well as using `kit` // to store it into a file. func main() { browser := rod.New().MustConnect() // capture screenshot of an element browser.MustPage("https://google.com").MustElement("body div").MustScreenshot("elementScreenshot.png") // capture entire browser viewport, returning jpg with quality=90 buf, err := browser.MustPage("https://brank.as/").Screenshot(true, &proto.PageCaptureScreenshot{ Format: proto.PageCaptureScreenshotFormatJpeg, Quality: gson.Int(90), }) if err != nil { panic(err) } err = os.WriteFile("fullScreenshot.png", buf, 0o644) if err != nil { panic(err) } } golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/submit/000077500000000000000000000000001520674146500255055ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/submit/main.go000066400000000000000000000010041520674146500267530ustar00rootroot00000000000000// Package main ... package main import ( "log" "strings" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/input" ) // This example demonstrates how to fill out and submit a form. func main() { page := rod.New().MustConnect().MustPage("https://github.com/search") page.MustElement(`input[name=q]`).MustWaitVisible().MustInput("chromedp").MustType(input.Enter) res := page.MustElementR("a", "chromedp").MustParent().MustParent().MustNext().MustText() log.Printf("got: `%s`", strings.TrimSpace(res)) } golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/subtree/000077500000000000000000000000001520674146500256535ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/subtree/main.go000066400000000000000000000027551520674146500271370ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "io" "log" "net/http" "net/http/httptest" "os" "strings" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) func main() { // create a test server to serve the page ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprint(w, ` Title

content of h1 1 hello world

`, ) })) defer ts.Close() page := rod.New().MustConnect().MustPage(ts.URL) node, err := page.MustElement("body").Describe(-1, true) if err != nil { log.Fatal(err) } printNodes(os.Stdout, node.Children, "", " ") } func printNodes(w io.Writer, nodes []*proto.DOMNode, padding, indent string) { for _, node := range nodes { switch { case node.NodeName == "#text": fmt.Fprintf(w, "%s#text: %q\n", padding, node.NodeValue) default: fmt.Fprintf(w, "%s%s:\n", padding, strings.ToLower(node.NodeName)) if n := len(node.Attributes); n > 0 { fmt.Fprintf(w, "%sattributes:\n", padding+indent) for i := 0; i < n; i += 2 { fmt.Fprintf(w, "%s%s: %q\n", padding+indent+indent, node.Attributes[i], node.Attributes[i+1]) } } } if node.ChildNodeCount != nil { fmt.Fprintf(w, "%schildren:\n", padding+indent) printNodes(w, node.Children, padding+indent+indent, indent) } } } golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/text/000077500000000000000000000000001520674146500251665ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/text/main.go000066400000000000000000000005421520674146500264420ustar00rootroot00000000000000// Package main ... package main import ( "log" "strings" "github.com/go-rod/rod" ) // This example demonstrates how to extract text from a specific element. func main() { page := rod.New().MustConnect().MustPage("https://pkg.go.dev/time") res := page.MustElement("#pkg-overview").MustParent().MustText() log.Println(strings.TrimSpace(res)) } golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/upload/000077500000000000000000000000001520674146500254665ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/upload/main.go000066400000000000000000000032711520674146500267440ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "io" "log" "net" "net/http" "os" "github.com/go-rod/rod" ) // This example demonstrates how to upload a file on a form. func main() { host := uploadServer() page := rod.New().MustConnect().MustPage(host) page.MustElement(`input[name="upload"]`).MustSetFiles("./main.go") page.MustElement(`input[name="submit"]`).MustClick() log.Printf( "original size: %d, upload size: %s", size("./main.go"), page.MustElement("#result").MustText(), ) } // get some info about the file. func size(file string) int { fi, err := os.Stat(file) if err != nil { panic(err) } return int(fi.Size()) } func uploadServer() string { // create http server and result channel mux := http.NewServeMux() mux.HandleFunc("/", func(res http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprint(res, uploadHTML) }) mux.HandleFunc("/upload", func(res http.ResponseWriter, req *http.Request) { f, _, err := req.FormFile("upload") if err != nil { http.Error(res, err.Error(), http.StatusBadRequest) return } defer func() { _ = f.Close() }() buf, err := io.ReadAll(f) if err != nil { http.Error(res, err.Error(), http.StatusBadRequest) return } _, _ = fmt.Fprintf(res, resultHTML, len(buf)) }) l, _ := net.Listen("tcp4", "127.0.0.1:0") go func() { _ = http.Serve(l, mux) }() return "http://" + l.Addr().String() } const ( uploadHTML = `
` resultHTML = `
%d
` ) golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/visible/000077500000000000000000000000001520674146500256375ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/compare-chromedp/visible/main.go000066400000000000000000000031031520674146500271070ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "log" "net" "net/http" "github.com/go-rod/rod" ) func main() { page := rod.New().MustConnect().MustPage(testServer()) page.MustEval(makeVisibleScript) log.Printf("waiting 3s for box to become visible") page.MustElement("#box1").MustWaitVisible() log.Printf(">>>>>>>>>>>>>>>>>>>> BOX1 IS VISIBLE") page.MustElement("#box2").MustWaitVisible() log.Printf(">>>>>>>>>>>>>>>>>>>> BOX2 IS VISIBLE") } const ( makeVisibleScript = `() => setTimeout(function() { document.querySelector('#box1').style.display = ''; }, 3000)` ) // testServer is a simple HTTP server that serves a static html page. func testServer() string { l, _ := net.Listen("tcp4", "127.0.0.1:0") go func() { _ = http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprint(w, indexHTML) })) }() return "http://" + l.Addr().String() } const indexHTML = ` example

box3

box4 text



` golang-github-go-rod-rod-0.116.2/lib/examples/connect-browser/000077500000000000000000000000001520674146500240675ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/connect-browser/main.go000066400000000000000000000011471520674146500253450ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" ) // To manually launch a browser. func main() { // Launch your local browser first: // // chrome --headless --remote-debugging-port=9222 // // Or use docker: // // 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 // u := launcher.MustResolveURL("") browser := rod.New().ControlURL(u).MustConnect() fmt.Println( browser.MustPage("https://mdn.dev/").MustEval("() => document.title"), ) } golang-github-go-rod-rod-0.116.2/lib/examples/custom-launch/000077500000000000000000000000001520674146500235375ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/custom-launch/main.go000066400000000000000000000006231520674146500250130ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" ) func main() { l := launcher.New() // For more info: https://pkg.go.dev/github.com/go-rod/rod/lib/launcher u := l.MustLaunch() browser := rod.New().ControlURL(u).MustConnect() page := browser.MustPage("http://example.com").MustWaitStable() fmt.Println(page.MustInfo().Title) } golang-github-go-rod-rod-0.116.2/lib/examples/custom-websocket/000077500000000000000000000000001520674146500242535ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/custom-websocket/go.mod000066400000000000000000000004211520674146500253560ustar00rootroot00000000000000module github.com/go-rod/rod/lib/examples/custom-websocket go 1.18 require github.com/gobwas/ws v1.1.0 require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d // indirect ) golang-github-go-rod-rod-0.116.2/lib/examples/custom-websocket/go.sum000066400000000000000000000012741520674146500254120ustar00rootroot00000000000000github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d h1:MiWWjyhUzZ+jvhZvloX6ZrUsdEghn8a64Upd8EMHglE= golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang-github-go-rod-rod-0.116.2/lib/examples/custom-websocket/main.go000066400000000000000000000016461520674146500255350ustar00rootroot00000000000000// Package main ... package main import ( "context" "fmt" "log" "net" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/launcher" "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" ) func main() { w := NewWebSocket(launcher.New().MustLaunch()) client := cdp.New().Start(w) p := rod.New().Client(client).MustConnect().MustPage("http://example.com") fmt.Println(p.MustInfo().Title) } // WebSocket is a custom websocket that uses gobwas/ws as the transport layer. type WebSocket struct { conn net.Conn } // NewWebSocket ... func NewWebSocket(u string) *WebSocket { conn, _, _, err := ws.Dial(context.Background(), u) if err != nil { log.Fatal(err) } return &WebSocket{conn} } // Send ... func (w *WebSocket) Send(b []byte) error { return wsutil.WriteClientText(w.conn, b) } // Read ... func (w *WebSocket) Read() ([]byte, error) { return wsutil.ReadServerText(w.conn) } golang-github-go-rod-rod-0.116.2/lib/examples/debug-deadlock/000077500000000000000000000000001520674146500236075ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/debug-deadlock/main.go000066400000000000000000000020251520674146500250610ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "github.com/go-rod/rod" "github.com/ysmood/gotrace" ) // This example shows how to detect the hanging points of golang code. // It's actually a general way to debug any golang project. func main() { defer checkLock()() go yourCodeHere() } // Put your code here, press Ctrl+C when you feel the program is hanging. // Read each goroutine's stack that is related to your own code logic. func yourCodeHere() { page := rod.New().MustConnect().MustPage("http://mdn.dev") go page.MustElement("not-exists") } // For this example you will find something like this below: /* goroutine 7 [select]: github.com/go-rod/rod.(*Page).MustElement(0xc00037e000, 0xc00063a0f0, 0x1, 0x1, 0x0) rod/must.go:425 +0x4d created by main.yourCodeHere rod/lib/examples/debug-deadlock/main.go:22 +0xb8 */ // From it we know the line 22 is blocking the code. func checkLock() func() { ctx := gotrace.Signal() ignored := gotrace.IgnoreCurrent() return func() { fmt.Println(gotrace.Wait(ctx, ignored)) } } golang-github-go-rod-rod-0.116.2/lib/examples/disable-window-alert/000077500000000000000000000000001520674146500247725ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/disable-window-alert/main.go000066400000000000000000000015441520674146500262510ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "net/http" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/utils" ) func main() { go serve() browser := rod.New().MustConnect() defer browser.MustClose() // Creating a Page Object page := browser.MustPage() // Evaluates given script in every frame upon creation // Disable all alerts by making window.alert no-op. page.MustEvalOnNewDocument(`window.alert = () => {}`) // Navigate to the website you want to visit page.MustNavigate("http://localhost:8080") fmt.Println(page.MustElement("script").MustText()) } const testPage = `` // mock a server. func serve() { mux := http.NewServeMux() mux.HandleFunc("/", func(res http.ResponseWriter, _ *http.Request) { utils.E(fmt.Fprint(res, testPage)) }) utils.E(http.ListenAndServe(":8080", mux)) } golang-github-go-rod-rod-0.116.2/lib/examples/e2e-testing/000077500000000000000000000000001520674146500231035ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/e2e-testing/README.md000066400000000000000000000006141520674146500243630ustar00rootroot00000000000000# Overview This is a sample project to demonstrate how to use Rod to setup an end-to-end testing (e2e testing) project. The test cases will run in parallel. Use standard Go commands to test the project, such as run `go test` to execute all tests. ## Debugging 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) golang-github-go-rod-rod-0.116.2/lib/examples/e2e-testing/calculator_test.go000066400000000000000000000014011520674146500266160ustar00rootroot00000000000000// Package main ... package main import "testing" const appURL = "https://go-rod.github.io/calculator/" // test case: 1 + 2 = 3. func TestAdd(t *testing.T) { g := setup(t) p := g.page(appURL) p.MustElementR("button", "1").MustClick() p.MustElementR("button", `^\+$`).MustClick() p.MustElementR("button", "2").MustClick() p.MustElementR("button", "=").MustClick() // assert the result with t.Eq g.Eq(p.MustElement(".component-display").MustText(), "3") } // test case: 2 * 3 = 6. func TestMultiple(t *testing.T) { g := setup(t) p := g.page(appURL) // use for-loop to click each button for _, regex := range []string{"2", "x", "3", "="} { p.MustElementR("button", regex).MustClick() } g.Eq(p.MustElement(".component-display").MustText(), "6") } golang-github-go-rod-rod-0.116.2/lib/examples/e2e-testing/go.mod000066400000000000000000000000771520674146500242150ustar00rootroot00000000000000module github.com/go-rod/rod/lib/examples/e2e-testing go 1.18 golang-github-go-rod-rod-0.116.2/lib/examples/e2e-testing/setup_test.go000066400000000000000000000011271520674146500256320ustar00rootroot00000000000000// This is the setup file for this test suite. package main import ( "testing" "github.com/go-rod/rod" "github.com/ysmood/got" ) // test context. type G struct { got.G browser *rod.Browser } // setup for tests. var setup = func() func(t *testing.T) G { browser := rod.New().MustConnect() return func(t *testing.T) G { t.Parallel() // run each test concurrently return G{got.New(t), browser} } }() // a helper function to create an incognito page. func (g G) page(url string) *rod.Page { page := g.browser.MustIncognito().MustPage(url) g.Cleanup(page.MustClose) return page } golang-github-go-rod-rod-0.116.2/lib/examples/launch-managed/000077500000000000000000000000001520674146500236215ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/launch-managed/main.go000066400000000000000000000031661520674146500251020ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/utils" ) func main() { // This example is to launch a browser remotely, not connect to a running browser remotely, // to connect to a running browser check the "../connect-browser" example. // Rod provides a docker image for beginners, run the below to start a launcher.Manager: // // docker run --rm -p 7317:7317 ghcr.io/go-rod/rod // // For available CLI flags run: docker run --rm ghcr.io/go-rod/rod rod-manager -h // For more information, check the doc of launcher.Manager l := launcher.MustNewManaged("") // You can also set any flag remotely before you launch the remote browser. // Available flags: https://peter.sh/experiments/chromium-command-line-switches l.Set("disable-gpu").Delete("disable-gpu") // Launch with headful mode l.Headless(false).XVFB("--server-num=5", "--server-args=-screen 0 1600x900x16") browser := rod.New().Client(l.MustClient()).MustConnect() // You may want to start a server to watch the screenshots of the remote browser. launcher.Open(browser.ServeMonitor("")) fmt.Println( browser.MustPage("https://developer.mozilla.org").MustEval("() => document.title"), ) // Launch another browser with the same docker container. ll := launcher.MustNewManaged("") // You can set different flags for each browser. ll.Set("disable-sync").Delete("disable-sync") anotherBrowser := rod.New().Client(ll.MustClient()).MustConnect() fmt.Println( anotherBrowser.MustPage("https://go-rod.github.io").MustEval("() => document.title"), ) utils.Pause() } golang-github-go-rod-rod-0.116.2/lib/examples/stripe/000077500000000000000000000000001520674146500222635ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/stripe/main.go000066400000000000000000000026321520674146500235410ustar00rootroot00000000000000// Package main ... package main import ( "bytes" "io" "net/http" "github.com/go-rod/rod" "github.com/ysmood/gson" ) // An example to handle stripe 3DS callback. func main() { page := rod.New().MustConnect().MustPage(getRedirectURL()) // Get the button from the nested iframes frame01 := page.MustElement("div iframe").MustFrame() frame02 := frame01.MustElement("#challengeFrame").MustFrame() btn := frame02.MustElementR("button", "COMPLETE").MustWaitStable() wait := frame02.MustWaitRequestIdle() btn.MustClick() wait() } // Create a card payment that requires Visa's confirmation. func getRedirectURL() string { token := post( "/tokens", "card[number]=4000000000003220&card[exp_month]=7&card[exp_year]=2025&card[cvc]=314", ).Get("id").Str() return post( "/payment_intents", "amount=100¤cy=usd&payment_method_data[type]=card&confirm=true&return_url=https%3A%2F%2Fmdn.dev"+ // cSpell:ignore Fmdn "&payment_method_data[card][token]="+token, ).Get("next_action.redirect_to_url.url").Str() } func post(path, body string) gson.JSON { req, _ := http.NewRequest(http.MethodPost, "https://api.stripe.com/v1"+path, bytes.NewBufferString(body)) req.Header.Add("Authorization", "Bearer sk_test_4eC39HqLyjWDarjtT1zdp7dc") // cSpell:ignore Darjt res, _ := http.DefaultClient.Do(req) if res != nil { defer func() { _ = res.Body.Close() }() } data, _ := io.ReadAll(res.Body) return gson.New(data) } golang-github-go-rod-rod-0.116.2/lib/examples/translator/000077500000000000000000000000001520674146500231465ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/translator/README.md000066400000000000000000000000621520674146500244230ustar00rootroot00000000000000# Run the Example ```bash go run . 茶叶蛋 ``` golang-github-go-rod-rod-0.116.2/lib/examples/translator/main.go000066400000000000000000000013101520674146500244140ustar00rootroot00000000000000// Package main ... package main import ( "flag" "fmt" "log" "strings" "github.com/go-rod/rod" ) func main() { flag.Parse() // get the commandline arguments source := strings.TrimSpace(strings.Join(flag.Args(), " ")) if source == "" { log.Fatal("usage: go run main.go -- 'This is the phrase to translate to Spanish.'") } browser := rod.New().MustConnect() page := browser.MustPage("https://translate.google.com/?sl=auto&tl=es&op=translate") el := page.MustElement(`textarea[aria-label="Source text"]`) wait := page.MustWaitRequestIdle("https://accounts.google.com") el.MustInput(source) wait() result := page.MustElement("[role=region] span[lang]").MustText() fmt.Println(result) } golang-github-go-rod-rod-0.116.2/lib/examples/use-rod-like-chrome-extension/000077500000000000000000000000001520674146500265425ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/examples/use-rod-like-chrome-extension/main.go000066400000000000000000000054221520674146500300200ustar00rootroot00000000000000// Package main ... package main import ( "encoding/base64" "fmt" "io" "net/http" "os" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/gson" ) // For example, when you log into your github account, and you want to reuse the login session for automation task. // You can use this example to achieve such functionality. Rod will be just like your browser extension. func main() { // Make sure you have closed your browser completely, UserMode can't control a browser that is not launched by it. // Launches a new browser with the "new user mode" option, and returns the URL to control that browser. wsURL := launcher.NewUserMode().MustLaunch() browser := rod.New().ControlURL(wsURL).MustConnect().NoDefaultDevice() // Run a extension. Here we created a link previewer extension as an example. // With this extension, whenever you hover on a link a preview of the linked page will popup. linkPreviewer(browser) browser.MustPage() waitExit() } func linkPreviewer(browser *rod.Browser) { // Create a headless browser to generate preview of links on background. previewer := rod.New().MustConnect() previewer.MustSetCookies(browser.MustGetCookies()...) // share cookies pool := rod.NewPagePool(5) create := func() *rod.Page { return previewer.MustPage() } go browser.EachEvent(func(e *proto.TargetTargetCreated) { if e.TargetInfo.Type != proto.TargetTargetInfoTypePage { return } page := browser.MustPageFromTargetID(e.TargetInfo.TargetID) // Inject js to every new page page.MustEvalOnNewDocument(js) // Expose a function to the page to provide preview page.MustExpose("getPreview", func(url gson.JSON) (interface{}, error) { p := pool.MustGet(create) defer pool.Put(p) p.MustNavigate(url.Str()) return base64.StdEncoding.EncodeToString(p.MustScreenshot()), nil }) })() } var jsLib = get("https://unpkg.com/@popperjs/core@2") + get("https://unpkg.com/tippy.js@6") var js = fmt.Sprintf(`window.addEventListener('load', () => { %s function setup(el) { el.classList.add('x-set') tippy(el, {onShow: async (it) => { if (it.props.content.src) return let img = document.createElement('img') img.style.width = '400px' img.src = "data:image/png;base64," + await getPreview(el.href) it.setContent(img) }, content: 'loading...', maxWidth: 500}) } (function check() { Array.from(document.querySelectorAll('a:not(.x-set)')).forEach(setup) setTimeout(check, 1000) })() })`, jsLib) func get(u string) string { res, err := http.Get(u) utils.E(err) defer func() { _ = res.Body.Close() }() b, err := io.ReadAll(res.Body) utils.E(err) return string(b) } func waitExit() { fmt.Println("Press Enter to exit...") utils.E(fmt.Scanln()) os.Exit(0) } golang-github-go-rod-rod-0.116.2/lib/input/000077500000000000000000000000001520674146500202765ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/input/README.md000066400000000000000000000000461520674146500215550ustar00rootroot00000000000000# input A lib to help encode inputs. golang-github-go-rod-rod-0.116.2/lib/input/keyboard.go000066400000000000000000000053441520674146500224330ustar00rootroot00000000000000// Package input ... package input import ( "github.com/go-rod/rod/lib/proto" "github.com/ysmood/gson" ) // Modifier values. const ( ModifierAlt = 1 ModifierControl = 2 ModifierMeta = 4 ModifierShift = 8 ) // Key symbol. type Key rune // keyMap for key description. var keyMap = map[Key]KeyInfo{} // keyMapShifted for shifted key description. var keyMapShifted = map[Key]KeyInfo{} var keyShiftedMap = map[Key]Key{} // AddKey to KeyMap. func AddKey(key string, shiftedKey string, code string, keyCode int, location int) Key { if len(key) == 1 { r := Key(key[0]) if _, has := keyMap[r]; !has { keyMap[r] = KeyInfo{key, code, keyCode, location} if len(shiftedKey) == 1 { rs := Key(shiftedKey[0]) keyMapShifted[rs] = KeyInfo{shiftedKey, code, keyCode, location} keyShiftedMap[r] = rs } return r } } k := Key(keyCode + (location+1)*256) keyMap[k] = KeyInfo{key, code, keyCode, location} return k } // Info of the key. func (k Key) Info() KeyInfo { if k, has := keyMap[k]; has { return k } if k, has := keyMapShifted[k]; has { return k } panic("key not defined") } // KeyInfo of a key // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent type KeyInfo struct { // Here's the value for Shift key on the keyboard Key string // Shift Code string // ShiftLeft KeyCode int // 16 Location int // 1 } // Shift returns the shifted key, such as shifted "1" is "!". func (k Key) Shift() (Key, bool) { s, has := keyShiftedMap[k] return s, has } // Printable returns true if the key is printable. func (k Key) Printable() bool { return len(k.Info().Key) == 1 } // Modifier returns the modifier value of the key. func (k Key) Modifier() int { switch k.Info().KeyCode { case 18: return ModifierAlt case 17: return ModifierControl case 91, 92: return ModifierMeta case 16: return ModifierShift } return 0 } // Encode general key event. func (k Key) Encode(t proto.InputDispatchKeyEventType, modifiers int) *proto.InputDispatchKeyEvent { tp := t if t == proto.InputDispatchKeyEventTypeKeyDown && !k.Printable() { tp = proto.InputDispatchKeyEventTypeRawKeyDown } info := k.Info() l := gson.Int(info.Location) keypad := false if info.Location == 3 { l = nil keypad = true } txt := "" if k.Printable() { txt = info.Key } var cmd []string if IsMac { cmd = macCommands[info.Key] } e := &proto.InputDispatchKeyEvent{ Type: tp, WindowsVirtualKeyCode: info.KeyCode, Code: info.Code, Key: info.Key, Text: txt, UnmodifiedText: txt, Location: l, IsKeypad: keypad, Modifiers: modifiers, Commands: cmd, } return e } golang-github-go-rod-rod-0.116.2/lib/input/keyboard_test.go000066400000000000000000000062501520674146500234670ustar00rootroot00000000000000package input_test import ( "testing" "github.com/go-rod/rod/lib/input" "github.com/go-rod/rod/lib/proto" "github.com/ysmood/got" "github.com/ysmood/gson" ) func TestKeyMap(t *testing.T) { g := got.T(t) k := input.Key('a') g.Eq(k.Info(), input.KeyInfo{ Key: "a", Code: "KeyA", KeyCode: 65, Location: 0, }) k = input.Key('A') g.Eq(k.Info(), input.KeyInfo{ Key: "A", Code: "KeyA", KeyCode: 65, Location: 0, }) g.True(k.Printable()) k = input.Enter g.Eq(k.Info(), input.KeyInfo{ Key: "\r", Code: "Enter", KeyCode: 13, Location: 0, }) k = input.ShiftLeft g.Eq(k.Info(), input.KeyInfo /* len=4 */ { Key: "Shift", Code: "ShiftLeft", KeyCode: 16, Location: 1, }) g.False(k.Printable()) k = input.ShiftRight g.Eq(k.Info(), input.KeyInfo /* len=4 */ { Key: "Shift", Code: "ShiftRight", KeyCode: 16, Location: 2, }) k, has := input.Digit1.Shift() g.True(has) g.Eq(k.Info().Key, "!") _, has = input.Enter.Shift() g.False(has) g.Panic(func() { input.Key('\n').Info() }) } func TestKeyModifier(t *testing.T) { g := got.T(t) check := func(k input.Key, m int) { g.Helper() g.Eq(k.Modifier(), m) } check(input.KeyA, 0) check(input.AltLeft, 1) check(input.ControlLeft, 2) check(input.MetaLeft, 4) check(input.ShiftLeft, 8) } func TestKeyEncode(t *testing.T) { g := got.T(t) g.Eq(input.Key('a').Encode(proto.InputDispatchKeyEventTypeKeyDown, 0), &proto.InputDispatchKeyEvent{ Type: "keyDown", Text: "a", UnmodifiedText: "a", Code: "KeyA", Key: "a", WindowsVirtualKeyCode: 65, Location: gson.Int(0), }) g.Eq(input.Key('a').Encode(proto.InputDispatchKeyEventTypeKeyUp, 0), &proto.InputDispatchKeyEvent{ Type: "keyUp", Text: "a", UnmodifiedText: "a", Code: "KeyA", Key: "a", WindowsVirtualKeyCode: 65, Location: gson.Int(0), }) g.Eq(input.AltLeft.Encode(proto.InputDispatchKeyEventTypeKeyDown, 0), &proto.InputDispatchKeyEvent{ Type: "rawKeyDown", Code: "AltLeft", Key: "Alt", WindowsVirtualKeyCode: 18, Location: gson.Int(1), }) g.Eq(input.Numpad1.Encode(proto.InputDispatchKeyEventTypeKeyDown, 0), &proto.InputDispatchKeyEvent{ Type: "keyDown", Code: "Numpad1", Key: "1", Text: "1", UnmodifiedText: "1", WindowsVirtualKeyCode: 35, IsKeypad: true, }) } func TestMac(t *testing.T) { g := got.T(t) old := input.IsMac input.IsMac = true defer func() { input.IsMac = old }() zero := 0 g.Eq(input.ArrowDown.Encode(proto.InputDispatchKeyEventTypeKeyDown, 0), &proto.InputDispatchKeyEvent{ Type: "rawKeyDown", Code: "ArrowDown", Key: "ArrowDown", WindowsVirtualKeyCode: 40, AutoRepeat: false, IsKeypad: false, IsSystemKey: false, Location: &zero, Commands: []string{ "moveDown", }, }) } golang-github-go-rod-rod-0.116.2/lib/input/keymap.go000066400000000000000000000126731520674146500221240ustar00rootroot00000000000000package input // Key names // Reference: https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/usKeyboardLayout.ts var ( // Functions row. // Escape = AddKey("Escape", "", "Escape", 27, 0) F1 = AddKey("F1", "", "F1", 112, 0) F2 = AddKey("F2", "", "F2", 113, 0) F3 = AddKey("F3", "", "F3", 114, 0) F4 = AddKey("F4", "", "F4", 115, 0) F5 = AddKey("F5", "", "F5", 116, 0) F6 = AddKey("F6", "", "F6", 117, 0) F7 = AddKey("F7", "", "F7", 118, 0) F8 = AddKey("F8", "", "F8", 119, 0) F9 = AddKey("F9", "", "F9", 120, 0) F10 = AddKey("F10", "", "F10", 121, 0) F11 = AddKey("F11", "", "F11", 122, 0) F12 = AddKey("F12", "", "F12", 123, 0) // Numbers row. // Backquote = AddKey("`", "~", "Backquote", 192, 0) Digit1 = AddKey("1", "!", "Digit1", 49, 0) Digit2 = AddKey("2", "@", "Digit2", 50, 0) Digit3 = AddKey("3", "#", "Digit3", 51, 0) Digit4 = AddKey("4", "$", "Digit4", 52, 0) Digit5 = AddKey("5", "%", "Digit5", 53, 0) Digit6 = AddKey("6", "^", "Digit6", 54, 0) Digit7 = AddKey("7", "&", "Digit7", 55, 0) Digit8 = AddKey("8", "*", "Digit8", 56, 0) Digit9 = AddKey("9", "(", "Digit9", 57, 0) Digit0 = AddKey("0", ")", "Digit0", 48, 0) Minus = AddKey("-", "_", "Minus", 189, 0) Equal = AddKey("=", "+", "Equal", 187, 0) Backslash = AddKey(`\`, "|", "Backslash", 220, 0) Backspace = AddKey("Backspace", "", "Backspace", 8, 0) // First row. // Tab = AddKey("\t", "", "Tab", 9, 0) KeyQ = AddKey("q", "Q", "KeyQ", 81, 0) KeyW = AddKey("w", "W", "KeyW", 87, 0) KeyE = AddKey("e", "E", "KeyE", 69, 0) KeyR = AddKey("r", "R", "KeyR", 82, 0) KeyT = AddKey("t", "T", "KeyT", 84, 0) KeyY = AddKey("y", "Y", "KeyY", 89, 0) KeyU = AddKey("u", "U", "KeyU", 85, 0) KeyI = AddKey("i", "I", "KeyI", 73, 0) KeyO = AddKey("o", "O", "KeyO", 79, 0) KeyP = AddKey("p", "P", "KeyP", 80, 0) BracketLeft = AddKey("[", "{", "BracketLeft", 219, 0) BracketRight = AddKey("]", "}", "BracketRight", 221, 0) // Second row. // CapsLock = AddKey("CapsLock", "", "CapsLock", 20, 0) KeyA = AddKey("a", "A", "KeyA", 65, 0) KeyS = AddKey("s", "S", "KeyS", 83, 0) KeyD = AddKey("d", "D", "KeyD", 68, 0) KeyF = AddKey("f", "F", "KeyF", 70, 0) KeyG = AddKey("g", "G", "KeyG", 71, 0) KeyH = AddKey("h", "H", "KeyH", 72, 0) KeyJ = AddKey("j", "J", "KeyJ", 74, 0) KeyK = AddKey("k", "K", "KeyK", 75, 0) KeyL = AddKey("l", "L", "KeyL", 76, 0) Semicolon = AddKey(";", ":", "Semicolon", 186, 0) Quote = AddKey("'", `"`, "Quote", 222, 0) Enter = AddKey("\r", "", "Enter", 13, 0) // Third row. // ShiftLeft = AddKey("Shift", "", "ShiftLeft", 16, 1) KeyZ = AddKey("z", "Z", "KeyZ", 90, 0) KeyX = AddKey("x", "X", "KeyX", 88, 0) KeyC = AddKey("c", "C", "KeyC", 67, 0) KeyV = AddKey("v", "V", "KeyV", 86, 0) KeyB = AddKey("b", "B", "KeyB", 66, 0) KeyN = AddKey("n", "N", "KeyN", 78, 0) KeyM = AddKey("m", "M", "KeyM", 77, 0) Comma = AddKey(",", "<", "Comma", 188, 0) Period = AddKey(".", ">", "Period", 190, 0) Slash = AddKey("/", "?", "Slash", 191, 0) ShiftRight = AddKey("Shift", "", "ShiftRight", 16, 2) // Last row. // ControlLeft = AddKey("Control", "", "ControlLeft", 17, 1) MetaLeft = AddKey("Meta", "", "MetaLeft", 91, 1) AltLeft = AddKey("Alt", "", "AltLeft", 18, 1) Space = AddKey(" ", "", "Space", 32, 0) AltRight = AddKey("Alt", "", "AltRight", 18, 2) AltGraph = AddKey("AltGraph", "", "AltGraph", 225, 0) MetaRight = AddKey("Meta", "", "MetaRight", 92, 2) ContextMenu = AddKey("ContextMenu", "", "ContextMenu", 93, 0) ControlRight = AddKey("Control", "", "ControlRight", 17, 2) // Center block. // PrintScreen = AddKey("PrintScreen", "", "PrintScreen", 44, 0) ScrollLock = AddKey("ScrollLock", "", "ScrollLock", 145, 0) Pause = AddKey("Pause", "", "Pause", 19, 0) PageUp = AddKey("PageUp", "", "PageUp", 33, 0) PageDown = AddKey("PageDown", "", "PageDown", 34, 0) Insert = AddKey("Insert", "", "Insert", 45, 0) Delete = AddKey("Delete", "", "Delete", 46, 0) Home = AddKey("Home", "", "Home", 36, 0) End = AddKey("End", "", "End", 35, 0) ArrowLeft = AddKey("ArrowLeft", "", "ArrowLeft", 37, 0) ArrowUp = AddKey("ArrowUp", "", "ArrowUp", 38, 0) ArrowRight = AddKey("ArrowRight", "", "ArrowRight", 39, 0) ArrowDown = AddKey("ArrowDown", "", "ArrowDown", 40, 0) // Numpad. // NumLock = AddKey("NumLock", "", "NumLock", 144, 0) NumpadDivide = AddKey("/", "", "NumpadDivide", 111, 3) NumpadMultiply = AddKey("*", "", "NumpadMultiply", 106, 3) NumpadSubtract = AddKey("-", "", "NumpadSubtract", 109, 3) Numpad7 = AddKey("7", "", "Numpad7", 36, 3) Numpad8 = AddKey("8", "", "Numpad8", 38, 3) Numpad9 = AddKey("9", "", "Numpad9", 33, 3) Numpad4 = AddKey("4", "", "Numpad4", 37, 3) Numpad5 = AddKey("5", "", "Numpad5", 12, 3) Numpad6 = AddKey("6", "", "Numpad6", 39, 3) NumpadAdd = AddKey("+", "", "NumpadAdd", 107, 3) Numpad1 = AddKey("1", "", "Numpad1", 35, 3) Numpad2 = AddKey("2", "", "Numpad2", 40, 3) Numpad3 = AddKey("3", "", "Numpad3", 34, 3) Numpad0 = AddKey("0", "", "Numpad0", 45, 3) NumpadDecimal = AddKey(".", "", "NumpadDecimal", 46, 3) NumpadEnter = AddKey("\r", "", "NumpadEnter", 13, 3) ) golang-github-go-rod-rod-0.116.2/lib/input/mac_comands.go000066400000000000000000000152171520674146500230770ustar00rootroot00000000000000package input import "runtime" // IsMac OS. var IsMac = runtime.GOOS == "darwin" // commands for macOS // https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/macEditingCommands.ts var macCommands = map[string][]string{ "Backspace": {"deleteBackward"}, "Enter": {"insertNewline"}, "NumpadEnter": {"insertNewline"}, "Escape": {"cancelOperation"}, "ArrowUp": {"moveUp"}, "ArrowDown": {"moveDown"}, "ArrowLeft": {"moveLeft"}, "ArrowRight": {"moveRight"}, "F5": {"complete"}, "Delete": {"deleteForward"}, "Home": {"scrollToBeginningOfDocument"}, "End": {"scrollToEndOfDocument"}, "PageUp": {"scrollPageUp"}, "PageDown": {"scrollPageDown"}, "Shift+Backspace": {"deleteBackward"}, "Shift+Enter": {"insertNewline"}, "Shift+NumpadEnter": {"insertNewline"}, "Shift+Escape": {"cancelOperation"}, "Shift+ArrowUp": {"moveUpAndModifySelection"}, "Shift+ArrowDown": {"moveDownAndModifySelection"}, "Shift+ArrowLeft": {"moveLeftAndModifySelection"}, "Shift+ArrowRight": {"moveRightAndModifySelection"}, "Shift+F5": {"complete"}, "Shift+Delete": {"deleteForward"}, "Shift+Home": {"moveToBeginningOfDocumentAndModifySelection"}, "Shift+End": {"moveToEndOfDocumentAndModifySelection"}, "Shift+PageUp": {"pageUpAndModifySelection"}, "Shift+PageDown": {"pageDownAndModifySelection"}, "Shift+Numpad5": {"delete"}, "Control+Tab": {"selectNextKeyView"}, "Control+Enter": {"insertLineBreak"}, "Control+NumpadEnter": {"insertLineBreak"}, "Control+Quote": {"insertSingleQuoteIgnoringSubstitution"}, "Control+KeyA": {"moveToBeginningOfParagraph"}, "Control+KeyB": {"moveBackward"}, "Control+KeyD": {"deleteForward"}, "Control+KeyE": {"moveToEndOfParagraph"}, "Control+KeyF": {"moveForward"}, "Control+KeyH": {"deleteBackward"}, "Control+KeyK": {"deleteToEndOfParagraph"}, "Control+KeyL": {"centerSelectionInVisibleArea"}, "Control+KeyN": {"moveDown"}, "Control+KeyO": {"insertNewlineIgnoringFieldEditor", "moveBackward"}, "Control+KeyP": {"moveUp"}, "Control+KeyT": {"transpose"}, "Control+KeyV": {"pageDown"}, "Control+KeyY": {"yank"}, "Control+Backspace": {"deleteBackwardByDecomposingPreviousCharacter"}, "Control+ArrowUp": {"scrollPageUp"}, "Control+ArrowDown": {"scrollPageDown"}, "Control+ArrowLeft": {"moveToLeftEndOfLine"}, "Control+ArrowRight": {"moveToRightEndOfLine"}, "Shift+Control+Enter": {"insertLineBreak"}, "Shift+Control+NumpadEnter": {"insertLineBreak"}, "Shift+Control+Tab": {"selectPreviousKeyView"}, "Shift+Control+Quote": {"insertDoubleQuoteIgnoringSubstitution"}, "Shift+Control+KeyA": {"moveToBeginningOfParagraphAndModifySelection"}, "Shift+Control+KeyB": {"moveBackwardAndModifySelection"}, "Shift+Control+KeyE": {"moveToEndOfParagraphAndModifySelection"}, "Shift+Control+KeyF": {"moveForwardAndModifySelection"}, "Shift+Control+KeyN": {"moveDownAndModifySelection"}, "Shift+Control+KeyP": {"moveUpAndModifySelection"}, "Shift+Control+KeyV": {"pageDownAndModifySelection"}, "Shift+Control+Backspace": {"deleteBackwardByDecomposingPreviousCharacter"}, "Shift+Control+ArrowUp": {"scrollPageUp"}, "Shift+Control+ArrowDown": {"scrollPageDown"}, "Shift+Control+ArrowLeft": {"moveToLeftEndOfLineAndModifySelection"}, "Shift+Control+ArrowRight": {"moveToRightEndOfLineAndModifySelection"}, "Alt+Backspace": {"deleteWordBackward"}, "Alt+Enter": {"insertNewlineIgnoringFieldEditor"}, "Alt+NumpadEnter": {"insertNewlineIgnoringFieldEditor"}, "Alt+Escape": {"complete"}, "Alt+ArrowUp": {"moveBackward", "moveToBeginningOfParagraph"}, "Alt+ArrowDown": {"moveForward", "moveToEndOfParagraph"}, "Alt+ArrowLeft": {"moveWordLeft"}, "Alt+ArrowRight": {"moveWordRight"}, "Alt+Delete": {"deleteWordForward"}, "Alt+PageUp": {"pageUp"}, "Alt+PageDown": {"pageDown"}, "Shift+Alt+Backspace": {"deleteWordBackward"}, "Shift+Alt+Enter": {"insertNewlineIgnoringFieldEditor"}, "Shift+Alt+NumpadEnter": {"insertNewlineIgnoringFieldEditor"}, "Shift+Alt+Escape": {"complete"}, "Shift+Alt+ArrowUp": {"moveParagraphBackwardAndModifySelection"}, "Shift+Alt+ArrowDown": {"moveParagraphForwardAndModifySelection"}, "Shift+Alt+ArrowLeft": {"moveWordLeftAndModifySelection"}, "Shift+Alt+ArrowRight": {"moveWordRightAndModifySelection"}, "Shift+Alt+Delete": {"deleteWordForward"}, "Shift+Alt+PageUp": {"pageUp"}, "Shift+Alt+PageDown": {"pageDown"}, "Control+Alt+KeyB": {"moveWordBackward"}, "Control+Alt+KeyF": {"moveWordForward"}, "Control+Alt+Backspace": {"deleteWordBackward"}, "Shift+Control+Alt+KeyB": {"moveWordBackwardAndModifySelection"}, "Shift+Control+Alt+KeyF": {"moveWordForwardAndModifySelection"}, "Shift+Control+Alt+Backspace": {"deleteWordBackward"}, "Meta+NumpadSubtract": {"cancel"}, "Meta+Backspace": {"deleteToBeginningOfLine"}, "Meta+ArrowUp": {"moveToBeginningOfDocument"}, "Meta+ArrowDown": {"moveToEndOfDocument"}, "Meta+ArrowLeft": {"moveToLeftEndOfLine"}, "Meta+ArrowRight": {"moveToRightEndOfLine"}, "Shift+Meta+NumpadSubtract": {"cancel"}, "Shift+Meta+Backspace": {"deleteToBeginningOfLine"}, "Shift+Meta+ArrowUp": {"moveToBeginningOfDocumentAndModifySelection"}, "Shift+Meta+ArrowDown": {"moveToEndOfDocumentAndModifySelection"}, "Shift+Meta+ArrowLeft": {"moveToLeftEndOfLineAndModifySelection"}, "Shift+Meta+ArrowRight": {"moveToRightEndOfLineAndModifySelection"}, "Meta+KeyA": {"selectAll"}, "Meta+KeyC": {"copy"}, "Meta+KeyV": {"paste"}, "Meta+KeyZ": {"undo"}, "Shift+Meta+KeyZ": {"redo"}, } golang-github-go-rod-rod-0.116.2/lib/input/mouse.go000066400000000000000000000011671520674146500217620ustar00rootroot00000000000000package input import "github.com/go-rod/rod/lib/proto" // MouseKeys is the map for mouse keys. var MouseKeys = map[proto.InputMouseButton]int{ proto.InputMouseButtonLeft: 1, proto.InputMouseButtonRight: 2, proto.InputMouseButtonMiddle: 4, proto.InputMouseButtonBack: 8, proto.InputMouseButtonForward: 16, } // EncodeMouseButton into button flag. func EncodeMouseButton(buttons []proto.InputMouseButton) (proto.InputMouseButton, int) { flag := int(0) for _, btn := range buttons { flag |= MouseKeys[btn] } btn := proto.InputMouseButton("none") if len(buttons) > 0 { btn = buttons[0] } return btn, flag } golang-github-go-rod-rod-0.116.2/lib/input/mouse_test.go000066400000000000000000000005211520674146500230120ustar00rootroot00000000000000package input_test import ( "testing" "github.com/go-rod/rod/lib/input" "github.com/go-rod/rod/lib/proto" "github.com/ysmood/got" ) func TestMouseEncode(t *testing.T) { g := got.T(t) b, flag := input.EncodeMouseButton([]proto.InputMouseButton{proto.InputMouseButtonLeft}) g.Eq(b, proto.InputMouseButtonLeft) g.Eq(flag, 1) } golang-github-go-rod-rod-0.116.2/lib/js/000077500000000000000000000000001520674146500175535ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/js/generate/000077500000000000000000000000001520674146500213455ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/js/generate/main.go000066400000000000000000000031721520674146500226230ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "regexp" "strings" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/gson" ) func main() { list := getList() out := "// Package js generated by \"lib/js/generate\"\npackage js\n\n" for _, fn := range list.Arr() { name := fn.Get("name").Str() def := fn.Get("definition").Str() out += utils.S(` // {{.Name}} ... var {{.Name}} = &Function{ Name: "{{.name}}", Definition: {{.definition}}, Dependencies: {{.dependencies}}, } `, "Name", fnName(name), "name", name, "definition", utils.EscapeGoString(def), "dependencies", getDeps(def), ) } utils.E(utils.OutputFile("lib/js/helper.go", out)) utils.Exec("gofumpt -w lib/js/helper.go") } var regDeps = regexp.MustCompile(`\Wfunctions.(\w+)`) func getDeps(fn string) string { ms := regDeps.FindAllStringSubmatch(fn, -1) list := []string{} for _, m := range ms { list = append(list, fnName(m[1])) } return "[]*Function{" + strings.Join(list, ",") + "}" } func fnName(name string) string { return strings.ToUpper(name[0:1]) + name[1:] } func getList() gson.JSON { utils.UseNode(false) code := utils.ExecLine(false, "npx -ys -- uglify-js@3.17.4 -c -m -- lib/js/helper.js") script := fmt.Sprintf(` %s const list = [] for (const name in functions) { const reg = new RegExp('^(async )?' + name) const definition = functions[name].toString().replace(reg, '$1function') list.push({name, definition}) } console.log(JSON.stringify(list)) `, code) tmp := "tmp/helper.js" utils.E(utils.OutputFile(tmp, script)) return gson.NewFrom(utils.ExecLine(false, "node", tmp)) } golang-github-go-rod-rod-0.116.2/lib/js/helper.go000066400000000000000000000273661520674146500213770ustar00rootroot00000000000000// Package js generated by "lib/js/generate" package js // Element ... var Element = &Function{ Name: "element", Definition: `function(e){return functions.selectable(this).querySelector(e)}`, Dependencies: []*Function{Selectable}, } // TriggerFavicon ... var TriggerFavicon = &Function{ Name: "triggerFavicon", Definition: `function(){return new Promise((e,t)=>{var n=document.querySelector("link[rel~=icon]"),n=n&&n.href||"/favicon.ico",n=new URL(n,window.location).toString();const r=new XMLHttpRequest;r.open("GET",n),r.ontimeout=function(){t({errorType:"timeout_error",xhr:r})},r.onreadystatechange=function(){4===r.readyState&&(200<=r.status&&r.status<300||304===r.status?e({status:r.status,statusText:r.statusText,responseText:r.responseText}):t({errorType:"status_error",xhr:r,status:r.status,statusText:r.statusText,responseText:r.responseText}))},r.onerror=function(){t({errorType:"onerror",xhr:r,status:r.status,statusText:r.statusText,responseText:r.responseText})},r.send()})}`, Dependencies: []*Function{}, } // Elements ... var Elements = &Function{ Name: "elements", Definition: `function(e){return functions.selectable(this).querySelectorAll(e)}`, Dependencies: []*Function{Selectable}, } // ElementX ... var ElementX = &Function{ Name: "elementX", Definition: `function(e){var t=functions.selectable(this);return document.evaluate(e,t,null,XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue}`, Dependencies: []*Function{Selectable}, } // ElementsX ... var ElementsX = &Function{ Name: "elementsX", Definition: `function(e){for(var t,n=functions.selectable(this),r=document.evaluate(e,n,null,XPathResult.ORDERED_NODE_ITERATOR_TYPE),i=[];t=r.iterateNext();)i.push(t);return i}`, Dependencies: []*Function{Selectable}, } // ElementR ... var ElementR = &Function{ Name: "elementR", Definition: `function(e,t){var n=t.match(/(\/?)(.+)\1([a-z]*)/i),r=n[3]&&!/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(n[3])?new RegExp(t):new RegExp(n[2],n[3]),t=functions.selectable(this),n=Array.from(t.querySelectorAll(e)).find(e=>r.test(functions.text.call(e)));return n||null}`, Dependencies: []*Function{Selectable, Text}, } // Parents ... var Parents = &Function{ Name: "parents", Definition: `function(e){let t=this.parentElement;for(var n=[];t;)t.matches(e)&&n.push(t),t=t.parentElement;return n}`, Dependencies: []*Function{}, } // ContainsElement ... var ContainsElement = &Function{ Name: "containsElement", Definition: `function(e){for(var t=e;null!=t;){if(t===this)return!0;t=t.parentElement}return!1}`, Dependencies: []*Function{}, } // InitMouseTracer ... var InitMouseTracer = &Function{ Name: "initMouseTracer", Definition: `async function(e,t){var n;await functions.waitLoad(),document.getElementById(e)||((n=document.createElement("div")).innerHTML=t,(t=n.lastChild).id=e,t.style="position: absolute; z-index: 2147483647; width: 17px; pointer-events: none;",t.removeAttribute("width"),t.removeAttribute("height"),document.body.parentElement.appendChild(t))}`, Dependencies: []*Function{WaitLoad}, } // UpdateMouseTracer ... var UpdateMouseTracer = &Function{ Name: "updateMouseTracer", Definition: `function(e,t,n){e=document.getElementById(e);return!!e&&(e.style.left=t-2+"px",e.style.top=n-3+"px",!0)}`, Dependencies: []*Function{}, } // Rect ... var Rect = &Function{ Name: "rect", Definition: `function(){var e=functions.tag(this).getBoundingClientRect();return{x:e.x,y:e.y,width:e.width,height:e.height}}`, Dependencies: []*Function{Tag}, } // Overlay ... var Overlay = &Function{ Name: "overlay", Definition: `async function(e,t,n,r,i,o){await functions.waitLoad();var s=document.createElement("div");s.id=e,s.style=` + "`" + `position: fixed; z-index:2147483647; border: 2px dashed red; border-radius: 3px; box-shadow: #5f3232 0 0 3px; pointer-events: none; box-sizing: border-box; left: ${t}px; top: ${n}px; height: ${i}px; width: ${r}px;` + "`" + `,r*i==0&&(s.style.border="none"),o?((e=document.createElement("div")).style=` + "`" + `position: absolute; color: #cc26d6; font-size: 12px; background: #ffffffeb; box-shadow: #333 0 0 3px; padding: 2px 5px; border-radius: 3px; white-space: nowrap; top: ${i}px;` + "`" + `,e.innerHTML=o,s.appendChild(e),document.body.parentElement.appendChild(s),window.innerHeight{var e,t=document.getElementById(n);null!==t&&(e=i.getBoundingClientRect(),o.left===e.left&&o.top===e.top&&o.width===e.width&&o.height===e.height||(t.style.left=e.left+"px",t.style.top=e.top+"px",t.style.width=e.width+"px",t.style.height=e.height+"px",o=e),setTimeout(s,r))};setTimeout(s,r)}`, Dependencies: []*Function{Tag, Overlay}, } // RemoveOverlay ... var RemoveOverlay = &Function{ Name: "removeOverlay", Definition: `function(e){e=document.getElementById(e);e&&Element.prototype.remove.call(e)}`, Dependencies: []*Function{}, } // WaitIdle ... var WaitIdle = &Function{ Name: "waitIdle", Definition: `function(t){return new Promise(e=>{window.requestIdleCallback(e,{timeout:t})})}`, Dependencies: []*Function{}, } // WaitLoad ... var WaitLoad = &Function{ Name: "waitLoad", Definition: `function(){const n=this===window;return new Promise((e,t)=>{if(n){if("complete"===document.readyState)return e();window.addEventListener("load",e)}else void 0===this.complete||this.complete?e():(this.addEventListener("load",e),this.addEventListener("error",t))})}`, Dependencies: []*Function{}, } // InputEvent ... var InputEvent = &Function{ Name: "inputEvent", Definition: `function(){this.dispatchEvent(new Event("input",{bubbles:!0})),this.dispatchEvent(new Event("change",{bubbles:!0}))}`, Dependencies: []*Function{}, } // InputTime ... var InputTime = &Function{ Name: "inputTime", Definition: `function(e){var e=new Date(e),t=e=>e.toString().padStart(2,"0"),n=e.getFullYear(),r=t(e.getMonth()+1),i=t(e.getDate()),o=t(e.getHours()),s=t(e.getMinutes());switch(this.type){case"date":this.value=n+` + "`" + `-${r}-` + "`" + `+i;break;case"datetime-local":this.value=n+` + "`" + `-${r}-${i}T${o}:` + "`" + `+s;break;case"month":this.value=n+"-"+r;break;case"time":this.value=o+":"+s}functions.inputEvent.call(this)}`, Dependencies: []*Function{InputEvent}, } // InputColor ... var InputColor = &Function{ Name: "inputColor", Definition: `function(e){this.value=""+e,functions.inputEvent.call(this)}`, Dependencies: []*Function{InputEvent}, } // SelectText ... var SelectText = &Function{ Name: "selectText", Definition: `function(e){e=this.value.match(new RegExp(e));e&&this.setSelectionRange(e.index,e.index+e[0].length)}`, Dependencies: []*Function{}, } // SelectAllText ... var SelectAllText = &Function{ Name: "selectAllText", Definition: `function(){this.select()}`, Dependencies: []*Function{}, } // Select ... var Select = &Function{ Name: "select", Definition: `function(e,t,n){let r;switch(n){case"regex":r=e.map(e=>{const t=new RegExp(e);return e=>t.test(e.innerText)});break;case"css-selector":r=e.map(t=>e=>e.matches(t));break;default:r=e.map(t=>e=>e.innerText.includes(t))}const i=Array.from(this.options);let o=!1;return r.forEach(e=>{e=i.find(e);e&&(e.selected=t,o=!0)}),this.dispatchEvent(new Event("input",{bubbles:!0})),this.dispatchEvent(new Event("change",{bubbles:!0})),o}`, Dependencies: []*Function{}, } // Visible ... var Visible = &Function{ Name: "visible", Definition: `function(){var e=functions.tag(this),t=e.getBoundingClientRect(),e=window.getComputedStyle(e);return"none"!==e.display&&"hidden"!==e.visibility&&!!(t.top||t.bottom||t.width||t.height)}`, Dependencies: []*Function{Tag}, } // Invisible ... var Invisible = &Function{ Name: "invisible", Definition: `function(){return!functions.visible.apply(this)}`, Dependencies: []*Function{Visible}, } // Text ... var Text = &Function{ Name: "text", Definition: `function(){switch(this.tagName){case"INPUT":case"TEXTAREA":return this.value||this.placeholder;case"SELECT":return Array.from(this.selectedOptions).map(e=>e.innerText).join();case void 0:return this.textContent;default:return this.innerText}}`, Dependencies: []*Function{}, } // Resource ... var Resource = &Function{ Name: "resource", Definition: `function(){return new Promise((e,t)=>{if(this.complete)return e(this.currentSrc);this.addEventListener("load",()=>e(this.currentSrc)),this.addEventListener("error",e=>t(e))})}`, Dependencies: []*Function{}, } // AddScriptTag ... var AddScriptTag = &Function{ Name: "addScriptTag", Definition: `function(r,i,o){if(!document.getElementById(r))return new Promise((e,t)=>{var n=document.createElement("script");i?(n.src=i,n.onload=e):(n.type="text/javascript",n.text=o,e()),n.id=r,n.onerror=t,document.head.appendChild(n)})}`, Dependencies: []*Function{}, } // AddStyleTag ... var AddStyleTag = &Function{ Name: "addStyleTag", Definition: `function(r,i,o){if(!document.getElementById(r))return new Promise((e,t)=>{var n;i?((n=document.createElement("link")).rel="stylesheet",n.href=i):((n=document.createElement("style")).type="text/css",n.appendChild(document.createTextNode(o)),e()),n.id=r,n.onload=e,n.onerror=t,document.head.appendChild(n)})}`, Dependencies: []*Function{}, } // Selectable ... var Selectable = &Function{ Name: "selectable", Definition: `function(e){return e.querySelector?e:document}`, Dependencies: []*Function{}, } // Tag ... var Tag = &Function{ Name: "tag", Definition: `function(e){return e.tagName?e:e.parentElement}`, Dependencies: []*Function{}, } // ExposeFunc ... var ExposeFunc = &Function{ Name: "exposeFunc", Definition: `function(e,t){let o=0;window[e]=e=>new Promise((n,r)=>{const i=t+"_cb"+o++;window[i]=(e,t)=>{delete window[i],t?r(t):n(e)},window[t](JSON.stringify({req:e,cb:i}))})}`, Dependencies: []*Function{}, } // GetXPath ... var GetXPath = &Function{ Name: "getXPath", Definition: `function(e){class i{constructor(e,t){this.value=e,this.optimized=t||!1}toString(){return this.value}}function o(t){function n(e,t){return e===t||(e.nodeType===Node.ELEMENT_NODE&&t.nodeType===Node.ELEMENT_NODE?e.localName===t.localName:e.nodeType===t.nodeType||(e.nodeType===Node.CDATA_SECTION_NODE?Node.TEXT_NODE:e.nodeType)===(t.nodeType===Node.CDATA_SECTION_NODE?Node.TEXT_NODE:t.nodeType))}var e=t.parentNode,r=e?e.children:null;if(!r)return 0;let i;for(let e=0;e { const faviconElement = document.querySelector('link[rel~=icon]') const href = (faviconElement && faviconElement.href) || '/favicon.ico' const faviconUrl = new URL(href, window.location).toString() const xhr = new XMLHttpRequest() xhr.open('GET', faviconUrl) xhr.ontimeout = function () { reject({ errorType: 'timeout_error', xhr: xhr }) } xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { resolve({ status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText }) } else { reject({ errorType: 'status_error', xhr: xhr, status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText }) } } } xhr.onerror = function () { reject({ errorType: 'onerror', xhr: xhr, status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText }) } xhr.send() }) }, elements(selector) { return functions.selectable(this).querySelectorAll(selector) }, elementX(xPath) { const s = functions.selectable(this) return document.evaluate( xPath, s, null, XPathResult.FIRST_ORDERED_NODE_TYPE ).singleNodeValue }, elementsX(xpath) { const s = functions.selectable(this) const iter = document.evaluate( xpath, s, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE ) const list = [] let el while ((el = iter.iterateNext())) list.push(el) return list }, elementR(selector, regex) { var reg var m = regex.match(/(\/?)(.+)\1([a-z]*)/i) // cSpell:ignore gmix if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) reg = new RegExp(regex) else reg = new RegExp(m[2], m[3]) const s = functions.selectable(this) const el = Array.from(s.querySelectorAll(selector)).find((e) => reg.test(functions.text.call(e)) ) return el ? el : null }, parents(selector) { let p = this.parentElement const list = [] while (p) { if (p.matches(selector)) { list.push(p) } p = p.parentElement } return list }, containsElement(target) { var node = target while (node != null) { if (node === this) { return true } node = node.parentElement } return false }, async initMouseTracer(iconId, icon) { await functions.waitLoad() if (document.getElementById(iconId)) { return } const tmp = document.createElement('div') tmp.innerHTML = icon const svg = tmp.lastChild svg.id = iconId svg.style = 'position: absolute; z-index: 2147483647; width: 17px; pointer-events: none;' svg.removeAttribute('width') svg.removeAttribute('height') document.body.parentElement.appendChild(svg) }, updateMouseTracer(iconId, x, y) { const svg = document.getElementById(iconId) if (!svg) { return false } svg.style.left = x - 2 + 'px' svg.style.top = y - 3 + 'px' return true }, rect() { const b = functions.tag(this).getBoundingClientRect() return { x: b.x, y: b.y, width: b.width, height: b.height } }, async overlay(id, left, top, width, height, msg) { await functions.waitLoad() const div = document.createElement('div') div.id = id div.style = `position: fixed; z-index:2147483647; border: 2px dashed red; border-radius: 3px; box-shadow: #5f3232 0 0 3px; pointer-events: none; box-sizing: border-box; left: ${left}px; top: ${top}px; height: ${height}px; width: ${width}px;` if (width * height === 0) { div.style.border = 'none' } if (!msg) { document.body.parentElement.appendChild(div) return } const msgDiv = document.createElement('div') msgDiv.style = `position: absolute; color: #cc26d6; font-size: 12px; background: #ffffffeb; box-shadow: #333 0 0 3px; padding: 2px 5px; border-radius: 3px; white-space: nowrap; top: ${height}px;` msgDiv.innerHTML = msg div.appendChild(msgDiv) document.body.parentElement.appendChild(div) if (window.innerHeight < msgDiv.offsetHeight + top + height) { msgDiv.style.top = -msgDiv.offsetHeight - 2 + 'px' } if (window.innerWidth < msgDiv.offsetWidth + left) { msgDiv.style.left = window.innerWidth - msgDiv.offsetWidth - left + 'px' } }, async elementOverlay(id, msg) { const interval = 100 const el = functions.tag(this) let pre = el.getBoundingClientRect() await functions.overlay(id, pre.left, pre.top, pre.width, pre.height, msg) const update = () => { const overlay = document.getElementById(id) if (overlay === null) return const box = el.getBoundingClientRect() if ( pre.left === box.left && pre.top === box.top && pre.width === box.width && pre.height === box.height ) { setTimeout(update, interval) return } overlay.style.left = box.left + 'px' overlay.style.top = box.top + 'px' overlay.style.width = box.width + 'px' overlay.style.height = box.height + 'px' pre = box setTimeout(update, interval) } setTimeout(update, interval) }, removeOverlay(id) { const el = document.getElementById(id) // prevent override like prototype.js el && Element.prototype.remove.call(el) }, waitIdle(timeout) { return new Promise((resolve) => { window.requestIdleCallback(resolve, { timeout }) }) }, waitLoad() { const isWin = this === window return new Promise((resolve, reject) => { if (isWin) { if (document.readyState === 'complete') return resolve() window.addEventListener('load', resolve) } else { if (this.complete === undefined || this.complete) { resolve() } else { this.addEventListener('load', resolve) this.addEventListener('error', reject) } } }) }, inputEvent() { this.dispatchEvent(new Event('input', { bubbles: true })) this.dispatchEvent(new Event('change', { bubbles: true })) }, inputTime(stamp) { const time = new Date(stamp) const pad = (n) => n.toString().padStart(2, '0') const y = time.getFullYear() const mon = pad(time.getMonth() + 1) const d = pad(time.getDate()) const h = pad(time.getHours()) const min = pad(time.getMinutes()) switch (this.type) { case 'date': this.value = `${y}-${mon}-${d}` break case 'datetime-local': this.value = `${y}-${mon}-${d}T${h}:${min}` break case 'month': this.value = `${y}-${mon}` break case 'time': this.value = `${h}:${min}` break } functions.inputEvent.call(this) }, inputColor(color) { this.value = `${color}` functions.inputEvent.call(this) }, selectText(pattern) { const m = this.value.match(new RegExp(pattern)) if (m) { this.setSelectionRange(m.index, m.index + m[0].length) } }, selectAllText() { this.select() }, select(selectors, selected, type) { let matchers switch (type) { case 'regex': matchers = selectors.map((s) => { const reg = new RegExp(s) return (el) => reg.test(el.innerText) }) break case 'css-selector': matchers = selectors.map((s) => (el) => el.matches(s)) break default: matchers = selectors.map((s) => (el) => el.innerText.includes(s)) break } const opts = Array.from(this.options) let has = false matchers.forEach((s) => { const el = opts.find(s) if (el) { el.selected = selected has = true return } }) this.dispatchEvent(new Event('input', { bubbles: true })) this.dispatchEvent(new Event('change', { bubbles: true })) return has }, visible() { const el = functions.tag(this) const box = el.getBoundingClientRect() const style = window.getComputedStyle(el) return ( style.display !== 'none' && style.visibility !== 'hidden' && !!(box.top || box.bottom || box.width || box.height) ) }, invisible() { return !functions.visible.apply(this) }, text() { switch (this.tagName) { case 'INPUT': case 'TEXTAREA': return this.value || this.placeholder case 'SELECT': return Array.from(this.selectedOptions) .map((el) => el.innerText) .join() case undefined: return this.textContent default: return this.innerText } }, resource() { return new Promise((resolve, reject) => { if (this.complete) { return resolve(this.currentSrc) } this.addEventListener('load', () => resolve(this.currentSrc)) this.addEventListener('error', (e) => reject(e)) }) }, addScriptTag(id, url, content) { if (document.getElementById(id)) return return new Promise((resolve, reject) => { var s = document.createElement('script') if (url) { s.src = url s.onload = resolve } else { s.type = 'text/javascript' s.text = content resolve() } s.id = id s.onerror = reject document.head.appendChild(s) }) }, addStyleTag(id, url, content) { if (document.getElementById(id)) return return new Promise((resolve, reject) => { var el if (url) { el = document.createElement('link') el.rel = 'stylesheet' el.href = url } else { el = document.createElement('style') el.type = 'text/css' el.appendChild(document.createTextNode(content)) resolve() } el.id = id el.onload = resolve el.onerror = reject document.head.appendChild(el) }) }, selectable(s) { return s.querySelector ? s : document }, tag(el) { return el.tagName ? el : el.parentElement }, exposeFunc(name, bind) { let callbackCount = 0 window[name] = (req) => new Promise((resolve, reject) => { const cb = bind + '_cb' + callbackCount++ window[cb] = (res, err) => { delete window[cb] err ? reject(err) : resolve(res) } window[bind](JSON.stringify({ req, cb })) }) }, getXPath(optimized) { class Step { constructor(value, optimized) { this.value = value this.optimized = optimized || false } toString() { return this.value } } const xPathValue = function xPathValue(node, optimized) { let ownValue const ownIndex = xPathIndex(node) if (ownIndex === -1) { return null } switch (node.nodeType) { case Node.ELEMENT_NODE: if (optimized && node.id) { return new Step(`//*[@id='${node.id}']`, true) } ownValue = node.localName break case Node.ATTRIBUTE_NODE: ownValue = `@${node.nodeName}` break case Node.TEXT_NODE: case Node.CDATA_SECTION_NODE: ownValue = 'text()' break case Node.PROCESSING_INSTRUCTION_NODE: ownValue = 'processing-instruction()' break case Node.COMMENT_NODE: ownValue = 'comment()' break case Node.DOCUMENT_NODE: ownValue = '' break default: ownValue = '' break } if (ownIndex > 0) { ownValue += `[${ownIndex}]` } return new Step(ownValue, node.nodeType === Node.DOCUMENT_NODE) } const xPathIndex = function xPathIndex(node) { function areNodesSimilar(left, right) { if (left === right) { return true } if ( left.nodeType === Node.ELEMENT_NODE && right.nodeType === Node.ELEMENT_NODE ) { return left.localName === right.localName } if (left.nodeType === right.nodeType) { return true } const leftType = left.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType const rightType = right.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType return leftType === rightType } const parentNode = node.parentNode const siblings = parentNode ? parentNode.children : null if (!siblings) { return 0 } let hasSameNamedElements for (let i = 0; i < siblings.length; ++i) { if (areNodesSimilar(node, siblings[i]) && !(siblings[i] === node)) { hasSameNamedElements = true break } } if (!hasSameNamedElements) { return 0 } let ownIndex = 1 for (let i = 0; i < siblings.length; ++i) { if (areNodesSimilar(node, siblings[i])) { if (siblings[i] === node) { return ownIndex } ++ownIndex } } return -1 } const node = this if (node.nodeType === Node.DOCUMENT_NODE) { return '/' } const steps = [] let contextNode = node while (contextNode) { const step = xPathValue(contextNode, optimized) if (!step) { break } steps.push(step) if (step.optimized) { break } contextNode = contextNode.parentNode } steps.reverse() return (steps.length && steps[0].optimized ? '' : '/') + steps.join('/') } } golang-github-go-rod-rod-0.116.2/lib/js/js.go000066400000000000000000000010061520674146500205130ustar00rootroot00000000000000package js // Function definition. type Function struct { // Name must be unique and not conflict with the function names in "helper.js" Name string // Definition holds the code of a js function from "helper.js", // the js code is compressed by uglify-js. Definition string // Dependencies will be preloaded and assigned to the global js object "functions" Dependencies []*Function } // Functions ... var Functions = &Function{ Name: "functions", Definition: "() => ({})", Dependencies: nil, } golang-github-go-rod-rod-0.116.2/lib/launcher/000077500000000000000000000000001520674146500207405ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/launcher/README.md000066400000000000000000000001661520674146500222220ustar00rootroot00000000000000# Overview A lib helps to find, launch or download the browser. You can also use it as a standalone lib without Rod. golang-github-go-rod-rod-0.116.2/lib/launcher/browser.go000066400000000000000000000160101520674146500227500ustar00rootroot00000000000000package launcher import ( "bytes" "context" "errors" "fmt" "log" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/go-rod/rod/lib/defaults" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/fetchup" "github.com/ysmood/leakless" ) // Host formats a revision number to a downloadable URL for the browser. type Host func(revision int) string var hostConf = map[string]struct { urlPrefix string zipName string }{ "darwin_amd64": {"Mac", "chrome-mac.zip"}, "darwin_arm64": {"Mac_Arm", "chrome-mac.zip"}, "linux_amd64": {"Linux_x64", "chrome-linux.zip"}, "windows_386": {"Win", "chrome-win.zip"}, "windows_amd64": {"Win_x64", "chrome-win.zip"}, }[runtime.GOOS+"_"+runtime.GOARCH] // HostGoogle to download browser. func HostGoogle(revision int) string { return fmt.Sprintf( "https://storage.googleapis.com/chromium-browser-snapshots/%s/%d/%s", hostConf.urlPrefix, revision, hostConf.zipName, ) } // HostNPM to download browser. func HostNPM(revision int) string { return fmt.Sprintf( "https://registry.npmmirror.com/-/binary/chromium-browser-snapshots/%s/%d/%s", hostConf.urlPrefix, revision, hostConf.zipName, ) } // HostPlaywright to download browser. func HostPlaywright(revision int) string { rev := RevisionPlaywright if !(runtime.GOOS == "linux" && runtime.GOARCH == "arm64") { rev = revision } return fmt.Sprintf( "https://playwright.azureedge.net/builds/chromium/%d/chromium-linux-arm64.zip", rev, ) } // DefaultBrowserDir for downloaded browser. For unix is "$HOME/.cache/rod/browser", // for Windows it's "%APPDATA%\rod\browser". var DefaultBrowserDir = filepath.Join(map[string]string{ "windows": os.Getenv("APPDATA"), "darwin": filepath.Join(os.Getenv("HOME"), ".cache"), "linux": filepath.Join(os.Getenv("HOME"), ".cache"), }[runtime.GOOS], "rod", "browser") // Browser is a helper to download browser smartly. type Browser struct { Context context.Context // Hosts are the candidates to download the browser. // Such as [HostGoogle] or [HostNPM]. Hosts []Host // Revision of the browser to use Revision int // RootDir to download different browser versions. RootDir string // Log to print output Logger utils.Logger // LockPort a tcp port to prevent race downloading. Default is 2968 . LockPort int // HTTPClient to download the browser HTTPClient *http.Client } // NewBrowser with default values. func NewBrowser() *Browser { return &Browser{ Context: context.Background(), Revision: RevisionDefault, Hosts: []Host{HostGoogle, HostNPM, HostPlaywright}, RootDir: DefaultBrowserDir, Logger: log.New(os.Stdout, "[launcher.Browser]", log.LstdFlags), LockPort: defaults.LockPort, } } // Dir to download the browser. func (lc *Browser) Dir() string { return filepath.Join(lc.RootDir, fmt.Sprintf("chromium-%d", lc.Revision)) } // BinPath to download the browser executable. func (lc *Browser) BinPath() string { bin := map[string]string{ "darwin": "Chromium.app/Contents/MacOS/Chromium", "linux": "chrome", "windows": "chrome.exe", }[runtime.GOOS] return filepath.Join(lc.Dir(), filepath.FromSlash(bin)) } // Download browser from the fastest host. // It will race downloading a TCP packet from each host and use the fastest host. func (lc *Browser) Download() error { us := []string{} for _, host := range lc.Hosts { us = append(us, host(lc.Revision)) } dir := lc.Dir() fu := fetchup.New(dir, us...) fu.Ctx = lc.Context fu.Logger = lc.Logger if lc.HTTPClient != nil { fu.HttpClient = lc.HTTPClient } err := fu.Fetch() if err != nil { return fmt.Errorf("can't find a browser binary for your OS, the doc might help https://go-rod.github.io/#/compatibility?id=os : %w", err) //nolint: lll } return fetchup.StripFirstDir(dir) } // Get is a smart helper to get the browser executable path. // If [Browser.BinPath] is not valid it will auto download the browser to [Browser.BinPath]. func (lc *Browser) Get() (string, error) { defer leakless.LockPort(lc.LockPort)() if lc.Validate() == nil { return lc.BinPath(), nil } // Try to cleanup before downloading _ = os.RemoveAll(lc.Dir()) return lc.BinPath(), lc.Download() } // MustGet is similar with Get. func (lc *Browser) MustGet() string { p, err := lc.Get() utils.E(err) return p } // Validate returns nil if the browser executable is valid. // If the executable is malformed it will return error. func (lc *Browser) Validate() error { _, err := os.Stat(lc.BinPath()) if err != nil { return err } cmd := exec.Command(lc.BinPath(), "--headless", "--no-sandbox", "--use-mock-keychain", "--disable-dev-shm-usage", "--disable-gpu", "--dump-dom", "about:blank") b, err := cmd.CombinedOutput() if err != nil { if strings.Contains(string(b), "error while loading shared libraries") { // When the os is missing some dependencies for chromium we treat it as valid binary. return nil } return fmt.Errorf("failed to run the browser: %w\n%s", err, b) } if !bytes.Contains(b, []byte(``)) { return errors.New("the browser executable doesn't support headless mode") } return nil } // LookPath searches for the browser executable from often used paths on current operating system. func LookPath() (found string, has bool) { list := map[string][]string{ "darwin": { "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", "/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/usr/bin/chromium", "/usr/bin/chromium-browser", }, "linux": { "chrome", "google-chrome", "/usr/bin/google-chrome", "microsoft-edge", "/usr/bin/microsoft-edge", "chromium", "chromium-browser", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/snap/bin/chromium", "/data/data/com.termux/files/usr/bin/chromium-browser", }, "openbsd": { "chrome", "chromium", }, "windows": append([]string{"chrome", "edge"}, expandWindowsExePaths( `Google\Chrome\Application\chrome.exe`, `Chromium\Application\chrome.exe`, `Microsoft\Edge\Application\msedge.exe`, )...), }[runtime.GOOS] for _, path := range list { var err error found, err = exec.LookPath(path) has = err == nil if has { break } } return } // interface for testing. var openExec = exec.Command // Open tries to open the url via system's default browser. func Open(url string) { // Windows doesn't support format [::] url = strings.Replace(url, "[::]", "[::1]", 1) if bin, has := LookPath(); has { p := openExec(bin, url) _ = p.Start() _ = p.Process.Release() } } func expandWindowsExePaths(list ...string) []string { newList := []string{} for _, p := range list { newList = append( newList, filepath.Join(os.Getenv("ProgramFiles"), p), filepath.Join(os.Getenv("ProgramFiles(x86)"), p), filepath.Join(os.Getenv("LocalAppData"), p), ) } return newList } golang-github-go-rod-rod-0.116.2/lib/launcher/error.go000066400000000000000000000002641520674146500224220ustar00rootroot00000000000000package launcher import "errors" // ErrAlreadyLaunched is an error that indicates the launcher has already been launched. var ErrAlreadyLaunched = errors.New("already launched") golang-github-go-rod-rod-0.116.2/lib/launcher/example_test.go000066400000000000000000000021471520674146500237650ustar00rootroot00000000000000package launcher_test import ( "os" "os/exec" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/leakless" ) func Example_use_system_browser() { if path, exists := launcher.LookPath(); exists { u := launcher.New().Bin(path).MustLaunch() rod.New().ControlURL(u).MustConnect() } } func Example_print_browser_CLI_output() { // Pipe the browser stderr and stdout to os.Stdout . u := launcher.New().Logger(os.Stdout).MustLaunch() rod.New().ControlURL(u).MustConnect() } func Example_custom_launch() { // get the browser executable path path := launcher.NewBrowser().MustGet() // use the FormatArgs to construct args, this line is optional, you can construct the args manually args := launcher.New().FormatArgs() var cmd *exec.Cmd if true { // decide whether to use leakless or not cmd = leakless.New().Command(path, args...) } else { cmd = exec.Command(path, args...) } parser := launcher.NewURLParser() cmd.Stderr = parser utils.E(cmd.Start()) u := launcher.MustResolveURL(<-parser.URL) rod.New().ControlURL(u).MustConnect() } golang-github-go-rod-rod-0.116.2/lib/launcher/fixtures/000077500000000000000000000000001520674146500226115ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/launcher/fixtures/chrome-empty/000077500000000000000000000000001520674146500252225ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/launcher/fixtures/chrome-empty/main.go000066400000000000000000000000611520674146500264720ustar00rootroot00000000000000// Package main ... package main func main() {} golang-github-go-rod-rod-0.116.2/lib/launcher/fixtures/chrome-exit-err/000077500000000000000000000000001520674146500256235ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/launcher/fixtures/chrome-exit-err/main.go000066400000000000000000000001131520674146500270710ustar00rootroot00000000000000// Package main ... package main import "os" func main() { os.Exit(1) } golang-github-go-rod-rod-0.116.2/lib/launcher/fixtures/chrome-lib-missing/000077500000000000000000000000001520674146500263015ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/launcher/fixtures/chrome-lib-missing/main.go000066400000000000000000000003401520674146500275510ustar00rootroot00000000000000// Package main ... package main import ( "fmt" "os" ) func main() { fmt.Println("./chrome: error while loading shared libraries: libcairo.so.2: cannot open shared object file: No such file or directory") os.Exit(1) } golang-github-go-rod-rod-0.116.2/lib/launcher/flags/000077500000000000000000000000001520674146500220345ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/launcher/flags/flags.go000066400000000000000000000034011520674146500234550ustar00rootroot00000000000000// Package flags ... package flags import "strings" // Flag name of a command line argument of the browser, also known as command line flag or switch. // List of available flags: https://peter.sh/experiments/chromium-command-line-switches type Flag string // TODO: we should automatically generate all the flags here. const ( // UserDataDir https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md UserDataDir Flag = "user-data-dir" // Headless mode. Whether to run browser in headless mode. A mode without visible UI. Headless Flag = "headless" // App flag. App Flag = "app" // RemoteDebuggingPort flag. RemoteDebuggingPort Flag = "remote-debugging-port" // NoSandbox flag. NoSandbox Flag = "no-sandbox" // ProxyServer flag. ProxyServer Flag = "proxy-server" // WorkingDir flag. WorkingDir Flag = "rod-working-dir" // Env flag. Env Flag = "rod-env" // XVFB flag. XVFB Flag = "rod-xvfb" // ProfileDir flag. ProfileDir = "profile-directory" // Preferences flag. Preferences Flag = "rod-preferences" // Leakless flag. Leakless Flag = "rod-leakless" // Bin is the browser executable file path. If it's empty, launcher will automatically search or download the bin. Bin Flag = "rod-bin" // KeepUserDataDir flag. KeepUserDataDir Flag = "rod-keep-user-data-dir" // Arguments for the command. Such as // chrome-bin http://a.com http://b.com // The "http://a.com" and "http://b.com" are the arguments. Arguments Flag = "" ) // Check if the flag name is valid. func (f Flag) Check() { if strings.Contains(string(f), "=") { panic("flag name should not contain '='") } } // NormalizeFlag normalize the flag name, remove the leading dash. func (f Flag) NormalizeFlag() Flag { return Flag(strings.TrimLeft(string(f), "-")) } golang-github-go-rod-rod-0.116.2/lib/launcher/launcher.go000066400000000000000000000352761520674146500231050ustar00rootroot00000000000000// Package launcher for launching browser utils. package launcher import ( "context" "crypto" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "sort" "strings" "sync/atomic" "github.com/go-rod/rod/lib/defaults" "github.com/go-rod/rod/lib/launcher/flags" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/leakless" ) // DefaultUserDataDirPrefix ... var DefaultUserDataDirPrefix = filepath.Join(os.TempDir(), "rod", "user-data") // Launcher is a helper to launch browser binary smartly. type Launcher struct { Flags map[flags.Flag][]string `json:"flags"` ctx context.Context ctxCancel func() logger io.Writer browser *Browser parser *URLParser pid int exit chan struct{} managed bool serviceURL string isLaunched int32 // zero means not launched } // New returns the default arguments to start browser. // Headless will be enabled by default. // Leakless will be enabled by default. // UserDataDir will use OS tmp dir by default, this folder will usually be cleaned up by the OS after reboot. // It will auto download the browser binary according to the current platform, // check [Launcher.Bin] and [Launcher.Revision] for more info. func New() *Launcher { dir := defaults.Dir if dir == "" { dir = filepath.Join(DefaultUserDataDirPrefix, utils.RandString(8)) } defaultFlags := map[flags.Flag][]string{ flags.Bin: {defaults.Bin}, flags.Leakless: nil, flags.UserDataDir: {dir}, // use random port by default flags.RemoteDebuggingPort: {defaults.Port}, // enable headless by default flags.Headless: nil, // to disable the init blank window "no-first-run": nil, "no-startup-window": nil, // TODO: about the "site-per-process" see https://github.com/puppeteer/puppeteer/issues/2548 "disable-features": {"site-per-process", "TranslateUI"}, "disable-dev-shm-usage": nil, "disable-background-networking": nil, "disable-background-timer-throttling": nil, "disable-backgrounding-occluded-windows": nil, "disable-breakpad": nil, "disable-client-side-phishing-detection": nil, "disable-component-extensions-with-background-pages": nil, "disable-default-apps": nil, "disable-hang-monitor": nil, "disable-ipc-flooding-protection": nil, "disable-popup-blocking": nil, "disable-prompt-on-repost": nil, "disable-renderer-backgrounding": nil, "disable-sync": nil, "disable-site-isolation-trials": nil, "enable-automation": nil, "enable-features": {"NetworkService", "NetworkServiceInProcess"}, "force-color-profile": {"srgb"}, "metrics-recording-only": nil, "use-mock-keychain": nil, } if defaults.Show { delete(defaultFlags, flags.Headless) } if defaults.Devtools { defaultFlags["auto-open-devtools-for-tabs"] = nil } if inContainer { defaultFlags[flags.NoSandbox] = nil } if defaults.Proxy != "" { defaultFlags[flags.ProxyServer] = []string{defaults.Proxy} } ctx, cancel := context.WithCancel(context.Background()) return &Launcher{ ctx: ctx, ctxCancel: cancel, Flags: defaultFlags, exit: make(chan struct{}), browser: NewBrowser(), parser: NewURLParser(), logger: io.Discard, } } // NewUserMode is a preset to enable reusing current user data. Useful for automation of personal browser. // If you see any error, it may because you can't launch debug port for existing browser, the solution is to // completely close the running browser. Unfortunately, there's no API for rod to tell it automatically yet. func NewUserMode() *Launcher { ctx, cancel := context.WithCancel(context.Background()) bin, _ := LookPath() return &Launcher{ ctx: ctx, ctxCancel: cancel, Flags: map[flags.Flag][]string{ flags.RemoteDebuggingPort: {"37712"}, "no-startup-window": nil, flags.Bin: {bin}, }, browser: NewBrowser(), exit: make(chan struct{}), parser: NewURLParser(), logger: io.Discard, } } // NewAppMode is a preset to run the browser like a native application. // The u should be a URL. func NewAppMode(u string) *Launcher { l := New() l.Set(flags.App, u). Set(flags.Env, "GOOGLE_API_KEY=no"). Headless(false). Delete("no-startup-window"). Delete("enable-automation") return l } // Context sets the context. func (l *Launcher) Context(ctx context.Context) *Launcher { ctx, cancel := context.WithCancel(ctx) l.ctx = ctx l.parser.Context(ctx) l.ctxCancel = cancel return l } // Set a command line argument when launching the browser. // Be careful the first argument is a flag name, it shouldn't contain values. The values the will be joined with comma. // A flag can have multiple values. If no values are provided the flag will be a boolean flag. // You can use the [Launcher.FormatArgs] to debug the final CLI arguments. // List of available flags: https://peter.sh/experiments/chromium-command-line-switches func (l *Launcher) Set(name flags.Flag, values ...string) *Launcher { name.Check() l.Flags[name.NormalizeFlag()] = values return l } // Get flag's first value. func (l *Launcher) Get(name flags.Flag) string { if list, has := l.GetFlags(name); has { return list[0] } return "" } // Has flag or not. func (l *Launcher) Has(name flags.Flag) bool { _, has := l.GetFlags(name) return has } // GetFlags from settings. func (l *Launcher) GetFlags(name flags.Flag) ([]string, bool) { flag, has := l.Flags[name.NormalizeFlag()] return flag, has } // Append values to the flag. func (l *Launcher) Append(name flags.Flag, values ...string) *Launcher { flags, has := l.GetFlags(name) if !has { flags = []string{} } return l.Set(name, append(flags, values...)...) } // Delete a flag. func (l *Launcher) Delete(name flags.Flag) *Launcher { delete(l.Flags, name.NormalizeFlag()) return l } // Bin of the browser binary path to launch, if the path is not empty the auto download will be disabled. func (l *Launcher) Bin(path string) *Launcher { return l.Set(flags.Bin, path) } // Revision of the browser to auto download. func (l *Launcher) Revision(rev int) *Launcher { l.browser.Revision = rev return l } // Headless switch. Whether to run browser in headless mode. A mode without visible UI. func (l *Launcher) Headless(enable bool) *Launcher { if enable { return l.Set(flags.Headless) } return l.Delete(flags.Headless) } // HeadlessNew switch is the "--headless=new" switch: https://developer.chrome.com/docs/chromium/new-headless func (l *Launcher) HeadlessNew(enable bool) *Launcher { if enable { return l.Set(flags.Headless, "new") } return l.Delete(flags.Headless) } // NoSandbox switch. Whether to run browser in no-sandbox mode. // Linux users may face "running as root without --no-sandbox is not supported" in some Linux/Chrome combinations. // This function helps switch mode easily. // Be aware disabling sandbox is not trivial. Use at your own risk. // Related doc: https://bugs.chromium.org/p/chromium/issues/detail?id=638180 func (l *Launcher) NoSandbox(enable bool) *Launcher { if enable { return l.Set(flags.NoSandbox) } return l.Delete(flags.NoSandbox) } // XVFB enables to run browser in by XVFB. Useful when you want to run headful mode on linux. func (l *Launcher) XVFB(args ...string) *Launcher { return l.Set(flags.XVFB, args...) } // Preferences set chromium user preferences, such as set the default search engine or disable the pdf viewer. // The pref is a json string, the doc is here // https://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/pref_names.cc func (l *Launcher) Preferences(pref string) *Launcher { return l.Set(flags.Preferences, pref) } // AlwaysOpenPDFExternally switch. // It will set chromium user preferences to enable the always_open_pdf_externally option. func (l *Launcher) AlwaysOpenPDFExternally() *Launcher { return l.Set(flags.Preferences, `{"plugins":{"always_open_pdf_externally": true}}`) } // Leakless switch. If enabled, the browser will be force killed after the Go process exits. // The doc of leakless: https://github.com/ysmood/leakless. func (l *Launcher) Leakless(enable bool) *Launcher { if enable { return l.Set(flags.Leakless) } return l.Delete(flags.Leakless) } // Devtools switch to auto open devtools for each tab. func (l *Launcher) Devtools(autoOpenForTabs bool) *Launcher { if autoOpenForTabs { return l.Set("auto-open-devtools-for-tabs") } return l.Delete("auto-open-devtools-for-tabs") } // IgnoreCerts configure the Chrome's ignore-certificate-errors-spki-list argument with the public keys. func (l *Launcher) IgnoreCerts(pks []crypto.PublicKey) error { spkis := make([]string, 0, len(pks)) for _, pk := range pks { spki, err := certSPKI(pk) if err != nil { return fmt.Errorf("certSPKI: %w", err) } spkis = append(spkis, string(spki)) } l.Set("ignore-certificate-errors-spki-list", spkis...) return nil } // UserDataDir is where the browser will look for all of its state, such as cookie and cache. // When set to empty, browser will use current OS home dir. // Related doc: https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md func (l *Launcher) UserDataDir(dir string) *Launcher { if dir == "" { l.Delete(flags.UserDataDir) } else { l.Set(flags.UserDataDir, dir) } return l } // ProfileDir is the browser profile the browser will use. // When set to empty, the profile 'Default' is used. // Related article: https://superuser.com/a/377195 func (l *Launcher) ProfileDir(dir string) *Launcher { if dir == "" { l.Delete(flags.ProfileDir) } else { l.Set(flags.ProfileDir, dir) } return l } // RemoteDebuggingPort to launch the browser. Zero for a random port. Zero is the default value. // If it's not zero and the Launcher.Leakless is disabled, the launcher will try to reconnect to it first, // if the reconnection fails it will launch a new browser. func (l *Launcher) RemoteDebuggingPort(port int) *Launcher { return l.Set(flags.RemoteDebuggingPort, fmt.Sprintf("%d", port)) } // Proxy for the browser. func (l *Launcher) Proxy(host string) *Launcher { return l.Set(flags.ProxyServer, host) } // WorkingDir to launch the browser process. func (l *Launcher) WorkingDir(path string) *Launcher { return l.Set(flags.WorkingDir, path) } // Env to launch the browser process. The default value is [os.Environ](). // Usually you use it to set the timezone env. Such as: // // Env(append(os.Environ(), "TZ=Asia/Tokyo")...) func (l *Launcher) Env(env ...string) *Launcher { return l.Set(flags.Env, env...) } // StartURL to launch. func (l *Launcher) StartURL(u string) *Launcher { return l.Set("", u) } // FormatArgs returns the formatted arg list for cli. func (l *Launcher) FormatArgs() []string { execArgs := []string{} for k, v := range l.Flags { if k == flags.Arguments { continue } if strings.HasPrefix(string(k), "rod-") { continue } // fix a bug of chrome, if path is not absolute chrome will hang if k == flags.UserDataDir { abs, err := filepath.Abs(v[0]) utils.E(err) v[0] = abs } str := "--" + string(k) if v != nil { str += "=" + strings.Join(v, ",") } execArgs = append(execArgs, str) } execArgs = append(execArgs, l.Flags[flags.Arguments]...) sort.Strings(execArgs) return execArgs } // Logger to handle stdout and stderr from browser. // For example, pipe all browser output to stdout: // // launcher.New().Logger(os.Stdout) func (l *Launcher) Logger(w io.Writer) *Launcher { l.logger = w return l } // MustLaunch is similar to Launch. func (l *Launcher) MustLaunch() string { u, err := l.Launch() utils.E(err) return u } // Launch a standalone temp browser instance and returns the debug url. // bin and profileDir are optional, set them to empty to use the default values. // If you want to reuse sessions, such as cookies, set the [Launcher.UserDataDir] to the same location. // // Please note launcher can only be used once. func (l *Launcher) Launch() (string, error) { if l.hasLaunched() { return "", ErrAlreadyLaunched } defer l.ctxCancel() bin, err := l.getBin() if err != nil { return "", err } l.setupUserPreferences() var ll *leakless.Launcher var cmd *exec.Cmd args := l.FormatArgs() if l.Has(flags.Leakless) && leakless.Support() { ll = leakless.New() cmd = ll.Command(bin, args...) } else { port := l.Get(flags.RemoteDebuggingPort) u, err := ResolveURL(port) if err == nil { return u, nil } cmd = exec.Command(bin, args...) } l.setupCmd(cmd) err = cmd.Start() if err != nil { return "", err } if ll == nil { l.pid = cmd.Process.Pid } else { l.pid = <-ll.Pid() if ll.Err() != "" { return "", errors.New(ll.Err()) } } go func() { _ = cmd.Wait() close(l.exit) }() u, err := l.getURL() if err != nil { l.Kill() return "", err } return ResolveURL(u) } func (l *Launcher) hasLaunched() bool { return !atomic.CompareAndSwapInt32(&l.isLaunched, 0, 1) } func (l *Launcher) setupUserPreferences() { userDir := l.Get(flags.UserDataDir) pref := l.Get(flags.Preferences) if userDir == "" || pref == "" { return } userDir, err := filepath.Abs(userDir) utils.E(err) profile := l.Get(flags.ProfileDir) if profile == "" { profile = "Default" } path := filepath.Join(userDir, profile, "Preferences") utils.E(utils.OutputFile(path, pref)) } func (l *Launcher) setupCmd(cmd *exec.Cmd) { l.osSetupCmd(cmd) dir := l.Get(flags.WorkingDir) env, _ := l.GetFlags(flags.Env) cmd.Dir = dir cmd.Env = env cmd.Stdout = io.MultiWriter(l.logger, l.parser) cmd.Stderr = io.MultiWriter(l.logger, l.parser) } func (l *Launcher) getBin() (string, error) { bin := l.Get(flags.Bin) if bin == "" { l.browser.Context = l.ctx return l.browser.Get() } return bin, nil } func (l *Launcher) getURL() (u string, err error) { select { case <-l.ctx.Done(): err = l.ctx.Err() case u = <-l.parser.URL: case <-l.exit: err = l.parser.Err() } return } // PID returns the browser process pid. func (l *Launcher) PID() int { return l.pid } // Kill the browser process. func (l *Launcher) Kill() { // TODO: If kill too fast, the browser's children processes may not be ready. // Browser don't have an API to tell if the children processes are ready. utils.Sleep(1) if l.PID() == 0 { // avoid killing the current process return } killGroup(l.PID()) p, err := os.FindProcess(l.PID()) if err == nil { _ = p.Kill() } } // Cleanup wait until the Browser exits and remove [flags.UserDataDir]. func (l *Launcher) Cleanup() { <-l.exit dir := l.Get(flags.UserDataDir) _ = os.RemoveAll(dir) } golang-github-go-rod-rod-0.116.2/lib/launcher/launcher_test.go000066400000000000000000000165761520674146500241460ustar00rootroot00000000000000package launcher_test import ( "archive/zip" "bytes" "crypto" "crypto/x509" "encoding/pem" "flag" "io" "net/http" "net/url" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/go-rod/rod/lib/defaults" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/launcher/flags" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/got" ) var setup = got.Setup(nil) func TestDownloadHosts(t *testing.T) { g := setup(t) g.Has(launcher.HostGoogle(launcher.RevisionDefault), "https://storage.googleapis.com/chromium-browser-snapshots") g.Has(launcher.HostNPM(launcher.RevisionDefault), "https://registry.npmmirror.com/-/binary/chromium-browser-snapshots") g.Has(launcher.HostPlaywright(launcher.RevisionDefault), "https://playwright.azureedge.net/") } func TestDownload(t *testing.T) { g := got.T(t) buf := bytes.NewBuffer(nil) z := zip.NewWriter(buf) f, _ := z.Create(filepath.FromSlash("a/b/c.txt")) _, _ = f.Write([]byte(g.RandStr(500 * 1024))) _ = z.Close() s := g.Serve() s.Route("/", ".zip", buf.Bytes()) b := launcher.NewBrowser() b.Revision = 1 b.Logger = utils.LoggerQuiet b.Hosts = []launcher.Host{func(_ int) string { return s.URL("/a.zip") }} g.Cleanup(func() { _ = os.RemoveAll(b.Dir()) }) b.MustGet() g.PathExists(b.Dir()) } func TestLaunch(t *testing.T) { g := setup(t) defaults.Proxy = "test.com" defer func() { defaults.ResetWith("") }() l := launcher.New().Preferences("").AlwaysOpenPDFExternally() defer l.Kill() u := l.MustLaunch() g.Regex(`\Aws://.+\z`, u) parsed, _ := url.Parse(u) { // test GetWebSocketDebuggerURL for _, prefix := range []string{"", ":", "127.0.0.1:", "ws://127.0.0.1:"} { u2 := launcher.MustResolveURL(prefix + parsed.Port()) g.Regex(u, u2) } _, err := launcher.ResolveURL("") g.Err(err) } { _, err := launcher.NewManaged("") g.Err(err) _, err = launcher.NewManaged("1://") g.Err(err) _, err = launcher.NewManaged("ws://not-exists") g.Err(err) } { g.Panic(func() { launcher.New().Set("a=b") }) } } func TestLaunchUserMode(t *testing.T) { g := setup(t) l := launcher.NewUserMode() defer l.Kill() l.Kill() // empty kill should do nothing has := l.Has("not-exists") g.False(has) l.Append("test-append", "a") f := l.Get("test-append") g.Eq("a", f) dir := l.Get(flags.UserDataDir) port := 58472 l = l.Context(g.Context()).Delete("test").Bin(""). Revision(launcher.RevisionDefault). Logger(io.Discard). Leakless(false).Leakless(true). HeadlessNew(true).HeadlessNew(false). Headless(false).Headless(true).RemoteDebuggingPort(port). NoSandbox(true).NoSandbox(false). Devtools(true).Devtools(false). StartURL("about:blank"). Proxy("test.com"). UserDataDir("test").UserDataDir(dir). WorkingDir(""). Env(append(os.Environ(), "TZ=Asia/Tokyo")...) g.Eq(l.FormatArgs(), []string /* len=6 cap=8 */ { "--headless", `--no-startup-window`, /* len=19 */ `--proxy-server=test.com`, /* len=23 */ `--remote-debugging-port=58472`, /* len=29 */ "--test-append=a", "about:blank", }) url := l.MustLaunch() g.Eq(url, launcher.NewUserMode().RemoteDebuggingPort(port).MustLaunch()) } func TestUserModeErr(t *testing.T) { g := setup(t) _, err := launcher.NewUserMode().RemoteDebuggingPort(48277).Bin("not-exists").Launch() g.Err(err) _, err = launcher.NewUserMode().RemoteDebuggingPort(58217).Bin("echo").Launch() g.Err(err) } func TestAppMode(t *testing.T) { g := setup(t) l := launcher.NewAppMode("http://example.com") g.Eq(l.Get(flags.App), "http://example.com") } func TestGetWebSocketDebuggerURLErr(t *testing.T) { g := setup(t) _, err := launcher.ResolveURL("1://") g.Err(err) } func TestLaunchErr(t *testing.T) { g := setup(t) g.Panic(func() { launcher.New().Bin("not-exists").MustLaunch() }) g.Panic(func() { launcher.New().Headless(false).Bin("not-exists").MustLaunch() }) g.Panic(func() { launcher.New().ClientHeader() }) { l := launcher.New().XVFB() _, _ = l.Launch() l.Kill() } } var testProfileDir = flag.Bool("test-profile-dir", false, "set it to test profile dir") func TestProfileDir(t *testing.T) { g := setup(t) url := launcher.New().Headless(false). ProfileDir("").ProfileDir("test-profile-dir") if !*testProfileDir { g.Skip("It's not CI friendly, so we skip it!") } url.MustLaunch() userDataDir := url.Get(flags.UserDataDir) file, err := os.Stat(filepath.Join(userDataDir, "test-profile-dir")) g.E(err) g.True(file.IsDir()) } func TestBrowserValid(t *testing.T) { g := setup(t) b := launcher.NewBrowser() b.Revision = 0 g.Err(b.Validate()) g.E(utils.Mkdir(filepath.Dir(b.BinPath()))) g.Cleanup(func() { _ = os.RemoveAll(b.Dir()) }) g.E(exec.Command("go", "build", "-o", b.BinPath(), "./fixtures/chrome-exit-err").CombinedOutput()) g.Has(b.Validate().Error(), "failed to run the browser") g.E(exec.Command("go", "build", "-o", b.BinPath(), "./fixtures/chrome-empty").CombinedOutput()) g.Eq(b.Validate().Error(), "the browser executable doesn't support headless mode") g.E(exec.Command("go", "build", "-o", b.BinPath(), "./fixtures/chrome-lib-missing").CombinedOutput()) g.Nil(b.Validate()) } func TestIgnoreCerts(t *testing.T) { g := setup(t) // https://travistidwell.com/jsencrypt/demo/ testData := []string{ `-----BEGIN PUBLIC KEY----- MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgF9pr2zok5bivQIEUN7Y58a9uB1o sroMt3hxNfzOh/G+sXgYPPoEl2/Ys/2zbvym7Ze0eGbb6FrV8aueg89TPTNWAKlN N49q6S3zLG1WmI2rVYz4LtPgpg1YR9FQRIg4Ll0C02daufXgvUBGjIARH19FTw6P 61kEhnEQxUHhdAqbAgMBAAE= -----END PUBLIC KEY----- `, `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCvBTz/TOYc66qB97OyYenSHk4T hAUKX5RUWZ/80o0zyJoo1dfrrwW9PlT5o4DlGMs0NSbtJ8RMQRTLZwL/zxXjiEMv dKFs2OrefYKANTc0e2XAtQAm3Is5Ro8AF1S4Fk+eZXr2yZtBRKXvhJ/A2bilVoSn fmQnyBe7dVU43NXfrQIDAQAB -----END PUBLIC KEY----- `, } keys := make([]crypto.PublicKey, 0, len(testData)) for _, pubPEM := range testData { block, _ := pem.Decode([]byte(pubPEM)) if block == nil { g.Fatal("failed to parse PEM block containing the public key") return // no-op because g.Fatal calls t.FailNow() but `staticcheck` doesn't know it } pub, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { g.Fatalf("failed to parse DER encoded public key: " + err.Error()) } keys = append(keys, pub) } l := launcher.New() err := l.IgnoreCerts(keys) if err != nil { g.Fatalf("IgnoreCerts: %s", err) } expected := "--ignore-certificate-errors-spki-list=" + strings.Join([]string{ "+ZqfrXb+V/36nZecO59bghHlNhiHTzImjYLnNWGUd1I=", "llpTCSqZ2/IKsMg4tz+o1mCkXIOdKcM6sKu9kC6o7S4=", }, ",") g.Has(l.FormatArgs(), expected) } func TestIgnoreCerts_InvalidCert(t *testing.T) { g := setup(t) l := launcher.New() err := l.IgnoreCerts([]crypto.PublicKey{nil}) if err == nil { g.Fatalf("IgnoreCerts: %s", err) } } func TestBrowserDownloadErr(t *testing.T) { g := setup(t) b := launcher.NewBrowser() b.Logger = utils.LoggerQuiet b.HTTPClient = http.DefaultClient b.Hosts = []launcher.Host{} g.Err(b.Download()) s := g.Serve() s.Route("/download", ".txt", "ok") b = launcher.NewBrowser() b.Hosts = []launcher.Host{func(_ int) string { return s.URL("/download/file") }} g.Err(b.Download()) } func TestLaunchMultiTimes(t *testing.T) { g := setup(t) // first time launch, success. l := launcher.New() u, e := l.Launch() g.Neq(u, "") g.E(e) // second time launch, failed with ErrAlreadyLaunched. _, e = l.Launch() g.Eq(e, launcher.ErrAlreadyLaunched) } golang-github-go-rod-rod-0.116.2/lib/launcher/load_test.go000066400000000000000000000027471520674146500232570ustar00rootroot00000000000000package launcher_test import ( "context" "math/rand" "sync" "testing" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/got" ) func BenchmarkManager(b *testing.B) { const concurrent = 30 // how many browsers will run at the same time const num = 300 // how many browsers we will launch limiter := make(chan int, concurrent) s := got.New(b).Serve() // docker run --rm -p 7317:7317 ghcr.io/go-rod/rod s.HostURL.Host = "host.docker.internal" s.Route("/", ".html", ` ok `) wg := &sync.WaitGroup{} wg.Add(num) for i := 0; i < num; i++ { limiter <- 0 go func() { utils.Sleep(rand.Float64()) ctx, cancel := context.WithCancel(context.Background()) defer func() { go func() { utils.Sleep(2) cancel() }() }() l := launcher.MustNewManaged("") u, h := l.ClientHeader() browser := rod.New().Client(cdp.MustStartWithURL(ctx, u, h)).MustConnect() page := browser.MustPage() wait := page.MustWaitNavigation() page.MustNavigate(s.URL()) wait() page.MustEval(`wait()`) if rand.Int()%10 == 0 { // 10% we will drop the websocket connection without call the api to gracefully close the browser cancel() } else { browser.MustClose() } wg.Done() <-limiter }() } wg.Wait() } golang-github-go-rod-rod-0.116.2/lib/launcher/manager.go000066400000000000000000000136141520674146500227060ustar00rootroot00000000000000package launcher import ( "encoding/json" "fmt" "net/http" "net/http/httputil" "net/url" "os" "strings" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/launcher/flags" "github.com/go-rod/rod/lib/utils" ) const ( // HeaderName for remote launch. HeaderName = "Rod-Launcher" ) // MustNewManaged is similar to NewManaged. func MustNewManaged(serviceURL string) *Launcher { l, err := NewManaged(serviceURL) utils.E(err) // TODO: remove this after we have a better way to handle this // The latest chromium in docker will crash pages that use http2 l.Set("disable-http2") return l } // NewManaged creates a default Launcher instance from launcher.Manager. // The serviceURL must point to a launcher.Manager. It will send a http request to the serviceURL // to get the default settings of the Launcher instance. For example if the launcher.Manager running on a // Linux machine will return different default settings from the one on Mac. // If Launcher.Leakless is enabled, the remote browser will be killed after the websocket is closed. func NewManaged(serviceURL string) (*Launcher, error) { if serviceURL == "" { serviceURL = "ws://127.0.0.1:7317" } u, err := url.Parse(serviceURL) if err != nil { return nil, err } l := New() l.managed = true l.serviceURL = toWS(*u).String() l.Flags = nil res, err := http.Get(toHTTP(*u).String()) //nolint: noctx if err != nil { return nil, err } defer func() { _ = res.Body.Close() }() return l, json.NewDecoder(res.Body).Decode(l) } // KeepUserDataDir after remote browser is closed. By default launcher.FlagUserDataDir will be removed. func (l *Launcher) KeepUserDataDir() *Launcher { l.mustManaged() l.Set(flags.KeepUserDataDir) return l } // JSON serialization. func (l *Launcher) JSON() []byte { return utils.MustToJSONBytes(l) } // MustClient similar to Launcher.Client. func (l *Launcher) MustClient() *cdp.Client { u, h := l.ClientHeader() return cdp.MustStartWithURL(l.ctx, u, h) } // Client for launching browser remotely via the launcher.Manager. func (l *Launcher) Client() (*cdp.Client, error) { u, h := l.ClientHeader() return cdp.StartWithURL(l.ctx, u, h) } // ClientHeader for launching browser remotely via the launcher.Manager. func (l *Launcher) ClientHeader() (string, http.Header) { l.mustManaged() header := http.Header{} header.Add(string(HeaderName), utils.MustToJSON(l)) return l.serviceURL, header } func (l *Launcher) mustManaged() { if !l.managed { panic("Must be used with launcher.NewManaged") } } var _ http.Handler = &Manager{} // Manager is used to launch browsers via http server on another machine. // The reason why we have Manager is after we launcher a browser, we can't dynamically change its // CLI arguments, such as "--headless". The Manager allows us to decide what CLI arguments to // pass to the browser when launch it remotely. // The work flow looks like: // // | Machine X | Machine Y | // | NewManaged("a.com") -|-> http.ListenAndServe("a.com", launcher.NewManager()) --> launch browser | // // 1. X send a http request to Y, Y respond default Launcher settings based the OS of Y. // 2. X start a websocket connect to Y with the Launcher settings // 3. Y launches a browser with the Launcher settings X // 4. Y transparently proxy the websocket connect between X and the launched browser type Manager struct { // Logger for key events Logger utils.Logger // Defaults should return the default Launcher settings Defaults func(http.ResponseWriter, *http.Request) *Launcher // BeforeLaunch hook is called right before the launching with the Launcher instance that will be used // to launch the browser. // Such as use it to filter malicious values of Launcher.UserDataDir, Launcher.Bin, or Launcher.WorkingDir. BeforeLaunch func(*Launcher, http.ResponseWriter, *http.Request) } // NewManager instance. func NewManager() *Manager { allowedPath := map[flags.Flag]string{ flags.Bin: DefaultBrowserDir, flags.WorkingDir: func() string { p, _ := os.Getwd() return p }(), flags.UserDataDir: DefaultUserDataDirPrefix, } return &Manager{ Logger: utils.LoggerQuiet, Defaults: func(_ http.ResponseWriter, _ *http.Request) *Launcher { return New() }, BeforeLaunch: func(l *Launcher, w http.ResponseWriter, _ *http.Request) { for f, allowed := range allowedPath { p := l.Get(f) if p != "" && !strings.HasPrefix(p, allowed) { b := []byte(fmt.Sprintf("[rod-manager] not allowed %s path: %s (use --allow-all to disable the protection)", f, p)) w.Header().Add("Content-Length", fmt.Sprintf("%d", len(b))) w.WriteHeader(http.StatusBadRequest) utils.E(w.Write(b)) w.(http.Flusher).Flush() //nolint: forcetypeassert panic(http.ErrAbortHandler) } } }, } } func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Upgrade") == "websocket" { m.launch(w, r) return } l := m.Defaults(w, r) utils.E(w.Write(l.JSON())) } func (m *Manager) launch(w http.ResponseWriter, r *http.Request) { l := New() options := r.Header.Get(string(HeaderName)) if options != "" { l.Flags = nil utils.E(json.Unmarshal([]byte(options), l)) } m.BeforeLaunch(l, w, r) kill := l.Has(flags.Leakless) // Always enable leakless so that if the Manager process crashes // all the managed browsers will be killed. u := l.Leakless(true).MustLaunch() defer m.cleanup(l, kill) parsedURL, err := url.Parse(u) utils.E(err) m.Logger.Println("Launch", u, options) defer m.Logger.Println("Close", u) parsedWS, err := url.Parse(u) utils.E(err) parsedURL.Path = parsedWS.Path httputil.NewSingleHostReverseProxy(toHTTP(*parsedURL)).ServeHTTP(w, r) } func (m *Manager) cleanup(l *Launcher, kill bool) { if kill { l.Kill() m.Logger.Println("Killed PID:", l.PID()) } if !l.Has(flags.KeepUserDataDir) { l.Cleanup() dir := l.Get(flags.UserDataDir) m.Logger.Println("Removed", dir) } } golang-github-go-rod-rod-0.116.2/lib/launcher/os_unix.go000066400000000000000000000010241520674146500227500ustar00rootroot00000000000000//go:build !windows package launcher import ( "os/exec" "syscall" "github.com/go-rod/rod/lib/launcher/flags" ) func killGroup(pid int) { _ = syscall.Kill(-pid, syscall.SIGKILL) } func (l *Launcher) osSetupCmd(cmd *exec.Cmd) { if flags, has := l.GetFlags(flags.XVFB); has { var command []string // flags must append before cmd.Args command = append(command, flags...) command = append(command, cmd.Args...) *cmd = *exec.Command("xvfb-run", command...) } cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} } golang-github-go-rod-rod-0.116.2/lib/launcher/os_windows.go000066400000000000000000000007521520674146500234660ustar00rootroot00000000000000//go:build windows package launcher import ( "os/exec" "syscall" ) func killGroup(pid int) { terminateProcess(pid) } func (l *Launcher) osSetupCmd(cmd *exec.Cmd) { cmd.SysProcAttr = &syscall.SysProcAttr{ CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, } } func terminateProcess(pid int) { handle, err := syscall.OpenProcess(syscall.PROCESS_TERMINATE, true, uint32(pid)) if err != nil { return } _ = syscall.TerminateProcess(handle, 0) _ = syscall.CloseHandle(handle) } golang-github-go-rod-rod-0.116.2/lib/launcher/private_test.go000066400000000000000000000074511520674146500240070ustar00rootroot00000000000000package launcher import ( "fmt" "net/url" "os" "os/exec" "path/filepath" "sync" "testing" "time" "github.com/go-rod/rod/lib/cdp" "github.com/go-rod/rod/lib/defaults" "github.com/go-rod/rod/lib/launcher/flags" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/got" ) func HostTest(host string) Host { return func(revision int) string { return fmt.Sprintf( "%s/chromium-browser-snapshots/%s/%d/%s", host, hostConf.urlPrefix, revision, hostConf.zipName, ) } } var setup = got.Setup(nil) func TestToHTTP(t *testing.T) { g := setup(t) u, _ := url.Parse("wss://a.com") g.Eq("https", toHTTP(*u).Scheme) u, _ = url.Parse("ws://a.com") g.Eq("http", toHTTP(*u).Scheme) } func TestToWS(t *testing.T) { g := setup(t) u, _ := url.Parse("https://a.com") g.Eq("wss", toWS(*u).Scheme) u, _ = url.Parse("http://a.com") g.Eq("ws", toWS(*u).Scheme) } func TestLaunchOptions(t *testing.T) { g := setup(t) defaults.Show = true defaults.Devtools = true inContainer = true // restore defer func() { defaults.ResetWith("") inContainer = utils.InContainer }() l := New() g.False(l.Has(flags.Headless)) g.True(l.Has(flags.NoSandbox)) g.True(l.Has("auto-open-devtools-for-tabs")) } func TestGetURLErr(t *testing.T) { g := setup(t) l := New() l.ctxCancel() _, err := l.getURL() g.Err(err) l = New() l.parser.lock.Lock() l.parser.Buffer = "err" l.parser.lock.Unlock() close(l.exit) _, err = l.getURL() g.Eq("[launcher] Failed to get the debug url: err", err.Error()) } func TestManaged(t *testing.T) { g := setup(t) ctx := g.Timeout(5 * time.Second) s := got.New(g).Serve() rl := NewManager() s.Mux.Handle("/", rl) l := MustNewManaged(s.URL()).KeepUserDataDir().Delete(flags.KeepUserDataDir) c := l.MustClient() g.E(c.Call(ctx, "", "Browser.getVersion", nil)) utils.Sleep(1) _, _ = c.Call(ctx, "", "Browser.crash", nil) dir := l.Get(flags.UserDataDir) for ctx.Err() == nil { utils.Sleep(0.1) _, err := os.Stat(dir) if err != nil { break } } g.Err(os.Stat(dir)) u, h := MustNewManaged(s.URL()).Bin("go").ClientHeader() _, err := cdp.StartWithURL(ctx, u, h) g.Eq(err.(*cdp.BadHandshakeError).Body, "[rod-manager] not allowed rod-bin path: go (use --allow-all to disable the protection)") } func TestLaunchErrs(t *testing.T) { g := setup(t) l := New().Bin("echo") _, err := l.Launch() g.Err(err) s := g.Serve() s.Route("/", "", nil) l = New().Bin("") l.browser.Logger = utils.LoggerQuiet l.browser.RootDir = filepath.Join("tmp", "browser-from-mirror", g.RandStr(16)) l.browser.Hosts = []Host{HostTest(s.URL())} _, err = l.Launch() g.Err(err) } func TestURLParserErr(t *testing.T) { g := setup(t) u := &URLParser{ Buffer: "error", lock: &sync.Mutex{}, } g.Eq(u.Err().Error(), "[launcher] Failed to get the debug url: error") u.Buffer = "/tmp/rod/chromium-818858/chrome: error while loading shared libraries: libgobject-2.0.so.0: cannot open shared object file: No such file or directory" g.Eq(u.Err().Error(), "[launcher] Failed to launch the browser, the doc might help https://go-rod.github.io/#/compatibility?id=os: /tmp/rod/chromium-818858/chrome: error while loading shared libraries: libgobject-2.0.so.0: cannot open shared object file: No such file or directory") } func TestTestOpen(_ *testing.T) { openExec = func(_ string, _ ...string) *exec.Cmd { cmd := exec.Command("not-exists") cmd.Process = &os.Process{} return cmd } defer func() { openExec = exec.Command }() Open("about:blank") } func TestLaunchClient(t *testing.T) { g := setup(t) ctx := g.Timeout(5 * time.Second) s := got.New(g).Serve() rl := NewManager() s.Mux.Handle("/", rl) l := MustNewManaged(s.URL()).KeepUserDataDir().Delete(flags.KeepUserDataDir) c, err := l.Client() if err != nil { g.Err(err) } g.E(c.Call(ctx, "", "Browser.getVersion", nil)) } golang-github-go-rod-rod-0.116.2/lib/launcher/revision.go000066400000000000000000000003021520674146500231200ustar00rootroot00000000000000// generated by "lib/launcher/revision" package launcher // RevisionDefault for chromium. const RevisionDefault = 1321438 // RevisionPlaywright for arm linux. const RevisionPlaywright = 1124 golang-github-go-rod-rod-0.116.2/lib/launcher/revision/000077500000000000000000000000001520674146500225765ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/launcher/revision/main.go000066400000000000000000000053061520674146500240550ustar00rootroot00000000000000// Package main ... package main import ( "encoding/json" "fmt" "io" "log" "net/http" "path/filepath" "sort" "strconv" "strings" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/gson" ) const mirror = "https://registry.npmmirror.com/-/binary/chromium-browser-snapshots/" func main() { list := getList(mirror) revLists := [][]int{} for _, os := range list { // skip win32 if os == "Win" { continue } revList := []int{} for _, s := range getList(mirror + os + "/") { rev, err := strconv.ParseInt(s, 10, 32) if err != nil { log.Fatal(err) } revList = append(revList, int(rev)) } sort.Ints(revList) revLists = append(revLists, revList) } rev := largestCommonRevision(revLists) if rev < 969819 { utils.E(fmt.Errorf("cannot match version of the latest chromium from %s", mirror)) } playwright := getFromPlaywright() out := utils.S(`// generated by "lib/launcher/revision" package launcher // RevisionDefault for chromium. const RevisionDefault = {{.default}} // RevisionPlaywright for arm linux. const RevisionPlaywright = {{.playwright}} `, "default", rev, "playwright", playwright, ) utils.E(utils.OutputFile(filepath.FromSlash("lib/launcher/revision.go"), out)) } func getList(path string) []string { res, err := http.Get(path) utils.E(err) defer func() { _ = res.Body.Close() }() var data interface{} err = json.NewDecoder(res.Body).Decode(&data) utils.E(err) list := data.([]interface{}) names := []string{} for _, it := range list { name := it.(map[string]interface{})["name"].(string) names = append(names, strings.TrimRight(name, "/")) } return names } func largestCommonRevision(revLists [][]int) int { sort.Slice(revLists, func(i, j int) bool { return len(revLists[i]) < len(revLists[j]) }) shortest := revLists[0] for i := len(shortest) - 1; i >= 0; i-- { r := shortest[i] isCommon := true for i := 1; i < len(revLists); i++ { if !has(revLists[i], r) { isCommon = false break } } if isCommon { return r } } return 0 } func has(list []int, i int) bool { index := sort.SearchInts(list, i) return index < len(list) && list[index] == i } func getFromPlaywright() int { pv := strings.TrimSpace(utils.ExecLine(false, "npm --no-update-notifier -s show playwright version")) out := fetch(fmt.Sprintf( "https://raw.githubusercontent.com/microsoft/playwright/v%s/packages/playwright-core/browsers.json", pv)) rev, err := strconv.ParseInt(gson.NewFrom(out).Get("browsers.0.revision").Str(), 10, 32) utils.E(err) return int(rev) } func fetch(u string) string { res, err := http.Get(u) utils.E(err) defer func() { _ = res.Body.Close() }() b, err := io.ReadAll(res.Body) utils.E(err) return string(b) } golang-github-go-rod-rod-0.116.2/lib/launcher/rod-manager/000077500000000000000000000000001520674146500231345ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/launcher/rod-manager/main.go000066400000000000000000000015331520674146500244110ustar00rootroot00000000000000// A server to help launch browser remotely package main import ( "flag" "fmt" "log" "net" "net/http" "os" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/utils" ) var ( addr = flag.String("address", ":7317", "the address to listen to") quiet = flag.Bool("quiet", false, "silence the log") allowAllPath = flag.Bool("allow-all", false, "allow all path set by the client") ) func main() { flag.Parse() m := launcher.NewManager() if !*quiet { m.Logger = log.New(os.Stdout, "", 0) } if *allowAllPath { m.BeforeLaunch = func(_ *launcher.Launcher, _ http.ResponseWriter, _ *http.Request) {} } l, err := net.Listen("tcp", *addr) if err != nil { utils.E(err) } if !*quiet { fmt.Println("[rod-manager] listening on:", l.Addr().String()) } srv := &http.Server{Handler: m} utils.E(srv.Serve(l)) } golang-github-go-rod-rod-0.116.2/lib/launcher/url_parser.go000066400000000000000000000053631520674146500234540ustar00rootroot00000000000000package launcher import ( "context" "errors" "io" "net/http" "net/url" "regexp" "strings" "sync" "github.com/go-rod/rod/lib/utils" "github.com/ysmood/gson" ) var _ io.Writer = &URLParser{} // URLParser to get control url from stderr. type URLParser struct { URL chan string Buffer string // buffer for the browser stdout lock *sync.Mutex ctx context.Context done bool } // NewURLParser instance. func NewURLParser() *URLParser { return &URLParser{ URL: make(chan string), lock: &sync.Mutex{}, ctx: context.Background(), } } var regWS = regexp.MustCompile(`ws://.+/`) // Context sets the context. func (r *URLParser) Context(ctx context.Context) *URLParser { r.ctx = ctx return r } // Write interface. func (r *URLParser) Write(p []byte) (n int, err error) { r.lock.Lock() defer r.lock.Unlock() if !r.done { r.Buffer += string(p) str := regWS.FindString(r.Buffer) if str != "" { u, err := url.Parse(strings.TrimSpace(str)) utils.E(err) select { case <-r.ctx.Done(): case r.URL <- "http://" + u.Host: } r.done = true r.Buffer = "" } } return len(p), nil } // Err returns the common error parsed from stdout and stderr. func (r *URLParser) Err() error { r.lock.Lock() defer r.lock.Unlock() msg := "[launcher] Failed to get the debug url: " if strings.Contains(r.Buffer, "error while loading shared libraries") { msg = "[launcher] Failed to launch the browser, the doc might help https://go-rod.github.io/#/compatibility?id=os: " } return errors.New(msg + r.Buffer) } // MustResolveURL is similar to ResolveURL. func MustResolveURL(u string) string { u, err := ResolveURL(u) utils.E(err) return u } var ( regPort = regexp.MustCompile(`^\:?(\d+)$`) regProtocol = regexp.MustCompile(`^\w+://`) ) // ResolveURL by requesting the u, it will try best to normalize the u. // The format of u can be "9222", ":9222", "host:9222", "ws://host:9222", "wss://host:9222", // "https://host:9222" "http://host:9222". The return string will look like: // "ws://host:9222/devtools/browser/4371405f-84df-4ad6-9e0f-eab81f7521cc" func ResolveURL(u string) (string, error) { if u == "" { u = "9222" } u = strings.TrimSpace(u) u = regPort.ReplaceAllString(u, "127.0.0.1:$1") if !regProtocol.MatchString(u) { u = "http://" + u } parsed, err := url.Parse(u) if err != nil { return "", err } parsed = toHTTP(*parsed) parsed.Path = "/json/version" res, err := http.Get(parsed.String()) //nolint: noctx if err != nil { return "", err } defer func() { _ = res.Body.Close() }() data, err := io.ReadAll(res.Body) utils.E(err) wsURL := gson.New(data).Get("webSocketDebuggerUrl").Str() parsedWS, err := url.Parse(wsURL) utils.E(err) parsedWS.Host = parsed.Host return parsedWS.String(), nil } golang-github-go-rod-rod-0.116.2/lib/launcher/utils.go000066400000000000000000000017771520674146500224430ustar00rootroot00000000000000package launcher import ( "crypto" "crypto/sha256" "crypto/x509" "encoding/base64" "fmt" "net/url" "github.com/go-rod/rod/lib/utils" ) var inContainer = utils.InContainer func toHTTP(u url.URL) *url.URL { newURL := u if newURL.Scheme == "ws" { newURL.Scheme = "http" } else if newURL.Scheme == "wss" { newURL.Scheme = "https" } return &newURL } func toWS(u url.URL) *url.URL { newURL := u if newURL.Scheme == "http" { newURL.Scheme = "ws" } else if newURL.Scheme == "https" { newURL.Scheme = "wss" } return &newURL } // certSPKI generates the SPKI of a certificate public key // https://blog.afoolishmanifesto.com/posts/golang-self-signed-and-pinned-certs/ func certSPKI(pk crypto.PublicKey) ([]byte, error) { pubDER, err := x509.MarshalPKIXPublicKey(pk) if err != nil { return nil, fmt.Errorf("x509.MarshalPKIXPublicKey: %w", err) } sum := sha256.Sum256(pubDER) pin := make([]byte, base64.StdEncoding.EncodedLen(len(sum))) base64.StdEncoding.Encode(pin, sum[:]) return pin, nil } golang-github-go-rod-rod-0.116.2/lib/proto/000077500000000000000000000000001520674146500203025ustar00rootroot00000000000000golang-github-go-rod-rod-0.116.2/lib/proto/README.md000066400000000000000000000005301520674146500215570ustar00rootroot00000000000000# Overview A lib to encode/decode the data of the cdp protocol. 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. Here's an [usage example](https://github.com/go-rod/rod/blob/9e847f3bab313a1d233c0c868fe5125e2e70de70/examples_test.go#L370-L393). golang-github-go-rod-rod-0.116.2/lib/proto/a_interface.go000066400000000000000000000032711520674146500230740ustar00rootroot00000000000000// Package proto is a lib to encode/decode the data of the cdp protocol. package proto import ( "context" "encoding/json" "reflect" "strings" ) // Client interface to send the request. // So that this lib doesn't handle anything has side effect. type Client interface { Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) } // Sessionable type has a proto.TargetSessionID for its methods. type Sessionable interface { GetSessionID() TargetSessionID } // Contextable type has a context.Context for its methods. type Contextable interface { GetContext() context.Context } // Request represents a cdp.Request.Method. type Request interface { // ProtoReq returns the cdp.Request.Method ProtoReq() string } // Event represents a cdp.Event.Params. type Event interface { // ProtoEvent returns the cdp.Event.Method ProtoEvent() string } // GetType from method name of this package, // such as proto.GetType("Page.enable") will return the type of proto.PageEnable. func GetType(methodName string) reflect.Type { return types[methodName] } // ParseMethodName to domain and name. func ParseMethodName(method string) (domain, name string) { arr := strings.Split(method, ".") return arr[0], arr[1] } // call method with request and response containers. func call(method string, req, res interface{}, c Client) error { ctx := context.Background() if cta, ok := c.(Contextable); ok { ctx = cta.GetContext() } sessionID := "" if tsa, ok := c.(Sessionable); ok { sessionID = string(tsa.GetSessionID()) } bin, err := c.Call(ctx, sessionID, method, req) if err != nil { return err } if res == nil { return nil } return json.Unmarshal(bin, res) } golang-github-go-rod-rod-0.116.2/lib/proto/a_interface_test.go000066400000000000000000000070571520674146500241410ustar00rootroot00000000000000package proto_test import ( "context" "encoding/json" "errors" "reflect" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/utils" ) type Client struct { sessionID string methodName string params interface{} err error ret interface{} } var ( _ proto.Client = &Client{} _ proto.Sessionable = &Client{} _ proto.Contextable = &Client{} ) func (c *Client) Call(_ context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) { c.sessionID = sessionID c.methodName = methodName c.params = params return utils.MustToJSONBytes(c.ret), c.err } func (c *Client) GetSessionID() proto.TargetSessionID { return "" } func (c *Client) GetContext() context.Context { return nil } func (t T) CallErr() { client := &Client{err: errors.New("err")} t.Eq(proto.PageEnable{}.Call(client).Error(), "err") } func (t T) ParseMethodName() { d, n := proto.ParseMethodName("Page.enable") t.Eq("Page", d) t.Eq("enable", n) } func (t T) GetType() { method := proto.GetType("Page.enable") t.Eq(reflect.TypeOf(proto.PageEnable{}), method) } func (t T) TimeCodec() { raw := []byte("123.123") var duration proto.MonotonicTime t.E(json.Unmarshal(raw, &duration)) t.Eq(123123, duration.Duration().Milliseconds()) t.Eq("2m3.123s", duration.String()) data, err := json.Marshal(duration) t.E(err) t.Eq(raw, data) raw = []byte("1234567890") var datetime proto.TimeSinceEpoch t.E(json.Unmarshal(raw, &datetime)) t.Eq(1234567890, datetime.Time().Unix()) t.Has(datetime.String(), "2009-02") data, err = json.Marshal(datetime) t.E(err) t.Eq(raw, data) } func (t T) Rect() { rect := proto.DOMQuad{ 336, 382, 361, 382, 361, 421, 336, 412, } t.Eq(348.5, rect.Center().X) t.Eq(399.25, rect.Center().Y) res := &proto.DOMGetContentQuadsResult{} t.Nil(res.OnePointInside()) res = &proto.DOMGetContentQuadsResult{Quads: []proto.DOMQuad{{1, 1, 2, 1, 2, 1, 1, 1}}} t.Nil(res.OnePointInside()) res = &proto.DOMGetContentQuadsResult{Quads: []proto.DOMQuad{rect}} pt := res.OnePointInside() t.Eq(348.5, pt.X) t.Eq(399.25, pt.Y) } func (t T) Area() { t.Eq(proto.DOMQuad{1, 1, 2, 1, 2, 1, 1, 1}.Area(), 0) t.Eq(proto.DOMQuad{1, 1, 2, 1, 2, 2, 1, 2}.Area(), 1) t.Eq(proto.DOMQuad{1, 1, 2, 1, 2, 4, 1, 3}.Area(), 2.5) } func (t T) Box() { res := &proto.DOMGetContentQuadsResult{Quads: []proto.DOMQuad{ {1, 1, 2, 1, 2, 2, 1, 2}, {2, 0, 3, 0, 3, 1, 2, 1}, {0, 2, 1, 2, 1, 3, 0, 3}, }} t.Eq(res.Box(), &proto.DOMRect{ X: 0, Y: 0, Width: 3, Height: 3, }) t.Nil((&proto.DOMGetContentQuadsResult{}).Box()) } func (t T) InputTouchPointMoveTo() { p := &proto.InputTouchPoint{} p.MoveTo(1, 2) t.Eq(1, p.X) t.Eq(2, p.Y) } func (t T) CookiesToParams() { list := proto.CookiesToParams([]*proto.NetworkCookie{{ Name: "name", Value: "val", }}) t.Eq(list[0].Name, "name") t.Eq(list[0].Value, "val") } func (t T) GeneratorOptimize() { var _ proto.TargetTargetInfoType = proto.TargetTargetInfoTypeBackgroundPage var _ proto.TargetTargetInfoType = proto.TargetTargetInfoTypePage var _ proto.PageLifecycleEventName = proto.PageLifecycleEventNameInit var _ proto.PageLifecycleEventName = proto.PageLifecycleEventNameFirstContentfulPaint var _ proto.PageLifecycleEventName = proto.PageLifecycleEventNameFirstImagePaint a := proto.InputDispatchKeyEvent{} var _ proto.TimeSinceEpoch = a.Timestamp b := proto.NetworkCookie{} var _ proto.TimeSinceEpoch = b.Expires c := proto.NetworkDataReceived{} var _ proto.MonotonicTime = c.Timestamp d := proto.NetworkCookie{} var _ proto.TimeSinceEpoch = d.Expires } golang-github-go-rod-rod-0.116.2/lib/proto/a_patch.go000066400000000000000000000071621520674146500222360ustar00rootroot00000000000000// Patches to normalize the proto types package proto import ( "time" ) // TimeSinceEpoch UTC time in seconds, counted from January 1, 1970. // To convert a time.Time to TimeSinceEpoch, for example: // // proto.TimeSinceEpoch(time.Now().Unix()) // // For session cookie, the value should be -1. type TimeSinceEpoch float64 // Time interface. func (t TimeSinceEpoch) Time() time.Time { return (time.Unix(0, 0)).Add( time.Duration(t * TimeSinceEpoch(time.Second)), ) } // String interface. func (t TimeSinceEpoch) String() string { return t.Time().String() } // MonotonicTime Monotonically increasing time in seconds since an arbitrary point in the past. type MonotonicTime float64 // Duration interface. func (t MonotonicTime) Duration() time.Duration { return time.Duration(t * MonotonicTime(time.Second)) } // String interface. func (t MonotonicTime) String() string { return t.Duration().String() } // Point from the origin (0, 0). type Point struct { X float64 `json:"x"` Y float64 `json:"y"` } // NewPoint instance. func NewPoint(x, y float64) Point { return Point{x, y} } // Add v to p and returns a new Point. func (p Point) Add(v Point) Point { return NewPoint(p.X+v.X, p.Y+v.Y) } // Minus v from p and returns a new Point. func (p Point) Minus(v Point) Point { return NewPoint(p.X-v.X, p.Y-v.Y) } // Scale p with s and returns a new Point. func (p Point) Scale(s float64) Point { return NewPoint(p.X*s, p.Y*s) } // Len is the number of vertices. func (q DOMQuad) Len() int { return len(q) / 2 //nolint: mnd } // Each point. func (q DOMQuad) Each(fn func(pt Point, i int)) { for i := 0; i < q.Len(); i++ { fn(Point{q[i*2], q[i*2+1]}, i) } } // Center of the polygon. func (q DOMQuad) Center() Point { var x, y float64 q.Each(func(pt Point, _ int) { x += pt.X y += pt.Y }) return Point{x / float64(q.Len()), y / float64(q.Len())} } // Area of the polygon // https://en.wikipedia.org/wiki/Polygon#Area func (q DOMQuad) Area() float64 { area := 0.0 l := len(q)/2 - 1 //nolint: mnd for i := 0; i < l; i++ { area += q[i*2]*q[i*2+3] - q[i*2+2]*q[i*2+1] } area += q[l*2]*q[1] - q[0]*q[l*2+1] return area / 2 //nolint: mnd } // OnePointInside the shape. func (res *DOMGetContentQuadsResult) OnePointInside() *Point { for _, q := range res.Quads { if q.Area() >= 1 { pt := q.Center() return &pt } } return nil } // Box returns the smallest leveled rectangle that can cover the whole shape. func (res *DOMGetContentQuadsResult) Box() (box *DOMRect) { return Shape(res.Quads).Box() } // Shape is a list of DOMQuad. type Shape []DOMQuad // Box returns the smallest leveled rectangle that can cover the whole shape. func (qs Shape) Box() (box *DOMRect) { if len(qs) == 0 { return } left := qs[0][0] top := qs[0][1] right := left bottom := top for _, q := range qs { q.Each(func(pt Point, _ int) { if pt.X < left { left = pt.X } if pt.Y < top { top = pt.Y } if pt.X > right { right = pt.X } if pt.Y > bottom { bottom = pt.Y } }) } box = &DOMRect{left, top, right - left, bottom - top} return } // MoveTo X and Y to x and y. func (p *InputTouchPoint) MoveTo(x, y float64) { p.X = x p.Y = y } // CookiesToParams converts Cookies list to NetworkCookieParam list. func CookiesToParams(cookies []*NetworkCookie) []*NetworkCookieParam { list := []*NetworkCookieParam{} for _, c := range cookies { list = append(list, &NetworkCookieParam{ Name: c.Name, Value: c.Value, Domain: c.Domain, Path: c.Path, Secure: c.Secure, HTTPOnly: c.HTTPOnly, SameSite: c.SameSite, Expires: c.Expires, Priority: c.Priority, }) } return list } golang-github-go-rod-rod-0.116.2/lib/proto/a_patch_test.go000066400000000000000000000003261520674146500232700ustar00rootroot00000000000000package proto_test import "github.com/go-rod/rod/lib/proto" func (t T) Point() { p := proto.NewPoint(1, 2). Add(proto.NewPoint(3, 4)). Minus(proto.NewPoint(1, 1)). Scale(2) t.Eq(p.X, 6) t.Eq(p.Y, 10) } golang-github-go-rod-rod-0.116.2/lib/proto/a_utils.go000066400000000000000000000007551520674146500223000ustar00rootroot00000000000000package proto import ( "regexp" "strings" ) var ( regAsterisk = regexp.MustCompile(`([^\\])\*`) regBackSlash = regexp.MustCompile(`([^\\])\?`) ) // PatternToReg FetchRequestPattern.URLPattern to regular expression. func PatternToReg(pattern string) string { if pattern == "" { return "" } pattern = " " + pattern pattern = regAsterisk.ReplaceAllString(pattern, "$1.*") pattern = regBackSlash.ReplaceAllString(pattern, "$1.") return `\A` + strings.TrimSpace(pattern) + `\z` } golang-github-go-rod-rod-0.116.2/lib/proto/a_utils_test.go000066400000000000000000000010351520674146500233270ustar00rootroot00000000000000package proto_test import ( "testing" "github.com/go-rod/rod/lib/proto" "github.com/ysmood/got" ) type T struct { got.G } func Test(t *testing.T) { got.Each(t, T{}) } func (t T) PatternToReg() { t.Eq(``, proto.PatternToReg("")) t.Eq(`\A.*\z`, proto.PatternToReg("*")) t.Eq(`\A.\z`, proto.PatternToReg("?")) t.Eq(`\Aa\z`, proto.PatternToReg("a")) t.Eq(`\Aa.com/.*/test\z`, proto.PatternToReg("a.com/*/test")) t.Eq(`\A\?\*\z`, proto.PatternToReg(`\?\*`)) t.Eq(`\Aa.com\?a=10&b=\*\z`, proto.PatternToReg(`a.com\?a=10&b=\*`)) } golang-github-go-rod-rod-0.116.2/lib/proto/accessibility.go000066400000000000000000000564621520674146500234750ustar00rootroot00000000000000// This file is generated by "./lib/proto/generate" package proto import ( "github.com/ysmood/gson" ) /* Accessibility */ // AccessibilityAXNodeID Unique accessibility node identifier. type AccessibilityAXNodeID string // AccessibilityAXValueType Enum of possible property types. type AccessibilityAXValueType string const ( // AccessibilityAXValueTypeBoolean enum const. AccessibilityAXValueTypeBoolean AccessibilityAXValueType = "boolean" // AccessibilityAXValueTypeTristate enum const. AccessibilityAXValueTypeTristate AccessibilityAXValueType = "tristate" // AccessibilityAXValueTypeBooleanOrUndefined enum const. AccessibilityAXValueTypeBooleanOrUndefined AccessibilityAXValueType = "booleanOrUndefined" // AccessibilityAXValueTypeIdref enum const. AccessibilityAXValueTypeIdref AccessibilityAXValueType = "idref" // AccessibilityAXValueTypeIdrefList enum const. AccessibilityAXValueTypeIdrefList AccessibilityAXValueType = "idrefList" // AccessibilityAXValueTypeInteger enum const. AccessibilityAXValueTypeInteger AccessibilityAXValueType = "integer" // AccessibilityAXValueTypeNode enum const. AccessibilityAXValueTypeNode AccessibilityAXValueType = "node" // AccessibilityAXValueTypeNodeList enum const. AccessibilityAXValueTypeNodeList AccessibilityAXValueType = "nodeList" // AccessibilityAXValueTypeNumber enum const. AccessibilityAXValueTypeNumber AccessibilityAXValueType = "number" // AccessibilityAXValueTypeString enum const. AccessibilityAXValueTypeString AccessibilityAXValueType = "string" // AccessibilityAXValueTypeComputedString enum const. AccessibilityAXValueTypeComputedString AccessibilityAXValueType = "computedString" // AccessibilityAXValueTypeToken enum const. AccessibilityAXValueTypeToken AccessibilityAXValueType = "token" // AccessibilityAXValueTypeTokenList enum const. AccessibilityAXValueTypeTokenList AccessibilityAXValueType = "tokenList" // AccessibilityAXValueTypeDomRelation enum const. AccessibilityAXValueTypeDomRelation AccessibilityAXValueType = "domRelation" // AccessibilityAXValueTypeRole enum const. AccessibilityAXValueTypeRole AccessibilityAXValueType = "role" // AccessibilityAXValueTypeInternalRole enum const. AccessibilityAXValueTypeInternalRole AccessibilityAXValueType = "internalRole" // AccessibilityAXValueTypeValueUndefined enum const. AccessibilityAXValueTypeValueUndefined AccessibilityAXValueType = "valueUndefined" ) // AccessibilityAXValueSourceType Enum of possible property sources. type AccessibilityAXValueSourceType string const ( // AccessibilityAXValueSourceTypeAttribute enum const. AccessibilityAXValueSourceTypeAttribute AccessibilityAXValueSourceType = "attribute" // AccessibilityAXValueSourceTypeImplicit enum const. AccessibilityAXValueSourceTypeImplicit AccessibilityAXValueSourceType = "implicit" // AccessibilityAXValueSourceTypeStyle enum const. AccessibilityAXValueSourceTypeStyle AccessibilityAXValueSourceType = "style" // AccessibilityAXValueSourceTypeContents enum const. AccessibilityAXValueSourceTypeContents AccessibilityAXValueSourceType = "contents" // AccessibilityAXValueSourceTypePlaceholder enum const. AccessibilityAXValueSourceTypePlaceholder AccessibilityAXValueSourceType = "placeholder" // AccessibilityAXValueSourceTypeRelatedElement enum const. AccessibilityAXValueSourceTypeRelatedElement AccessibilityAXValueSourceType = "relatedElement" ) // AccessibilityAXValueNativeSourceType Enum of possible native property sources (as a subtype of a particular AXValueSourceType). type AccessibilityAXValueNativeSourceType string const ( // AccessibilityAXValueNativeSourceTypeDescription enum const. AccessibilityAXValueNativeSourceTypeDescription AccessibilityAXValueNativeSourceType = "description" // AccessibilityAXValueNativeSourceTypeFigcaption enum const. AccessibilityAXValueNativeSourceTypeFigcaption AccessibilityAXValueNativeSourceType = "figcaption" // AccessibilityAXValueNativeSourceTypeLabel enum const. AccessibilityAXValueNativeSourceTypeLabel AccessibilityAXValueNativeSourceType = "label" // AccessibilityAXValueNativeSourceTypeLabelfor enum const. AccessibilityAXValueNativeSourceTypeLabelfor AccessibilityAXValueNativeSourceType = "labelfor" // AccessibilityAXValueNativeSourceTypeLabelwrapped enum const. AccessibilityAXValueNativeSourceTypeLabelwrapped AccessibilityAXValueNativeSourceType = "labelwrapped" // AccessibilityAXValueNativeSourceTypeLegend enum const. AccessibilityAXValueNativeSourceTypeLegend AccessibilityAXValueNativeSourceType = "legend" // AccessibilityAXValueNativeSourceTypeRubyannotation enum const. AccessibilityAXValueNativeSourceTypeRubyannotation AccessibilityAXValueNativeSourceType = "rubyannotation" // AccessibilityAXValueNativeSourceTypeTablecaption enum const. AccessibilityAXValueNativeSourceTypeTablecaption AccessibilityAXValueNativeSourceType = "tablecaption" // AccessibilityAXValueNativeSourceTypeTitle enum const. AccessibilityAXValueNativeSourceTypeTitle AccessibilityAXValueNativeSourceType = "title" // AccessibilityAXValueNativeSourceTypeOther enum const. AccessibilityAXValueNativeSourceTypeOther AccessibilityAXValueNativeSourceType = "other" ) // AccessibilityAXValueSource A single source for a computed AX property. type AccessibilityAXValueSource struct { // Type What type of source this is. Type AccessibilityAXValueSourceType `json:"type"` // Value (optional) The value of this property source. Value *AccessibilityAXValue `json:"value,omitempty"` // Attribute (optional) The name of the relevant attribute, if any. Attribute string `json:"attribute,omitempty"` // AttributeValue (optional) The value of the relevant attribute, if any. AttributeValue *AccessibilityAXValue `json:"attributeValue,omitempty"` // Superseded (optional) Whether this source is superseded by a higher priority source. Superseded bool `json:"superseded,omitempty"` // NativeSource (optional) The native markup source for this value, e.g. a `