├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md └── workflows │ ├── build.yml │ ├── docs.yml │ └── verify_type_generation.yml ├── .gitignore ├── .gitmodules ├── .golangci.yaml ├── .nojekyll ├── 404.html ├── CONTRIBUTING.md ├── Dockerfile.example ├── LICENSE ├── README.md ├── _config.yml ├── apiresponse_assertions.go ├── artifact.go ├── assertions.go ├── binding_call.go ├── browser.go ├── browser_context.go ├── browser_type.go ├── cdp_session.go ├── channel.go ├── channel_owner.go ├── clock.go ├── cmd └── playwright │ └── main.go ├── connection.go ├── console_message.go ├── dialog.go ├── docs ├── .gitignore ├── Gemfile └── Gemfile.lock ├── download.go ├── element_handle.go ├── errors.go ├── event_emitter.go ├── event_emitter_test.go ├── examples ├── download │ └── main.go ├── end-to-end-testing │ └── main.go ├── javascript │ └── main.go ├── mobile-and-geolocation │ └── main.go ├── network-monitoring │ └── main.go ├── parallel-scraping │ └── main.go ├── pdf │ └── main.go ├── scraping │ └── main.go ├── screenshot │ └── main.go ├── use-local-chrome │ └── main.go └── video │ └── main.go ├── fetch.go ├── file_chooser.go ├── frame.go ├── frame_locator.go ├── generated-enums.go ├── generated-interfaces.go ├── generated-structs.go ├── glob.go ├── glob_test.go ├── go.mod ├── go.sum ├── har_router.go ├── helpers.go ├── helpers_test.go ├── input.go ├── input_files_helper.go ├── internal └── safe │ └── map.go ├── js_handle.go ├── jsonPipe.go ├── local_utils.go ├── locator.go ├── locator_assertions.go ├── locator_helpers.go ├── network.go ├── objectFactory.go ├── page.go ├── page_assertions.go ├── patches └── main.patch ├── playwright.go ├── request.go ├── response.go ├── route.go ├── run.go ├── run_test.go ├── run_unix.go ├── run_win.go ├── scripts ├── apply-patch.sh ├── generate-api.sh ├── install-browsers │ └── main.go ├── update-patch.sh └── update-readme-versions │ └── main.go ├── selectors.go ├── stream.go ├── tests ├── apiresponse_assertions_test.go ├── assets │ ├── background-color.html │ ├── beforeunload.html │ ├── button-overlay-oopif.html │ ├── cached │ │ ├── one-style.css │ │ └── one-style.html │ ├── callback.js │ ├── checkerboard.html │ ├── chromium-linux.zip │ ├── client-certificates │ │ ├── README.md │ │ ├── client │ │ │ ├── self-signed │ │ │ │ ├── cert.pem │ │ │ │ ├── csr.pem │ │ │ │ └── key.pem │ │ │ └── trusted │ │ │ │ ├── cert-legacy.pfx │ │ │ │ ├── cert.pem │ │ │ │ ├── cert.pfx │ │ │ │ ├── csr.pem │ │ │ │ └── key.pem │ │ └── server │ │ │ ├── server_cert.pem │ │ │ └── server_key.pem │ ├── consolelog.html │ ├── csp.html │ ├── css-transition.html │ ├── csscoverage │ │ ├── Dosis-Regular.ttf │ │ ├── OFL.txt │ │ ├── involved.html │ │ ├── media.html │ │ ├── multiple.html │ │ ├── simple.html │ │ ├── sourceurl.html │ │ ├── stylesheet1.css │ │ ├── stylesheet2.css │ │ └── unused.html │ ├── deep-shadow.html │ ├── detect-touch.html │ ├── digits │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ ├── dom.html │ ├── download-blob.html │ ├── drag-n-drop.html │ ├── dummy_bad_browser_executable.js │ ├── dynamic-oopif.html │ ├── empty-standard-mode.html │ ├── empty.html │ ├── empty.pdf │ ├── error.html │ ├── es6 │ │ ├── .eslintrc │ │ ├── es6import.js │ │ ├── es6module.js │ │ └── es6pathimport.js │ ├── example.mp3 │ ├── file-to-upload-2.txt │ ├── file-to-upload.txt │ ├── formatted-number.html │ ├── frames │ │ ├── child-redirect.html │ │ ├── frame.html │ │ ├── frameset.html │ │ ├── lazy-frame.html │ │ ├── nested-frames.html │ │ ├── one-frame.html │ │ ├── redirect-my-parent.html │ │ ├── script.js │ │ ├── style.css │ │ └── two-frames.html │ ├── geolocation.html │ ├── global-var.html │ ├── grid-iframe-in-shadow.html │ ├── grid.html │ ├── har-fulfill.har │ ├── har-redirect.har │ ├── har-sha1-main-response.txt │ ├── har-sha1.har │ ├── har.html │ ├── headings.html │ ├── highdpi.html │ ├── historyapi.html │ ├── injectedfile.js │ ├── injectedstyle.css │ ├── input │ │ ├── animating-button.html │ │ ├── button.html │ │ ├── checkbox.html │ │ ├── drag-n-drop-manual.html │ │ ├── fileupload.html │ │ ├── folderupload.html │ │ ├── handle-locator.html │ │ ├── keyboard.html │ │ ├── mouse-helper.js │ │ ├── rotatedButton.html │ │ ├── scrollable.html │ │ ├── scrollable2.html │ │ ├── select.html │ │ ├── textarea.html │ │ └── touches.html │ ├── jscoverage │ │ ├── eval.html │ │ ├── involved.html │ │ ├── multiple.html │ │ ├── ranges.html │ │ ├── script1.js │ │ ├── script2.js │ │ ├── simple.html │ │ ├── sourceurl.html │ │ └── unused.html │ ├── load-event │ │ ├── load-event.html │ │ └── module.js │ ├── media-query-prefers-color-scheme.svg │ ├── mobile.html │ ├── modernizr.html │ ├── modernizr.js │ ├── modernizr │ │ ├── mobile-safari-14-1.json │ │ └── safari-14-1.json │ ├── movie.mp4 │ ├── movie.ogv │ ├── mui.html │ ├── networkidle-frame.html │ ├── networkidle.html │ ├── networkidle.js │ ├── offscreenbuttons.html │ ├── one-style.css │ ├── one-style.html │ ├── overflow-large.html │ ├── overflow.html │ ├── player.html │ ├── playground.html │ ├── popup │ │ ├── popup.html │ │ └── window-open.html │ ├── pptr.png │ ├── redirectloop1.html │ ├── redirectloop2.html │ ├── resetcss.html │ ├── rotate-pseudo.html │ ├── rotate-z-shadow-dom.html │ ├── rotate-z.html │ ├── screenshots │ │ ├── canvas.html │ │ ├── controls.html │ │ ├── translateZ.html │ │ └── webgl.html │ ├── sectionselectorengine.js │ ├── selenium-grid │ │ ├── broken-selenium-driver.js │ │ └── selenium-config-standalone.json │ ├── self-request.html │ ├── serviceworkers │ │ ├── empty │ │ │ ├── sw.html │ │ │ └── sw.js │ │ ├── fetch │ │ │ ├── style.css │ │ │ ├── sw.html │ │ │ └── sw.js │ │ └── fetchdummy │ │ │ ├── sw.html │ │ │ └── sw.js │ ├── shadow-dom-link.html │ ├── shadow.html │ ├── simple-extension │ │ ├── content-script.js │ │ ├── index.js │ │ └── manifest.json │ ├── simple.json │ ├── simplezip.json │ ├── tamperable.html │ ├── title.html │ ├── video.html │ ├── video_mp4.html │ ├── wasm │ │ ├── table2.html │ │ └── table2.wasm │ ├── web-animation.html │ ├── webfont │ │ ├── README.md │ │ ├── iconfont.svg │ │ ├── iconfont.woff2 │ │ └── webfont.html │ ├── window-stop.html │ ├── worker │ │ ├── import-me.js │ │ ├── worker-http-import.html │ │ ├── worker-http-import.js │ │ ├── worker.html │ │ └── worker.js │ └── wrappedlink.html ├── binding_test.go ├── browser_context_client_certificates_test.go ├── browser_context_storage_state_test.go ├── browser_context_test.go ├── browser_test.go ├── browser_type_test.go ├── cdp_session_test.go ├── console_message_test.go ├── dialog_test.go ├── download_test.go ├── element_handle_test.go ├── fetch_test.go ├── file_chooser_test.go ├── frame_locator_test.go ├── frame_test.go ├── har_test.go ├── helper_test.go ├── input_test.go ├── js_handle_test.go ├── locator_assertions_test.go ├── locator_get_by_test.go ├── locator_test.go ├── network_test.go ├── page_add_locator_handler_test.go ├── page_aria_snapshot_test.go ├── page_assertions_test.go ├── page_clock_test.go ├── page_request_gc_test.go ├── page_test.go ├── remote_server_test.go ├── route_test.go ├── route_web_socket_test.go ├── screenshot-snapshots │ ├── mask-should-work-chromium.png │ ├── mask-should-work-firefox.png │ ├── mask-should-work-webkit.png │ ├── mask-should-work-with-elementhandle-chromium.png │ ├── mask-should-work-with-elementhandle-firefox.png │ ├── mask-should-work-with-elementhandle-webkit.png │ ├── mask-should-work-with-locator-chromium.png │ ├── mask-should-work-with-locator-firefox.png │ ├── mask-should-work-with-locator-webkit.png │ ├── screenshot-element-bounding-box-chromium.png │ ├── screenshot-element-bounding-box-firefox.png │ └── screenshot-element-bounding-box-webkit.png ├── screenshot_test.go ├── selectors_get_by_test.go ├── selectors_test.go ├── selectors_text_test.go ├── tracing_test.go ├── unroute_behavior_test.go ├── utils_test.go ├── video_test.go ├── websocket_test.go └── worker_test.go ├── tracing.go ├── transport.go ├── type_helpers.go ├── video.go ├── waiter.go ├── waiter_test.go ├── web_error.go ├── websocket.go ├── websocket_route.go ├── worker.go └── writable_stream.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # text files must be lf for golden file tests to work 2 | * text=auto eol=lf 3 | 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: mxschmitt 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug report \U0001FAB2" 3 | about: Create a bug report to help us improve 4 | title: "[Bug]: " 5 | labels: p2-bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 17 | **Environments** 18 | 19 | - playwright-go Version: [e.g. v0.4201.1] 20 | - Browser: [e.g. firefox] 21 | - OS and version: [e.g. macOS / Windows 11/ Ubuntu 22.04] 22 | 23 | **Bug description** 24 | 26 | 27 | **To Reproduce** 28 | Please provide a mini reproduction rather than just a description. For example: 29 | 30 | ```go 31 | package main 32 | 33 | import "github.com/playwright-community/playwright-go" 34 | 35 | func main() { 36 | // ignore unnecessary error handling code 37 | pw, _ := playwright.Run() 38 | browser, _ := pw.Chromium.Launch() 39 | context, _ := browser.NewContext() 40 | page, _ := context.NewPage() 41 | 42 | _, _ = page.Goto("https://playwright.dev") 43 | 44 | _, err := page.PDF(playwright.PagePdfOptions{ 45 | Path: playwright.String("playwright-example.pdf"), 46 | }) 47 | // should no error 48 | if err != nil { 49 | panic(err) 50 | } 51 | } 52 | 53 | ``` 54 | 55 | **Additional context** 56 | Add any other context about the problem here. 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature request \U0001F680" 3 | about: Suggest an idea for this project 4 | title: "[Feature]: " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Does the upstream have similar features?** 11 | `playwright-go` is just a community-driven client for [playwright](https://playwright.dev). For feature requests, please refer to other officially supported clients first. [Nodejs](https://github.com/microsoft/playwright) / [Java](https://github.com/microsoft/playwright-java) / [.Net](https://github.com/microsoft/playwright-dotnet) / [Python](https://github.com/microsoft/playwright-python) 12 | 13 | **Is your feature request related to a problem? Please describe.** 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | **Describe the solution you'd like** 17 | A clear and concise description of what you want to happen. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: 3 | push: 4 | branches: [ main ] 5 | jobs: 6 | deploy: 7 | name: Deploy docs 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: ruby/setup-ruby@v1 12 | with: 13 | ruby-version: '3.3' 14 | - name: Install Ruby dependencies 15 | run: bundler install 16 | working-directory: docs 17 | - name: Build site 18 | run: jekyll build 19 | - name: Deploy 20 | uses: peaceiris/actions-gh-pages@v4 21 | with: 22 | github_token: ${{ secrets.GITHUB_TOKEN }} 23 | publish_dir: ./_site -------------------------------------------------------------------------------- /.github/workflows/verify_type_generation.yml: -------------------------------------------------------------------------------- 1 | name: Verify Types 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | jobs: 8 | verify: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | submodules: true 14 | - name: Set up Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: oldstable 18 | - name: Install gofumpt 19 | run: go install mvdan.cc/gofumpt@latest 20 | - name: Install Browsers 21 | run: | 22 | go install ./... 23 | playwright install --with-deps 24 | - name: Regenerate APIs 25 | run: | 26 | git config --global user.email "no-reply@github.com" 27 | git config --global user.name "Github Actions" 28 | go generate 29 | - name: Verify API is up to date 30 | run: git diff --exit-code --ignore-submodules --ignore-cr-at-eol --ignore-space-at-eol -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/go 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=go 3 | 4 | ### Go ### 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | ### Go Patch ### 22 | /vendor/ 23 | /Godeps/ 24 | 25 | # End of https://www.toptal.com/developers/gitignore/api/go 26 | covprofile 27 | .idea/ 28 | .DS_Store 29 | 30 | api.json 31 | _site/ 32 | .jekyll-cache/ 33 | 34 | .vscode/settings.json -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "playwright"] 2 | path = playwright 3 | url = https://github.com/microsoft/playwright 4 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | linters: 3 | enable-all: false 4 | disable-all: false 5 | enable: 6 | - gofumpt -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/.nojekyll -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /404.html 3 | layout: default 4 | --- 5 | 6 | 19 | 20 |
21 |

404

22 | 23 |

Page not found :(

24 |

The requested page could not be found.

25 |
26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Code style 4 | The Go code is linted with [golangci-lint](https://golangci-lint.run/) and formatted with [gofumpt](https://github.com/mvdan/gofumpt). Please configure your editor to run the tools while developing and make sure to run the tools before committing any code. 5 | 6 | ## Tests 7 | 8 | ### Test coverage 9 | 10 | For every Pull Request on GitHub and on the main branch the coverage data will get sent over to [Coveralls](https://coveralls.io/github/playwright-community/playwright-go), this is helpful for finding functions that aren't covered by tests. 11 | 12 | ### Running tests 13 | 14 | You can use the `BROWSER` environment variable to use a different browser than Chromium for the tests and use the `HEADLESS` environment variable which is useful for debugging. 15 | 16 | ``` 17 | BROWSER=chromium HEADLESS=1 go test -v --race ./... 18 | ``` 19 | 20 | ### Roll 21 | 22 | 1. Find out to which upstream version you want to roll, and change the value of `playwrightCliVersion` in the **run.go** to the new version. 23 | 1. Download current version of Playwright driver `go run scripts/install-browsers/main.go` 24 | 1. Apply patch `bash scripts/apply-patch.sh` 25 | 1. Fix merge conflicts if any, otherwise ignore this step. Once you are happy you can commit the changes `cd playwright; git commit -am "apply patch" && cd ..` 26 | 1. Regenerate a new patch `bash scripts/update-patch.sh` 27 | 1. Generate go code `go generate ./...` 28 | 29 | To adapt to the new version of Playwright's protocol and feature updates, you may need to modify the patch. Refer to the following steps: 30 | 31 | 1. Apply patch `bash scripts/apply-patch.sh` 32 | 1. `cd playwright` 33 | 1. Revert the patch`git reset HEAD~1` 34 | 1. Modify the files under `docs/src/api`, etc. as needed. Available references: 35 | - Protocol `packages/protocol/src/protocol.yml` 36 | - [Playwright python](https://github.com/microsoft/playwright-python) 37 | 1. Commit the changes `git commit -am "apply patch"` 38 | 1. Regenerate a new patch `bash scripts/update-patch.sh` 39 | 1. Generate go code `go generate ./...`. 40 | -------------------------------------------------------------------------------- /Dockerfile.example: -------------------------------------------------------------------------------- 1 | # Stage 1: Modules caching 2 | FROM golang:1.22 as modules 3 | COPY go.mod go.sum /modules/ 4 | WORKDIR /modules 5 | RUN go mod download 6 | 7 | # Stage 2: Build 8 | FROM golang:1.22 as builder 9 | COPY --from=modules /go/pkg /go/pkg 10 | COPY . /workdir 11 | WORKDIR /workdir 12 | # Install playwright cli with right version for later use 13 | RUN PWGO_VER=$(grep -oE "playwright-go v\S+" /workdir/go.mod | sed 's/playwright-go //g') \ 14 | && go install github.com/playwright-community/playwright-go/cmd/playwright@${PWGO_VER} 15 | # Build your app 16 | RUN GOOS=linux GOARCH=amd64 go build -o /bin/myapp 17 | 18 | # Stage 3: Final 19 | FROM ubuntu:noble 20 | COPY --from=builder /go/bin/playwright /bin/myapp / 21 | RUN apt-get update && apt-get install -y ca-certificates tzdata \ 22 | # Install dependencies and all browsers (or specify one) 23 | && /playwright install --with-deps \ 24 | && rm -rf /var/lib/apt/lists/* 25 | CMD ["/myapp"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Max Schmitt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: Playwright for Go 2 | email: max@schmitt.mx 3 | description: >- # this means to ignore newlines until "baseurl:" 4 | Playwright is a Node.js library to automate Chromium, Firefox and WebKit with a single API. 5 | Playwright is built to enable cross-browser web automation that is ever-green, capable, reliable and fast. 6 | baseurl: "/playwright-go" 7 | url: "https://playwright-community.github.io/" 8 | twitter_username: maxibanki 9 | github_username: playwright-community 10 | remote_theme: pages-themes/cayman@v0.2.0 11 | plugins: 12 | - jekyll-remote-theme 13 | - jekyll-optional-front-matter 14 | - jekyll-readme-index 15 | exclude: 16 | - playwright/ 17 | defaults: 18 | - scope: 19 | path: "" # an empty string here means all files in the project 20 | values: 21 | layout: "default" 22 | -------------------------------------------------------------------------------- /apiresponse_assertions.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | type apiResponseAssertionsImpl struct { 11 | actual APIResponse 12 | isNot bool 13 | } 14 | 15 | func newAPIResponseAssertions(actual APIResponse, isNot bool) *apiResponseAssertionsImpl { 16 | return &apiResponseAssertionsImpl{ 17 | actual: actual, 18 | isNot: isNot, 19 | } 20 | } 21 | 22 | func (ar *apiResponseAssertionsImpl) Not() APIResponseAssertions { 23 | return newAPIResponseAssertions(ar.actual, true) 24 | } 25 | 26 | func (ar *apiResponseAssertionsImpl) ToBeOK() error { 27 | if ar.isNot != ar.actual.Ok() { 28 | return nil 29 | } 30 | message := fmt.Sprintf(`Response status expected to be within [200..299] range, was %v`, ar.actual.Status()) 31 | if ar.isNot { 32 | message = strings.ReplaceAll(message, "expected to", "expected not to") 33 | } 34 | logList, err := ar.actual.(*apiResponseImpl).fetchLog() 35 | if err != nil { 36 | return err 37 | } 38 | log := strings.Join(logList, "\n") 39 | if log != "" { 40 | message += "\nCall log:\n" + log 41 | } 42 | 43 | isTextEncoding := false 44 | contentType, ok := ar.actual.Headers()["content-type"] 45 | if ok { 46 | isTextEncoding = isTexualMimeType(contentType) 47 | } 48 | if isTextEncoding { 49 | text, err := ar.actual.Text() 50 | if err == nil { 51 | message += fmt.Sprintf(`\n Response Text:\n %s`, subString(text, 0, 1000)) 52 | } 53 | } 54 | return errors.New(message) 55 | } 56 | 57 | func isTexualMimeType(mimeType string) bool { 58 | re := regexp.MustCompile(`^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$`) 59 | return re.MatchString(mimeType) 60 | } 61 | 62 | func subString(s string, start, length int) string { 63 | if start < 0 { 64 | start = 0 65 | } 66 | if length < 0 { 67 | length = 0 68 | } 69 | rs := []rune(s) 70 | end := start + length 71 | if end > len(rs) { 72 | end = len(rs) 73 | } 74 | return string(rs[start:end]) 75 | } 76 | -------------------------------------------------------------------------------- /artifact.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | type artifactImpl struct { 9 | channelOwner 10 | } 11 | 12 | func (a *artifactImpl) AbsolutePath() string { 13 | return a.initializer["absolutePath"].(string) 14 | } 15 | 16 | func (a *artifactImpl) PathAfterFinished() (string, error) { 17 | if a.connection.isRemote { 18 | return "", errors.New("Path is not available when connecting remotely. Use SaveAs() to save a local copy") 19 | } 20 | path, err := a.channel.Send("pathAfterFinished") 21 | return path.(string), err 22 | } 23 | 24 | func (a *artifactImpl) SaveAs(path string) error { 25 | if !a.connection.isRemote { 26 | _, err := a.channel.Send("saveAs", map[string]interface{}{ 27 | "path": path, 28 | }) 29 | return err 30 | } 31 | streamChannel, err := a.channel.Send("saveAsStream") 32 | if err != nil { 33 | return err 34 | } 35 | stream := fromChannel(streamChannel).(*streamImpl) 36 | return stream.SaveAs(path) 37 | } 38 | 39 | func (a *artifactImpl) Failure() error { 40 | reason, err := a.channel.Send("failure") 41 | if reason == nil { 42 | return err 43 | } 44 | return fmt.Errorf("%w: %v", ErrPlaywright, reason) 45 | } 46 | 47 | func (a *artifactImpl) Delete() error { 48 | _, err := a.channel.Send("delete") 49 | return err 50 | } 51 | 52 | func (a *artifactImpl) Cancel() error { 53 | _, err := a.channel.Send("cancel") 54 | return err 55 | } 56 | 57 | func (a *artifactImpl) ReadIntoBuffer() ([]byte, error) { 58 | streamChannel, err := a.channel.Send("stream") 59 | if err != nil { 60 | return nil, err 61 | } 62 | stream := fromChannel(streamChannel) 63 | return stream.(*streamImpl).ReadAll() 64 | } 65 | 66 | func newArtifact(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *artifactImpl { 67 | artifact := &artifactImpl{} 68 | artifact.createChannelOwner(artifact, parent, objectType, guid, initializer) 69 | return artifact 70 | } 71 | -------------------------------------------------------------------------------- /binding_call.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/go-stack/stack" 8 | ) 9 | 10 | type BindingCall interface { 11 | Call(f BindingCallFunction) 12 | } 13 | 14 | type bindingCallImpl struct { 15 | channelOwner 16 | } 17 | 18 | // BindingSource is the value passed to a binding call execution 19 | type BindingSource struct { 20 | Context BrowserContext 21 | Page Page 22 | Frame Frame 23 | } 24 | 25 | // ExposedFunction represents the func signature of an exposed function 26 | type ExposedFunction = func(args ...interface{}) interface{} 27 | 28 | // BindingCallFunction represents the func signature of an exposed binding call func 29 | type BindingCallFunction func(source *BindingSource, args ...interface{}) interface{} 30 | 31 | func (b *bindingCallImpl) Call(f BindingCallFunction) { 32 | defer func() { 33 | if r := recover(); r != nil { 34 | if _, err := b.channel.Send("reject", map[string]interface{}{ 35 | "error": serializeError(r.(error)), 36 | }); err != nil { 37 | logger.Error("could not reject BindingCall", "error", err) 38 | } 39 | } 40 | }() 41 | 42 | frame := fromChannel(b.initializer["frame"]).(*frameImpl) 43 | source := &BindingSource{ 44 | Context: frame.Page().Context(), 45 | Page: frame.Page(), 46 | Frame: frame, 47 | } 48 | var result interface{} 49 | if handle, ok := b.initializer["handle"]; ok { 50 | result = f(source, fromChannel(handle)) 51 | } else { 52 | initializerArgs := b.initializer["args"].([]interface{}) 53 | funcArgs := []interface{}{} 54 | for i := 0; i < len(initializerArgs); i++ { 55 | funcArgs = append(funcArgs, parseResult(initializerArgs[i])) 56 | } 57 | result = f(source, funcArgs...) 58 | } 59 | _, err := b.channel.Send("resolve", map[string]interface{}{ 60 | "result": serializeArgument(result), 61 | }) 62 | if err != nil { 63 | logger.Error("could not resolve BindingCall", "error", err) 64 | } 65 | } 66 | 67 | func serializeError(err error) map[string]interface{} { 68 | st := stack.Trace().TrimRuntime() 69 | if len(st) == 0 { // https://github.com/go-stack/stack/issues/27 70 | st = stack.Trace() 71 | } 72 | return map[string]interface{}{ 73 | "error": &Error{ 74 | Name: "Playwright for Go Error", 75 | Message: err.Error(), 76 | Stack: strings.ReplaceAll(strings.TrimFunc(fmt.Sprintf("%+v", st), func(r rune) bool { 77 | return r == '[' || r == ']' 78 | }), " ", "\n"), 79 | }, 80 | } 81 | } 82 | 83 | func newBindingCall(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *bindingCallImpl { 84 | bt := &bindingCallImpl{} 85 | bt.createChannelOwner(bt, parent, objectType, guid, initializer) 86 | return bt 87 | } 88 | -------------------------------------------------------------------------------- /cdp_session.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | type cdpSessionImpl struct { 4 | channelOwner 5 | } 6 | 7 | func (c *cdpSessionImpl) Detach() error { 8 | _, err := c.channel.Send("detach") 9 | return err 10 | } 11 | 12 | func (c *cdpSessionImpl) Send(method string, params map[string]interface{}) (interface{}, error) { 13 | result, err := c.channel.Send("send", map[string]interface{}{ 14 | "method": method, 15 | "params": params, 16 | }) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | return result, err 22 | } 23 | 24 | func (c *cdpSessionImpl) onEvent(params map[string]interface{}) { 25 | c.Emit(params["method"].(string), params["params"]) 26 | } 27 | 28 | func newCDPSession(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *cdpSessionImpl { 29 | bt := &cdpSessionImpl{} 30 | 31 | bt.createChannelOwner(bt, parent, objectType, guid, initializer) 32 | 33 | bt.channel.On("event", func(params map[string]interface{}) { 34 | bt.onEvent(params) 35 | }) 36 | 37 | return bt 38 | } 39 | -------------------------------------------------------------------------------- /channel.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type channel struct { 9 | eventEmitter 10 | guid string 11 | connection *connection 12 | owner *channelOwner // to avoid type conversion 13 | object interface{} // retain type info (for fromChannel needed) 14 | } 15 | 16 | func (c *channel) MarshalJSON() ([]byte, error) { 17 | return json.Marshal(map[string]string{ 18 | "guid": c.guid, 19 | }) 20 | } 21 | 22 | // for catch errors of route handlers etc. 23 | func (c *channel) CreateTask(fn func()) { 24 | go func() { 25 | defer func() { 26 | if e := recover(); e != nil { 27 | err, ok := e.(error) 28 | if ok { 29 | c.connection.err.Set(err) 30 | } else { 31 | c.connection.err.Set(fmt.Errorf("%v", e)) 32 | } 33 | } 34 | }() 35 | fn() 36 | }() 37 | } 38 | 39 | func (c *channel) Send(method string, options ...interface{}) (interface{}, error) { 40 | return c.connection.WrapAPICall(func() (interface{}, error) { 41 | return c.innerSend(method, options...).GetResultValue() 42 | }, c.owner.isInternalType) 43 | } 44 | 45 | func (c *channel) SendReturnAsDict(method string, options ...interface{}) (map[string]interface{}, error) { 46 | ret, err := c.connection.WrapAPICall(func() (interface{}, error) { 47 | return c.innerSend(method, options...).GetResult() 48 | }, c.owner.isInternalType) 49 | return ret.(map[string]interface{}), err 50 | } 51 | 52 | func (c *channel) innerSend(method string, options ...interface{}) *protocolCallback { 53 | if err := c.connection.err.Get(); err != nil { 54 | c.connection.err.Set(nil) 55 | pc := newProtocolCallback(false, c.connection.abort) 56 | pc.SetError(err) 57 | return pc 58 | } 59 | params := transformOptions(options...) 60 | return c.connection.sendMessageToServer(c.owner, method, params, false) 61 | } 62 | 63 | // SendNoReply ignores return value and errors 64 | // almost equivalent to `send(...).catch(() => {})` 65 | func (c *channel) SendNoReply(method string, options ...interface{}) { 66 | c.innerSendNoReply(method, c.owner.isInternalType, options...) 67 | } 68 | 69 | func (c *channel) SendNoReplyInternal(method string, options ...interface{}) { 70 | c.innerSendNoReply(method, true, options...) 71 | } 72 | 73 | func (c *channel) innerSendNoReply(method string, isInternal bool, options ...interface{}) { 74 | params := transformOptions(options...) 75 | _, err := c.connection.WrapAPICall(func() (interface{}, error) { 76 | return c.connection.sendMessageToServer(c.owner, method, params, true).GetResult() 77 | }, isInternal) 78 | if err != nil { 79 | // ignore error actively, log only for debug 80 | logger.Error("SendNoReply failed", "error", err) 81 | } 82 | } 83 | 84 | func newChannel(owner *channelOwner, object interface{}) *channel { 85 | channel := &channel{ 86 | connection: owner.connection, 87 | guid: owner.guid, 88 | owner: owner, 89 | object: object, 90 | } 91 | return channel 92 | } 93 | -------------------------------------------------------------------------------- /clock.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | type clockImpl struct { 9 | browserCtx *browserContextImpl 10 | } 11 | 12 | func newClock(bCtx *browserContextImpl) Clock { 13 | return &clockImpl{ 14 | browserCtx: bCtx, 15 | } 16 | } 17 | 18 | func (c *clockImpl) FastForward(ticks interface{}) error { 19 | params, err := parseTicks(ticks) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | _, err = c.browserCtx.channel.Send("clockFastForward", params) 25 | return err 26 | } 27 | 28 | func (c *clockImpl) Install(options ...ClockInstallOptions) (err error) { 29 | params := map[string]any{} 30 | if len(options) == 1 { 31 | if options[0].Time != nil { 32 | params, err = parseTime(options[0].Time) 33 | if err != nil { 34 | return err 35 | } 36 | } 37 | } 38 | 39 | _, err = c.browserCtx.channel.Send("clockInstall", params) 40 | 41 | return err 42 | } 43 | 44 | func (c *clockImpl) PauseAt(time interface{}) error { 45 | params, err := parseTime(time) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | _, err = c.browserCtx.channel.Send("clockPauseAt", params) 51 | return err 52 | } 53 | 54 | func (c *clockImpl) Resume() error { 55 | _, err := c.browserCtx.channel.Send("clockResume") 56 | return err 57 | } 58 | 59 | func (c *clockImpl) RunFor(ticks interface{}) error { 60 | params, err := parseTicks(ticks) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | _, err = c.browserCtx.channel.Send("clockRunFor", params) 66 | return err 67 | } 68 | 69 | func (c *clockImpl) SetFixedTime(time interface{}) error { 70 | params, err := parseTime(time) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | _, err = c.browserCtx.channel.Send("clockSetFixedTime", params) 76 | return err 77 | } 78 | 79 | func (c *clockImpl) SetSystemTime(time interface{}) error { 80 | params, err := parseTime(time) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | _, err = c.browserCtx.channel.Send("clockSetSystemTime", params) 86 | return err 87 | } 88 | 89 | func parseTime(t interface{}) (map[string]any, error) { 90 | switch v := t.(type) { 91 | case int, int64: 92 | return map[string]any{"timeNumber": v}, nil 93 | case string: 94 | return map[string]any{"timeString": v}, nil 95 | case time.Time: 96 | return map[string]any{"timeNumber": v.UnixMilli()}, nil 97 | default: 98 | return nil, errors.New("time should be one of: int, int64, string, time.Time") 99 | } 100 | } 101 | 102 | func parseTicks(ticks interface{}) (map[string]any, error) { 103 | switch v := ticks.(type) { 104 | case int, int64: 105 | return map[string]any{"ticksNumber": v}, nil 106 | case string: 107 | return map[string]any{"ticksString": v}, nil 108 | default: 109 | return nil, errors.New("ticks should be one of: int, int64, string") 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /cmd/playwright/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/playwright-community/playwright-go" 8 | ) 9 | 10 | func main() { 11 | driver, err := playwright.NewDriver(&playwright.RunOptions{}) 12 | if err != nil { 13 | log.Fatalf("could not start driver: %v", err) 14 | } 15 | if err = driver.DownloadDriver(); err != nil { 16 | log.Fatalf("could not download driver: %v", err) 17 | } 18 | cmd := driver.Command(os.Args[1:]...) 19 | cmd.Stdout = os.Stdout 20 | cmd.Stderr = os.Stderr 21 | if err := cmd.Run(); err != nil { 22 | log.Fatalf("could not run driver: %v", err) 23 | } 24 | os.Exit(cmd.ProcessState.ExitCode()) 25 | } 26 | -------------------------------------------------------------------------------- /console_message.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | type consoleMessageImpl struct { 4 | event map[string]interface{} 5 | page Page 6 | } 7 | 8 | func (c *consoleMessageImpl) Type() string { 9 | return c.event["type"].(string) 10 | } 11 | 12 | func (c *consoleMessageImpl) Text() string { 13 | return c.event["text"].(string) 14 | } 15 | 16 | func (c *consoleMessageImpl) String() string { 17 | return c.Text() 18 | } 19 | 20 | func (c *consoleMessageImpl) Args() []JSHandle { 21 | args := c.event["args"].([]interface{}) 22 | out := []JSHandle{} 23 | for idx := range args { 24 | out = append(out, fromChannel(args[idx]).(*jsHandleImpl)) 25 | } 26 | return out 27 | } 28 | 29 | func (c *consoleMessageImpl) Location() *ConsoleMessageLocation { 30 | location := &ConsoleMessageLocation{} 31 | remapMapToStruct(c.event["location"], location) 32 | return location 33 | } 34 | 35 | func (c *consoleMessageImpl) Page() Page { 36 | return c.page 37 | } 38 | 39 | func newConsoleMessage(event map[string]interface{}) *consoleMessageImpl { 40 | bt := &consoleMessageImpl{} 41 | bt.event = event 42 | page := fromNullableChannel(event["page"]) 43 | if page != nil { 44 | bt.page = page.(*pageImpl) 45 | } 46 | return bt 47 | } 48 | -------------------------------------------------------------------------------- /dialog.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | type dialogImpl struct { 4 | channelOwner 5 | page Page 6 | } 7 | 8 | func (d *dialogImpl) Type() string { 9 | return d.initializer["type"].(string) 10 | } 11 | 12 | func (d *dialogImpl) Message() string { 13 | return d.initializer["message"].(string) 14 | } 15 | 16 | func (d *dialogImpl) DefaultValue() string { 17 | return d.initializer["defaultValue"].(string) 18 | } 19 | 20 | func (d *dialogImpl) Accept(promptTextInput ...string) error { 21 | var promptText *string 22 | if len(promptTextInput) == 1 { 23 | promptText = &promptTextInput[0] 24 | } 25 | _, err := d.channel.Send("accept", map[string]interface{}{ 26 | "promptText": promptText, 27 | }) 28 | return err 29 | } 30 | 31 | func (d *dialogImpl) Dismiss() error { 32 | _, err := d.channel.Send("dismiss") 33 | return err 34 | } 35 | 36 | func (d *dialogImpl) Page() Page { 37 | return d.page 38 | } 39 | 40 | func newDialog(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *dialogImpl { 41 | bt := &dialogImpl{} 42 | bt.createChannelOwner(bt, parent, objectType, guid, initializer) 43 | page := fromNullableChannel(initializer["page"]) 44 | if page != nil { 45 | bt.page = page.(*pageImpl) 46 | } 47 | return bt 48 | } 49 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | .jekyll-cache/ 3 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "jekyll", "~> 4.3.0" 4 | gem "jekyll-remote-theme" 5 | group :jekyll_plugins do 6 | gem 'jekyll-optional-front-matter' 7 | gem "jekyll-readme-index" 8 | gem "jekyll-seo-tag" 9 | end -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.7) 5 | public_suffix (>= 2.0.2, < 7.0) 6 | bigdecimal (3.1.8) 7 | colorator (1.1.0) 8 | concurrent-ruby (1.3.4) 9 | em-websocket (0.5.3) 10 | eventmachine (>= 0.12.9) 11 | http_parser.rb (~> 0) 12 | eventmachine (1.2.7) 13 | ffi (1.17.0-arm64-darwin) 14 | ffi (1.17.0-x86_64-darwin) 15 | ffi (1.17.0-x86_64-linux-gnu) 16 | forwardable-extended (2.6.0) 17 | google-protobuf (4.28.3-arm64-darwin) 18 | bigdecimal 19 | rake (>= 13) 20 | google-protobuf (4.28.3-x86_64-darwin) 21 | bigdecimal 22 | rake (>= 13) 23 | google-protobuf (4.28.3-x86_64-linux) 24 | bigdecimal 25 | rake (>= 13) 26 | http_parser.rb (0.8.0) 27 | i18n (1.14.6) 28 | concurrent-ruby (~> 1.0) 29 | jekyll (4.3.4) 30 | addressable (~> 2.4) 31 | colorator (~> 1.0) 32 | em-websocket (~> 0.5) 33 | i18n (~> 1.0) 34 | jekyll-sass-converter (>= 2.0, < 4.0) 35 | jekyll-watch (~> 2.0) 36 | kramdown (~> 2.3, >= 2.3.1) 37 | kramdown-parser-gfm (~> 1.0) 38 | liquid (~> 4.0) 39 | mercenary (>= 0.3.6, < 0.5) 40 | pathutil (~> 0.9) 41 | rouge (>= 3.0, < 5.0) 42 | safe_yaml (~> 1.0) 43 | terminal-table (>= 1.8, < 4.0) 44 | webrick (~> 1.7) 45 | jekyll-optional-front-matter (0.3.2) 46 | jekyll (>= 3.0, < 5.0) 47 | jekyll-readme-index (0.3.0) 48 | jekyll (>= 3.0, < 5.0) 49 | jekyll-remote-theme (0.4.3) 50 | addressable (~> 2.0) 51 | jekyll (>= 3.5, < 5.0) 52 | jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) 53 | rubyzip (>= 1.3.0, < 3.0) 54 | jekyll-sass-converter (3.0.0) 55 | sass-embedded (~> 1.54) 56 | jekyll-seo-tag (2.8.0) 57 | jekyll (>= 3.8, < 5.0) 58 | jekyll-watch (2.2.1) 59 | listen (~> 3.0) 60 | kramdown (2.4.0) 61 | rexml 62 | kramdown-parser-gfm (1.1.0) 63 | kramdown (~> 2.0) 64 | liquid (4.0.4) 65 | listen (3.9.0) 66 | rb-fsevent (~> 0.10, >= 0.10.3) 67 | rb-inotify (~> 0.9, >= 0.9.10) 68 | mercenary (0.4.0) 69 | pathutil (0.16.2) 70 | forwardable-extended (~> 2.6) 71 | public_suffix (6.0.1) 72 | rake (13.2.1) 73 | rb-fsevent (0.11.2) 74 | rb-inotify (0.11.1) 75 | ffi (~> 1.0) 76 | rexml (3.3.9) 77 | rouge (4.4.0) 78 | rubyzip (2.3.2) 79 | safe_yaml (1.0.5) 80 | sass-embedded (1.80.4-arm64-darwin) 81 | google-protobuf (~> 4.28) 82 | sass-embedded (1.80.4-x86_64-darwin) 83 | google-protobuf (~> 4.28) 84 | sass-embedded (1.80.4-x86_64-linux-gnu) 85 | google-protobuf (~> 4.28) 86 | terminal-table (3.0.2) 87 | unicode-display_width (>= 1.1.1, < 3) 88 | unicode-display_width (2.6.0) 89 | webrick (1.8.2) 90 | 91 | PLATFORMS 92 | universal-darwin-20 93 | x86_64-darwin-24 94 | x86_64-linux 95 | 96 | DEPENDENCIES 97 | jekyll (~> 4.3.0) 98 | jekyll-optional-front-matter 99 | jekyll-readme-index 100 | jekyll-remote-theme 101 | jekyll-seo-tag 102 | 103 | BUNDLED WITH 104 | 2.5.16 105 | -------------------------------------------------------------------------------- /download.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | type downloadImpl struct { 4 | page *pageImpl 5 | url string 6 | suggestedFilename string 7 | artifact *artifactImpl 8 | } 9 | 10 | func (d *downloadImpl) String() string { 11 | return d.SuggestedFilename() 12 | } 13 | 14 | func (d *downloadImpl) Page() Page { 15 | return d.page 16 | } 17 | 18 | func (d *downloadImpl) URL() string { 19 | return d.url 20 | } 21 | 22 | func (d *downloadImpl) SuggestedFilename() string { 23 | return d.suggestedFilename 24 | } 25 | 26 | func (d *downloadImpl) Delete() error { 27 | err := d.artifact.Delete() 28 | return err 29 | } 30 | 31 | func (d *downloadImpl) Failure() error { 32 | return d.artifact.Failure() 33 | } 34 | 35 | func (d *downloadImpl) Path() (string, error) { 36 | path, err := d.artifact.PathAfterFinished() 37 | return path, err 38 | } 39 | 40 | func (d *downloadImpl) SaveAs(path string) error { 41 | err := d.artifact.SaveAs(path) 42 | return err 43 | } 44 | 45 | func (d *downloadImpl) Cancel() error { 46 | return d.artifact.Cancel() 47 | } 48 | 49 | func newDownload(page *pageImpl, url string, suggestedFilename string, artifact *artifactImpl) *downloadImpl { 50 | return &downloadImpl{ 51 | page: page, 52 | url: url, 53 | suggestedFilename: suggestedFilename, 54 | artifact: artifact, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | // ErrPlaywright wraps all Playwright errors. 10 | // - Use errors.Is to check if the error is a Playwright error. 11 | // - Use errors.As to cast an error to [Error] if you want to access "Stack". 12 | ErrPlaywright = errors.New("playwright") 13 | // ErrTargetClosed usually wraps a reason. 14 | ErrTargetClosed = errors.New("target closed") 15 | // ErrTimeout wraps timeout errors. It can be either Playwright TimeoutError or client timeout. 16 | ErrTimeout = errors.New("timeout") 17 | ) 18 | 19 | // Error represents a Playwright error 20 | type Error struct { 21 | Name string `json:"name"` 22 | Message string `json:"message"` 23 | Stack string `json:"stack"` 24 | } 25 | 26 | func (e *Error) Error() string { 27 | return e.Message 28 | } 29 | 30 | func (e *Error) Is(target error) bool { 31 | err, ok := target.(*Error) 32 | if !ok { 33 | return false 34 | } 35 | if err.Name != e.Name { 36 | return false 37 | } 38 | if e.Name != "Error" { 39 | return true // same name and not normal error 40 | } 41 | return e.Message == err.Message 42 | } 43 | 44 | func parseError(err Error) error { 45 | if err.Name == "TimeoutError" { 46 | return fmt.Errorf("%w: %w: %w", ErrPlaywright, ErrTimeout, &err) 47 | } else if err.Name == "TargetClosedError" { 48 | return fmt.Errorf("%w: %w: %w", ErrPlaywright, ErrTargetClosed, &err) 49 | } 50 | return fmt.Errorf("%w: %w", ErrPlaywright, &err) 51 | } 52 | 53 | func targetClosedError(reason *string) error { 54 | if reason == nil { 55 | return ErrTargetClosed 56 | } 57 | return fmt.Errorf("%w: %s", ErrTargetClosed, *reason) 58 | } 59 | -------------------------------------------------------------------------------- /examples/download/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "net/http" 10 | 11 | "github.com/playwright-community/playwright-go" 12 | ) 13 | 14 | func assertErrorToNilf(message string, err error) { 15 | if err != nil { 16 | log.Fatalf(message, err) 17 | } 18 | } 19 | 20 | func main() { 21 | startHttpServer() 22 | 23 | pw, err := playwright.Run() 24 | assertErrorToNilf("could not launch playwright: %w", err) 25 | browser, err := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{ 26 | Headless: playwright.Bool(true), 27 | }) 28 | assertErrorToNilf("could not launch Chromium: %w", err) 29 | context, err := browser.NewContext() 30 | assertErrorToNilf("could not create context: %w", err) 31 | page, err := context.NewPage() 32 | assertErrorToNilf("could not create page: %w", err) 33 | _, err = page.Goto("http://localhost:1234") 34 | assertErrorToNilf("could not goto: %w", err) 35 | assertErrorToNilf("could not set content: %w", page.SetContent(`download`)) 36 | download, err := page.ExpectDownload(func() error { 37 | return page.Locator("text=download").Click() 38 | }) 39 | assertErrorToNilf("could not download: %w", err) 40 | fmt.Println(download.SuggestedFilename()) 41 | assertErrorToNilf("could not close browser: %w", browser.Close()) 42 | assertErrorToNilf("could not stop Playwright: %w", pw.Stop()) 43 | } 44 | 45 | func startHttpServer() { 46 | http.HandleFunc("/download", func(w http.ResponseWriter, r *http.Request) { 47 | w.Header().Add("Content-Type", "application/octet-stream") 48 | w.Header().Add("Content-Disposition", "attachment; filename=file.txt") 49 | if _, err := w.Write([]byte("foobar")); err != nil { 50 | log.Printf("could not write: %v", err) 51 | } 52 | }) 53 | go func() { 54 | log.Fatal(http.ListenAndServe(":1234", nil)) 55 | }() 56 | } 57 | -------------------------------------------------------------------------------- /examples/javascript/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "github.com/playwright-community/playwright-go" 11 | ) 12 | 13 | func main() { 14 | pw, err := playwright.Run() 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | browser, err := pw.Chromium.Launch() 19 | if err != nil { 20 | log.Fatalf("could not launch browser: %v\n", err) 21 | } 22 | page, err := browser.NewPage() 23 | if err != nil { 24 | log.Fatalf("could not create page: %v\n", err) 25 | } 26 | if _, err = page.Goto("https://en.wikipedia.org/wiki/JavaScript"); err != nil { 27 | log.Fatalf("could not goto: %v\n", err) 28 | } 29 | // mw.config.values is the JS object where Wikipedia stores wiki metadata 30 | handle, err := page.EvaluateHandle("mw.config.values", struct{}{}) 31 | if err != nil { 32 | log.Fatalf("could not acquire JSHandle: %v\n", err) 33 | } 34 | // mw.config.values.wgPageName is the name of the current page 35 | pageName, err := handle.GetProperty("wgPageName") 36 | if err != nil { 37 | log.Fatalf("could not get Wikipedia page name: %v\n", err) 38 | } 39 | 40 | fmt.Printf("Lots of type casting, brought to you by %s\n", pageName) 41 | 42 | if err := browser.Close(); err != nil { 43 | log.Fatalf("could not close browser: %v\n", err) 44 | } 45 | if err := pw.Stop(); err != nil { 46 | log.Fatalf("could not stop Playwright: %v\n", err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/mobile-and-geolocation/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | 9 | "github.com/playwright-community/playwright-go" 10 | ) 11 | 12 | func main() { 13 | pw, err := playwright.Run() 14 | if err != nil { 15 | log.Fatalf("could not start playwright: %v", err) 16 | } 17 | browser, err := pw.Chromium.Launch() 18 | if err != nil { 19 | log.Fatalf("could not launch browser: %v", err) 20 | } 21 | device := pw.Devices["Pixel 5"] 22 | context, err := browser.NewContext(playwright.BrowserNewContextOptions{ 23 | Geolocation: &playwright.Geolocation{ 24 | Longitude: 12.492507, 25 | Latitude: 41.889938, 26 | }, 27 | Permissions: []string{"geolocation"}, 28 | Viewport: device.Viewport, 29 | UserAgent: playwright.String(device.UserAgent), 30 | DeviceScaleFactor: playwright.Float(device.DeviceScaleFactor), 31 | IsMobile: playwright.Bool(device.IsMobile), 32 | HasTouch: playwright.Bool(device.HasTouch), 33 | }) 34 | if err != nil { 35 | log.Fatalf("could not create context: %v", err) 36 | } 37 | page, err := context.NewPage() 38 | if err != nil { 39 | log.Fatalf("could not create page: %v", err) 40 | } 41 | if _, err = page.Goto("https://www.openstreetmap.org"); err != nil { 42 | log.Fatalf("could not goto: %v", err) 43 | } 44 | if err = page.Locator("a[data-bs-original-title='Show My Location']").Click(); err != nil { 45 | log.Fatalf("could not click on location: %v", err) 46 | } 47 | if _, err = page.Screenshot(playwright.PageScreenshotOptions{ 48 | Path: playwright.String("colosseum-iphone.png"), 49 | }); err != nil { 50 | log.Fatalf("could not make screenshot: %v", err) 51 | } 52 | if err = browser.Close(); err != nil { 53 | log.Fatalf("could not close browser: %v", err) 54 | } 55 | if err = pw.Stop(); err != nil { 56 | log.Fatalf("could not stop Playwright: %v", err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/network-monitoring/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "github.com/playwright-community/playwright-go" 11 | ) 12 | 13 | func main() { 14 | pw, err := playwright.Run() 15 | if err != nil { 16 | log.Fatalf("could not launch playwright: %v", err) 17 | } 18 | browser, err := pw.Chromium.Launch() 19 | if err != nil { 20 | log.Fatalf("could not launch Chromium: %v", err) 21 | } 22 | page, err := browser.NewPage() 23 | if err != nil { 24 | log.Fatalf("could not create page: %v", err) 25 | } 26 | page.OnRequest(func(request playwright.Request) { 27 | fmt.Printf(">> %s %s\n", request.Method(), request.URL()) 28 | }) 29 | page.OnResponse(func(response playwright.Response) { 30 | fmt.Printf("<< %v %s\n", response.Status(), response.URL()) 31 | }) 32 | if _, err = page.Goto("http://todomvc.com/examples/react/", playwright.PageGotoOptions{ 33 | WaitUntil: playwright.WaitUntilStateNetworkidle, 34 | }); err != nil { 35 | log.Fatalf("could not goto: %v", err) 36 | } 37 | if err = browser.Close(); err != nil { 38 | log.Fatalf("could not close browser: %v", err) 39 | } 40 | if err = pw.Stop(); err != nil { 41 | log.Fatalf("could not stop Playwright: %v", err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/pdf/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | 9 | "github.com/playwright-community/playwright-go" 10 | ) 11 | 12 | func assertErrorToNilf(message string, err error) { 13 | if err != nil { 14 | log.Fatalf(message, err) 15 | } 16 | } 17 | 18 | func main() { 19 | pw, err := playwright.Run() 20 | assertErrorToNilf("could not launch playwright: %w", err) 21 | browser, err := pw.Chromium.Launch() 22 | assertErrorToNilf("could not launch Chromium: %w", err) 23 | context, err := browser.NewContext() 24 | assertErrorToNilf("could not create context: %w", err) 25 | page, err := context.NewPage() 26 | assertErrorToNilf("could not create page: %w", err) 27 | _, err = page.Goto("https://github.com/microsoft/playwright") 28 | assertErrorToNilf("could not goto: %w", err) 29 | _, err = page.PDF(playwright.PagePdfOptions{ 30 | Path: playwright.String("playwright-example.pdf"), 31 | }) 32 | assertErrorToNilf("could not create PDF: %w", err) 33 | assertErrorToNilf("could not close browser: %w", browser.Close()) 34 | assertErrorToNilf("could not stop Playwright: %w", pw.Stop()) 35 | } 36 | -------------------------------------------------------------------------------- /examples/scraping/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "github.com/playwright-community/playwright-go" 11 | ) 12 | 13 | func main() { 14 | pw, err := playwright.Run() 15 | if err != nil { 16 | log.Fatalf("could not start playwright: %v", err) 17 | } 18 | browser, err := pw.Chromium.Launch() 19 | if err != nil { 20 | log.Fatalf("could not launch browser: %v", err) 21 | } 22 | page, err := browser.NewPage() 23 | if err != nil { 24 | log.Fatalf("could not create page: %v", err) 25 | } 26 | if _, err = page.Goto("https://news.ycombinator.com"); err != nil { 27 | log.Fatalf("could not goto: %v", err) 28 | } 29 | entries, err := page.Locator(".athing").All() 30 | if err != nil { 31 | log.Fatalf("could not get entries: %v", err) 32 | } 33 | for i, entry := range entries { 34 | title, err := entry.Locator("td.title > span > a").TextContent() 35 | if err != nil { 36 | log.Fatalf("could not get text content: %v", err) 37 | } 38 | fmt.Printf("%d: %s\n", i+1, title) 39 | } 40 | if err = browser.Close(); err != nil { 41 | log.Fatalf("could not close browser: %v", err) 42 | } 43 | if err = pw.Stop(); err != nil { 44 | log.Fatalf("could not stop Playwright: %v", err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/screenshot/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | 9 | "github.com/playwright-community/playwright-go" 10 | ) 11 | 12 | func main() { 13 | pw, err := playwright.Run() 14 | if err != nil { 15 | log.Fatalf("could not launch playwright: %v", err) 16 | } 17 | browser, err := pw.Chromium.Launch() 18 | if err != nil { 19 | log.Fatalf("could not launch Chromium: %v", err) 20 | } 21 | page, err := browser.NewPage() 22 | if err != nil { 23 | log.Fatalf("could not create page: %v", err) 24 | } 25 | if _, err = page.Goto("https://playwright.dev/", playwright.PageGotoOptions{ 26 | WaitUntil: playwright.WaitUntilStateDomcontentloaded, 27 | }); err != nil { 28 | log.Fatalf("could not goto: %v", err) 29 | } 30 | if _, err = page.Screenshot(playwright.PageScreenshotOptions{ 31 | Path: playwright.String("foo.png"), 32 | }); err != nil { 33 | log.Fatalf("could not create screenshot: %v", err) 34 | } 35 | if err = browser.Close(); err != nil { 36 | log.Fatalf("could not close browser: %v", err) 37 | } 38 | if err = pw.Stop(); err != nil { 39 | log.Fatalf("could not stop Playwright: %v", err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/use-local-chrome/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "github.com/playwright-community/playwright-go" 11 | ) 12 | 13 | func main() { 14 | runOption := &playwright.RunOptions{ 15 | SkipInstallBrowsers: true, 16 | } 17 | err := playwright.Install(runOption) 18 | if err != nil { 19 | log.Fatalf("could not install playwright dependencies: %v", err) 20 | } 21 | pw, err := playwright.Run() 22 | if err != nil { 23 | log.Fatalf("could not start playwright: %v", err) 24 | } 25 | option := playwright.BrowserTypeLaunchOptions{ 26 | Channel: playwright.String("chrome"), 27 | } 28 | browser, err := pw.Chromium.Launch(option) 29 | if err != nil { 30 | log.Fatalf("could not launch browser: %v", err) 31 | } 32 | page, err := browser.NewPage() 33 | if err != nil { 34 | log.Fatalf("could not create page: %v", err) 35 | } 36 | if _, err = page.Goto("https://news.ycombinator.com"); err != nil { 37 | log.Fatalf("could not goto: %v", err) 38 | } 39 | entries, err := page.Locator(".athing").All() 40 | if err != nil { 41 | log.Fatalf("could not get entries: %v", err) 42 | } 43 | for i, entry := range entries { 44 | title, err := entry.Locator("td.title > span > a").TextContent() 45 | if err != nil { 46 | log.Fatalf("could not get text content: %v", err) 47 | } 48 | fmt.Printf("%d: %s\n", i+1, title) 49 | } 50 | if err = browser.Close(); err != nil { 51 | log.Fatalf("could not close browser: %v", err) 52 | } 53 | if err = pw.Stop(); err != nil { 54 | log.Fatalf("could not stop Playwright: %v", err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/video/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "github.com/playwright-community/playwright-go" 11 | ) 12 | 13 | func main() { 14 | pw, err := playwright.Run() 15 | if err != nil { 16 | log.Fatalf("could not launch playwright: %v", err) 17 | } 18 | browser, err := pw.Chromium.Launch() 19 | if err != nil { 20 | log.Fatalf("could not launch Chromium: %v", err) 21 | } 22 | page, err := browser.NewPage(playwright.BrowserNewPageOptions{ 23 | RecordVideo: &playwright.RecordVideo{ 24 | Dir: "videos/", 25 | }, 26 | }) 27 | if err != nil { 28 | log.Fatalf("could not create page: %v", err) 29 | } 30 | gotoPage := func(url string) { 31 | fmt.Printf("Visiting %s\n", url) 32 | if _, err = page.Goto(url); err != nil { 33 | log.Fatalf("could not goto: %v", err) 34 | } 35 | fmt.Printf("Visited %s\n", url) 36 | } 37 | gotoPage("https://playwright.dev") 38 | gotoPage("https://github.com") 39 | gotoPage("https://microsoft.com") 40 | if err := page.Close(); err != nil { 41 | log.Fatalf("failed to close page: %v", err) 42 | } 43 | path, err := page.Video().Path() 44 | if err != nil { 45 | log.Fatalf("failed to get video path: %v", err) 46 | } 47 | fmt.Printf("Saved to %s\n", path) 48 | if err = browser.Close(); err != nil { 49 | log.Fatalf("could not close browser: %v", err) 50 | } 51 | if err = pw.Stop(); err != nil { 52 | log.Fatalf("could not stop Playwright: %v", err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /file_chooser.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | type fileChooserImpl struct { 4 | page Page 5 | elementHandle ElementHandle 6 | isMultiple bool 7 | } 8 | 9 | func (f *fileChooserImpl) Page() Page { 10 | return f.page 11 | } 12 | 13 | func (f *fileChooserImpl) Element() ElementHandle { 14 | return f.elementHandle 15 | } 16 | 17 | func (f *fileChooserImpl) IsMultiple() bool { 18 | return f.isMultiple 19 | } 20 | 21 | // InputFile represents the input file for: 22 | // - FileChooser.SetFiles() 23 | // - ElementHandle.SetInputFiles() 24 | // - Page.SetInputFiles() 25 | type InputFile struct { 26 | Name string `json:"name"` 27 | MimeType string `json:"mimeType,omitempty"` 28 | Buffer []byte `json:"buffer"` 29 | } 30 | 31 | func (f *fileChooserImpl) SetFiles(files interface{}, options ...FileChooserSetFilesOptions) error { 32 | if len(options) == 1 { 33 | return f.elementHandle.SetInputFiles(files, ElementHandleSetInputFilesOptions(options[0])) 34 | } 35 | return f.elementHandle.SetInputFiles(files) 36 | } 37 | 38 | func newFileChooser(page Page, elementHandle ElementHandle, isMultiple bool) *fileChooserImpl { 39 | return &fileChooserImpl{ 40 | page: page, 41 | elementHandle: elementHandle, 42 | isMultiple: isMultiple, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/playwright-community/playwright-go 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/coder/websocket v1.8.12 7 | github.com/deckarep/golang-set/v2 v2.7.0 8 | github.com/go-jose/go-jose/v3 v3.0.4 9 | github.com/go-stack/stack v1.8.1 10 | github.com/h2non/filetype v1.1.3 11 | github.com/mitchellh/go-ps v1.0.0 12 | github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785 13 | github.com/stretchr/testify v1.8.4 14 | github.com/tidwall/gjson v1.17.0 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/kr/pretty v0.3.1 // indirect 20 | github.com/pmezard/go-difflib v1.0.0 // indirect 21 | github.com/rogpeppe/go-internal v1.11.0 // indirect 22 | github.com/tidwall/match v1.1.1 // indirect 23 | github.com/tidwall/pretty v1.2.1 // indirect 24 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 25 | gopkg.in/yaml.v3 v3.0.1 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /har_router.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type harRouter struct { 8 | localUtils *localUtilsImpl 9 | harId string 10 | notFoundAction HarNotFound 11 | urlOrPredicate interface{} 12 | err error 13 | } 14 | 15 | func (r *harRouter) addContextRoute(context BrowserContext) error { 16 | if r.err != nil { 17 | return r.err 18 | } 19 | err := context.Route(r.urlOrPredicate, func(route Route) { 20 | err := r.handle(route) 21 | if err != nil { 22 | logger.Error("Error handling context route", "error", err) 23 | } 24 | }) 25 | if err != nil { 26 | return err 27 | } 28 | return r.err 29 | } 30 | 31 | func (r *harRouter) addPageRoute(page Page) error { 32 | if r.err != nil { 33 | return r.err 34 | } 35 | err := page.Route(r.urlOrPredicate, func(route Route) { 36 | err := r.handle(route) 37 | if err != nil { 38 | logger.Error("Error handling page route", "error", err) 39 | } 40 | }) 41 | if err != nil { 42 | return err 43 | } 44 | return r.err 45 | } 46 | 47 | func (r *harRouter) dispose() { 48 | go r.localUtils.HarClose(r.harId) 49 | } 50 | 51 | func (r *harRouter) handle(route Route) error { 52 | if r.err != nil { 53 | return r.err 54 | } 55 | request := route.Request() 56 | postData, err := request.PostDataBuffer() 57 | if err != nil { 58 | return err 59 | } 60 | response, err := r.localUtils.HarLookup(harLookupOptions{ 61 | HarId: r.harId, 62 | URL: request.URL(), 63 | Method: request.Method(), 64 | Headers: request.Headers(), 65 | IsNavigationRequest: request.IsNavigationRequest(), 66 | PostData: postData, 67 | }) 68 | if err != nil { 69 | return err 70 | } 71 | switch response.Action { 72 | case "redirect": 73 | if response.RedirectURL == nil { 74 | return errors.New("redirect url is null") 75 | } 76 | return route.(*routeImpl).redirectedNavigationRequest(*response.RedirectURL) 77 | case "fulfill": 78 | if response.Body == nil { 79 | return errors.New("fulfill body is null") 80 | } 81 | return route.Fulfill(RouteFulfillOptions{ 82 | Body: *response.Body, 83 | Status: response.Status, 84 | Headers: deserializeNameAndValueToMap(response.Headers), 85 | }) 86 | case "error": 87 | logger.Error("har action error", "error", *response.Message) 88 | fallthrough 89 | case "noentry": 90 | } 91 | if r.notFoundAction == *HarNotFoundAbort { 92 | return route.Abort() 93 | } 94 | return route.Fallback() 95 | } 96 | 97 | func newHarRouter(localUtils *localUtilsImpl, file string, notFoundAction HarNotFound, urlOrPredicate interface{}) *harRouter { 98 | harId, err := localUtils.HarOpen(file) 99 | var url interface{} = "**/*" 100 | if urlOrPredicate != nil { 101 | url = urlOrPredicate 102 | } 103 | return &harRouter{ 104 | localUtils: localUtils, 105 | harId: harId, 106 | notFoundAction: notFoundAction, 107 | urlOrPredicate: url, 108 | err: err, 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | type mouseImpl struct { 4 | channel *channel 5 | } 6 | 7 | func newMouse(channel *channel) *mouseImpl { 8 | return &mouseImpl{ 9 | channel: channel, 10 | } 11 | } 12 | 13 | func (m *mouseImpl) Move(x float64, y float64, options ...MouseMoveOptions) error { 14 | _, err := m.channel.Send("mouseMove", map[string]interface{}{ 15 | "x": x, 16 | "y": y, 17 | }, options) 18 | return err 19 | } 20 | 21 | func (m *mouseImpl) Down(options ...MouseDownOptions) error { 22 | _, err := m.channel.Send("mouseDown", options) 23 | return err 24 | } 25 | 26 | func (m *mouseImpl) Up(options ...MouseUpOptions) error { 27 | _, err := m.channel.Send("mouseUp", options) 28 | return err 29 | } 30 | 31 | func (m *mouseImpl) Click(x, y float64, options ...MouseClickOptions) error { 32 | _, err := m.channel.Send("mouseClick", map[string]interface{}{ 33 | "x": x, 34 | "y": y, 35 | }, options) 36 | return err 37 | } 38 | 39 | func (m *mouseImpl) Dblclick(x, y float64, options ...MouseDblclickOptions) error { 40 | var option MouseDblclickOptions 41 | if len(options) == 1 { 42 | option = options[0] 43 | } 44 | return m.Click(x, y, MouseClickOptions{ 45 | ClickCount: Int(2), 46 | Button: option.Button, 47 | Delay: option.Delay, 48 | }) 49 | } 50 | 51 | func (m *mouseImpl) Wheel(deltaX, deltaY float64) error { 52 | _, err := m.channel.Send("mouseWheel", map[string]interface{}{ 53 | "deltaX": deltaX, 54 | "deltaY": deltaY, 55 | }) 56 | return err 57 | } 58 | 59 | type keyboardImpl struct { 60 | channel *channel 61 | } 62 | 63 | func newKeyboard(channel *channel) *keyboardImpl { 64 | return &keyboardImpl{ 65 | channel: channel, 66 | } 67 | } 68 | 69 | func (m *keyboardImpl) Down(key string) error { 70 | _, err := m.channel.Send("keyboardDown", map[string]interface{}{ 71 | "key": key, 72 | }) 73 | return err 74 | } 75 | 76 | func (m *keyboardImpl) Up(key string) error { 77 | _, err := m.channel.Send("keyboardUp", map[string]interface{}{ 78 | "key": key, 79 | }) 80 | return err 81 | } 82 | 83 | func (m *keyboardImpl) InsertText(text string) error { 84 | _, err := m.channel.Send("keyboardInsertText", map[string]interface{}{ 85 | "text": text, 86 | }) 87 | return err 88 | } 89 | 90 | func (m *keyboardImpl) Type(text string, options ...KeyboardTypeOptions) error { 91 | _, err := m.channel.Send("keyboardInsertText", map[string]interface{}{ 92 | "text": text, 93 | }, options) 94 | return err 95 | } 96 | 97 | func (m *keyboardImpl) Press(key string, options ...KeyboardPressOptions) error { 98 | _, err := m.channel.Send("keyboardPress", map[string]interface{}{ 99 | "key": key, 100 | }, options) 101 | return err 102 | } 103 | 104 | type touchscreenImpl struct { 105 | channel *channel 106 | } 107 | 108 | func newTouchscreen(channel *channel) *touchscreenImpl { 109 | return &touchscreenImpl{ 110 | channel: channel, 111 | } 112 | } 113 | 114 | func (t *touchscreenImpl) Tap(x int, y int) error { 115 | _, err := t.channel.Send("touchscreenTap", map[string]interface{}{"x": x, "y": y}) 116 | return err 117 | } 118 | -------------------------------------------------------------------------------- /internal/safe/map.go: -------------------------------------------------------------------------------- 1 | package safe 2 | 3 | import ( 4 | "maps" 5 | "sync" 6 | ) 7 | 8 | // SyncMap is a thread-safe map 9 | type SyncMap[K comparable, V any] struct { 10 | sync.RWMutex 11 | m map[K]V 12 | } 13 | 14 | // NewSyncMap creates a new thread-safe map 15 | func NewSyncMap[K comparable, V any]() *SyncMap[K, V] { 16 | return &SyncMap[K, V]{ 17 | m: make(map[K]V), 18 | } 19 | } 20 | 21 | func (m *SyncMap[K, V]) Store(k K, v V) { 22 | m.Lock() 23 | defer m.Unlock() 24 | m.m[k] = v 25 | } 26 | 27 | func (m *SyncMap[K, V]) Load(k K) (v V, ok bool) { 28 | m.RLock() 29 | defer m.RUnlock() 30 | v, ok = m.m[k] 31 | return 32 | } 33 | 34 | // LoadOrStore returns the existing value for the key if present. Otherwise, it stores and returns the given value. 35 | func (m *SyncMap[K, V]) LoadOrStore(k K, v V) (actual V, loaded bool) { 36 | m.Lock() 37 | defer m.Unlock() 38 | actual, loaded = m.m[k] 39 | if loaded { 40 | return 41 | } 42 | m.m[k] = v 43 | return v, false 44 | } 45 | 46 | // LoadAndDelete deletes the value for a key, and returns the previous value if any. 47 | func (m *SyncMap[K, V]) LoadAndDelete(k K) (v V, loaded bool) { 48 | m.Lock() 49 | defer m.Unlock() 50 | v, loaded = m.m[k] 51 | if loaded { 52 | delete(m.m, k) 53 | } 54 | return 55 | } 56 | 57 | func (m *SyncMap[K, V]) Delete(k K) { 58 | m.Lock() 59 | defer m.Unlock() 60 | delete(m.m, k) 61 | } 62 | 63 | func (m *SyncMap[K, V]) Clear() { 64 | m.Lock() 65 | defer m.Unlock() 66 | clear(m.m) 67 | } 68 | 69 | func (m *SyncMap[K, V]) Len() int { 70 | m.RLock() 71 | defer m.RUnlock() 72 | return len(m.m) 73 | } 74 | 75 | func (m *SyncMap[K, V]) Clone() map[K]V { 76 | m.RLock() 77 | defer m.RUnlock() 78 | return maps.Clone(m.m) 79 | } 80 | 81 | func (m *SyncMap[K, V]) Range(f func(k K, v V) bool) { 82 | m.RLock() 83 | defer m.RUnlock() 84 | 85 | for k, v := range m.m { 86 | if !f(k, v) { 87 | break 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /jsonPipe.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | type jsonPipe struct { 10 | channelOwner 11 | msgChan chan *message 12 | } 13 | 14 | func (j *jsonPipe) Send(message map[string]interface{}) error { 15 | _, err := j.channel.Send("send", map[string]interface{}{ 16 | "message": message, 17 | }) 18 | return err 19 | } 20 | 21 | func (j *jsonPipe) Close() error { 22 | _, err := j.channel.Send("close") 23 | return err 24 | } 25 | 26 | func (j *jsonPipe) Poll() (*message, error) { 27 | msg := <-j.msgChan 28 | if msg == nil { 29 | return nil, errors.New("jsonPipe closed") 30 | } 31 | return msg, nil 32 | } 33 | 34 | func newJsonPipe(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *jsonPipe { 35 | j := &jsonPipe{ 36 | msgChan: make(chan *message, 2), 37 | } 38 | j.createChannelOwner(j, parent, objectType, guid, initializer) 39 | j.channel.On("message", func(ev map[string]interface{}) { 40 | var msg message 41 | m, err := json.Marshal(ev["message"]) 42 | if err == nil { 43 | err = json.Unmarshal(m, &msg) 44 | } 45 | if err != nil { 46 | msg = message{ 47 | Error: &struct { 48 | Error Error "json:\"error\"" 49 | }{ 50 | Error: Error{ 51 | Name: "Error", 52 | Message: fmt.Sprintf("jsonPipe: could not decode message: %s", err.Error()), 53 | }, 54 | }, 55 | } 56 | } 57 | j.msgChan <- &msg 58 | }) 59 | j.channel.Once("closed", func() { 60 | j.Emit("closed") 61 | close(j.msgChan) 62 | }) 63 | return j 64 | } 65 | -------------------------------------------------------------------------------- /network.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type rawHeaders struct { 8 | headersArray []NameValue 9 | headersMap map[string][]string 10 | } 11 | 12 | func (r *rawHeaders) Get(name string) string { 13 | values := r.GetAll(name) 14 | if len(values) == 0 { 15 | return "" 16 | } 17 | sep := ", " 18 | if strings.ToLower(name) == "set-cookie" { 19 | sep = "\n" 20 | } 21 | return strings.Join(values, sep) 22 | } 23 | 24 | func (r *rawHeaders) GetAll(name string) []string { 25 | name = strings.ToLower(name) 26 | if _, ok := r.headersMap[name]; !ok { 27 | return []string{} 28 | } 29 | return r.headersMap[name] 30 | } 31 | 32 | func (r *rawHeaders) Headers() map[string]string { 33 | out := make(map[string]string) 34 | for key := range r.headersMap { 35 | out[key] = r.Get(key) 36 | } 37 | return out 38 | } 39 | 40 | func (r *rawHeaders) HeadersArray() []NameValue { 41 | return r.headersArray 42 | } 43 | 44 | func newRawHeaders(headers interface{}) *rawHeaders { 45 | r := &rawHeaders{} 46 | r.headersArray = make([]NameValue, 0) 47 | r.headersMap = make(map[string][]string) 48 | for _, header := range headers.([]interface{}) { 49 | entry := header.(map[string]interface{}) 50 | name := entry["name"].(string) 51 | value := entry["value"].(string) 52 | r.headersArray = append(r.headersArray, NameValue{ 53 | Name: name, 54 | Value: value, 55 | }) 56 | if _, ok := r.headersMap[strings.ToLower(name)]; !ok { 57 | r.headersMap[strings.ToLower(name)] = make([]string, 0) 58 | } 59 | r.headersMap[strings.ToLower(name)] = append(r.headersMap[strings.ToLower(name)], value) 60 | } 61 | return r 62 | } 63 | -------------------------------------------------------------------------------- /objectFactory.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | func createObjectFactory(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) interface{} { 4 | switch objectType { 5 | case "Android": 6 | return nil 7 | case "AndroidSocket": 8 | return nil 9 | case "AndroidDevice": 10 | return nil 11 | case "APIRequestContext": 12 | return newAPIRequestContext(parent, objectType, guid, initializer) 13 | case "Artifact": 14 | return newArtifact(parent, objectType, guid, initializer) 15 | case "BindingCall": 16 | return newBindingCall(parent, objectType, guid, initializer) 17 | case "Browser": 18 | return newBrowser(parent, objectType, guid, initializer) 19 | case "BrowserType": 20 | return newBrowserType(parent, objectType, guid, initializer) 21 | case "BrowserContext": 22 | return newBrowserContext(parent, objectType, guid, initializer) 23 | case "CDPSession": 24 | return newCDPSession(parent, objectType, guid, initializer) 25 | case "Dialog": 26 | return newDialog(parent, objectType, guid, initializer) 27 | case "Electron": 28 | return nil 29 | case "ElectronApplication": 30 | return nil 31 | case "ElementHandle": 32 | return newElementHandle(parent, objectType, guid, initializer) 33 | case "Frame": 34 | return newFrame(parent, objectType, guid, initializer) 35 | case "JSHandle": 36 | return newJSHandle(parent, objectType, guid, initializer) 37 | case "JsonPipe": 38 | return newJsonPipe(parent, objectType, guid, initializer) 39 | case "LocalUtils": 40 | localUtils := newLocalUtils(parent, objectType, guid, initializer) 41 | if localUtils.connection.localUtils == nil { 42 | localUtils.connection.localUtils = localUtils 43 | } 44 | return localUtils 45 | case "Page": 46 | return newPage(parent, objectType, guid, initializer) 47 | case "Playwright": 48 | return newPlaywright(parent, objectType, guid, initializer) 49 | case "Request": 50 | return newRequest(parent, objectType, guid, initializer) 51 | case "Response": 52 | return newResponse(parent, objectType, guid, initializer) 53 | case "Route": 54 | return newRoute(parent, objectType, guid, initializer) 55 | case "Selectors": 56 | return newSelectorsOwner(parent, objectType, guid, initializer) 57 | case "SocksSupport": 58 | return nil 59 | case "Stream": 60 | return newStream(parent, objectType, guid, initializer) 61 | case "Tracing": 62 | return newTracing(parent, objectType, guid, initializer) 63 | case "WebSocket": 64 | return newWebsocket(parent, objectType, guid, initializer) 65 | case "WebSocketRoute": 66 | return newWebSocketRoute(parent, objectType, guid, initializer) 67 | case "Worker": 68 | return newWorker(parent, objectType, guid, initializer) 69 | case "WritableStream": 70 | return newWritableStream(parent, objectType, guid, initializer) 71 | default: 72 | panic(objectType) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /page_assertions.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "net/url" 5 | "path" 6 | ) 7 | 8 | type pageAssertionsImpl struct { 9 | assertionsBase 10 | actualPage Page 11 | } 12 | 13 | func newPageAssertions(page Page, isNot bool, defaultTimeout *float64) *pageAssertionsImpl { 14 | return &pageAssertionsImpl{ 15 | assertionsBase: assertionsBase{ 16 | actualLocator: page.Locator(":root"), 17 | isNot: isNot, 18 | defaultTimeout: defaultTimeout, 19 | }, 20 | actualPage: page, 21 | } 22 | } 23 | 24 | func (pa *pageAssertionsImpl) ToHaveTitle(titleOrRegExp interface{}, options ...PageAssertionsToHaveTitleOptions) error { 25 | var timeout *float64 26 | if len(options) == 1 { 27 | timeout = options[0].Timeout 28 | } 29 | expectedValues, err := toExpectedTextValues([]interface{}{titleOrRegExp}, false, true, nil) 30 | if err != nil { 31 | return err 32 | } 33 | return pa.expect( 34 | "to.have.title", 35 | frameExpectOptions{ExpectedText: expectedValues, Timeout: timeout}, 36 | titleOrRegExp, 37 | "Page title expected to be", 38 | ) 39 | } 40 | 41 | func (pa *pageAssertionsImpl) ToHaveURL(urlOrRegExp interface{}, options ...PageAssertionsToHaveURLOptions) error { 42 | var timeout *float64 43 | var ignoreCase *bool 44 | if len(options) == 1 { 45 | timeout = options[0].Timeout 46 | ignoreCase = options[0].IgnoreCase 47 | } 48 | 49 | baseURL := pa.actualPage.Context().(*browserContextImpl).options.BaseURL 50 | if urlPath, ok := urlOrRegExp.(string); ok && baseURL != nil { 51 | u, _ := url.Parse(*baseURL) 52 | u.Path = path.Join(u.Path, urlPath) 53 | urlOrRegExp = u.String() 54 | } 55 | 56 | expectedValues, err := toExpectedTextValues([]interface{}{urlOrRegExp}, false, false, ignoreCase) 57 | if err != nil { 58 | return err 59 | } 60 | return pa.expect( 61 | "to.have.url", 62 | frameExpectOptions{ExpectedText: expectedValues, Timeout: timeout}, 63 | urlOrRegExp, 64 | "Page URL expected to be", 65 | ) 66 | } 67 | 68 | func (pa *pageAssertionsImpl) Not() PageAssertions { 69 | return newPageAssertions(pa.actualPage, true, pa.defaultTimeout) 70 | } 71 | -------------------------------------------------------------------------------- /playwright.go: -------------------------------------------------------------------------------- 1 | // Package playwright is a library to automate Chromium, Firefox and WebKit with 2 | // a single API. Playwright is built to enable cross-browser web automation that 3 | // is ever-green, capable, reliable and fast. 4 | package playwright 5 | 6 | // DeviceDescriptor represents a single device 7 | type DeviceDescriptor struct { 8 | UserAgent string `json:"userAgent"` 9 | Viewport *Size `json:"viewport"` 10 | Screen *Size `json:"screen"` 11 | DeviceScaleFactor float64 `json:"deviceScaleFactor"` 12 | IsMobile bool `json:"isMobile"` 13 | HasTouch bool `json:"hasTouch"` 14 | DefaultBrowserType string `json:"defaultBrowserType"` 15 | } 16 | 17 | // Playwright represents a Playwright instance 18 | type Playwright struct { 19 | channelOwner 20 | Selectors Selectors 21 | Chromium BrowserType 22 | Firefox BrowserType 23 | WebKit BrowserType 24 | Request APIRequest 25 | Devices map[string]*DeviceDescriptor 26 | } 27 | 28 | // Stop stops the Playwright instance 29 | func (p *Playwright) Stop() error { 30 | return p.connection.Stop() 31 | } 32 | 33 | func (p *Playwright) setSelectors(selectors Selectors) { 34 | selectorsOwner := fromChannel(p.initializer["selectors"]).(*selectorsOwnerImpl) 35 | p.Selectors.(*selectorsImpl).removeChannel(selectorsOwner) 36 | p.Selectors = selectors 37 | p.Selectors.(*selectorsImpl).addChannel(selectorsOwner) 38 | } 39 | 40 | func newPlaywright(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *Playwright { 41 | pw := &Playwright{ 42 | Selectors: newSelectorsImpl(), 43 | Chromium: fromChannel(initializer["chromium"]).(*browserTypeImpl), 44 | Firefox: fromChannel(initializer["firefox"]).(*browserTypeImpl), 45 | WebKit: fromChannel(initializer["webkit"]).(*browserTypeImpl), 46 | Devices: make(map[string]*DeviceDescriptor), 47 | } 48 | pw.createChannelOwner(pw, parent, objectType, guid, initializer) 49 | pw.Request = newApiRequestImpl(pw) 50 | pw.Chromium.(*browserTypeImpl).playwright = pw 51 | pw.Firefox.(*browserTypeImpl).playwright = pw 52 | pw.WebKit.(*browserTypeImpl).playwright = pw 53 | selectorsOwner := fromChannel(initializer["selectors"]).(*selectorsOwnerImpl) 54 | pw.Selectors.(*selectorsImpl).addChannel(selectorsOwner) 55 | pw.connection.afterClose = func() { 56 | pw.Selectors.(*selectorsImpl).removeChannel(selectorsOwner) 57 | } 58 | if pw.connection.localUtils != nil { 59 | pw.Devices = pw.connection.localUtils.Devices 60 | } 61 | return pw 62 | } 63 | 64 | //go:generate bash scripts/generate-api.sh 65 | -------------------------------------------------------------------------------- /run_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package playwright 4 | 5 | import "syscall" 6 | 7 | var defaultSysProcAttr = &syscall.SysProcAttr{} 8 | 9 | // for WritableStream.Copy 10 | const defaultCopyBufSize = 1024 * 1024 11 | -------------------------------------------------------------------------------- /run_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package playwright 4 | 5 | import "syscall" 6 | 7 | var defaultSysProcAttr = &syscall.SysProcAttr{HideWindow: true} 8 | 9 | // for WritableStream.Copy 10 | const defaultCopyBufSize = 64 * 1024 11 | -------------------------------------------------------------------------------- /scripts/apply-patch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | BRANCH_NAME_BUILD="playwright-build" 6 | SCRIPTS_DIR="$(dirname "$0")" 7 | 8 | echo "Applying patches..." 9 | echo "===================" 10 | 11 | pushd "$SCRIPTS_DIR/.." 12 | PW_VERSION="$(grep -oE "playwrightCliVersion.+\"[0-9\.]+" ./run.go | sed 's/playwrightCliVersion = "/v/g')" 13 | git submodule update --init 14 | pushd playwright 15 | 16 | git checkout HEAD --detach 17 | 18 | if git show-ref -q --heads "$BRANCH_NAME_BUILD"; then 19 | git fetch --tags 20 | git checkout "$PW_VERSION" 21 | git branch -D "$BRANCH_NAME_BUILD" 22 | fi 23 | 24 | git checkout -b "$BRANCH_NAME_BUILD" 25 | git apply --3way --whitespace=nowarn ../patches/* 26 | git add -A 27 | git commit -m "Applied patches" 28 | 29 | popd -------------------------------------------------------------------------------- /scripts/generate-api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set +x 5 | PWD=$(pwd -P) 6 | 7 | "$PWD"/scripts/apply-patch.sh 8 | 9 | echo 10 | echo "Generating code" 11 | echo "==================" 12 | PLAYWRIGHT_DIR="playwright" 13 | node $PLAYWRIGHT_DIR/utils/doclint/generateGoApi.js 14 | errCode=$? 15 | if [ $errCode -ne 0 ]; then 16 | echo 17 | exit $errCode 18 | else 19 | echo "Done!" 20 | echo 21 | fi 22 | mv $PLAYWRIGHT_DIR/utils/doclint/generate_types/go/generated-{enums,interfaces,structs}.go . 23 | # fmt first or not, gofumpt's result will be different 24 | go fmt generated-{enums,interfaces,structs}.go > /dev/null 25 | gofumpt -w generated-{enums,interfaces,structs}.go > /dev/null 26 | 27 | echo "Updating README" 28 | echo "===============" 29 | go run scripts/install-browsers/main.go 30 | go run scripts/update-readme-versions/main.go 31 | 32 | git submodule update 33 | -------------------------------------------------------------------------------- /scripts/install-browsers/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | 9 | "github.com/playwright-community/playwright-go" 10 | ) 11 | 12 | func main() { 13 | if err := playwright.Install(); err != nil { 14 | log.Fatalf("could not install playwright: %v", err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scripts/update-patch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set +x 5 | 6 | SCRIPTS_DIR="$(dirname "$0")" 7 | 8 | pushd "$SCRIPTS_DIR/../playwright" 9 | SCRIPTS_DIR="$(dirname "$0")" 10 | echo "Creating patch..." 11 | git add . 12 | git diff playwright-build^1..playwright-build > ../patches/main.patch 13 | git reset --hard playwright-build^1 14 | cd .. 15 | 16 | popd -------------------------------------------------------------------------------- /scripts/update-readme-versions/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "os" 10 | "regexp" 11 | 12 | "github.com/playwright-community/playwright-go" 13 | ) 14 | 15 | func main() { 16 | const readmePath = "README.md" 17 | readmeContent, err := os.ReadFile(readmePath) 18 | if err != nil { 19 | log.Fatalf("could not read readme: %v", err) 20 | } 21 | replaceValueInTemplate := func(name, value string) { 22 | re := regexp.MustCompile(fmt.Sprintf("([^<]+)", name)) 23 | readmeContent = re.ReplaceAll(readmeContent, []byte(fmt.Sprintf("%s", name, value))) 24 | } 25 | 26 | pw, err := playwright.Run() 27 | if err != nil { 28 | log.Fatalf("could not run playwright: %v", err) 29 | } 30 | browserTypes := []playwright.BrowserType{pw.Chromium, pw.Firefox, pw.WebKit} 31 | for _, browserType := range browserTypes { 32 | browser, err := browserType.Launch() 33 | if err != nil { 34 | log.Fatalf("could not launch browser: %v", err) 35 | } 36 | replaceValueInTemplate(fmt.Sprintf("%s-version", browserType.Name()), browser.Version()) 37 | 38 | var badgeFormat string 39 | switch browserType.Name() { 40 | case "chromium": 41 | badgeFormat = "[![Chromium version](https://img.shields.io/badge/chromium-%s-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)" 42 | case "firefox": 43 | badgeFormat = "[![Firefox version](https://img.shields.io/badge/firefox-%s-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/)" 44 | case "webkit": 45 | badgeFormat = "[![WebKit version](https://img.shields.io/badge/webkit-%s-blue.svg?logo=safari)](https://webkit.org/)" 46 | } 47 | replaceValueInTemplate(browserType.Name()+"-version-badge", fmt.Sprintf(badgeFormat, browser.Version())) 48 | 49 | if err := browser.Close(); err != nil { 50 | log.Fatalf("could not close browser: %v", err) 51 | } 52 | } 53 | if err := os.WriteFile(readmePath, readmeContent, 0o644); err != nil { 54 | log.Fatalf("could not write readme: %v", err) 55 | } 56 | if err := pw.Stop(); err != nil { 57 | log.Fatalf("could not stop Playwright: %v", err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /selectors.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "sync" 7 | ) 8 | 9 | type selectorsOwnerImpl struct { 10 | channelOwner 11 | } 12 | 13 | func (s *selectorsOwnerImpl) setTestIdAttributeName(name string) { 14 | s.channel.SendNoReply("setTestIdAttributeName", map[string]interface{}{ 15 | "testIdAttributeName": name, 16 | }) 17 | } 18 | 19 | func newSelectorsOwner(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *selectorsOwnerImpl { 20 | obj := &selectorsOwnerImpl{} 21 | obj.createChannelOwner(obj, parent, objectType, guid, initializer) 22 | return obj 23 | } 24 | 25 | type selectorsImpl struct { 26 | channels sync.Map 27 | registrations []map[string]interface{} 28 | } 29 | 30 | func (s *selectorsImpl) Register(name string, script Script, options ...SelectorsRegisterOptions) error { 31 | if script.Path == nil && script.Content == nil { 32 | return errors.New("Either source or path should be specified") 33 | } 34 | source := "" 35 | if script.Path != nil { 36 | content, err := os.ReadFile(*script.Path) 37 | if err != nil { 38 | return err 39 | } 40 | source = string(content) 41 | } else { 42 | source = *script.Content 43 | } 44 | params := map[string]interface{}{ 45 | "name": name, 46 | "source": source, 47 | } 48 | if len(options) == 1 && options[0].ContentScript != nil { 49 | params["contentScript"] = *options[0].ContentScript 50 | } 51 | var err error 52 | s.channels.Range(func(key, value any) bool { 53 | _, err = value.(*selectorsOwnerImpl).channel.Send("register", params) 54 | return err == nil 55 | }) 56 | if err != nil { 57 | return err 58 | } 59 | s.registrations = append(s.registrations, params) 60 | return nil 61 | } 62 | 63 | func (s *selectorsImpl) SetTestIdAttribute(name string) { 64 | setTestIdAttributeName(name) 65 | s.channels.Range(func(key, value any) bool { 66 | value.(*selectorsOwnerImpl).setTestIdAttributeName(name) 67 | return true 68 | }) 69 | } 70 | 71 | func (s *selectorsImpl) addChannel(channel *selectorsOwnerImpl) { 72 | s.channels.Store(channel.guid, channel) 73 | for _, params := range s.registrations { 74 | channel.channel.SendNoReply("register", params) 75 | channel.setTestIdAttributeName(getTestIdAttributeName()) 76 | } 77 | } 78 | 79 | func (s *selectorsImpl) removeChannel(channel *selectorsOwnerImpl) { 80 | s.channels.Delete(channel.guid) 81 | } 82 | 83 | func newSelectorsImpl() *selectorsImpl { 84 | return &selectorsImpl{ 85 | channels: sync.Map{}, 86 | registrations: make([]map[string]interface{}, 0), 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "bufio" 5 | "encoding/base64" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type streamImpl struct { 11 | channelOwner 12 | } 13 | 14 | func (s *streamImpl) SaveAs(path string) error { 15 | err := os.MkdirAll(filepath.Dir(path), 0o777) 16 | if err != nil { 17 | return err 18 | } 19 | file, err := os.Create(path) 20 | if err != nil { 21 | return err 22 | } 23 | defer file.Close() 24 | writer := bufio.NewWriter(file) 25 | for { 26 | binary, err := s.channel.Send("read", map[string]interface{}{"size": 1024 * 1024}) 27 | if err != nil { 28 | return err 29 | } 30 | bytes, err := base64.StdEncoding.DecodeString(binary.(string)) 31 | if err != nil { 32 | return err 33 | } 34 | if len(bytes) == 0 { 35 | break 36 | } 37 | _, err = writer.Write(bytes) 38 | if err != nil { 39 | return err 40 | } 41 | } 42 | return writer.Flush() 43 | } 44 | 45 | func (s *streamImpl) ReadAll() ([]byte, error) { 46 | var data []byte 47 | for { 48 | binary, err := s.channel.Send("read", map[string]interface{}{"size": 1024 * 1024}) 49 | if err != nil { 50 | return nil, err 51 | } 52 | bytes, err := base64.StdEncoding.DecodeString(binary.(string)) 53 | if err != nil { 54 | return nil, err 55 | } 56 | if len(bytes) == 0 { 57 | break 58 | } 59 | data = append(data, bytes...) 60 | } 61 | return data, nil 62 | } 63 | 64 | func newStream(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *streamImpl { 65 | stream := &streamImpl{} 66 | stream.createChannelOwner(stream, parent, objectType, guid, initializer) 67 | return stream 68 | } 69 | -------------------------------------------------------------------------------- /tests/apiresponse_assertions_test.go: -------------------------------------------------------------------------------- 1 | package playwright_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAssertionsResponseIsOKPass(t *testing.T) { 11 | BeforeEach(t) 12 | 13 | response, err := page.Request().Get(server.EMPTY_PAGE) 14 | require.NoError(t, err) 15 | require.NoError(t, expect.APIResponse(response).ToBeOK()) 16 | require.Error(t, expect.APIResponse(response).Not().ToBeOK()) 17 | } 18 | 19 | func TestAssertionsShouldPrintResponseWithTextContentTypeIfToBeOKFails(t *testing.T) { 20 | BeforeEach(t) 21 | 22 | server.SetRoute("/text-content-type", func(w http.ResponseWriter, r *http.Request) { 23 | w.Header().Set("Content-Type", "text/plain") 24 | w.WriteHeader(http.StatusNotFound) 25 | _, _ = w.Write([]byte("Text error")) 26 | }) 27 | server.SetRoute("/no-content-type", func(w http.ResponseWriter, r *http.Request) { 28 | w.Header().Set("Content-Type", "") 29 | w.WriteHeader(http.StatusNotFound) 30 | _, _ = w.Write([]byte("No content type error")) 31 | }) 32 | server.SetRoute("/binary-content-type", func(w http.ResponseWriter, r *http.Request) { 33 | w.Header().Set("Content-Type", "image/bmp") 34 | w.WriteHeader(http.StatusNotFound) 35 | _, _ = w.Write([]byte("Image content type error")) 36 | }) 37 | 38 | response, err := page.Request().Get(server.PREFIX + "/text-content-type") 39 | require.NoError(t, err) 40 | require.NoError(t, expect.APIResponse(response).Not().ToBeOK()) 41 | err = expect.APIResponse(response).ToBeOK() 42 | require.ErrorContains(t, err, "→ GET "+server.PREFIX+"/text-content-type") 43 | require.ErrorContains(t, err, "← 404 Not Found") 44 | require.ErrorContains(t, err, "Response Text:") 45 | require.ErrorContains(t, err, "Text error") 46 | 47 | response, err = page.Request().Get(server.PREFIX + "/no-content-type") 48 | require.NoError(t, err) 49 | require.NoError(t, expect.APIResponse(response).Not().ToBeOK()) 50 | err = expect.APIResponse(response).ToBeOK() 51 | require.ErrorContains(t, err, "→ GET "+server.PREFIX+"/no-content-type") 52 | require.ErrorContains(t, err, "← 404 Not Found") 53 | require.NotContains(t, err.Error(), "Response Text:") 54 | require.NotContains(t, err.Error(), "No content type error") 55 | 56 | response, err = page.Request().Get(server.PREFIX + "/binary-content-type") 57 | require.NoError(t, err) 58 | err = expect.APIResponse(response).ToBeOK() 59 | require.ErrorContains(t, err, "→ GET "+server.PREFIX+"/binary-content-type") 60 | require.ErrorContains(t, err, "← 404 Not Found") 61 | require.NotContains(t, err.Error(), "Response Text:") 62 | require.NotContains(t, err.Error(), "Image content type error") 63 | } 64 | -------------------------------------------------------------------------------- /tests/assets/background-color.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/assets/beforeunload.html: -------------------------------------------------------------------------------- 1 |
beforeunload demo.
2 | 10 | 11 | -------------------------------------------------------------------------------- /tests/assets/button-overlay-oopif.html: -------------------------------------------------------------------------------- 1 | 23 | 40 | -------------------------------------------------------------------------------- /tests/assets/cached/one-style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: pink; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/cached/one-style.html: -------------------------------------------------------------------------------- 1 | 2 |
hello, world!
3 | -------------------------------------------------------------------------------- /tests/assets/callback.js: -------------------------------------------------------------------------------- 1 | function callme(cb) { 2 | return cb(); 3 | } 4 | 5 | module.exports = callme; 6 | -------------------------------------------------------------------------------- /tests/assets/checkerboard.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 | 43 | -------------------------------------------------------------------------------- /tests/assets/chromium-linux.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/chromium-linux.zip -------------------------------------------------------------------------------- /tests/assets/client-certificates/README.md: -------------------------------------------------------------------------------- 1 | # Client Certificate test-certificates 2 | 3 | ## Server 4 | 5 | ```bash 6 | openssl req \ 7 | -x509 \ 8 | -newkey rsa:4096 \ 9 | -keyout server/server_key.pem \ 10 | -out server/server_cert.pem \ 11 | -nodes \ 12 | -days 365 \ 13 | -subj "/CN=localhost/O=Client\ Certificate\ Demo" \ 14 | -addext "subjectAltName=DNS:localhost,DNS:local.playwright" 15 | ``` 16 | 17 | ## Trusted client-certificate (server signed/valid) 18 | 19 | ``` 20 | mkdir -p client/trusted 21 | # generate server-signed (valid) certifcate 22 | openssl req \ 23 | -newkey rsa:4096 \ 24 | -keyout client/trusted/key.pem \ 25 | -out client/trusted/csr.pem \ 26 | -nodes \ 27 | -days 365 \ 28 | -subj "/CN=Alice" 29 | 30 | # sign with server_cert.pem 31 | openssl x509 \ 32 | -req \ 33 | -in client/trusted/csr.pem \ 34 | -CA server/server_cert.pem \ 35 | -CAkey server/server_key.pem \ 36 | -out client/trusted/cert.pem \ 37 | -set_serial 01 \ 38 | -days 365 39 | # create pfx 40 | openssl pkcs12 -export -out client/trusted/cert.pfx -inkey client/trusted/key.pem -in client/trusted/cert.pem -passout pass:secure 41 | ``` 42 | 43 | ## Self-signed certificate (invalid) 44 | 45 | ``` 46 | mkdir -p client/self-signed 47 | openssl req \ 48 | -newkey rsa:4096 \ 49 | -keyout client/self-signed/key.pem \ 50 | -out client/self-signed/csr.pem \ 51 | -nodes \ 52 | -days 365 \ 53 | -subj "/CN=Bob" 54 | 55 | # sign with self-signed/key.pem 56 | openssl x509 \ 57 | -req \ 58 | -in client/self-signed/csr.pem \ 59 | -signkey client/self-signed/key.pem \ 60 | -out client/self-signed/cert.pem \ 61 | -days 365 62 | ``` 63 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/client/self-signed/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEyzCCArOgAwIBAgIUYps4gh4MqFYg8zqQhHYL7zYfbLkwDQYJKoZIhvcNAQEL 3 | BQAwDjEMMAoGA1UEAwwDQm9iMB4XDTI0MDcxOTEyNDc0MFoXDTI1MDcxOTEyNDc0 4 | MFowDjEMMAoGA1UEAwwDQm9iMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC 5 | AgEA179eTsqcc1c3AOQHzCZEyYLPta2CCAscUFqcEZ9vWvjW0uzOv9TDlB33Unov 6 | jch4CElZOBhzTadVsbmnYKpxwyVU89WCuQKvedz4k1vu7S1YryfNbmS8PWbnQ4ds 7 | 9NB7SgJNHZILvx9DXuWeFEyzRIo1984z4HheBzrkf791LqpYKaKziANUo8h8t0dm 8 | TX/boOz8cEnQNwtTC0ZX3aD0obG/UAhr/22ZGPo/E659fh4ptyYX2LrIUHGy+Eux 9 | nJ9Y4cTqa88Ee6K6AkDiT/AoNQNxE4X++jqLuie8j/ZYpI1Oll38GwKVOyy1msRL 10 | toGmISNwkMIQDGABrJlxgpP4QQAQ+08v9srzXOlkdxdr7OCP81r+ccBXiSQEe7BA 11 | kdJ8l98l5dprJ++GJ+SZcV4+/iGR0dKU2IdAG5HiKZIFn6ch9Ux+UMqeGaYCpkHr 12 | TiietHwcXgtVBlE0jFmB/HspmI/O0abK+grMmueaH7XtTI8YHnw0mUpL8+yp7mfA 13 | 7zFusgFgyiBPXeD/NQgg8vja67k++d1VGoXm2xr+5WPQCSbgQoMkkOBMLHWJTefd 14 | 6F4Z5M+oI0VwYbf6eQW246wJgpCHSPR0Vdijd6MAGRWKUuLfDsA9+12iGbKvwJ2e 15 | nJlStft2V2LZcjBfdIMbigW1aSVNN5w6m6YVrQPry3WPkWcCAwEAAaMhMB8wHQYD 16 | VR0OBBYEFPxKWTFQJSg4HD2qjxL0dnXX/z4qMA0GCSqGSIb3DQEBCwUAA4ICAQBz 17 | 4H1d5eGRU9bekUvi7LbZ5CP/I6w6PL/9AlXqO3BZKxplK7fYGHd3uqyDorJEsvjV 18 | hxwvFlEnS0JIU3nRzhJU/h4Yaivf1WLRFwGZ4TPBjX9KFU27exFWD3rppazkWybJ 19 | i4WuEdP3TJMdKLcNTtXWUDroDOgPlS66u6oZ+mUyUROil+B+fgQgVDhjRc5fvRgZ 20 | Lng8wuejCo3ExQyxkwn2G5guyIimgHmOQghPtLO5xlc67Z4GPUZ1m4tC+BCiFO4D 21 | YIXl3QiIpmU7Pss39LLKMGXXAgLRqyMzqE52lsznu18v5vDLfTaRH4u/wjzULhXz 22 | SrV1IUJmhgEXta4EeDmPH0itgKtkbwjgCOD7drrFrJq/EnvIaJ5cpxiI1pFmYD8g 23 | VVD7/KT/CyT1Uz1dI8QaP/JX8XEgtMJaSkPfjPErIViN9rh9ECCNLgFyv7Y0Plar 24 | A6YlvdyV1Rta/BHndf5Hqz9QWNhbFCMQRGVQNEcoKwpFyjAE9SXoKJvFIK/w5WXu 25 | qKzIYA26QXE3p734Xu1n8QiFJIyltVHbyUlD0k06194t5a2WK+/eDeReIsk0QOI8 26 | FGqhyPZ7YjR5tSZTmgljtViqBO5AA23QOVFqtjOUrjXP5pTbPJel99Z/FTkqSwvB 27 | Rt4OX7HfuokWQDTT0TMn5jVtJyi54cH7f9MmsNJ23g== 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/client/self-signed/csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEUzCCAjsCAQAwDjEMMAoGA1UEAwwDQm9iMIICIjANBgkqhkiG9w0BAQEFAAOC 3 | Ag8AMIICCgKCAgEA179eTsqcc1c3AOQHzCZEyYLPta2CCAscUFqcEZ9vWvjW0uzO 4 | v9TDlB33Unovjch4CElZOBhzTadVsbmnYKpxwyVU89WCuQKvedz4k1vu7S1YryfN 5 | bmS8PWbnQ4ds9NB7SgJNHZILvx9DXuWeFEyzRIo1984z4HheBzrkf791LqpYKaKz 6 | iANUo8h8t0dmTX/boOz8cEnQNwtTC0ZX3aD0obG/UAhr/22ZGPo/E659fh4ptyYX 7 | 2LrIUHGy+EuxnJ9Y4cTqa88Ee6K6AkDiT/AoNQNxE4X++jqLuie8j/ZYpI1Oll38 8 | GwKVOyy1msRLtoGmISNwkMIQDGABrJlxgpP4QQAQ+08v9srzXOlkdxdr7OCP81r+ 9 | ccBXiSQEe7BAkdJ8l98l5dprJ++GJ+SZcV4+/iGR0dKU2IdAG5HiKZIFn6ch9Ux+ 10 | UMqeGaYCpkHrTiietHwcXgtVBlE0jFmB/HspmI/O0abK+grMmueaH7XtTI8YHnw0 11 | mUpL8+yp7mfA7zFusgFgyiBPXeD/NQgg8vja67k++d1VGoXm2xr+5WPQCSbgQoMk 12 | kOBMLHWJTefd6F4Z5M+oI0VwYbf6eQW246wJgpCHSPR0Vdijd6MAGRWKUuLfDsA9 13 | +12iGbKvwJ2enJlStft2V2LZcjBfdIMbigW1aSVNN5w6m6YVrQPry3WPkWcCAwEA 14 | AaAAMA0GCSqGSIb3DQEBCwUAA4ICAQCb07d2IjUy1PeHCj/2k/z9FrZSo6K3c8y6 15 | b/u/MZ0AXPKLPDSo7UYpOJ8Z2cBiJ8jQapjTSEL8POUYqcvCmP55R6u68KmvINHo 16 | +Ly7pP+xPrbA4Q0WmPnz37hQn+I1he0GuEQyjZZqUln9zwp67TsWNKxKtCH+1j8M 17 | Ltzx6kuHCdPtDUtv291yhVRqvbjiDs+gzdQYNJtAkUbHwHFxu8oZhg8QZGyXYMN8 18 | TGoQ1LTezFZXJtX69K7WnrDGrjsgB6EMvwkqAFSYNH0LFvI0xo13OOgXr9mrwohA 19 | 76uZtjXL9B15EqrMce6mdUZi46QJuQ2avTi57Lz+fqvsBYdQO89VcFSmqu2nfspN 20 | QZDrooyjHrlls8MpoBd8fde9oT4uA4/d9SJtuHUnjgGN7Qr7eTruWXL8wVMwFnvL 21 | igWE4detO9y2gpRLq6uEqzWYMGtN9PXJCGU8C8m9E2EBUKMrT/bpNbboatLcgRrW 22 | acj0BRVqoVzk1sRq7Sa6ejywqgARvIhTehg6DqdMdcENCPQ7rxDRu5PSDM8/mwIj 23 | 0KYl8d2PlECB4ofRyLcy17BZzjP6hSnkGzcFk0/bChZOSIRnwvKbvfXnB45hhPk8 24 | XwT/6UNSwC2STP3gtOmLqrWj+OE0gy0AkDMvP3UnQVGMUvgfYg+N4ROCVtlqzxe9 25 | W65c05Mm1g== 26 | -----END CERTIFICATE REQUEST----- 27 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/client/self-signed/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | dg6m3MAyZ1IXjHlogOdmGvnq07/w 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/client/trusted/cert-legacy.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/client-certificates/client/trusted/cert-legacy.pfx -------------------------------------------------------------------------------- /tests/assets/client-certificates/client/trusted/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFAzCCAuugAwIBAgIBATANBgkqhkiG9w0BAQsFADA2MRIwEAYDVQQDDAlsb2Nh 3 | bGhvc3QxIDAeBgNVBAoMF0NsaWVudCBDZXJ0aWZpY2F0ZSBEZW1vMB4XDTI0MDcx 4 | OTEyNDczN1oXDTI1MDcxOTEyNDczN1owEDEOMAwGA1UEAwwFQWxpY2UwggIiMA0G 5 | CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCac3+4rNmH4/N1s4HqR2X168tgS/aA 6 | 6sHW5at8mWRnq54Nm11RvnK55jHQYVAdBgJy5M07w0wakp8inxzlY95wqxBimYG6 7 | 3Un/1p7mX9FkB4LNISCc6j/s/Ufv85MXPbn0S5rm9UcQO9cINJb1RP1YgDDLN5cx 8 | Mz6X4nyofN8H6Lhvh4JDdBw4DfDEFERkVfF+bkZ7YW4XHEChgzm3RxCF0eeGzIXG 9 | rkkK9AsSdJAhOvTlHPFCQKXTYZhsL5+3Ma4RnWnDWvLTHx6KzoU+twTM2mYhhQuQ 10 | gQpnmDHxGge8kGeHGtfdgAjtVJTE57xF/shP0JU+tuIV8NNhQ/vEmhL0Wa093/Ev 11 | pTVp0EUEuDh9ORRH5K5M4bKJyU4XX5noiht6yOn00uaoJcWduUAWsU+cDSvDTMw8 12 | 1opWWm0QIAV3G2yuRSkumHAKqvQLeyeyiKz+OEhyEiZ7EZNExPD0TSpApSTU6aCT 13 | UAvPYGQ59VjsMHTuJ9r4wKIYaDvfL+t72vg2vTQma5cTOBJfIdxH9blFTjEnToH3 14 | LX8t0XndQ2RkiRnIze2p2jUShxo/lWCjCw+2Iaw0A0fNUK1BbOrFRPq1u7AnEuMJ 15 | t7HF50MloItM97R9vofDwgDIzlX/PzlVRcn1WCo8Fr/0EXxPPreX0YDIp1ANQ8fS 16 | v7bKb2vQIxWuCQIDAQABo0IwQDAdBgNVHQ4EFgQUVJVRJJ2k/Z4r0M1AXe6agyD4 17 | uCwwHwYDVR0jBBgwFoAUEHtrxWCk96Ehr60E0HBuwLk2i+IwDQYJKoZIhvcNAQEL 18 | BQADggIBAGEvSkxhxRKmlvKG8wCXop2OaUUAOG16+T96vd+aFYaJNlfGoPvqv4Lw 19 | qaHztVktnRrJ//fpNWOsdxkE1uPU4uyGjl2KbyH81JvkE6A3OX0P4B01n8lcimY2 20 | j3oje6KjORUouYVsypD1VcwfWJgsE3U2Txv5srD8BoemVWgWbWjfyim4kk8C5zlf 21 | tWEazVAaI4MWecqtU4P5gIEomCI7MG9ebxYp5oQhRxeOndOYdUbSzAkZj50gXFA1 22 | +TNkvuhTFlJF0F7qIFVJSJTmJ+6E5B4ddbkyUYwbOdO+P8mz5N5mSljE+EiIQTxo 23 | AwbG8cSivMy/jI3h048tCUONAJzcSWCF4k1r9Qr6xbyW2ud2GmKiFCEYJkYTsMWV 24 | fM/RujTHlGvJ2+bQK5HiNyW0tO9znW9kaoxolu1YBvTh2492v3agK7nALyGGgdo1 25 | /nN/ikgkQiyaCpZwFeooJv1YFU5aDhR9RjIIJ9UbJ8FdAv8Xd00E3viunLTvqqXK 26 | RVMokw+tFQTEzjKofKWYArPDjB9LUbN+vQbumKalis3+NlJ3WolYPrCg55tqt1o3 27 | zXi+xv7120cJFouilRFwrafNFV6F+pRMkMmiWopMnoVJPVXcoqyJRcsmO62uslhg 28 | BLFgAH4H/14drYrgWIMz0no78RInEz0z507zwLkWk5d9W9pJ/4Rf 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/client/trusted/cert.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/client-certificates/client/trusted/cert.pfx -------------------------------------------------------------------------------- /tests/assets/client-certificates/client/trusted/csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEVTCCAj0CAQAwEDEOMAwGA1UEAwwFQWxpY2UwggIiMA0GCSqGSIb3DQEBAQUA 3 | A4ICDwAwggIKAoICAQCac3+4rNmH4/N1s4HqR2X168tgS/aA6sHW5at8mWRnq54N 4 | m11RvnK55jHQYVAdBgJy5M07w0wakp8inxzlY95wqxBimYG63Un/1p7mX9FkB4LN 5 | ISCc6j/s/Ufv85MXPbn0S5rm9UcQO9cINJb1RP1YgDDLN5cxMz6X4nyofN8H6Lhv 6 | h4JDdBw4DfDEFERkVfF+bkZ7YW4XHEChgzm3RxCF0eeGzIXGrkkK9AsSdJAhOvTl 7 | HPFCQKXTYZhsL5+3Ma4RnWnDWvLTHx6KzoU+twTM2mYhhQuQgQpnmDHxGge8kGeH 8 | GtfdgAjtVJTE57xF/shP0JU+tuIV8NNhQ/vEmhL0Wa093/EvpTVp0EUEuDh9ORRH 9 | 5K5M4bKJyU4XX5noiht6yOn00uaoJcWduUAWsU+cDSvDTMw81opWWm0QIAV3G2yu 10 | RSkumHAKqvQLeyeyiKz+OEhyEiZ7EZNExPD0TSpApSTU6aCTUAvPYGQ59VjsMHTu 11 | J9r4wKIYaDvfL+t72vg2vTQma5cTOBJfIdxH9blFTjEnToH3LX8t0XndQ2RkiRnI 12 | ze2p2jUShxo/lWCjCw+2Iaw0A0fNUK1BbOrFRPq1u7AnEuMJt7HF50MloItM97R9 13 | vofDwgDIzlX/PzlVRcn1WCo8Fr/0EXxPPreX0YDIp1ANQ8fSv7bKb2vQIxWuCQID 14 | AQABoAAwDQYJKoZIhvcNAQELBQADggIBAGgf3EC8WL3RGmuGA+d/4wd1jNfrfU6n 15 | xjnDwdEEX0TQZGGPjh5xvoCK76yZPkO6+z0IYSepEmWBS27HJKl7nuoOvS7MjQyJ 16 | C+3Bdk3ToCeQjmNBlRBKsUw5ftTU902oMl5BptHGj1KGjYBLAkPdXb44wXSVKJ8q 17 | ihFhWlovsva6GDoUorksU3vOwijdlGzTANQHJGFncgrRud9ATavpGS3KVxR73R3A 18 | aBbu3Qw+QIfu8Qx5eBJp8CbMrpAmjfuq17STvqr5bC10Fnn4NegrnHOQG9JcK02+ 19 | 5Bn3+9X/n1mue7aohIdErLEiDMSqMOwFfrJeaH6YM1G4QkWyqGugtmHsWOUf0nlU 20 | nkH1krvfw9rb6b+03c4A6GSeHnbX5ufFDSf5gaR6Wy7c0jBnoxVbtBLH2zXlrd0k 21 | iRQG7C6XZzGMS7hb7GL7+bkRy9kWjmDL7z7Fp+EgzKhNmzuWII3E9X9va33HoQ/Q 22 | UdK3JVToxRQg6XRKOxL9+U/+8i6U8lxObLWkWh2cypZqbz5qJxa+2u5JYO/KEoHZ 23 | G963UX7XWezR98vZuTc1XHGZtBDMrjjDd7Kmb4/i/xBPeWwseeGtzFy9z2pnEnkL 24 | uKE4C8wUNpzUUlsn4LneZXObIoErE7FqAAlVFujVe7iaJBmXoUXZR36drbfiaODK 25 | vwAGyrYHaOlR 26 | -----END CERTIFICATE REQUEST----- 27 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/client/trusted/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | Y+eu85cike5J4vnz8uG04yt7rXjIrUav 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/server/server_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 31 | 2Ct10prMWB/3 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/server/server_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQC+K5JWhlfvI47Z 3 | L/AzL0xnOl+cMelr2BqH+7XS8187SbvluhFfFkq/7V7rwgsHI64sn8pgRCOnqKWV 4 | 6jtb651dGzn7Nby6InmyOQzF4VwfSVWQ6BYXgXuryS9Gm0gi8sOL1Ji/jV49n1gz 5 | LyIxLNhd7NG2DCCedTHJnxyz4xq8MWhI/qI85iWJqcHhxkDb8wtH1Vd6nd/ZRVDb 6 | jgTvPH3EDK7JqmnYG9+x4Jz0yEhvV7jL3gNu2mIyttvm7oRna9oHgaKFUJt4BCfP 7 | bT5U3ipvcq29hdD5/5QIDzTWcExTnklolg5xpFext1+3KPSppESxcfBBNoL3h1B8 8 | ZcZalEMC/IoFUIDJQj5gmSn4okwMWIxgf+AL0609MKEqQ2FavOsvBmhHcQsqLk4M 9 | O/v0NGFv1/xGe4tUkX4han6ykf1+sqzupJT5qnUONmvghb2SpIt83o4j4KHVzZwk 10 | 8JK0N6hN7JEjXQwSKCh3b0FFg+kPAe12d6BBcsNzEYmt2C1KNPbXMX84zIkgPN01 11 | XMg6kdCdjP6DH7CK+brW9qQufOqYpd3eNhJyeBm+oP3PhnhEiMTIO8X2GdSN5Rxo 12 | zgxlVIj/QWhLV64r5AqPr/Vpd1vcsxrg3aS5CASmoWQmTPuhEZptRtrkPkGw7k9N 13 | PZ34lnRenvKJ9e3DXhXRMqeYUY6wjwIDAQABAoICABfDfxpj2EowUdHvDR+AShZe 14 | M4Njs00AKLSUbjCpq91PRfUbjr8onHemVGW2jkU6nrHB1/q2mRQC3YpBxmAirbvs 15 | Qo8TNH24ACgWu/NgSXA5bEFa1yPh0M/zKH60uctwNaJcEyhgpIWjy1Q+EBJADduS 16 | 09PhaRQUBgAxa1dJSlZ5ABSbCS/9/HPa7Djn2sQBd4fm73MJlmbipAuDkDdLAlZE 17 | 1XSq4GYaeZYTQNnPy0lql1OWbyxjisDWm90cMhxwXELy3pm1LHBPaKAhgRf+2SOr 18 | G23i8m3DE778E3i2eLs8POUeVzi5NiIljYboTcaDGfhoigLEKpJ+7L5Ww3YfL85Q 19 | xk00Y0b+cYNrlJ3vCpflDXJunZ1gJHLDTixJeVMpXnMSi01+bSb8D/PTcbG3fZ0U 20 | y4f2G0M+gf+m3EMMD96yerPf6jhGlTqY+eMyNVwNVk4BIG+D/8nf13keAF4kVbPJ 21 | QMidnCNbu8ZiC12HqLyv3YZlseXPIkhpbYEhsj58sbG4Tms+mG/zPlTZjroIEdAX 22 | nwI1aoG+NAbe+WSH/P4SvIMi1o/fWoXBtb+t7uy1AG/Xbu414WED7iwvxtqJRQj5 23 | rhrqryWTGQKY1zVJIOxwZP0f5gSIkEITyE+rO6o6pbAZFX7N0aMIvksBkEN5mdoV 24 | RWzxfSVNGMWooRD5d3TZAoIBAQD1dvgOsLYP8lUfkKglLTqHQe3x75BVDR9zdTIt 25 | tQh9UIbyovPFdLcXrHHJMBVMPTRGeRNpjCT5BNSNbidrmAxYN7YXuSA4uy3bubNU 26 | 76km5kmL2Ji+5u+qMm9Xycyqn30rLH9hT+9c/MVuPW6CNmETKX9+v9zb1v//RrBS 27 | 2ZNAWjJcBYv/rS/vKsW9yH/DbM21eSeokUqpkejOk1UxVZEcb9vt8VF8p+jO1wv3 28 | +UgI4Gfkf3sjEL1m/hBvH5Z49RHTFj4npeK6Lko4NLLazU2904jbHxppH51UNH1j 29 | xp8Is+iNwW2qCOve8kSUUUjxLn4n45D2d+5qOqQTtsMWXHanAoIBAQDGVQ6UZqvo 30 | djfcULq0Jub1xpBfxIAg7jSY7aZ6H0YlG7KgpVTd2TUEEKgErxtfYufjtLjjWb/d 31 | lMG7UpkM5B4tFnpRDmvevltCqGsM3qi3AtPnzavgz2TAQy7qd2gJc8glE965LOfb 32 | l+mGzE4SzeFJ9WS7sUDf4WnX2xjt3OA0VCvcBRNIwCnEvXu81XLKZL6etBx6zdCt 33 | whWHIiqa4wkjuWEwvbeH4aWsh8gFY3E5mbvDdMFtyGWvTK8OGivl3CkdQxM+MOJD 34 | 3aAEBTr0M7tSMy5IKewASlAWZEVpFFPIyiyMCTI0XcEgA7ewHw/F3c7cstgVktjm 35 | OYZytZPF0ZvZAoIBAB5+z0aT8ap9gtHPGPS1b8YKDNO33YiTfsrLTpabHRjkfj96 36 | uypW28BXLjO+g4bbO7ldpWnBfX5qeTWw77jQRQhYs4iy+SvTJVlc8siklbE9fvme 37 | ySs+aZwNdAPGEGVKNzS77H9cfPJifOy7ORV4SAsnZq2KjJfLWDaQw6snWMHv8r23 38 | +rKjA4eFGtf/JtBSniPjj2fD1TDH7dJsP3NHnCWaSAqBpowEGEpKMTR3hdmEd6PN 39 | qrCqjb1T5xrHI9yXJcXBx6sJUueqhJIDCg1g4D2rIB+I97EDunoRo1pX/L4KC+RA 40 | ma08OoGSO67pglRkYEv4W7QjJj2QV34TgJ0wk5UCggEALINom0wT5z+pN+xyiv50 41 | NdNUEfpzW3C7I1urUpt0Td3SkJWq34Phj0EBxNNcTGNRclzcZkJ9eojpllZqfWcx 42 | kqMJ3ulisoJ8zxAnvqK2sSSUVOFnYzSJA1HQ1NTp570xvYihI2R9wV5uDlAKcdP9 43 | bXEDI9Ebo2PfMpA9Hx3EwFnn4iDNfDWM6lgwzmgFtIE5+zqnbbSF0onN9R9o+oxc 44 | P8Val+rspzWwznFHJlZ0Uh478xlgVHh2wgpu+7ZKBfQM0kF8ryefkOXMBTr7SVXX 45 | BBLyn0Wxbzs+kFf+8B+c0mL17pQdzX0BXGMZNhEypBEtXYFSWD02Ky3cDCDOwsZR 46 | uQKCAQAKQtsUSO80N/kzsWuSxHhuLMTvNZfiE/qK1Mz5Rw1qXxMXfYNFZbU/MqW7 47 | 5DLd4Kn7s3v1UlBn2tbLGLzghnHYRxT9kxF7ZnY6HZv2IrEUjE2I2YTTCQr/Q7Z5 48 | gRBQb5z+vJbKOYnlSHurTexKmuTjgJ/y/jRQiQABccVj1w5lIm1SPoxpdKzSFyWt 49 | 0NVmff9VetoiWKJYldPBTOmqPUytuBZyX5fJ4pPixwgAns6ZaqJtVNyMZkZ/GoDk 50 | XP2CvB/HyMiS7vXK5QJYYumk7oyC15H6eDChITNPV3VGH2QqcdEvDLT81W+JZ2mX 51 | 8ynLaTs3oV3BjQya9pAUyzIX5L67 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /tests/assets/consolelog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | console.log test 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/assets/csp.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/css-transition.html: -------------------------------------------------------------------------------- 1 | 2 | 16 |
17 | 22 | -------------------------------------------------------------------------------- /tests/assets/csscoverage/Dosis-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/csscoverage/Dosis-Regular.ttf -------------------------------------------------------------------------------- /tests/assets/csscoverage/involved.html: -------------------------------------------------------------------------------- 1 | 24 |
woof!
25 | fancy text 26 | 27 | -------------------------------------------------------------------------------- /tests/assets/csscoverage/media.html: -------------------------------------------------------------------------------- 1 | 3 |
hello, world
4 | 5 | -------------------------------------------------------------------------------- /tests/assets/csscoverage/multiple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /tests/assets/csscoverage/simple.html: -------------------------------------------------------------------------------- 1 | 5 |
hello, world
6 | 7 | -------------------------------------------------------------------------------- /tests/assets/csscoverage/sourceurl.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /tests/assets/csscoverage/stylesheet1.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/csscoverage/stylesheet2.css: -------------------------------------------------------------------------------- 1 | html { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | -------------------------------------------------------------------------------- /tests/assets/csscoverage/unused.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /tests/assets/deep-shadow.html: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /tests/assets/detect-touch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Detect Touch Test 5 | 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/assets/digits/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/digits/0.png -------------------------------------------------------------------------------- /tests/assets/digits/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/digits/1.png -------------------------------------------------------------------------------- /tests/assets/digits/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/digits/2.png -------------------------------------------------------------------------------- /tests/assets/digits/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/digits/3.png -------------------------------------------------------------------------------- /tests/assets/digits/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/digits/4.png -------------------------------------------------------------------------------- /tests/assets/digits/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/digits/5.png -------------------------------------------------------------------------------- /tests/assets/digits/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/digits/6.png -------------------------------------------------------------------------------- /tests/assets/digits/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/digits/7.png -------------------------------------------------------------------------------- /tests/assets/digits/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/digits/8.png -------------------------------------------------------------------------------- /tests/assets/digits/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/digits/9.png -------------------------------------------------------------------------------- /tests/assets/dom.html: -------------------------------------------------------------------------------- 1 |
Text, 2 | more text
3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/assets/download-blob.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Blob Download Example 5 | 6 | 7 | 27 | Download 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/assets/drag-n-drop.html: -------------------------------------------------------------------------------- 1 | 31 | 32 | 50 | 51 | 52 |
53 |

54 | Select this element, drag it to the Drop Zone and then release the selection to move the element.

55 |
56 |
Drop Zone
57 | 58 | -------------------------------------------------------------------------------- /tests/assets/dummy_bad_browser_executable.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.exit(1); 4 | -------------------------------------------------------------------------------- /tests/assets/dynamic-oopif.html: -------------------------------------------------------------------------------- 1 | 13 | 32 | -------------------------------------------------------------------------------- /tests/assets/empty-standard-mode.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/empty.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/empty.html -------------------------------------------------------------------------------- /tests/assets/empty.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/empty.pdf -------------------------------------------------------------------------------- /tests/assets/error.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /tests/assets/es6/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module" 4 | } 5 | } -------------------------------------------------------------------------------- /tests/assets/es6/es6import.js: -------------------------------------------------------------------------------- 1 | import num from './es6module.js'; 2 | window.__es6injected = num; -------------------------------------------------------------------------------- /tests/assets/es6/es6module.js: -------------------------------------------------------------------------------- 1 | export default 42; -------------------------------------------------------------------------------- /tests/assets/es6/es6pathimport.js: -------------------------------------------------------------------------------- 1 | import num from './es6/es6module.js'; 2 | window.__es6injected = num; -------------------------------------------------------------------------------- /tests/assets/example.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/example.mp3 -------------------------------------------------------------------------------- /tests/assets/file-to-upload-2.txt: -------------------------------------------------------------------------------- 1 | contents of the file -------------------------------------------------------------------------------- /tests/assets/file-to-upload.txt: -------------------------------------------------------------------------------- 1 | contents of the file -------------------------------------------------------------------------------- /tests/assets/formatted-number.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /tests/assets/frames/child-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/frames/frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 19 |
Hi, I'm frame
20 | -------------------------------------------------------------------------------- /tests/assets/frames/frameset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/assets/frames/lazy-frame.html: -------------------------------------------------------------------------------- 1 |
One
2 |
Two
3 |
Three
4 |
Four
5 |
Five
6 | 7 | -------------------------------------------------------------------------------- /tests/assets/frames/nested-frames.html: -------------------------------------------------------------------------------- 1 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/assets/frames/one-frame.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/frames/redirect-my-parent.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/assets/frames/script.js: -------------------------------------------------------------------------------- 1 | console.log('Cheers!'); 2 | -------------------------------------------------------------------------------- /tests/assets/frames/style.css: -------------------------------------------------------------------------------- 1 | div { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/frames/two-frames.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/assets/geolocation.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /tests/assets/global-var.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/assets/grid-iframe-in-shadow.html: -------------------------------------------------------------------------------- 1 | 6 | 17 | -------------------------------------------------------------------------------- /tests/assets/grid.html: -------------------------------------------------------------------------------- 1 | 28 | 29 | 63 | -------------------------------------------------------------------------------- /tests/assets/har-sha1-main-response.txt: -------------------------------------------------------------------------------- 1 | Hello, world -------------------------------------------------------------------------------- /tests/assets/har-sha1.har: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "version": "1.2", 4 | "creator": { 5 | "name": "Playwright", 6 | "version": "1.23.0-next" 7 | }, 8 | "browser": { 9 | "name": "chromium", 10 | "version": "103.0.5060.33" 11 | }, 12 | "pages": [ 13 | { 14 | "startedDateTime": "2022-06-10T04:27:32.125Z", 15 | "id": "page@b17b177f1c2e66459db3dcbe44636ffd", 16 | "title": "Hey", 17 | "pageTimings": { 18 | "onContentLoad": 70, 19 | "onLoad": 70 20 | } 21 | } 22 | ], 23 | "entries": [ 24 | { 25 | "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", 26 | "_monotonicTime": 270572145.898, 27 | "startedDateTime": "2022-06-10T04:27:32.146Z", 28 | "time": 8.286, 29 | "request": { 30 | "method": "GET", 31 | "url": "http://no.playwright/", 32 | "httpVersion": "HTTP/1.1", 33 | "cookies": [], 34 | "headers": [ 35 | { 36 | "name": "Accept", 37 | "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" 38 | }, 39 | { 40 | "name": "Upgrade-Insecure-Requests", 41 | "value": "1" 42 | }, 43 | { 44 | "name": "User-Agent", 45 | "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" 46 | } 47 | ], 48 | "queryString": [], 49 | "headersSize": 326, 50 | "bodySize": 0 51 | }, 52 | "response": { 53 | "status": 200, 54 | "statusText": "OK", 55 | "httpVersion": "HTTP/1.1", 56 | "cookies": [], 57 | "headers": [ 58 | { 59 | "name": "content-length", 60 | "value": "12" 61 | }, 62 | { 63 | "name": "content-type", 64 | "value": "text/html" 65 | } 66 | ], 67 | "content": { 68 | "size": 12, 69 | "mimeType": "text/html", 70 | "compression": 0, 71 | "_file": "har-sha1-main-response.txt" 72 | }, 73 | "headersSize": 64, 74 | "bodySize": 71, 75 | "redirectURL": "", 76 | "_transferSize": 71 77 | }, 78 | "cache": { 79 | "beforeRequest": null, 80 | "afterRequest": null 81 | }, 82 | "timings": { 83 | "dns": -1, 84 | "connect": -1, 85 | "ssl": -1, 86 | "send": 0, 87 | "wait": 8.286, 88 | "receive": -1 89 | }, 90 | "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", 91 | "_securityDetails": {} 92 | } 93 | ] 94 | } 95 | } -------------------------------------------------------------------------------- /tests/assets/har.html: -------------------------------------------------------------------------------- 1 | HAR Page 2 | 3 |
hello, world!
4 | -------------------------------------------------------------------------------- /tests/assets/headings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Headings 6 | 7 | 8 | 9 |

Title

10 |

Subtitle

11 |

Subsubtitle

12 |

Subtitle

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/assets/highdpi.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /tests/assets/historyapi.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tests/assets/injectedfile.js: -------------------------------------------------------------------------------- 1 | window.__injected = 42; 2 | window.injected = 123; 3 | window.__injectedError = new Error('hi'); -------------------------------------------------------------------------------- /tests/assets/injectedstyle.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/input/animating-button.html: -------------------------------------------------------------------------------- 1 | 8 | 43 | -------------------------------------------------------------------------------- /tests/assets/input/button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Button test 5 | 6 | 7 | 8 | 9 | 31 | 32 | -------------------------------------------------------------------------------- /tests/assets/input/checkbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Selection Test 5 | 6 | 7 | 8 | 9 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/assets/input/drag-n-drop-manual.html: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |
13 | Drop here 14 |
15 |
16 | Drag me 17 |
18 |
19 | 20 | 45 | -------------------------------------------------------------------------------- /tests/assets/input/fileupload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | File upload test 5 | 6 | 7 |
8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /tests/assets/input/folderupload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Folder upload test 5 | 6 | 7 |
8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /tests/assets/input/handle-locator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Interstitial test 5 | 6 | 7 | 37 |
38 |
A place on the side to hover
39 |
40 |
This interstitial covers the button
41 | 42 |
43 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /tests/assets/input/keyboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Keyboard test 5 | 6 | 7 | 8 | 41 | 42 | -------------------------------------------------------------------------------- /tests/assets/input/mouse-helper.js: -------------------------------------------------------------------------------- 1 | // This injects a box into the page that moves with the mouse; 2 | // Useful for debugging 3 | (function(){ 4 | const box = document.createElement('div'); 5 | box.classList.add('mouse-helper'); 6 | const styleElement = document.createElement('style'); 7 | styleElement.innerHTML = ` 8 | .mouse-helper { 9 | pointer-events: none; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | width: 20px; 14 | height: 20px; 15 | background: rgba(0,0,0,.4); 16 | border: 1px solid white; 17 | border-radius: 10px; 18 | margin-left: -10px; 19 | margin-top: -10px; 20 | transition: background .2s, border-radius .2s, border-color .2s; 21 | } 22 | .mouse-helper.button-1 { 23 | transition: none; 24 | background: rgba(0,0,0,0.9); 25 | } 26 | .mouse-helper.button-2 { 27 | transition: none; 28 | border-color: rgba(0,0,255,0.9); 29 | } 30 | .mouse-helper.button-3 { 31 | transition: none; 32 | border-radius: 4px; 33 | } 34 | .mouse-helper.button-4 { 35 | transition: none; 36 | border-color: rgba(255,0,0,0.9); 37 | } 38 | .mouse-helper.button-5 { 39 | transition: none; 40 | border-color: rgba(0,255,0,0.9); 41 | } 42 | `; 43 | document.head.appendChild(styleElement); 44 | document.body.appendChild(box); 45 | document.addEventListener('mousemove', event => { 46 | box.style.left = event.pageX + 'px'; 47 | box.style.top = event.pageY + 'px'; 48 | updateButtons(event.buttons); 49 | }, true); 50 | document.addEventListener('mousedown', event => { 51 | updateButtons(event.buttons); 52 | box.classList.add('button-' + event.which); 53 | }, true); 54 | document.addEventListener('mouseup', event => { 55 | updateButtons(event.buttons); 56 | box.classList.remove('button-' + event.which); 57 | }, true); 58 | function updateButtons(buttons) { 59 | for (let i = 0; i < 5; i++) 60 | box.classList.toggle('button-' + i, buttons & (1 << i)); 61 | } 62 | })(); 63 | -------------------------------------------------------------------------------- /tests/assets/input/rotatedButton.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rotated button test 5 | 6 | 7 | 8 | 9 | 14 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/assets/input/scrollable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollable test 5 | 6 | 7 | 8 | 22 | 23 | -------------------------------------------------------------------------------- /tests/assets/input/scrollable2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollable test 5 | 6 | 7 | 8 | 22 | 23 | -------------------------------------------------------------------------------- /tests/assets/input/select.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Selection Test 5 | 6 | 7 | 24 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tests/assets/input/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Textarea test 5 | 6 | 7 | 8 | 9 |
10 |
Plain div
11 | 12 | 19 | 20 | -------------------------------------------------------------------------------- /tests/assets/input/touches.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Touch test 5 | 6 | 7 | 8 | 9 | 34 | 35 | -------------------------------------------------------------------------------- /tests/assets/jscoverage/eval.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/jscoverage/involved.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /tests/assets/jscoverage/multiple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tests/assets/jscoverage/ranges.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /tests/assets/jscoverage/script1.js: -------------------------------------------------------------------------------- 1 | console.log(3); 2 | -------------------------------------------------------------------------------- /tests/assets/jscoverage/script2.js: -------------------------------------------------------------------------------- 1 | console.log(3); 2 | -------------------------------------------------------------------------------- /tests/assets/jscoverage/simple.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /tests/assets/jscoverage/sourceurl.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /tests/assets/jscoverage/unused.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/load-event/load-event.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Load Event Test 7 | 8 | 9 | 18 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /tests/assets/load-event/module.js: -------------------------------------------------------------------------------- 1 | import {foo} from '/slow.js'; 2 | console.log('foo is', foo); 3 | window.results.push('module'); 4 | -------------------------------------------------------------------------------- /tests/assets/media-query-prefers-color-scheme.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/assets/mobile.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/modernizr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | -------------------------------------------------------------------------------- /tests/assets/movie.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/movie.mp4 -------------------------------------------------------------------------------- /tests/assets/movie.ogv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/movie.ogv -------------------------------------------------------------------------------- /tests/assets/mui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 13 | 14 | -------------------------------------------------------------------------------- /tests/assets/networkidle-frame.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/networkidle.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/networkidle.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | window.ws = new WebSocket('ws://localhost:' + window.location.port + '/ws'); 3 | window.ws.addEventListener('message', message => {}); 4 | 5 | fetch('fetch-request-a.js'); 6 | window.top.fetchSecond = () => { 7 | // Do not return the promise here. 8 | fetch('fetch-request-b.js'); 9 | }; 10 | } 11 | 12 | main(); 13 | -------------------------------------------------------------------------------- /tests/assets/offscreenbuttons.html: -------------------------------------------------------------------------------- 1 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | 56 | -------------------------------------------------------------------------------- /tests/assets/one-style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: pink; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/one-style.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
hello, world!
4 | -------------------------------------------------------------------------------- /tests/assets/overflow-large.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /tests/assets/overflow.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /tests/assets/player.html: -------------------------------------------------------------------------------- 1 | 55 | 56 | 60 | 61 | -------------------------------------------------------------------------------- /tests/assets/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Playground 5 | 6 | 7 | 8 | 9 |
First div
10 |
11 | Second div 12 | Inner span 13 |
14 | 15 | -------------------------------------------------------------------------------- /tests/assets/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popup 5 | 8 | 9 | 10 | I am a popup 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/assets/popup/window-open.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popup test 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/assets/pptr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/pptr.png -------------------------------------------------------------------------------- /tests/assets/redirectloop1.html: -------------------------------------------------------------------------------- 1 | > 10 | -------------------------------------------------------------------------------- /tests/assets/redirectloop2.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tests/assets/resetcss.html: -------------------------------------------------------------------------------- 1 | 51 | -------------------------------------------------------------------------------- /tests/assets/rotate-pseudo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 28 | 29 | 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/assets/rotate-z-shadow-dom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 31 | 32 | -------------------------------------------------------------------------------- /tests/assets/rotate-z.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/assets/screenshots/canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | -------------------------------------------------------------------------------- /tests/assets/screenshots/controls.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |

4 |

5 |

6 |

7 |

8 | -------------------------------------------------------------------------------- /tests/assets/screenshots/translateZ.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
-------------------------------------------------------------------------------- /tests/assets/screenshots/webgl.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 64 | -------------------------------------------------------------------------------- /tests/assets/sectionselectorengine.js: -------------------------------------------------------------------------------- 1 | ({ 2 | query(root, selector) { 3 | return root.querySelector('section'); 4 | }, 5 | queryAll(root, selector) { 6 | return Array.from(root.querySelectorAll('section')); 7 | } 8 | }) -------------------------------------------------------------------------------- /tests/assets/selenium-grid/broken-selenium-driver.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.exit(1); 4 | -------------------------------------------------------------------------------- /tests/assets/selenium-grid/selenium-config-standalone.json: -------------------------------------------------------------------------------- 1 | { 2 | "capabilities": 3 | [ 4 | { 5 | "browserName": "chrome", 6 | "maxInstances": 5, 7 | "seleniumProtocol": "WebDriver" 8 | } 9 | ], 10 | "role": "standalone", 11 | "port": 4444, 12 | "debug": false, 13 | "browserTimeout": 0, 14 | "timeout": 1800, 15 | "enablePassThrough": true, 16 | "server": { 17 | "port": 4444 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/assets/self-request.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/empty/sw.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/empty/sw.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/serviceworkers/empty/sw.js -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetch/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: pink; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetch/sw.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetch/sw.js: -------------------------------------------------------------------------------- 1 | self.intercepted = []; 2 | 3 | self.addEventListener('fetch', event => { 4 | self.intercepted.push(event.request.url) 5 | event.respondWith(fetch(event.request)); 6 | }); 7 | 8 | self.addEventListener('activate', event => { 9 | event.waitUntil(clients.claim()); 10 | }); 11 | 12 | fetch('/request-from-within-worker.txt') 13 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetchdummy/sw.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetchdummy/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', event => { 2 | if (event.request.url.endsWith('.html') || event.request.url.includes('passthrough')) { 3 | event.respondWith(fetch(event.request)); 4 | return; 5 | } 6 | if (event.request.url.includes('error')) { 7 | event.respondWith(Promise.reject(new Error('uh oh'))); 8 | return; 9 | } 10 | const slash = event.request.url.lastIndexOf('/'); 11 | const name = event.request.url.substring(slash + 1); 12 | const blob = new Blob(["responseFromServiceWorker:" + name], {type : 'text/css'}); 13 | const response = new Response(blob, { "status" : 200 , "statusText" : "OK" }); 14 | event.respondWith(response); 15 | }); 16 | 17 | self.addEventListener('activate', event => { 18 | event.waitUntil(clients.claim()); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/assets/shadow-dom-link.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 23 | 26 | Sign up 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/assets/shadow.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /tests/assets/simple-extension/content-script.js: -------------------------------------------------------------------------------- 1 | console.log('hey from the content-script'); 2 | self.thisIsTheContentScript = true; 3 | 4 | -------------------------------------------------------------------------------- /tests/assets/simple-extension/index.js: -------------------------------------------------------------------------------- 1 | // Mock script for background extension 2 | window.MAGIC = 42; 3 | -------------------------------------------------------------------------------- /tests/assets/simple-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Simple extension", 3 | "version": "0.1", 4 | "background": { 5 | "scripts": ["index.js"] 6 | }, 7 | "content_scripts": [{ 8 | "matches": [""], 9 | "css": [], 10 | "js": ["content-script.js"] 11 | }], 12 | "permissions": ["background", "activeTab"], 13 | "manifest_version": 2 14 | } 15 | -------------------------------------------------------------------------------- /tests/assets/simple.json: -------------------------------------------------------------------------------- 1 | {"foo": "bar"} 2 | -------------------------------------------------------------------------------- /tests/assets/tamperable.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/assets/title.html: -------------------------------------------------------------------------------- 1 | 2 | Woof-Woof 3 | -------------------------------------------------------------------------------- /tests/assets/video.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 |

11 | Video courtesy of 12 | Big Buck Bunny. 13 |

14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/assets/video_mp4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 |

10 | Video courtesy of 11 | Big Buck Bunny. 12 |

13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/assets/wasm/table2.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /tests/assets/wasm/table2.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/wasm/table2.wasm -------------------------------------------------------------------------------- /tests/assets/web-animation.html: -------------------------------------------------------------------------------- 1 | 2 | 12 |
13 | 24 | -------------------------------------------------------------------------------- /tests/assets/webfont/README.md: -------------------------------------------------------------------------------- 1 | This icon font was generated: 2 | - using SVG icons from https://github.com/primer/octicons 3 | - bundling icons into webfonts using https://github.com/fontello/fontello 4 | -------------------------------------------------------------------------------- /tests/assets/webfont/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2022 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/assets/webfont/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/assets/webfont/iconfont.woff2 -------------------------------------------------------------------------------- /tests/assets/webfont/webfont.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | +- 18 | 19 | -------------------------------------------------------------------------------- /tests/assets/window-stop.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /tests/assets/worker/import-me.js: -------------------------------------------------------------------------------- 1 | console.log("hello from import-me.js"); 2 | -------------------------------------------------------------------------------- /tests/assets/worker/worker-http-import.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Remote importScripts Test 5 | 6 | 7 |
-
8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/assets/worker/worker-http-import.js: -------------------------------------------------------------------------------- 1 | console.log("hello from worker-http-import.js"); 2 | importScripts("./import-me.js") 3 | console.log("successfully imported"); 4 | self.postMessage("finished"); 5 | -------------------------------------------------------------------------------- /tests/assets/worker/worker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Worker test 5 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /tests/assets/worker/worker.js: -------------------------------------------------------------------------------- 1 | console.log('hello from the worker'); 2 | 3 | function workerFunction() { 4 | return 'worker function result'; 5 | } 6 | 7 | self.addEventListener('message', event => { 8 | console.log('got this data: ' + event.data); 9 | }); 10 | 11 | (async function() { 12 | while (true) { 13 | self.postMessage(workerFunction.toString()); 14 | await new Promise(x => setTimeout(x, 100)); 15 | } 16 | })(); -------------------------------------------------------------------------------- /tests/assets/wrappedlink.html: -------------------------------------------------------------------------------- 1 | 25 |
26 | 123321 27 |
28 | 33 | -------------------------------------------------------------------------------- /tests/cdp_session_test.go: -------------------------------------------------------------------------------- 1 | package playwright_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestCDPSessionSend(t *testing.T) { 10 | BeforeEach(t) 11 | 12 | cdpSession, err := browser.NewBrowserCDPSession() 13 | if isChromium { 14 | require.NoError(t, err) 15 | result, err := cdpSession.Send("Target.getTargets", nil) 16 | require.NoError(t, err) 17 | targetInfos := result.(map[string]interface{})["targetInfos"].([]interface{}) 18 | require.GreaterOrEqual(t, len(targetInfos), 1) 19 | } else { 20 | require.Error(t, err) 21 | } 22 | } 23 | 24 | func TestCDPSessionOn(t *testing.T) { 25 | BeforeEach(t) 26 | 27 | cdpSession, err := page.Context().NewCDPSession(page) 28 | if isChromium { 29 | require.NoError(t, err) 30 | _, err = cdpSession.Send("Console.enable", nil) 31 | require.NoError(t, err) 32 | cdpSession.On("Console.messageAdded", func(params map[string]interface{}) { 33 | require.NotNil(t, params) 34 | }) 35 | _, err = page.Evaluate(`console.log("hello")`) 36 | require.NoError(t, err) 37 | require.NoError(t, cdpSession.Detach()) 38 | } else { 39 | require.Error(t, err) 40 | } 41 | } 42 | 43 | func TestCDPSessionDetach(t *testing.T) { 44 | BeforeEach(t) 45 | 46 | cdpSession, err := browser.NewBrowserCDPSession() 47 | if isChromium { 48 | require.NoError(t, err) 49 | require.NoError(t, cdpSession.Detach()) 50 | } else { 51 | require.Error(t, err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/dialog_test.go: -------------------------------------------------------------------------------- 1 | package playwright_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/playwright-community/playwright-go" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestDialog(t *testing.T) { 11 | BeforeEach(t) 12 | 13 | page.OnDialog(func(dialog playwright.Dialog) { 14 | require.Equal(t, "alert", dialog.Type()) 15 | require.Equal(t, "", dialog.DefaultValue()) 16 | require.Equal(t, "yo", dialog.Message()) 17 | require.NoError(t, dialog.Accept()) 18 | }) 19 | _, err := page.Evaluate("alert('yo')") 20 | require.NoError(t, err) 21 | } 22 | 23 | func TestDialogDismiss(t *testing.T) { 24 | BeforeEach(t) 25 | 26 | page.OnDialog(func(dialog playwright.Dialog) { 27 | require.NoError(t, dialog.Dismiss()) 28 | }) 29 | result, err := page.Evaluate("prompt('question?')") 30 | require.NoError(t, err) 31 | require.Equal(t, result, nil) 32 | } 33 | 34 | func TestDialogAcceptWithText(t *testing.T) { 35 | BeforeEach(t) 36 | 37 | page.OnDialog(func(dialog playwright.Dialog) { 38 | require.NoError(t, dialog.Accept("hey foobar")) 39 | }) 40 | result, err := page.Evaluate("prompt('question?')") 41 | require.NoError(t, err) 42 | require.Equal(t, result, "hey foobar") 43 | } 44 | 45 | func TestDialogShouldWorkInPopup(t *testing.T) { 46 | BeforeEach(t) 47 | 48 | var d playwright.Dialog 49 | context.OnDialog(func(dialog playwright.Dialog) { 50 | d = dialog 51 | require.NoError(t, dialog.Accept("hello")) 52 | }) 53 | 54 | popup, err := page.ExpectPopup(func() error { 55 | ret, err := page.Evaluate("() => window.open('').prompt('hey?')") 56 | require.NoError(t, err) 57 | require.Equal(t, "hello", ret) 58 | return nil 59 | }) 60 | require.NoError(t, err) 61 | require.Equal(t, "hey?", d.Message()) 62 | require.Equal(t, d.Page(), popup) 63 | } 64 | -------------------------------------------------------------------------------- /tests/download_test.go: -------------------------------------------------------------------------------- 1 | package playwright_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestDownloadBasic(t *testing.T) { 15 | BeforeEach(t) 16 | 17 | server.SetRoute("/downloadWithFilename", func(w http.ResponseWriter, r *http.Request) { 18 | w.Header().Add("Content-Type", "application/octet-stream") 19 | w.Header().Add("Content-Disposition", "attachment; filename=file.txt") 20 | if _, err := w.Write([]byte("foobar")); err != nil { 21 | log.Printf("could not write: %v", err) 22 | } 23 | }) 24 | require.NoError(t, page.SetContent( 25 | fmt.Sprintf(`download`, server.PREFIX), 26 | )) 27 | 28 | download, err := page.ExpectDownload(func() error { 29 | return page.Locator("a").Click() 30 | }) 31 | require.NoError(t, err) 32 | require.Equal(t, page, download.Page()) 33 | require.Equal(t, download.URL(), fmt.Sprintf("%s/downloadWithFilename", server.PREFIX)) 34 | require.Equal(t, download.SuggestedFilename(), "file.txt") 35 | require.Equal(t, download.String(), "file.txt") 36 | require.NoError(t, download.Failure()) 37 | 38 | file, err := download.Path() 39 | require.NoError(t, err) 40 | require.FileExists(t, file) 41 | 42 | tmpFile := filepath.Join(t.TempDir(), download.SuggestedFilename()) 43 | require.NoFileExists(t, tmpFile) 44 | require.NoError(t, download.SaveAs(tmpFile)) 45 | require.FileExists(t, tmpFile) 46 | 47 | require.NoError(t, download.Delete()) 48 | require.NoFileExists(t, file) 49 | } 50 | 51 | func TestDownloadCancel(t *testing.T) { 52 | BeforeEach(t) 53 | 54 | server.SetRoute("/downloadWithDelay", func(w http.ResponseWriter, r *http.Request) { 55 | w.Header().Add("Content-Type", "application/octet-stream") 56 | w.Header().Add("Content-Disposition", "attachment") 57 | if _, err := w.Write([]byte(strings.Repeat("foobar", 8192))); err != nil { 58 | log.Printf("could not write: %v", err) 59 | } 60 | if h, ok := w.(http.Hijacker); ok { 61 | if _, _, err := h.Hijack(); err != nil { 62 | log.Printf("could not hijack connection: %v", err) 63 | } 64 | } 65 | }) 66 | require.NoError(t, page.SetContent( 67 | fmt.Sprintf(`download`, server.PREFIX), 68 | )) 69 | download, err := page.ExpectDownload(func() error { 70 | return page.Locator("a").Click() 71 | }) 72 | require.NoError(t, err) 73 | require.NoError(t, download.Cancel()) 74 | require.Error(t, download.Failure(), "canceled") 75 | } 76 | -------------------------------------------------------------------------------- /tests/file_chooser_test.go: -------------------------------------------------------------------------------- 1 | package playwright_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/playwright-community/playwright-go" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestFileChooser(t *testing.T) { 12 | BeforeEach(t) 13 | 14 | _, err := page.Goto(server.PREFIX + "/input/fileupload.html") 15 | require.NoError(t, err) 16 | //nolint:staticcheck 17 | input, err := page.QuerySelector("input") 18 | require.NoError(t, err) 19 | file, err := os.ReadFile(Asset("file-to-upload.txt")) 20 | require.NoError(t, err) 21 | //nolint:staticcheck 22 | require.NoError(t, input.SetInputFiles([]playwright.InputFile{ 23 | { 24 | Name: "file-to-upload.txt", 25 | MimeType: "text/plain", 26 | Buffer: file, 27 | }, 28 | })) 29 | fileName, err := page.Evaluate("e => e.files[0].name", input) 30 | require.NoError(t, err) 31 | require.Equal(t, "file-to-upload.txt", fileName) 32 | content, err := page.Evaluate(`e => { 33 | reader = new FileReader() 34 | promise = new Promise(fulfill => reader.onload = fulfill) 35 | reader.readAsText(e.files[0]) 36 | return promise.then(() => reader.result) 37 | }`, input) 38 | require.NoError(t, err) 39 | require.Equal(t, "contents of the file", content) 40 | } 41 | 42 | func TestFileChooserShouldEmitEvent(t *testing.T) { 43 | BeforeEach(t) 44 | 45 | _, err := page.Goto(server.EMPTY_PAGE) 46 | require.NoError(t, err) 47 | require.NoError(t, page.SetContent("")) 48 | 49 | fileChooser, err := page.ExpectFileChooser(func() error { 50 | return page.Locator("input").Click() 51 | }) 52 | require.NoError(t, err) 53 | require.False(t, fileChooser.IsMultiple()) 54 | require.Equal(t, page, fileChooser.Page()) 55 | //nolint:staticcheck 56 | elementHTML, err := fileChooser.Element().InnerHTML() 57 | require.NoError(t, err) 58 | //nolint:staticcheck 59 | inputElement, err := page.QuerySelector("input") 60 | require.NoError(t, err) 61 | //nolint:staticcheck 62 | inputElementHTML, err := inputElement.InnerHTML() 63 | require.NoError(t, err) 64 | 65 | require.Equal(t, elementHTML, inputElementHTML) 66 | 67 | require.NoError(t, fileChooser.SetFiles([]playwright.InputFile{ 68 | { 69 | Name: "file-to-upload.txt", 70 | MimeType: "text/plain", 71 | Buffer: []byte("123"), 72 | }, 73 | })) 74 | fileName, err := page.Evaluate("e => e.files[0].name", inputElement) 75 | require.NoError(t, err) 76 | require.Equal(t, "file-to-upload.txt", fileName) 77 | } 78 | -------------------------------------------------------------------------------- /tests/page_request_gc_test.go: -------------------------------------------------------------------------------- 1 | package playwright_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestPageRequestGC(t *testing.T) { 10 | BeforeEach(t) 11 | 12 | _, err := page.Evaluate(`() => { 13 | globalThis.objectToDestroy = { hello: 'world' }; 14 | globalThis.weakRef = new WeakRef(globalThis.objectToDestroy); 15 | }`) 16 | require.NoError(t, err) 17 | 18 | require.NoError(t, page.RequestGC()) 19 | ret, err := page.Evaluate(`() => globalThis.weakRef.deref()`) 20 | require.NoError(t, err) 21 | require.Equal(t, map[string]interface{}{"hello": "world"}, ret) 22 | 23 | require.NoError(t, page.RequestGC()) 24 | ret, err = page.Evaluate(`() => globalThis.weakRef.deref()`) 25 | require.NoError(t, err) 26 | require.Equal(t, map[string]interface{}{"hello": "world"}, ret) 27 | 28 | _, err = page.Evaluate(`() => globalThis.objectToDestroy = null`) 29 | require.NoError(t, err) 30 | 31 | require.NoError(t, page.RequestGC()) 32 | ret, err = page.Evaluate(`() => globalThis.weakRef.deref()`) 33 | require.NoError(t, err) 34 | require.Nil(t, ret) 35 | } 36 | -------------------------------------------------------------------------------- /tests/remote_server_test.go: -------------------------------------------------------------------------------- 1 | package playwright_test 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | 10 | "github.com/playwright-community/playwright-go" 11 | ) 12 | 13 | type remoteServer struct { 14 | url string 15 | cmd *exec.Cmd 16 | } 17 | 18 | func newRemoteServer() (*remoteServer, error) { 19 | driver, err := playwright.NewDriver(&playwright.RunOptions{}) 20 | if err != nil { 21 | return nil, fmt.Errorf("could not start Playwright: %v", err) 22 | } 23 | cmd := driver.Command("launch-server", "--browser", browserName) 24 | cmd.Stderr = os.Stderr 25 | stdout, err := cmd.StdoutPipe() 26 | if err != nil { 27 | return nil, fmt.Errorf("could not get stdout pipe: %v", err) 28 | } 29 | err = cmd.Start() 30 | if err != nil { 31 | return nil, fmt.Errorf("could not start server: %v", err) 32 | } 33 | scanner := bufio.NewReader(stdout) 34 | url, err := scanner.ReadString('\n') 35 | url = strings.TrimRight(url, "\n") 36 | if err != nil { 37 | return nil, fmt.Errorf("could not read url: %v", err) 38 | } 39 | return &remoteServer{ 40 | url: url, 41 | cmd: cmd, 42 | }, nil 43 | } 44 | 45 | func (s *remoteServer) Close() { 46 | _ = s.cmd.Process.Kill() 47 | _ = s.cmd.Wait() 48 | } 49 | -------------------------------------------------------------------------------- /tests/screenshot-snapshots/mask-should-work-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/screenshot-snapshots/mask-should-work-chromium.png -------------------------------------------------------------------------------- /tests/screenshot-snapshots/mask-should-work-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/screenshot-snapshots/mask-should-work-firefox.png -------------------------------------------------------------------------------- /tests/screenshot-snapshots/mask-should-work-webkit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/screenshot-snapshots/mask-should-work-webkit.png -------------------------------------------------------------------------------- /tests/screenshot-snapshots/mask-should-work-with-elementhandle-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/screenshot-snapshots/mask-should-work-with-elementhandle-chromium.png -------------------------------------------------------------------------------- /tests/screenshot-snapshots/mask-should-work-with-elementhandle-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/screenshot-snapshots/mask-should-work-with-elementhandle-firefox.png -------------------------------------------------------------------------------- /tests/screenshot-snapshots/mask-should-work-with-elementhandle-webkit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/screenshot-snapshots/mask-should-work-with-elementhandle-webkit.png -------------------------------------------------------------------------------- /tests/screenshot-snapshots/mask-should-work-with-locator-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/screenshot-snapshots/mask-should-work-with-locator-chromium.png -------------------------------------------------------------------------------- /tests/screenshot-snapshots/mask-should-work-with-locator-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/screenshot-snapshots/mask-should-work-with-locator-firefox.png -------------------------------------------------------------------------------- /tests/screenshot-snapshots/mask-should-work-with-locator-webkit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/screenshot-snapshots/mask-should-work-with-locator-webkit.png -------------------------------------------------------------------------------- /tests/screenshot-snapshots/screenshot-element-bounding-box-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/screenshot-snapshots/screenshot-element-bounding-box-chromium.png -------------------------------------------------------------------------------- /tests/screenshot-snapshots/screenshot-element-bounding-box-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/screenshot-snapshots/screenshot-element-bounding-box-firefox.png -------------------------------------------------------------------------------- /tests/screenshot-snapshots/screenshot-element-bounding-box-webkit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playwright-community/playwright-go/4d70e13a55649abcfe582deeecbeeaf6e0ba6427/tests/screenshot-snapshots/screenshot-element-bounding-box-webkit.png -------------------------------------------------------------------------------- /tests/screenshot_test.go: -------------------------------------------------------------------------------- 1 | package playwright_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/playwright-community/playwright-go" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestLocatorScreenshotShouldWork(t *testing.T) { 11 | BeforeEach(t) 12 | 13 | require.NoError(t, page.SetViewportSize(500, 500)) 14 | _, err := page.Goto(server.PREFIX + "/grid.html") 15 | require.NoError(t, err) 16 | _, err = page.Evaluate(`window.scrollBy(50, 100)`) 17 | require.NoError(t, err) 18 | screenshot, err := page.Locator(".box:nth-of-type(3)").Screenshot() 19 | require.NoError(t, err) 20 | require.NotEmpty(t, screenshot) 21 | AssertToBeGolden(t, screenshot, "screenshot-element-bounding-box.png") 22 | } 23 | 24 | func TestShouldScreenshotWithMask(t *testing.T) { 25 | BeforeEach(t) 26 | 27 | require.NoError(t, page.SetViewportSize(500, 500)) 28 | _, err := page.Goto(server.PREFIX + "/grid.html") 29 | require.NoError(t, err) 30 | 31 | screenshot, err := page.Screenshot(playwright.PageScreenshotOptions{ 32 | Mask: []playwright.Locator{ 33 | page.Locator("div").Nth(5), 34 | }, 35 | }) 36 | require.NoError(t, err) 37 | AssertToBeGolden(t, screenshot, "mask-should-work.png") 38 | 39 | screenshot, err = page.Locator("body").Screenshot(playwright.LocatorScreenshotOptions{ 40 | Mask: []playwright.Locator{ 41 | page.Locator("div").Nth(5), 42 | }, 43 | }) 44 | require.NoError(t, err) 45 | AssertToBeGolden(t, screenshot, "mask-should-work-with-locator.png") 46 | 47 | //nolint:staticcheck 48 | element, err := page.QuerySelector("body") 49 | require.NoError(t, err) 50 | //nolint:staticcheck 51 | screenshot, err = element.Screenshot(playwright.ElementHandleScreenshotOptions{ 52 | Mask: []playwright.Locator{ 53 | page.Locator("div").Nth(5), 54 | }, 55 | }) 56 | require.NoError(t, err) 57 | AssertToBeGolden(t, screenshot, "mask-should-work-with-elementhandle.png") 58 | } 59 | -------------------------------------------------------------------------------- /type_helpers.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | // String is a helper routine that allocates a new string value 4 | // to store v and returns a pointer to it. 5 | func String(v string) *string { 6 | return &v 7 | } 8 | 9 | // Bool is a helper routine that allocates a new bool value 10 | // to store v and returns a pointer to it. 11 | func Bool(v bool) *bool { 12 | return &v 13 | } 14 | 15 | // Int is a helper routine that allocates a new int32 value 16 | // to store v and returns a pointer to it. 17 | func Int(v int) *int { 18 | return &v 19 | } 20 | 21 | // Float is a helper routine that allocates a new float64 value 22 | // to store v and returns a pointer to it. 23 | func Float(v float64) *float64 { 24 | return &v 25 | } 26 | 27 | // Null will be used in certain scenarios where a strict nil pointer 28 | // check is not possible 29 | func Null() interface{} { 30 | return "PW_NULL" 31 | } 32 | 33 | // StringSlice is a helper routine that allocates a new StringSlice value 34 | // to store v and returns a pointer to it. 35 | func StringSlice(v ...string) *[]string { 36 | var o []string 37 | o = append(o, v...) 38 | return &o 39 | } 40 | 41 | // IntSlice is a helper routine that allocates a new IntSlice value 42 | // to store v and returns a pointer to it. 43 | func IntSlice(v ...int) *[]int { 44 | var o []int 45 | o = append(o, v...) 46 | return &o 47 | } 48 | 49 | // ToOptionalStorageState converts StorageState to OptionalStorageState for use directly in [Browser.NewContext] 50 | func (s StorageState) ToOptionalStorageState() *OptionalStorageState { 51 | cookies := make([]OptionalCookie, len(s.Cookies)) 52 | for i, c := range s.Cookies { 53 | cookies[i] = c.ToOptionalCookie() 54 | } 55 | return &OptionalStorageState{ 56 | Origins: s.Origins, 57 | Cookies: cookies, 58 | } 59 | } 60 | 61 | func (c Cookie) ToOptionalCookie() OptionalCookie { 62 | return OptionalCookie{ 63 | Name: c.Name, 64 | Value: c.Value, 65 | Domain: String(c.Domain), 66 | Path: String(c.Path), 67 | Expires: Float(c.Expires), 68 | HttpOnly: Bool(c.HttpOnly), 69 | Secure: Bool(c.Secure), 70 | SameSite: c.SameSite, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /video.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | type videoImpl struct { 9 | page *pageImpl 10 | artifact *artifactImpl 11 | artifactChan chan *artifactImpl 12 | done chan struct{} 13 | closeOnce sync.Once 14 | isRemote bool 15 | } 16 | 17 | func (v *videoImpl) Path() (string, error) { 18 | if v.isRemote { 19 | return "", errors.New("Path is not available when connecting remotely. Use SaveAs() to save a local copy.") 20 | } 21 | v.getArtifact() 22 | if v.artifact == nil { 23 | return "", errors.New("Page did not produce any video frames") 24 | } 25 | return v.artifact.AbsolutePath(), nil 26 | } 27 | 28 | func (v *videoImpl) Delete() error { 29 | v.getArtifact() 30 | if v.artifact == nil { 31 | return nil 32 | } 33 | return v.artifact.Delete() 34 | } 35 | 36 | func (v *videoImpl) SaveAs(path string) error { 37 | if !v.page.IsClosed() { 38 | return errors.New("Page is not yet closed. Close the page prior to calling SaveAs") 39 | } 40 | v.getArtifact() 41 | if v.artifact == nil { 42 | return errors.New("Page did not produce any video frames") 43 | } 44 | return v.artifact.SaveAs(path) 45 | } 46 | 47 | func (v *videoImpl) artifactReady(artifact *artifactImpl) { 48 | v.artifactChan <- artifact 49 | } 50 | 51 | func (v *videoImpl) pageClosed(p Page) { 52 | v.closeOnce.Do(func() { 53 | close(v.done) 54 | }) 55 | } 56 | 57 | func (v *videoImpl) getArtifact() { 58 | // prevent channel block if no video will be produced 59 | if v.page.browserContext.options == nil { 60 | v.pageClosed(v.page) 61 | } else { 62 | option := v.page.browserContext.options 63 | if option == nil || option.RecordVideo == nil { // no recordVideo option 64 | v.pageClosed(v.page) 65 | } 66 | } 67 | select { 68 | case artifact := <-v.artifactChan: 69 | if artifact != nil { 70 | v.artifact = artifact 71 | } 72 | case <-v.done: // page closed 73 | select { // make sure get artifact if it's ready before page closed 74 | case artifact := <-v.artifactChan: 75 | if artifact != nil { 76 | v.artifact = artifact 77 | } 78 | default: 79 | } 80 | } 81 | } 82 | 83 | func newVideo(page *pageImpl) *videoImpl { 84 | video := &videoImpl{ 85 | page: page, 86 | artifactChan: make(chan *artifactImpl, 1), 87 | done: make(chan struct{}, 1), 88 | isRemote: page.connection.isRemote, 89 | } 90 | 91 | if page.isClosed { 92 | video.pageClosed(page) 93 | } else { 94 | page.OnClose(video.pageClosed) 95 | } 96 | return video 97 | } 98 | -------------------------------------------------------------------------------- /web_error.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | type webErrorImpl struct { 4 | err error 5 | page Page 6 | } 7 | 8 | func (e *webErrorImpl) Page() Page { 9 | return e.page 10 | } 11 | 12 | func (e *webErrorImpl) Error() error { 13 | return e.err 14 | } 15 | 16 | func newWebError(page Page, err error) WebError { 17 | return &webErrorImpl{ 18 | err: err, 19 | page: page, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /worker.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | type workerImpl struct { 4 | channelOwner 5 | page *pageImpl 6 | context *browserContextImpl 7 | } 8 | 9 | func (w *workerImpl) URL() string { 10 | return w.initializer["url"].(string) 11 | } 12 | 13 | func (w *workerImpl) Evaluate(expression string, options ...interface{}) (interface{}, error) { 14 | var arg interface{} 15 | if len(options) == 1 { 16 | arg = options[0] 17 | } 18 | result, err := w.channel.Send("evaluateExpression", map[string]interface{}{ 19 | "expression": expression, 20 | "arg": serializeArgument(arg), 21 | }) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return parseResult(result), nil 26 | } 27 | 28 | func (w *workerImpl) EvaluateHandle(expression string, options ...interface{}) (JSHandle, error) { 29 | var arg interface{} 30 | if len(options) == 1 { 31 | arg = options[0] 32 | } 33 | result, err := w.channel.Send("evaluateExpressionHandle", map[string]interface{}{ 34 | "expression": expression, 35 | "arg": serializeArgument(arg), 36 | }) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return fromChannel(result).(*jsHandleImpl), nil 41 | } 42 | 43 | func (w *workerImpl) onClose() { 44 | if w.page != nil { 45 | w.page.Lock() 46 | workers := make([]Worker, 0) 47 | for i := 0; i < len(w.page.workers); i++ { 48 | if w.page.workers[i] != w { 49 | workers = append(workers, w.page.workers[i]) 50 | } 51 | } 52 | w.page.workers = workers 53 | w.page.Unlock() 54 | } 55 | if w.context != nil { 56 | w.context.Lock() 57 | workers := make([]Worker, 0) 58 | for i := 0; i < len(w.context.serviceWorkers); i++ { 59 | if w.context.serviceWorkers[i] != w { 60 | workers = append(workers, w.context.serviceWorkers[i]) 61 | } 62 | } 63 | w.context.serviceWorkers = workers 64 | w.context.Unlock() 65 | } 66 | w.Emit("close", w) 67 | } 68 | 69 | func (w *workerImpl) OnClose(fn func(Worker)) { 70 | w.On("close", fn) 71 | } 72 | 73 | func newWorker(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *workerImpl { 74 | bt := &workerImpl{} 75 | bt.createChannelOwner(bt, parent, objectType, guid, initializer) 76 | bt.channel.On("close", bt.onClose) 77 | return bt 78 | } 79 | -------------------------------------------------------------------------------- /writable_stream.go: -------------------------------------------------------------------------------- 1 | package playwright 2 | 3 | import ( 4 | "encoding/base64" 5 | "io" 6 | "os" 7 | ) 8 | 9 | type writableStream struct { 10 | channelOwner 11 | } 12 | 13 | func (s *writableStream) Copy(file string) error { 14 | f, err := os.OpenFile(file, os.O_RDONLY, 0) 15 | if err != nil { 16 | return err 17 | } 18 | defer f.Close() 19 | 20 | for { 21 | buf := make([]byte, defaultCopyBufSize) 22 | n, err := f.Read(buf) 23 | if err != nil && err != io.EOF { 24 | return err 25 | } 26 | if n == 0 { 27 | break 28 | } 29 | _, err = s.channel.Send("write", map[string]interface{}{ 30 | "binary": base64.StdEncoding.EncodeToString(buf[:n]), 31 | }) 32 | if err != nil { 33 | return err 34 | } 35 | } 36 | _, err = s.channel.Send("close") 37 | return err 38 | } 39 | 40 | func newWritableStream(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *writableStream { 41 | stream := &writableStream{} 42 | stream.createChannelOwner(stream, parent, objectType, guid, initializer) 43 | return stream 44 | } 45 | --------------------------------------------------------------------------------