1
0
mirror of synced 2026-05-23 03:03:15 +00:00

Compare commits

...

148 Commits

Author SHA1 Message Date
Yury Semikhatsky 44f355357a chore(release-1.12): set version to 1.12.0 (#477) 2021-06-09 09:12:46 -07:00
Yury Semikhatsky cc3cc401e5 chore(release-1.12): update driver to 1.12.0-1623185968000 (#476) 2021-06-08 15:06:47 -07:00
Yury Semikhatsky 8c93ddf232 feat: roll driver, deprecated channel enum (#468) 2021-06-04 09:26:52 -07:00
Yury Semikhatsky 502965ffec chore: update deps version in examples project (#459) 2021-05-24 13:12:27 -07:00
Yury Semikhatsky 9a994aba70 test: disable test that flakes in Chromium (#457) 2021-05-21 09:40:19 -07:00
Yury Semikhatsky a67bf05b05 test(screencast): close context with video before returning from test method (#456) 2021-05-20 15:25:23 -07:00
Yury Semikhatsky a5d5c0d960 test: close file reader when done (#454) 2021-05-20 14:58:23 -07:00
Yury Semikhatsky 8e7f958242 test: disable same site attribute test in WebKit Windows (#455) 2021-05-20 14:57:33 -07:00
Yury Semikhatsky c6d94b918f test: port more browserType.connect() tests (#453) 2021-05-20 14:28:13 -07:00
Yury Semikhatsky 061e2a961a chore: roll to 1.12.0-next-1621527598000 (#452) 2021-05-20 09:44:15 -07:00
Yury Semikhatsky 87d9957486 fix: wait for video to finish even if page was closed (#447) 2021-05-18 22:45:17 -07:00
Yury Semikhatsky b3629a704b test: use @TestInstance(PER_CLASS) instead of ThreadLocal (#446) 2021-05-18 12:20:13 -07:00
Yury Semikhatsky 02bd360319 feat: roll driver, implement context tracing and network APIs (#444) 2021-05-17 16:32:44 -07:00
Yury Semikhatsky bcf879b8f0 test: support parallel execution (#443) 2021-05-13 16:47:27 -07:00
Yury Semikhatsky b7ce984969 Revert "fix: manually pipe messages from child process sdtout/stderr (#426)" (#440)
This reverts commit 0fa416b459.
2021-05-12 16:12:16 -07:00
Yury Semikhatsky 47fe3aa076 chore: update working version to 1.12.0-SNAPSHOT (#437) 2021-05-11 11:55:51 -07:00
Yury Semikhatsky 06b88b3d4b fix: support slowMo options in browserType.connect (#434) 2021-05-07 16:21:07 -07:00
Yury Semikhatsky f143ebd053 docs: update browser versions in README.md (#430) 2021-05-07 10:59:34 -07:00
Yury Semikhatsky 6c3c47a9f1 chore: roll driver to 1.11.0-1620331022000 (#429) 2021-05-07 08:06:44 -07:00
Yury Semikhatsky 4049952751 chore: roll driver to 1.11.0-1620262237000 (#428) 2021-05-06 11:37:26 -07:00
Yury Semikhatsky d24eba79dc fix: avoid private field/class access from Gson (#427) 2021-05-06 00:04:52 -07:00
Yury Semikhatsky 0fa416b459 fix: manually pipe messages from child process sdtout/stderr (#426) 2021-05-05 19:24:57 -07:00
Yury Semikhatsky a95f8f3887 feat: support tracing API for chromium (#422) 2021-05-04 09:16:44 -07:00
Yury Semikhatsky 779d50ca53 fix: add back pw:api timstamps (#417) 2021-04-27 11:18:23 -07:00
Yury Semikhatsky 627a940bcd feat: protocol logging timestamps (#415) 2021-04-26 20:32:25 -07:00
Yury Semikhatsky c30e105e36 feat: support extra headers in connect and connectOverCDP (#414) 2021-04-26 14:30:55 -07:00
Yury Semikhatsky 8ee5b8b436 feat: support channel logging (#410) 2021-04-23 14:25:32 -07:00
Yury Semikhatsky 21a7f4da02 feat(inspector): add logging to wait for event methods (#409) 2021-04-21 15:55:17 -07:00
Yury Semikhatsky e995996e4f test: update beforeunload expected text in firefox (#408) 2021-04-21 09:33:17 -07:00
Yury Semikhatsky c23afbf9cf test: check that SameSiteAttribute is properly propagated (#405) 2021-04-21 09:05:16 -07:00
Yury Semikhatsky 2e57a7a101 fix(websocket): use incoming queue with unlimited capacity (#406) 2021-04-21 08:58:55 -07:00
Yury Semikhatsky 20322470b5 chore: roll driver to 1.11.0-next-1618951606000 (#404) 2021-04-20 16:55:37 -07:00
Yury Semikhatsky ed25d1877d chore: roll driver with new connect logic (#400) 2021-04-17 09:16:27 -07:00
Yury Semikhatsky 8e27ec6bc3 test(lolcale): port locale tests from typescript (#395) 2021-04-12 12:30:45 -07:00
Yury Semikhatsky 2d956e4a8d feat: roll driver, support connectOverCDP with plain URL (#392) 2021-04-08 17:07:53 -07:00
Yury Semikhatsky 40c663ccce feat: support connectOverCDP (#387) 2021-04-06 23:20:07 -07:00
Max Schmitt 7f52faf400 devops: adjust pushed Docker tags to be consistent with pw (#386) 2021-04-06 09:36:54 -07:00
Yury Semikhatsky 5cd00cc4c7 chore: roll to 1.11.0-next-1617387566000 (#384) 2021-04-02 14:10:43 -07:00
Yury Semikhatsky 1ff5671b12 feat: roll driver, implement waitForUrl and new Video APIs (#382) 2021-04-02 08:53:26 -07:00
Yury Semikhatsky f332a536b1 devops: use install-deps in docker file (#378) 2021-03-30 13:09:14 -07:00
Yury Semikhatsky 5d5e85502e fix: align test-local-installation version with other projects (#380) 2021-03-30 12:32:53 -07:00
Yury Semikhatsky f8b4e93f25 chore: roll driver to 1.11.0-next-1617087307000 (#379) 2021-03-30 12:29:58 -07:00
Yury Semikhatsky 38aa88d59f chore: roll driver to 1.11.0-next-1617006126000 (#376) 2021-03-29 09:02:39 -07:00
Yury Semikhatsky 91282d3401 docs: point to java specific path on playwright.dev (#373) 2021-03-26 09:41:41 -07:00
Yury Semikhatsky 1904087722 chore: bump working version to 1.11.0-SNAPSHOT (#372) 2021-03-25 18:37:25 -07:00
Yury Semikhatsky db810d1117 devops: remove hardcoded docker version tag (#370) 2021-03-25 18:09:17 -07:00
Yury Semikhatsky 2ffca6a0b4 devops: force set current tag to v1.10.0 to push docker release (#368) 2021-03-25 13:27:21 -07:00
Yury Semikhatsky af1bf963dc devops: add docker release workflow to master branch (#367) 2021-03-25 12:44:20 -07:00
Yury Semikhatsky abada21f3a docs: update system requirements link (#365) 2021-03-25 11:30:23 -07:00
Yury Semikhatsky 34f34d869f docs: update note about API readiness (#364) 2021-03-25 11:27:53 -07:00
Yury Semikhatsky 1e4763c80d docs: add devtools instructions to CONTRIBUTING.md (#361) 2021-03-24 16:27:48 -07:00
Yury Semikhatsky ac7b1e4a7a chore: roll driver v1.10.0 (#360) 2021-03-24 16:14:57 -07:00
Yury Semikhatsky 41735ff8ca devops: fix docker script path (#359) 2021-03-24 14:49:01 -07:00
Yury Semikhatsky 866bf3587c devops: publish docker canary (#358) 2021-03-24 14:32:34 -07:00
Yury Semikhatsky d75a7d76a9 devops(docker): add docker test bot (#357) 2021-03-24 13:56:18 -07:00
Yury Semikhatsky d2b6e1c2b4 feat(docker): download browsers to local cache (#356) 2021-03-23 17:49:02 -07:00
Yury Semikhatsky a334baab49 chore: streamline set_maven_version.sh (#355) 2021-03-23 17:38:26 -07:00
Yury Semikhatsky 31c029a50a fix(docker): set java home, use JDK 14 (#352) 2021-03-22 16:31:52 -07:00
Yury Semikhatsky b1b5b43948 fix(launcher): firefoxUserPrefs serialization (#351) 2021-03-22 14:35:29 -07:00
Yury Semikhatsky bddd52e360 chore: update cli to 1.10.0-next-1616100617000 (#348) 2021-03-18 17:59:05 -07:00
Yury Semikhatsky cbc671dd16 fix: supported driver jar packed into another jar (#342) 2021-03-11 23:58:23 -08:00
Yury Semikhatsky a9a2eba2f6 chore: update alpha revision in the docs (#340) 2021-03-10 12:59:53 -08:00
Yury Semikhatsky 4bcb7afeda tests: port new postData tests from upstream (#335) 2021-03-08 12:29:19 -08:00
Yury Semikhatsky 7ca9b33f46 chore: roll driver to 1.10.0-next-1615230258000 (#334) 2021-03-08 12:20:26 -08:00
Yury Semikhatsky ae15e3050d chore: update working version to 1.10.0-SNAPSHOT (#333) 2021-03-08 11:41:56 -08:00
Yury Semikhatsky ed68a789ef docs: update browser versions, fix wording in readme (#331) 2021-03-06 08:04:03 -08:00
Yury Semikhatsky 93a54d0a52 chore: update version to alpha in docs (#330) 2021-03-05 16:57:59 -08:00
Yury Semikhatsky 10da11cd7c chore: update javadocs to reference set* methods (#328) 2021-03-05 15:17:28 -08:00
Max Schmitt 71b5865d5e docs(readme): use aka.ms Playwright Slack invite link (#327) 2021-03-05 14:38:40 -08:00
Yury Semikhatsky ce54e385c5 docs: add more badges (#326) 2021-03-05 14:32:34 -08:00
Yury Semikhatsky 7d333f994e docs: update examples to use set* instead of with* (#325) 2021-03-05 14:29:41 -08:00
Yury Semikhatsky 617822fa18 fix: rename builder methods from with* to set* (#324) 2021-03-05 13:38:26 -08:00
Yury Semikhatsky 86c06c4fd3 fix(api): throw TimeoutError on timeout (#323) 2021-03-05 13:18:24 -08:00
Yury Semikhatsky d57c449aef test: add basic cli test (#321) 2021-03-05 10:12:45 -08:00
Yury Semikhatsky 9fe7496261 fix: set default codegen language to java (#320) 2021-03-04 22:33:49 -08:00
Yury Semikhatsky 9b568ab4ae feat: add class for launching cli using mvn (#319) 2021-03-04 21:52:12 -08:00
Yury Semikhatsky e9d5d6c5cd fix: correctly terminate connection on remote browser close (#318) 2021-03-04 14:26:22 -08:00
Yury Semikhatsky 2094d40ecb fix: delete deprecated accessibility API, add onceDialog (#317) 2021-03-03 21:15:34 -08:00
Yury Semikhatsky 315bda9b48 feat: provide stacktraces for inspector (#316) 2021-03-03 14:45:35 -08:00
Yury Semikhatsky db5bb8b9c2 fix: use default file system for relative path resolution (#315) 2021-03-03 11:42:17 -08:00
Yury Semikhatsky 6d982db6b9 fix: implement BrowserType.connect (#314) 2021-03-02 18:13:27 -08:00
Yury Semikhatsky 65fdde3e36 docs: use double quotes, linkify Promise (#312) 2021-03-02 12:27:48 -08:00
Yury Semikhatsky 33cbcd6fc7 fix: roll 1.9.1-1614654987000, rename onConsole->onConsoleMessage (#311) 2021-03-01 21:29:12 -08:00
Yury Semikhatsky 5ce2bb6527 docs: generate even documentation (#310) 2021-03-01 17:36:52 -08:00
Yury Semikhatsky 7e3cec5aac docs: patch javadoc links to API members and external sources (#309) 2021-03-01 16:38:29 -08:00
Yury Semikhatsky 606d9947d4 docs: add code snippets from upstream (#308) 2021-03-01 15:56:57 -08:00
Yury Semikhatsky aea9218162 chore: roll driver to 1.9.1-1614633211000 (#307) 2021-03-01 15:04:54 -08:00
Yury Semikhatsky 9885f72a66 docs: add examples from intro (#304) 2021-02-23 13:18:15 -08:00
Yury Semikhatsky d63a8e31c3 fix(logging): use thread-safe time formatter (#303) 2021-02-23 12:52:02 -08:00
Yury Semikhatsky 32afbc065b docs: fix examples to work with the latest api (#302) 2021-02-23 12:30:03 -08:00
Yury Semikhatsky 08d84cf629 chore: harden custom type mapping (#299) 2021-02-19 18:54:06 -08:00
Yury Semikhatsky 85553f691f chore: remove custom generator for cookie methods (#298) 2021-02-19 18:22:23 -08:00
Yury Semikhatsky 2c6aa8f8b3 chore: remove Types.java (#297) 2021-02-19 16:45:57 -08:00
Yury Semikhatsky 80c11f76aa fix: change postData field type to Object (#296) 2021-02-19 16:28:26 -08:00
Yury Semikhatsky aa853e8386 chore: generate selectOption from api.json (#295) 2021-02-19 15:44:10 -08:00
Yury Semikhatsky 99890b57bc fix: generate helper builder methods for nested types (#294) 2021-02-19 14:34:01 -08:00
Yury Semikhatsky ab5cd1c511 fix: generate overloaded builders for options (#293) 2021-02-19 13:46:08 -08:00
Yury Semikhatsky b7a822ee41 chore: always use number for polling interval option (#292) 2021-02-19 12:39:23 -08:00
Yury Semikhatsky c9787e2102 chore: user array instead of list only for overloaded params (#291) 2021-02-19 11:44:58 -08:00
Yury Semikhatsky 8bfb69b93d fix: run tests on release-* branches too (#289) 2021-02-19 09:35:30 -08:00
Yury Semikhatsky 8e62a47f6d fix: generate FilePayload and setInputFiles from api.json (#286) 2021-02-12 18:14:09 -08:00
Yury Semikhatsky b8a774eecc fix: generate frameByUrl from api.json (#285) 2021-02-12 16:23:04 -08:00
Yury Semikhatsky 6fb0b01a17 fix: generate javadocs for all overloads (#284) 2021-02-12 15:15:41 -08:00
Yury Semikhatsky 5fa5a513d2 fix: start generating overloaded method for union params from api.json (#283) 2021-02-12 14:53:47 -08:00
Yury Semikhatsky 5f122784f8 chore: roll cli to 1.9.0-next-1613157126000 (#282) 2021-02-12 12:21:45 -08:00
Yury Semikhatsky f3003bad20 chore: remove custom signatures for non-java methods (#281) 2021-02-12 10:08:11 -08:00
Yury Semikhatsky 0afb42bb0f fix: remove devices from the API (#280) 2021-02-11 12:53:18 -08:00
Yury Semikhatsky 8db59ba9a1 chore: update cli to match current api (#279) 2021-02-11 08:29:53 -08:00
Yury Semikhatsky 5bf883a456 fix: move exposed binding and funcion callbacks to top level (#278) 2021-02-10 18:14:24 -08:00
Yury Semikhatsky d4ef6d6431 chore: simplify generator (#277) 2021-02-10 16:20:33 -08:00
Yury Semikhatsky ea34deeb2b fix: generate return and param types from api.json (#276) 2021-02-10 15:42:03 -08:00
Yury Semikhatsky 442a577506 fix: drop RecordHar and RecordVideo clasess, use flat list of their options (#275) 2021-02-10 12:00:43 -08:00
Yury Semikhatsky fcc1d8672a fix: take required options as constructor params (#274) 2021-02-09 17:58:55 -08:00
Yury Semikhatsky 429f2969aa fix: extract some option classes to top level (#273) 2021-02-09 17:15:09 -08:00
Yury Semikhatsky 7642097291 chore: remove special case for WebSocketFrame (#272) 2021-02-09 16:50:01 -08:00
Yury Semikhatsky bd14c560d2 fix: import options package in the example (#271) 2021-02-09 10:59:19 -08:00
Yury Semikhatsky 18ca3e6ace chore: remove custom mapping for pdf options (#268) 2021-02-08 18:35:27 -08:00
Yury Semikhatsky b77b9b0d20 chore: move enums and second level nested classes into options package (#267) 2021-02-08 18:33:38 -08:00
Yury Semikhatsky c2690f925c fix: allow building persistent context options from device (#266) 2021-02-08 15:52:32 -08:00
Yury Semikhatsky f409965c8e chore: remove unused code from generator (#265) 2021-02-08 15:52:02 -08:00
Yury Semikhatsky 566e0f90e2 fix(api): generate enums from api.json, put them into separate files (#264) 2021-02-08 15:36:01 -08:00
Yury Semikhatsky b98e920efb docs: fix network interception example to work with 0.190.0 (#262) 2021-02-08 09:20:16 -08:00
Yury Semikhatsky 90940b23f3 fix: generate waitFor* methods from upstream docs (#260) 2021-02-04 22:29:55 -08:00
Yury Semikhatsky 2c9fa04a43 tests: stop using waitFor* methods which will be removed (#259) 2021-02-04 20:36:29 -08:00
Yury Semikhatsky eebb223bc3 chore(generator): use case-sensitive event names from upstream (#257) 2021-02-04 11:55:36 -08:00
Yury Semikhatsky f99d6643cd feat: implement Page.pause (#256) 2021-02-04 09:39:44 -08:00
ephung01 37e5de729f test: add new test 'Test Browser Context Viewport' (#255) 2021-02-04 09:23:34 -08:00
Yury Semikhatsky 811ca89fd4 chore: remove custom generator for bodyBytes (#254) 2021-02-03 18:59:42 -08:00
Yury Semikhatsky 6dbebaed5a fix: rename Route.continue_ to Route.resume (#253) 2021-02-03 14:14:35 -08:00
Yury Semikhatsky ee4f8698da fix: auto dismiss dialogs if there are no listeners (#252) 2021-02-03 13:18:01 -08:00
Yury Semikhatsky e31ea3cbe9 fix: change Accessibility.snapshot type to String (#251) 2021-02-03 12:58:38 -08:00
Yury Semikhatsky b60bbc8b88 fix: change storage state type to String (#250) 2021-02-02 18:05:57 -08:00
Yury Semikhatsky a577e62ece chore: delete code for generating custom option names (#249) 2021-02-02 18:05:34 -08:00
Yury Semikhatsky d14d35610e chore: remove custom generator code for Response.finished (#248) 2021-02-02 12:49:20 -08:00
Yury Semikhatsky 301234aa4f chore: roll cli, remove custom ignoreDefaultArgs generator (#247) 2021-02-02 12:15:30 -08:00
Yury Semikhatsky 7a7edea189 fix: add snapshot repo to the examples project (#246) 2021-02-02 11:10:07 -08:00
JB Nizet a6f25595f3 feat: extend AutoCloseable in Playwright, BrowserContext, Browser and… (#238) 2021-02-01 17:31:51 -08:00
Yury Semikhatsky 5af091880f chore: remove more upstreamed stuff (#244) 2021-02-01 13:34:23 -08:00
Yury Semikhatsky 4c165c2069 feat: update driver to 1.9.0-next-1612209901000 (#243) 2021-02-01 12:51:25 -08:00
Yury Semikhatsky 65df7be823 fix: remove Exception from Playwright.close signature (#240) 2021-01-29 21:12:25 -08:00
JB Nizet 2c6e278167 chore: avoid creating a useless DataOutputStream when writing to a file (#236) 2021-01-29 11:14:19 -08:00
JB Nizet 68525d1426 fix: avoid NPE when saving storage state (#235) 2021-01-29 11:13:52 -08:00
Yury Semikhatsky 1da197fdf5 chore: delete Page.LoadState, use Frame.LoadState instead (#234) 2021-01-25 16:00:30 -08:00
Yury Semikhatsky e545829bf5 fix(api): make Dialog.type() return string (#233) 2021-01-25 15:11:05 -08:00
Yury Semikhatsky 982cc6a3bc chore: streamline enum serialization (#232) 2021-01-25 12:35:13 -08:00
Yury Semikhatsky dc139d25d1 chore: delete Event interface (#231) 2021-01-25 11:18:01 -08:00
ephung01 4f9d4f170f test: add new test 'Selectors CSS' (#229) 2021-01-25 11:17:08 -08:00
Yury Semikhatsky 55344516ea chore: move on to 0.190.0-SNAPSHOT (#228) 2021-01-22 18:19:31 -08:00
203 changed files with 15951 additions and 7045 deletions
@@ -0,0 +1,26 @@
name: "devrelease:docker"
on:
push:
branches:
- master
jobs:
publish-canary-docker:
name: "publish to DockerHub"
runs-on: ubuntu-20.04
if: github.repository == 'microsoft/playwright-java'
steps:
- uses: actions/checkout@v2
- uses: azure/docker-login@v1
with:
login-server: playwright.azurecr.io
username: playwright
password: ${{ secrets.DOCKER_PASSWORD }}
- uses: actions/checkout@v2
- name: Build Docker image
run: docker build -t playwright-java:localbuild-focal -f Dockerfile.focal .
- name: tag & publish
run: |
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:next
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:next-focal
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:sha-${{ github.sha }}
@@ -0,0 +1,36 @@
name: Publish Release Docker
on:
release:
types: [published]
workflow_dispatch:
branches:
- release-*
jobs:
publish-canary-docker:
name: publish to DockerHub
runs-on: ubuntu-20.04
if: github.repository == 'microsoft/playwright-java'
steps:
- uses: actions/checkout@v2
- uses: azure/docker-login@v1
with:
login-server: playwright.azurecr.io
username: playwright
password: ${{ secrets.DOCKER_PASSWORD }}
- uses: actions/checkout@v2
- name: Build Docker image
run: docker build -t playwright-java:localbuild-focal -f Dockerfile.focal .
- name: tag & publish
run: |
# GITHUB_REF has a form of `refs/tags/v1.3.0`.
# TAG_NAME would be `v1.3.0`
TAG_NAME=${GITHUB_REF#refs/tags/}
if [[ ! "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]];
then
echo "Wrong TAG_NAME format: $TAG_NAME"
exit 1
fi
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:latest
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:focal
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:${TAG_NAME}
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:${TAG_NAME}-focal
+58 -4
View File
@@ -1,11 +1,15 @@
name: Test
name: Build & Test
on:
push:
branches: [ master ]
branches:
- master
- release-*
pull_request:
branches: [ master ]
branches:
- master
- release-*
jobs:
build:
dev:
timeout-minutes: 30
strategy:
fail-fast: false
@@ -35,3 +39,53 @@ jobs:
run: mvn test --no-transfer-progress
env:
BROWSER: ${{ matrix.browser }}
- name: Test Spring Boot Starter
shell: bash
env:
BROWSER: ${{ matrix.browser }}
run: |
mvn -B install -D skipTests --no-transfer-progress
cd tools/test-spring-boot-starter
mvn package -D skipTests --no-transfer-progress
java -jar target/test-spring-boot*.jar
stable:
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
browser-channel: [chrome]
include:
- os: windows-latest
browser-channel: msedge
- os: macos-latest
browser-channel: msedge
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1
- name: Install Media Pack
if: matrix.os == 'windows-latest'
shell: powershell
run: Install-WindowsFeature Server-Media-Foundation
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Download drivers
shell: bash
run: scripts/download_driver_for_all_platforms.sh
- name: Build with Maven
run: mvn -B package -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress
env:
BROWSER: chromium
BROWSER_CHANNEL: ${{ matrix.browser-channel }}
+30
View File
@@ -0,0 +1,30 @@
name: Test CLI
on:
push:
branches:
- master
- release-*
pull_request:
branches:
- master
- release-*
jobs:
verify:
timeout-minutes: 30
strategy:
fail-fast: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Download drivers
run: scripts/download_driver_for_all_platforms.sh
- name: Intall Playwright
run: mvn install -D skipTests --no-transfer-progress
- name: Test CLI
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -f playwright/pom.xml -D exec.args=-V
+30
View File
@@ -0,0 +1,30 @@
name: Test Docker
on:
push:
paths:
- '.github/workflows/test_docker.yml'
- 'Dockerfile*'
branches:
- master
- release-*
pull_request:
paths:
- .github/workflows/test_docker.yml
- Dockerfile.*
- scripts/CLI_VERSION
- '**/pom.xml'
branches:
- master
- release-*
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Build Docker image
run: docker build -t playwright-java:localbuild-focal -f Dockerfile.focal .
- name: Test
run: |
CONTAINER_ID="$(docker run --rm --ipc=host -v $(pwd):/root/playwright --name playwright-docker-test -d -t playwright-java:localbuild-focal /bin/bash)"
docker exec "${CONTAINER_ID}" /root/playwright/tools/test-local-installation/create_project_and_run_tests.sh
+6 -2
View File
@@ -1,12 +1,16 @@
name: Verify API
on:
push:
branches: [ master ]
branches:
- master
- release-*
paths:
- 'scripts/*'
- 'api-generator/*'
pull_request:
branches: [ master ]
branches:
- master
- release-*
paths:
- 'scripts/**'
- 'api-generator/**'
+11 -2
View File
@@ -2,7 +2,16 @@
## How to Contribute
### Getting Code
### Installing Developer Tools
Install git, Java JDK (version >= 8), Maven (tested with version 3.6.3), on Ubuntu 20.04
just run the following command:
```sh
sudo apt-get install git openjdk-11-jdk maven
```
### Getting the Code
1. Clone this repository
@@ -19,7 +28,7 @@ scripts/download_driver_for_all_platforms.sh
Names of published driver archives can be found at https://github.com/microsoft/playwright-cli/actions
### Compiling and running the tests with Maven
### Building and running the tests with Maven
```bash
mvn compile
+17 -56
View File
@@ -1,71 +1,32 @@
FROM ubuntu:focal
# === INSTALL BROWSER DEPENDENCIES ===
# Install WebKit dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
libwoff1 \
libopus0 \
libwebp6 \
libwebpdemux2 \
libenchant1c2a \
libgudev-1.0-0 \
libsecret-1-0 \
libhyphen0 \
libgdk-pixbuf2.0-0 \
libegl1 \
libnotify4 \
libxslt1.1 \
libevent-2.1-7 \
libgles2 \
libxcomposite1 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libepoxy0 \
libgtk-3-0 \
libharfbuzz-icu0
# Install gstreamer and plugins to support video playback in WebKit.
RUN apt-get update && apt-get install -y --no-install-recommends \
libgstreamer-gl1.0-0 \
libgstreamer-plugins-bad1.0-0 \
gstreamer1.0-plugins-good \
gstreamer1.0-libav
# Install Chromium dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
libnss3 \
libxss1 \
libasound2 \
fonts-noto-color-emoji \
libxtst6
# Install Firefox dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
libdbus-glib-1-2 \
libxt6
# Install ffmpeg to bring in audio and video codecs necessary for playing videos in Firefox.
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg
# (Optional) Install XVFB if there's a need to run browsers in headful mode
RUN apt-get update && apt-get install -y --no-install-recommends \
xvfb
# === INSTALL JDK and Maven ===
RUN apt-get update && apt-get install -y --no-install-recommends \
openjdk-8-jdk maven
openjdk-11-jdk maven
ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
# Install utilities required for downloading driver
RUN apt-get update && apt-get install -y --no-install-recommends \
curl unzip
# === INSTALL Playwright-java ===
# === INSTALL playwright maven modules & browsers ===
# Browsers will remain downloaded in `/ms-playwright`.
# Note: make sure to set 777 to the registry so that any user can access
# registry.
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN mkdir /ms-playwright && chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH
RUN mkdir /tmp/pw-java
COPY . /tmp/pw-java
RUN cd /tmp/pw-java && ./scripts/download_driver_for_all_platforms.sh && \
RUN cd /tmp/pw-java && \
./scripts/download_driver_for_all_platforms.sh && \
mvn install -D skipTests --no-transfer-progress && \
DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="install" -f playwright/pom.xml --no-transfer-progress && \
rm -rf /tmp/pw-java
+68 -66
View File
@@ -1,18 +1,21 @@
# 🎭 [Playwright](https://playwright.dev) for Java
[![maven version](https://img.shields.io/maven-central/v/com.microsoft.playwright/playwright)](https://search.maven.org/search?q=com.microsoft.playwright) [![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://join.slack.com/t/playwright/shared_invite/enQtOTEyMTUxMzgxMjIwLThjMDUxZmIyNTRiMTJjNjIyMzdmZDA3MTQxZWUwZTFjZjQwNGYxZGM5MzRmNzZlMWI5ZWUyOTkzMjE5Njg1NDg)
[![javadoc](https://javadoc.io/badge2/com.microsoft.playwright/playwright/javadoc.svg)](https://javadoc.io/doc/com.microsoft.playwright/playwright)
[![maven version](https://img.shields.io/maven-central/v/com.microsoft.playwright/playwright)](https://search.maven.org/search?q=com.microsoft.playwright)
[![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/https/oss.sonatype.org/com.microsoft.playwright/playwright.svg)](https://oss.sonatype.org/content/repositories/snapshots/com/microsoft/playwright/playwright/)
[![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://aka.ms/playwright-slack)
#### [Website](https://playwright.dev/) | [API reference](https://www.javadoc.io/doc/com.microsoft.playwright/playwright/latest/index.html)
#### [Website](https://playwright.dev/java/) | [API reference](https://www.javadoc.io/doc/com.microsoft.playwright/playwright/latest/index.html)
Playwright is a Java library to automate [Chromium](https://www.chromium.org/Home), [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) with a single API. Playwright is built to enable cross-browser web automation that is **ever-green**, **capable**, **reliable** and **fast**.
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->89.0.4344.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->14.1<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->85.0b1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->92.0.4498.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->14.2<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->89.0b6<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all the browsers on all platforms. Check out [system requirements](https://playwright.dev/#?path=docs/intro.md&q=system-requirements) for details.
Headless execution is supported for all the browsers on all platforms. Check out [system requirements](https://playwright.dev/java/docs/next/intro/#system-requirements) for details.
* [Usage](#usage)
- [Add Maven dependency](#add-maven-dependency)
@@ -32,7 +35,7 @@ Playwright requires **Java 8** or newer.
#### Add Maven dependency
Playwright is distributed as a set of [Maven](https://maven.apache.org/what-is-maven.html) modules. The easiest way to use it is to add a couple of dependencies to your Maven `pom.xml` file as described below. If you're not familiar with Maven please refer to its [documentation](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html).
Playwright is distributed as a set of [Maven](https://maven.apache.org/what-is-maven.html) modules. The easiest way to use it is to add one dependency to your Maven `pom.xml` file as described below. If you're not familiar with Maven please refer to its [documentation](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html).
To run Playwright simply add following dependency to your Maven project:
@@ -40,13 +43,13 @@ To run Playwright simply add following dependency to your Maven project:
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>0.171.0</version>
<version>1.11.0</version>
</dependency>
```
#### Is Playwright thread-safe?
No, Playwright is not thread safe, i.e. all its methods as well as methods on all objects created by it (such as BrowserContext, Browser, Page etc.) are expected to be called on the same thread where Playwright object was created or proper synchronization should be implemented to ensure only one thread calls Playwright methods at any given time. Having said that it's okay to create new many Playwright instances each on its own thread.
No, Playwright is not thread safe, i.e. all its methods as well as methods on all objects created by it (such as BrowserContext, Browser, Page etc.) are expected to be called on the same thread where Playwright object was created or proper synchronization should be implemented to ensure only one thread calls Playwright methods at any given time. Having said that it's okay to create multiple Playwright instances each on its own thread.
## Examples
@@ -64,23 +67,22 @@ import java.util.Arrays;
import java.util.List;
public class PageScreenshot {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
List<BrowserType> browserTypes = Arrays.asList(
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
List<BrowserType> browserTypes = Arrays.asList(
playwright.chromium(),
playwright.webkit(),
playwright.firefox()
);
for (BrowserType browserType : browserTypes) {
Browser browser = browserType.launch();
BrowserContext context = browser.newContext(
new Browser.NewContextOptions().withViewport(800, 600));
Page page = context.newPage();
page.navigate("http://whatsmyuseragent.org/");
page.screenshot(new Page.ScreenshotOptions().withPath(Paths.get("screenshot-" + browserType.name() + ".png")));
browser.close();
);
for (BrowserType browserType : browserTypes) {
try (Browser browser = browserType.launch()) {
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("http://whatsmyuseragent.org/");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("screenshot-" + browserType.name() + ".png")));
}
}
}
playwright.close();
}
}
```
@@ -90,27 +92,31 @@ public class PageScreenshot {
This snippet emulates Mobile Chromium on a device at a given geolocation, navigates to openstreetmap.org, performs action and takes a screenshot.
```java
import com.microsoft.playwright.options.*;
import com.microsoft.playwright.*;
import java.nio.file.Paths;
import static java.util.Arrays.asList;
public class MobileAndGeolocation {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
BrowserType browserType = playwright.chromium();
Browser browser = browserType.launch(new BrowserType.LaunchOptions().withHeadless(false));
DeviceDescriptor pixel2 = playwright.devices().get("Pixel 2");
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.withDevice(pixel2)
.withLocale("en-US")
.withGeolocation(new Geolocation(41.889938, 12.492507))
.withPermissions(asList("geolocation")));
Page page = context.newPage();
page.navigate("https://www.openstreetmap.org/");
page.click("a[data-original-title=\"Show My Location\"]");
page.screenshot(new Page.ScreenshotOptions().withPath(Paths.get("colosseum-pixel2.png")));
browser.close();
playwright.close();
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setUserAgent("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36")
.setViewportSize(411, 731)
.setDeviceScaleFactor(2.625)
.setIsMobile(true)
.setHasTouch(true)
.setLocale("en-US")
.setGeolocation(41.889938, 12.492507)
.setPermissions(asList("geolocation")));
Page page = context.newPage();
page.navigate("https://www.openstreetmap.org/");
page.click("a[data-original-title=\"Show My Location\"]");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("colosseum-pixel2.png")));
}
}
}
```
@@ -123,23 +129,21 @@ This code snippet navigates to example.com in Firefox, and executes a script in
import com.microsoft.playwright.*;
public class EvaluateInBrowserContext {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
BrowserType browserType = playwright.firefox();
Browser browser = browserType.launch(new BrowserType.LaunchOptions().withHeadless(false));
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://www.example.com/");
Object dimensions = page.evaluate("() => {\n" +
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.firefox().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://www.example.com/");
Object dimensions = page.evaluate("() => {\n" +
" return {\n" +
" width: document.documentElement.clientWidth,\n" +
" height: document.documentElement.clientHeight,\n" +
" deviceScaleFactor: window.devicePixelRatio\n" +
" }\n" +
"}");
System.out.println(dimensions);
browser.close();
playwright.close();
System.out.println(dimensions);
}
}
}
```
@@ -152,26 +156,26 @@ This code snippet sets up request routing for a WebKit page to log all network r
import com.microsoft.playwright.*;
public class InterceptNetworkRequests {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
BrowserType browserType = playwright.webkit();
Browser browser = browserType.launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.route("**", route -> {
System.out.println(route.request().url());
route.continue_();
});
page.navigate("http://todomvc.com");
browser.close();
playwright.close();
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.webkit().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.route("**", route -> {
System.out.println(route.request().url());
route.resume();
});
page.navigate("http://todomvc.com");
}
}
}
```
## Documentation
We are in the process of converting our documentation from the Node.js form to [Javadocs](https://www.javadoc.io/doc/com.microsoft.playwright/playwright/latest/index.html). You can go ahead and use the Node.js [documentation](https://playwright.dev/) since the API is pretty much the same.
Check out our [new documentation site](https://playwright.dev/java)!.
You can also browse [javadoc online](https://www.javadoc.io/doc/com.microsoft.playwright/playwright/latest/index.html).
## Contributing
@@ -179,6 +183,4 @@ Follow [the instructions](https://github.com/microsoft/playwright-java/blob/mast
## Is Playwright for Java ready?
Yes, Playwright for Java is ready. We are still not at the version v1.0, so breaking API changes could potentially happen. But a) this is unlikely and b) we will only do that if we know it improves your experience with the new library. We'd like to collect your feedback before we freeze the API for v1.0.
> Note: We don't yet support some of the edge-cases of the vendor-specific APIs such as collecting Chromium trace, coverage report, etc.
Yes, Playwright for Java is ready. v1.10.0 is the first stable release. Going forward we will adhere to [semantic versioning](https://semver.org/) of the API.
+4 -2
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>0.180.0-SNAPSHOT</version>
<version>1.12.0</version>
</parent>
<artifactId>driver-bundle</artifactId>
@@ -44,7 +44,9 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration>
</plugin>
</plugins>
</build>
@@ -48,6 +48,9 @@ public class DriverJar extends Driver {
p.destroy();
throw new RuntimeException("Timed out waiting for browsers to install");
}
if (p.exitValue() != 0) {
throw new RuntimeException("Failed to install browsers, exit code: " + p.exitValue());
}
}
private static boolean isExecutable(Path filePath) {
@@ -57,12 +60,19 @@ public class DriverJar extends Driver {
private void extractDriverToTempDir() throws URISyntaxException, IOException {
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
URI uri = classloader.getResource("driver/" + platformDir()).toURI();
URI originalUri = classloader.getResource("driver/" + platformDir()).toURI();
URI uri = maybeExtractNestedJar(originalUri);
// Create zip filesystem if loading from jar.
try (FileSystem fileSystem = "jar".equals(uri.getScheme()) ? FileSystems.newFileSystem(uri, Collections.emptyMap()) : null) {
Path srcRoot = Paths.get(uri);
// jar file system's .relativize gives wrong results when used with
// spring-boot-maven-plugin, convert to the default filesystem to
// have predictable results.
// See https://github.com/microsoft/playwright-java/issues/306
Path srcRootDefaultFs = Paths.get(srcRoot.toString());
Files.walk(srcRoot).forEach(fromPath -> {
Path relative = srcRoot.relativize(fromPath);
Path relative = srcRootDefaultFs.relativize(Paths.get(fromPath.toString()));
Path toPath = driverTempDir.resolve(relative.toString());
try {
if (Files.isDirectory(fromPath)) {
@@ -75,12 +85,34 @@ public class DriverJar extends Driver {
}
toPath.toFile().deleteOnExit();
} catch (IOException e) {
throw new RuntimeException("Failed to extract driver from " + uri, e);
throw new RuntimeException("Failed to extract driver from " + uri + ", full uri: " + originalUri, e);
}
});
}
}
private URI maybeExtractNestedJar(final URI uri) throws URISyntaxException {
if (!"jar".equals(uri.getScheme())) {
return uri;
}
final String JAR_URL_SEPARATOR = "!/";
String[] parts = uri.toString().split("!/");
if (parts.length != 3) {
return uri;
}
String innerJar = String.join(JAR_URL_SEPARATOR, parts[0], parts[1]);
URI jarUri = new URI(innerJar);
try (FileSystem fs = FileSystems.newFileSystem(jarUri, Collections.emptyMap())) {
Path fromPath = Paths.get(jarUri);
Path toPath = driverTempDir.resolve(fromPath.getFileName().toString());
Files.copy(fromPath, toPath);
toPath.toFile().deleteOnExit();
return new URI("jar:" + toPath.toUri() + JAR_URL_SEPARATOR + parts[2]);
} catch (IOException e) {
throw new RuntimeException("Failed to extract driver's nested .jar from " + jarUri + "; full uri: " + uri, e);
}
}
private static String platformDir() {
String name = System.getProperty("os.name").toLowerCase();
if (name.contains("windows")) {
@@ -31,19 +31,14 @@ public class TestInstall {
void playwrightCliInstalled() throws Exception {
// Clear system property to ensure that the driver is loaded from jar.
System.clearProperty("playwright.cli.dir");
try {
Path cli = Driver.ensureDriverInstalled();
assertTrue(Files.exists(cli));
Path cli = Driver.ensureDriverInstalled();
assertTrue(Files.exists(cli));
ProcessBuilder pb = new ProcessBuilder(cli.toString(), "install");
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
Process p = pb.start();
boolean result = p.waitFor(1, TimeUnit.MINUTES);
assertTrue(result, "Timed out waiting for browsers to install");
} catch (Exception e) {
e.printStackTrace();
assertNull(e);
}
ProcessBuilder pb = new ProcessBuilder(cli.toString(), "install");
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
Process p = pb.start();
boolean result = p.waitFor(1, TimeUnit.MINUTES);
assertTrue(result, "Timed out waiting for browsers to install");
}
}
+4 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>0.180.0-SNAPSHOT</version>
<version>1.12.0</version>
</parent>
<artifactId>driver</artifactId>
@@ -40,6 +40,9 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration>
</plugin>
</plugins>
</build>
+10 -2
View File
@@ -6,7 +6,7 @@
<groupId>org.example</groupId>
<artifactId>examples</artifactId>
<version>0.1-SNAPSHOT</version>
<version>1.12.0</version>
<name>Playwright Client Examples</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -15,7 +15,7 @@
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>0.180.0-SNAPSHOT</version>
<version>1.11.1</version>
</dependency>
</dependencies>
<build>
@@ -31,4 +31,12 @@
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>snapshots-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases><enabled>false</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
</project>
@@ -19,22 +19,20 @@ package org.example;
import com.microsoft.playwright.*;
public class EvaluateInBrowserContext {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
BrowserType browserType = playwright.firefox();
Browser browser = browserType.launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://www.example.com/");
Object dimensions = page.evaluate("() => {\n" +
" return {\n" +
" width: document.documentElement.clientWidth,\n" +
" height: document.documentElement.clientHeight,\n" +
" deviceScaleFactor: window.devicePixelRatio\n" +
" }\n" +
"}");
System.out.println(dimensions);
browser.close();
playwright.close();
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.firefox().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://www.example.com/");
Object dimensions = page.evaluate("() => {\n" +
" return {\n" +
" width: document.documentElement.clientWidth,\n" +
" height: document.documentElement.clientHeight,\n" +
" deviceScaleFactor: window.devicePixelRatio\n" +
" }\n" +
"}");
System.out.println(dimensions);
}
}
}
@@ -19,18 +19,16 @@ package org.example;
import com.microsoft.playwright.*;
public class InterceptNetworkRequests {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
BrowserType browserType = playwright.webkit();
Browser browser = browserType.launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.route("**", route -> {
System.out.println(route.request().url());
route.continue_();
});
page.navigate("http://todomvc.com");
browser.close();
playwright.close();
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.webkit().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.route("**", route -> {
System.out.println(route.request().url());
route.resume();
});
page.navigate("http://todomvc.com");
}
}
}
@@ -16,6 +16,7 @@
package org.example;
import com.microsoft.playwright.options.*;
import com.microsoft.playwright.*;
import java.nio.file.Paths;
@@ -23,21 +24,22 @@ import java.nio.file.Paths;
import static java.util.Arrays.asList;
public class MobileAndGeolocation {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
BrowserType browserType = playwright.chromium();
Browser browser = browserType.launch(new BrowserType.LaunchOptions().withHeadless(false));
DeviceDescriptor pixel2 = playwright.devices().get("Pixel 2");
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.withDevice(pixel2)
.withLocale("en-US")
.withGeolocation(new Geolocation(41.889938, 12.492507))
.withPermissions(asList("geolocation")));
Page page = context.newPage();
page.navigate("https://www.openstreetmap.org/");
page.click("a[data-original-title=\"Show My Location\"]");
page.screenshot(new Page.ScreenshotOptions().withPath(Paths.get("colosseum-pixel2.png")));
browser.close();
playwright.close();
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setUserAgent("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36")
.setViewportSize(411, 731)
.setDeviceScaleFactor(2.625)
.setIsMobile(true)
.setHasTouch(true)
.setLocale("en-US")
.setGeolocation(41.889938, 12.492507)
.setPermissions(asList("geolocation")));
Page page = context.newPage();
page.navigate("https://www.openstreetmap.org/");
page.click("a[data-original-title=\"Show My Location\"]");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("colosseum-pixel2.png")));
}
}
}
@@ -23,22 +23,21 @@ import java.util.Arrays;
import java.util.List;
public class PageScreenshot {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
List<BrowserType> browserTypes = Arrays.asList(
playwright.chromium(),
playwright.webkit(),
playwright.firefox()
);
for (BrowserType browserType : browserTypes) {
Browser browser = browserType.launch();
BrowserContext context = browser.newContext(
new Browser.NewContextOptions().withViewport(800, 600));
Page page = context.newPage();
page.navigate("http://whatsmyuseragent.org/");
page.screenshot(new Page.ScreenshotOptions().withPath(Paths.get("screenshot-" + browserType.name() + ".png")));
browser.close();
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
List<BrowserType> browserTypes = Arrays.asList(
playwright.chromium(),
playwright.webkit(),
playwright.firefox()
);
for (BrowserType browserType : browserTypes) {
try (Browser browser = browserType.launch()) {
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("http://whatsmyuseragent.org/");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("screenshot-" + browserType.name() + ".png")));
}
}
}
playwright.close();
}
}
@@ -0,0 +1,30 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.example;
import com.microsoft.playwright.*;
public class PrintTitle {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
Page page = browser.newPage();
page.navigate("http://playwright.dev");
System.out.println(page.title());
}
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.example;
import com.microsoft.playwright.*;
import java.nio.file.Paths;
public class WebKitScreenshot {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.webkit().launch();
Page page = browser.newPage();
page.navigate("http://whatsmyuseragent.org/");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("example.png")));
}
}
}
+5 -9
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>0.180.0-SNAPSHOT</version>
<version>1.12.0</version>
</parent>
<artifactId>playwright</artifactId>
@@ -50,9 +50,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration>
</plugin>
</plugins>
</build>
@@ -61,6 +58,10 @@
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
@@ -73,10 +74,5 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>driver-bundle</artifactId>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -1,68 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import java.util.*;
/**
* The Accessibility class provides methods for inspecting Chromium's accessibility tree. The accessibility tree is used by
* assistive technology such as [screen readers](https://en.wikipedia.org/wiki/Screen_reader) or
* [switches](https://en.wikipedia.org/wiki/Switch_access).
*
* <p> Accessibility is a very platform-specific thing. On different platforms, there are different screen readers that might
* have wildly different output.
*
* <p> Rendering engines of Chromium, Firefox and Webkit have a concept of "accessibility tree", which is then translated into
* different platform-specific APIs. Accessibility namespace gives access to this Accessibility Tree.
*
* <p> Most of the accessibility tree gets filtered out when converting from internal browser AX Tree to Platform-specific
* AX-Tree or by assistive technologies themselves. By default, Playwright tries to approximate this filtering, exposing
* only the "interesting" nodes of the tree.
*/
public interface Accessibility {
class SnapshotOptions {
/**
* Prune uninteresting nodes from the tree. Defaults to {@code true}.
*/
public Boolean interestingOnly;
/**
* The root DOM element for the snapshot. Defaults to the whole page.
*/
public ElementHandle root;
public SnapshotOptions withInterestingOnly(boolean interestingOnly) {
this.interestingOnly = interestingOnly;
return this;
}
public SnapshotOptions withRoot(ElementHandle root) {
this.root = root;
return this;
}
}
default AccessibilityNode snapshot() {
return snapshot(null);
}
/**
* Captures the current state of the accessibility tree. The returned object represents the root accessible node of the
* page.
*
* <p> <strong>NOTE:</strong> The Chromium accessibility tree contains nodes that go unused on most platforms and by most screen readers.
* Playwright will discard them as well for an easier to process tree, unless {@code interestingOnly} is set to {@code false}.
*/
AccessibilityNode snapshot(SnapshotOptions options);
}
@@ -1,51 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import java.util.List;
public interface AccessibilityNode {
String role();
String name();
String valueString();
Double valueNumber();
String description();
String keyshortcuts();
String roledescription();
String valuetext();
Boolean disabled();
Boolean expanded();
Boolean focused();
Boolean modal();
Boolean multiline();
Boolean multiselectable();
Boolean readonly();
Boolean required();
Boolean selected();
enum CheckedState { CHECKED, UNCHECKED, MIXED }
CheckedState checked();
enum PressedState { PRESSED, RELEASED, MIXED }
PressedState pressed();
Integer level();
Double valuemin();
Double valuemax();
String autocomplete();
String haspopup();
String invalid();
String orientation();
List<AccessibilityNode> children();
}
@@ -16,134 +16,46 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
/**
* - extends: [EventEmitter]
* A Browser is created via {@link BrowserType#launch BrowserType.launch()}. An example of using a {@code Browser} to create a
* {@code Page}:
* <pre>{@code
* import com.microsoft.playwright.*;
*
* <p> A Browser is created via [{@code method: BrowserType.launch}]. An example of using a {@code Browser} to create a [Page]:
* public class Example {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType firefox = playwright.firefox()
* Browser browser = firefox.launch();
* Page page = browser.newPage();
* page.navigate('https://example.com');
* browser.close();
* }
* }
* }
* }</pre>
*/
public interface Browser {
class VideoSize {
private final int width;
private final int height;
public VideoSize(int width, int height) {
this.width = width;
this.height = height;
}
public int width() {
return width;
}
public int height() {
return height;
}
}
public interface Browser extends AutoCloseable {
/**
* Emitted when Browser gets disconnected from the browser application. This might happen because of one of the following:
* <ul>
* <li> Browser application is closed or crashed.</li>
* <li> The {@link Browser#close Browser.close()} method was called.</li>
* </ul>
*/
void onDisconnected(Consumer<Browser> handler);
/**
* Removes handler that was previously added with {@link #onDisconnected onDisconnected(handler)}.
*/
void offDisconnected(Consumer<Browser> handler);
class NewContextOptions {
public class Proxy {
/**
* Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example {@code http://myproxy.com:3128} or
* {@code socks5://myproxy.com:3128}. Short form {@code myproxy.com:3128} is considered an HTTP proxy.
*/
public String server;
/**
* Optional coma-separated domains to bypass proxy, for example {@code ".com, chromium.org, .domain.com"}.
*/
public String bypass;
/**
* Optional username to use if HTTP proxy requires authentication.
*/
public String username;
/**
* Optional password to use if HTTP proxy requires authentication.
*/
public String password;
Proxy() {
}
public NewContextOptions done() {
return NewContextOptions.this;
}
public Proxy withServer(String server) {
this.server = server;
return this;
}
public Proxy withBypass(String bypass) {
this.bypass = bypass;
return this;
}
public Proxy withUsername(String username) {
this.username = username;
return this;
}
public Proxy withPassword(String password) {
this.password = password;
return this;
}
}
public class RecordHar {
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
public Boolean omitContent;
/**
* Path on the filesystem to write the HAR file to.
*/
public Path path;
RecordHar() {
}
public NewContextOptions done() {
return NewContextOptions.this;
}
public RecordHar withOmitContent(boolean omitContent) {
this.omitContent = omitContent;
return this;
}
public RecordHar withPath(Path path) {
this.path = path;
return this;
}
}
public class RecordVideo {
/**
* Path to the directory to put videos into.
*/
public Path dir;
/**
* Optional dimensions of the recorded videos. If not specified the size will be equal to {@code viewport}. If {@code viewport} is not
* configured explicitly the video size defaults to 1280x720. Actual picture of each page will be scaled down if necessary
* to fit the specified size.
*/
public VideoSize size;
RecordVideo() {
}
public NewContextOptions done() {
return NewContextOptions.this;
}
public RecordVideo withDir(Path dir) {
this.dir = dir;
return this;
}
public RecordVideo withSize(int width, int height) {
this.size = new VideoSize(width, height);
return this;
}
}
/**
* Whether to automatically download all the attachments. Defaults to {@code false} where all the downloads are canceled.
*/
@@ -153,8 +65,8 @@ public interface Browser {
*/
public Boolean bypassCSP;
/**
* Emulates {@code 'prefers-colors-scheme'} media feature, supported values are {@code 'light'}, {@code 'dark'}, {@code 'no-preference'}. See
* [{@code method: Page.emulateMedia}] for more details. Defaults to '{@code light}'.
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code "no-preference"}. See
* {@link Page#emulateMedia Page.emulateMedia()} for more details. Defaults to {@code "light"}.
*/
public ColorScheme colorScheme;
/**
@@ -171,9 +83,9 @@ public interface Browser {
*/
public Boolean hasTouch;
/**
* Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>.
*/
public BrowserContext.HTTPCredentials httpCredentials;
public HttpCredentials httpCredentials;
/**
* Whether to ignore HTTPS errors during navigation. Defaults to {@code false}.
*/
@@ -197,37 +109,64 @@ public interface Browser {
*/
public Boolean offline;
/**
* A list of permissions to grant to all pages in this context. See [{@code method: BrowserContext.grantPermissions}] for more
* details.
* A list of permissions to grant to all pages in this context. See {@link BrowserContext#grantPermissions
* BrowserContext.grantPermissions()} for more details.
*/
public List<String> permissions;
/**
* Network proxy settings to use with this context. Note that browser needs to be launched with the global proxy for this
* option to work. If all contexts override the proxy, global proxy will be never used and can be any string, for example
* {@code launch({ proxy: { server: 'per-context' } })}.
* Network proxy settings to use with this context.
*
* <p> <strong>NOTE:</strong> For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If all contexts
* override the proxy, global proxy will be never used and can be any string, for example {@code launch({ proxy: { server:
* 'http://per-context' } })}.
*/
public Proxy proxy;
/**
* Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into {@code recordHar.path} file. If not
* specified, the HAR is not recorded. Make sure to await [{@code method: BrowserContext.close}] for the HAR to be saved.
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
public RecordHar recordHar;
public Boolean recordHarOmitContent;
/**
* Enables video recording for all pages into {@code recordVideo.dir} directory. If not specified videos are not recorded. Make
* sure to await [{@code method: BrowserContext.close}] for videos to be saved.
* Enables <a href="http://www.softwareishard.com/blog/har-12-spec">HAR</a> recording for all pages into the specified HAR
* file on the filesystem. If not specified, the HAR is not recorded. Make sure to call {@link BrowserContext#close
* BrowserContext.close()} for the HAR to be saved.
*/
public RecordVideo recordVideo;
public Path recordHarPath;
/**
* Populates context with given storage state. This method can be used to initialize context with logged-in information
* obtained via [{@code method: BrowserContext.storageState}]. Either a path to the file with saved storage, or an object with
* the following fields:
* Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure
* to call {@link BrowserContext#close BrowserContext.close()} for videos to be saved.
*/
public Path recordVideoDir;
/**
* Dimensions of the recorded videos. If not specified the size will be equal to {@code viewport} scaled down to fit into
* 800x800. If {@code viewport} is not configured explicitly the video size defaults to 800x450. Actual picture of each page will
* be scaled down if necessary to fit the specified size.
*/
public RecordVideoSize recordVideoSize;
/**
* Emulates {@code "prefers-reduced-motion"} media feature, supported values are {@code "reduce"}, {@code "no-preference"}. See {@link
* Page#emulateMedia Page.emulateMedia()} for more details. Defaults to {@code "no-preference"}.
*/
public ReducedMotion reducedMotion;
/**
* Emulates consistent window screen size available inside web page via {@code window.screen}. Is only used when the {@code viewport}
* is set.
*/
public ScreenSize screenSize;
/**
* Populates context with given storage state. This option can be used to initialize context with logged-in information
* obtained via {@link BrowserContext#storageState BrowserContext.storageState()}.
*/
public String storageState;
/**
* Populates context with given storage state. This option can be used to initialize context with logged-in information
* obtained via {@link BrowserContext#storageState BrowserContext.storageState()}. Path to the file with saved storage
* state.
*/
public BrowserContext.StorageState storageState;
public Path storageStatePath;
/**
* Changes the timezone of the context. See
* [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1)
* for a list of supported timezone IDs.
* Changes the timezone of the context. See <a
* href="https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1">ICU's
* metaZones.txt</a> for a list of supported timezone IDs.
*/
public String timezoneId;
/**
@@ -235,204 +174,134 @@ public interface Browser {
*/
public String userAgent;
/**
* Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. {@code null} disables the default viewport.
* Emulates consistent viewport for each page. Defaults to an 1280x720 viewport. {@code null} disables the default viewport.
*/
public Page.Viewport viewport;
public Optional<ViewportSize> viewportSize;
public NewContextOptions withAcceptDownloads(boolean acceptDownloads) {
public NewContextOptions setAcceptDownloads(boolean acceptDownloads) {
this.acceptDownloads = acceptDownloads;
return this;
}
public NewContextOptions withBypassCSP(boolean bypassCSP) {
public NewContextOptions setBypassCSP(boolean bypassCSP) {
this.bypassCSP = bypassCSP;
return this;
}
public NewContextOptions withColorScheme(ColorScheme colorScheme) {
public NewContextOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = colorScheme;
return this;
}
public NewContextOptions withDeviceScaleFactor(double deviceScaleFactor) {
public NewContextOptions setDeviceScaleFactor(double deviceScaleFactor) {
this.deviceScaleFactor = deviceScaleFactor;
return this;
}
public NewContextOptions withExtraHTTPHeaders(Map<String, String> extraHTTPHeaders) {
public NewContextOptions setExtraHTTPHeaders(Map<String, String> extraHTTPHeaders) {
this.extraHTTPHeaders = extraHTTPHeaders;
return this;
}
public NewContextOptions withGeolocation(Geolocation geolocation) {
public NewContextOptions setGeolocation(double latitude, double longitude) {
return setGeolocation(new Geolocation(latitude, longitude));
}
public NewContextOptions setGeolocation(Geolocation geolocation) {
this.geolocation = geolocation;
return this;
}
public NewContextOptions withHasTouch(boolean hasTouch) {
public NewContextOptions setHasTouch(boolean hasTouch) {
this.hasTouch = hasTouch;
return this;
}
public NewContextOptions withHttpCredentials(String username, String password) {
this.httpCredentials = new BrowserContext.HTTPCredentials(username, password);
public NewContextOptions setHttpCredentials(String username, String password) {
return setHttpCredentials(new HttpCredentials(username, password));
}
public NewContextOptions setHttpCredentials(HttpCredentials httpCredentials) {
this.httpCredentials = httpCredentials;
return this;
}
public NewContextOptions withIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) {
public NewContextOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) {
this.ignoreHTTPSErrors = ignoreHTTPSErrors;
return this;
}
public NewContextOptions withIsMobile(boolean isMobile) {
public NewContextOptions setIsMobile(boolean isMobile) {
this.isMobile = isMobile;
return this;
}
public NewContextOptions withJavaScriptEnabled(boolean javaScriptEnabled) {
public NewContextOptions setJavaScriptEnabled(boolean javaScriptEnabled) {
this.javaScriptEnabled = javaScriptEnabled;
return this;
}
public NewContextOptions withLocale(String locale) {
public NewContextOptions setLocale(String locale) {
this.locale = locale;
return this;
}
public NewContextOptions withOffline(boolean offline) {
public NewContextOptions setOffline(boolean offline) {
this.offline = offline;
return this;
}
public NewContextOptions withPermissions(List<String> permissions) {
public NewContextOptions setPermissions(List<String> permissions) {
this.permissions = permissions;
return this;
}
public Proxy setProxy() {
this.proxy = new Proxy();
return this.proxy;
public NewContextOptions setProxy(String server) {
return setProxy(new Proxy(server));
}
public RecordHar setRecordHar() {
this.recordHar = new RecordHar();
return this.recordHar;
}
public RecordVideo setRecordVideo() {
this.recordVideo = new RecordVideo();
return this.recordVideo;
}
public NewContextOptions withStorageState(BrowserContext.StorageState storageState) {
this.storageState = storageState;
this.storageStatePath = null;
public NewContextOptions setProxy(Proxy proxy) {
this.proxy = proxy;
return this;
}
public NewContextOptions withStorageState(Path storageStatePath) {
this.storageState = null;
public NewContextOptions setRecordHarOmitContent(boolean recordHarOmitContent) {
this.recordHarOmitContent = recordHarOmitContent;
return this;
}
public NewContextOptions setRecordHarPath(Path recordHarPath) {
this.recordHarPath = recordHarPath;
return this;
}
public NewContextOptions setRecordVideoDir(Path recordVideoDir) {
this.recordVideoDir = recordVideoDir;
return this;
}
public NewContextOptions setRecordVideoSize(int width, int height) {
return setRecordVideoSize(new RecordVideoSize(width, height));
}
public NewContextOptions setRecordVideoSize(RecordVideoSize recordVideoSize) {
this.recordVideoSize = recordVideoSize;
return this;
}
public NewContextOptions setReducedMotion(ReducedMotion reducedMotion) {
this.reducedMotion = reducedMotion;
return this;
}
public NewContextOptions setScreenSize(int width, int height) {
return setScreenSize(new ScreenSize(width, height));
}
public NewContextOptions setScreenSize(ScreenSize screenSize) {
this.screenSize = screenSize;
return this;
}
public NewContextOptions setStorageState(String storageState) {
this.storageState = storageState;
return this;
}
public NewContextOptions setStorageStatePath(Path storageStatePath) {
this.storageStatePath = storageStatePath;
return this;
}
public NewContextOptions withTimezoneId(String timezoneId) {
public NewContextOptions setTimezoneId(String timezoneId) {
this.timezoneId = timezoneId;
return this;
}
public NewContextOptions withUserAgent(String userAgent) {
public NewContextOptions setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
public NewContextOptions withViewport(int width, int height) {
this.viewport = new Page.Viewport(width, height);
return this;
public NewContextOptions setViewportSize(int width, int height) {
return setViewportSize(new ViewportSize(width, height));
}
public NewContextOptions withDevice(DeviceDescriptor device) {
withViewport(device.viewport().width(), device.viewport().height());
withUserAgent(device.userAgent());
withDeviceScaleFactor(device.deviceScaleFactor());
withIsMobile(device.isMobile());
withHasTouch(device.hasTouch());
public NewContextOptions setViewportSize(ViewportSize viewportSize) {
this.viewportSize = Optional.ofNullable(viewportSize);
return this;
}
}
class NewPageOptions {
public class Proxy {
/**
* Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example {@code http://myproxy.com:3128} or
* {@code socks5://myproxy.com:3128}. Short form {@code myproxy.com:3128} is considered an HTTP proxy.
*/
public String server;
/**
* Optional coma-separated domains to bypass proxy, for example {@code ".com, chromium.org, .domain.com"}.
*/
public String bypass;
/**
* Optional username to use if HTTP proxy requires authentication.
*/
public String username;
/**
* Optional password to use if HTTP proxy requires authentication.
*/
public String password;
Proxy() {
}
public NewPageOptions done() {
return NewPageOptions.this;
}
public Proxy withServer(String server) {
this.server = server;
return this;
}
public Proxy withBypass(String bypass) {
this.bypass = bypass;
return this;
}
public Proxy withUsername(String username) {
this.username = username;
return this;
}
public Proxy withPassword(String password) {
this.password = password;
return this;
}
}
public class RecordHar {
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
public Boolean omitContent;
/**
* Path on the filesystem to write the HAR file to.
*/
public Path path;
RecordHar() {
}
public NewPageOptions done() {
return NewPageOptions.this;
}
public RecordHar withOmitContent(boolean omitContent) {
this.omitContent = omitContent;
return this;
}
public RecordHar withPath(Path path) {
this.path = path;
return this;
}
}
public class RecordVideo {
/**
* Path to the directory to put videos into.
*/
public Path dir;
/**
* Optional dimensions of the recorded videos. If not specified the size will be equal to {@code viewport}. If {@code viewport} is not
* configured explicitly the video size defaults to 1280x720. Actual picture of each page will be scaled down if necessary
* to fit the specified size.
*/
public VideoSize size;
RecordVideo() {
}
public NewPageOptions done() {
return NewPageOptions.this;
}
public RecordVideo withDir(Path dir) {
this.dir = dir;
return this;
}
public RecordVideo withSize(int width, int height) {
this.size = new VideoSize(width, height);
return this;
}
}
/**
* Whether to automatically download all the attachments. Defaults to {@code false} where all the downloads are canceled.
*/
@@ -442,8 +311,8 @@ public interface Browser {
*/
public Boolean bypassCSP;
/**
* Emulates {@code 'prefers-colors-scheme'} media feature, supported values are {@code 'light'}, {@code 'dark'}, {@code 'no-preference'}. See
* [{@code method: Page.emulateMedia}] for more details. Defaults to '{@code light}'.
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code "no-preference"}. See
* {@link Page#emulateMedia Page.emulateMedia()} for more details. Defaults to {@code "light"}.
*/
public ColorScheme colorScheme;
/**
@@ -460,9 +329,9 @@ public interface Browser {
*/
public Boolean hasTouch;
/**
* Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>.
*/
public BrowserContext.HTTPCredentials httpCredentials;
public HttpCredentials httpCredentials;
/**
* Whether to ignore HTTPS errors during navigation. Defaults to {@code false}.
*/
@@ -486,37 +355,64 @@ public interface Browser {
*/
public Boolean offline;
/**
* A list of permissions to grant to all pages in this context. See [{@code method: BrowserContext.grantPermissions}] for more
* details.
* A list of permissions to grant to all pages in this context. See {@link BrowserContext#grantPermissions
* BrowserContext.grantPermissions()} for more details.
*/
public List<String> permissions;
/**
* Network proxy settings to use with this context. Note that browser needs to be launched with the global proxy for this
* option to work. If all contexts override the proxy, global proxy will be never used and can be any string, for example
* {@code launch({ proxy: { server: 'per-context' } })}.
* Network proxy settings to use with this context.
*
* <p> <strong>NOTE:</strong> For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If all contexts
* override the proxy, global proxy will be never used and can be any string, for example {@code launch({ proxy: { server:
* 'http://per-context' } })}.
*/
public Proxy proxy;
/**
* Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into {@code recordHar.path} file. If not
* specified, the HAR is not recorded. Make sure to await [{@code method: BrowserContext.close}] for the HAR to be saved.
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
public RecordHar recordHar;
public Boolean recordHarOmitContent;
/**
* Enables video recording for all pages into {@code recordVideo.dir} directory. If not specified videos are not recorded. Make
* sure to await [{@code method: BrowserContext.close}] for videos to be saved.
* Enables <a href="http://www.softwareishard.com/blog/har-12-spec">HAR</a> recording for all pages into the specified HAR
* file on the filesystem. If not specified, the HAR is not recorded. Make sure to call {@link BrowserContext#close
* BrowserContext.close()} for the HAR to be saved.
*/
public RecordVideo recordVideo;
public Path recordHarPath;
/**
* Populates context with given storage state. This method can be used to initialize context with logged-in information
* obtained via [{@code method: BrowserContext.storageState}]. Either a path to the file with saved storage, or an object with
* the following fields:
* Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure
* to call {@link BrowserContext#close BrowserContext.close()} for videos to be saved.
*/
public Path recordVideoDir;
/**
* Dimensions of the recorded videos. If not specified the size will be equal to {@code viewport} scaled down to fit into
* 800x800. If {@code viewport} is not configured explicitly the video size defaults to 800x450. Actual picture of each page will
* be scaled down if necessary to fit the specified size.
*/
public RecordVideoSize recordVideoSize;
/**
* Emulates {@code "prefers-reduced-motion"} media feature, supported values are {@code "reduce"}, {@code "no-preference"}. See {@link
* Page#emulateMedia Page.emulateMedia()} for more details. Defaults to {@code "no-preference"}.
*/
public ReducedMotion reducedMotion;
/**
* Emulates consistent window screen size available inside web page via {@code window.screen}. Is only used when the {@code viewport}
* is set.
*/
public ScreenSize screenSize;
/**
* Populates context with given storage state. This option can be used to initialize context with logged-in information
* obtained via {@link BrowserContext#storageState BrowserContext.storageState()}.
*/
public String storageState;
/**
* Populates context with given storage state. This option can be used to initialize context with logged-in information
* obtained via {@link BrowserContext#storageState BrowserContext.storageState()}. Path to the file with saved storage
* state.
*/
public BrowserContext.StorageState storageState;
public Path storageStatePath;
/**
* Changes the timezone of the context. See
* [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1)
* for a list of supported timezone IDs.
* Changes the timezone of the context. See <a
* href="https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1">ICU's
* metaZones.txt</a> for a list of supported timezone IDs.
*/
public String timezoneId;
/**
@@ -524,112 +420,163 @@ public interface Browser {
*/
public String userAgent;
/**
* Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. {@code null} disables the default viewport.
* Emulates consistent viewport for each page. Defaults to an 1280x720 viewport. {@code null} disables the default viewport.
*/
public Page.Viewport viewport;
public Optional<ViewportSize> viewportSize;
public NewPageOptions withAcceptDownloads(boolean acceptDownloads) {
public NewPageOptions setAcceptDownloads(boolean acceptDownloads) {
this.acceptDownloads = acceptDownloads;
return this;
}
public NewPageOptions withBypassCSP(boolean bypassCSP) {
public NewPageOptions setBypassCSP(boolean bypassCSP) {
this.bypassCSP = bypassCSP;
return this;
}
public NewPageOptions withColorScheme(ColorScheme colorScheme) {
public NewPageOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = colorScheme;
return this;
}
public NewPageOptions withDeviceScaleFactor(double deviceScaleFactor) {
public NewPageOptions setDeviceScaleFactor(double deviceScaleFactor) {
this.deviceScaleFactor = deviceScaleFactor;
return this;
}
public NewPageOptions withExtraHTTPHeaders(Map<String, String> extraHTTPHeaders) {
public NewPageOptions setExtraHTTPHeaders(Map<String, String> extraHTTPHeaders) {
this.extraHTTPHeaders = extraHTTPHeaders;
return this;
}
public NewPageOptions withGeolocation(Geolocation geolocation) {
public NewPageOptions setGeolocation(double latitude, double longitude) {
return setGeolocation(new Geolocation(latitude, longitude));
}
public NewPageOptions setGeolocation(Geolocation geolocation) {
this.geolocation = geolocation;
return this;
}
public NewPageOptions withHasTouch(boolean hasTouch) {
public NewPageOptions setHasTouch(boolean hasTouch) {
this.hasTouch = hasTouch;
return this;
}
public NewPageOptions withHttpCredentials(String username, String password) {
this.httpCredentials = new BrowserContext.HTTPCredentials(username, password);
public NewPageOptions setHttpCredentials(String username, String password) {
return setHttpCredentials(new HttpCredentials(username, password));
}
public NewPageOptions setHttpCredentials(HttpCredentials httpCredentials) {
this.httpCredentials = httpCredentials;
return this;
}
public NewPageOptions withIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) {
public NewPageOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) {
this.ignoreHTTPSErrors = ignoreHTTPSErrors;
return this;
}
public NewPageOptions withIsMobile(boolean isMobile) {
public NewPageOptions setIsMobile(boolean isMobile) {
this.isMobile = isMobile;
return this;
}
public NewPageOptions withJavaScriptEnabled(boolean javaScriptEnabled) {
public NewPageOptions setJavaScriptEnabled(boolean javaScriptEnabled) {
this.javaScriptEnabled = javaScriptEnabled;
return this;
}
public NewPageOptions withLocale(String locale) {
public NewPageOptions setLocale(String locale) {
this.locale = locale;
return this;
}
public NewPageOptions withOffline(boolean offline) {
public NewPageOptions setOffline(boolean offline) {
this.offline = offline;
return this;
}
public NewPageOptions withPermissions(List<String> permissions) {
public NewPageOptions setPermissions(List<String> permissions) {
this.permissions = permissions;
return this;
}
public Proxy setProxy() {
this.proxy = new Proxy();
return this.proxy;
public NewPageOptions setProxy(String server) {
return setProxy(new Proxy(server));
}
public RecordHar setRecordHar() {
this.recordHar = new RecordHar();
return this.recordHar;
}
public RecordVideo setRecordVideo() {
this.recordVideo = new RecordVideo();
return this.recordVideo;
}
public NewPageOptions withStorageState(BrowserContext.StorageState storageState) {
this.storageState = storageState;
this.storageStatePath = null;
public NewPageOptions setProxy(Proxy proxy) {
this.proxy = proxy;
return this;
}
public NewPageOptions withStorageState(Path storageStatePath) {
this.storageState = null;
public NewPageOptions setRecordHarOmitContent(boolean recordHarOmitContent) {
this.recordHarOmitContent = recordHarOmitContent;
return this;
}
public NewPageOptions setRecordHarPath(Path recordHarPath) {
this.recordHarPath = recordHarPath;
return this;
}
public NewPageOptions setRecordVideoDir(Path recordVideoDir) {
this.recordVideoDir = recordVideoDir;
return this;
}
public NewPageOptions setRecordVideoSize(int width, int height) {
return setRecordVideoSize(new RecordVideoSize(width, height));
}
public NewPageOptions setRecordVideoSize(RecordVideoSize recordVideoSize) {
this.recordVideoSize = recordVideoSize;
return this;
}
public NewPageOptions setReducedMotion(ReducedMotion reducedMotion) {
this.reducedMotion = reducedMotion;
return this;
}
public NewPageOptions setScreenSize(int width, int height) {
return setScreenSize(new ScreenSize(width, height));
}
public NewPageOptions setScreenSize(ScreenSize screenSize) {
this.screenSize = screenSize;
return this;
}
public NewPageOptions setStorageState(String storageState) {
this.storageState = storageState;
return this;
}
public NewPageOptions setStorageStatePath(Path storageStatePath) {
this.storageStatePath = storageStatePath;
return this;
}
public NewPageOptions withTimezoneId(String timezoneId) {
public NewPageOptions setTimezoneId(String timezoneId) {
this.timezoneId = timezoneId;
return this;
}
public NewPageOptions withUserAgent(String userAgent) {
public NewPageOptions setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
public NewPageOptions withViewport(int width, int height) {
this.viewport = new Page.Viewport(width, height);
public NewPageOptions setViewportSize(int width, int height) {
return setViewportSize(new ViewportSize(width, height));
}
public NewPageOptions setViewportSize(ViewportSize viewportSize) {
this.viewportSize = Optional.ofNullable(viewportSize);
return this;
}
public NewPageOptions withDevice(DeviceDescriptor device) {
withViewport(device.viewport().width(), device.viewport().height());
withUserAgent(device.userAgent());
withDeviceScaleFactor(device.deviceScaleFactor());
withIsMobile(device.isMobile());
withHasTouch(device.hasTouch());
}
class StartTracingOptions {
/**
* specify custom categories to use instead of default.
*/
public List<String> categories;
/**
* A path to write the trace file to.
*/
public Path path;
/**
* captures screenshots in the trace.
*/
public Boolean screenshots;
public StartTracingOptions setCategories(List<String> categories) {
this.categories = categories;
return this;
}
public StartTracingOptions setPath(Path path) {
this.path = path;
return this;
}
public StartTracingOptions setScreenshots(boolean screenshots) {
this.screenshots = screenshots;
return this;
}
}
/**
* In case this browser is obtained using [{@code method: BrowserType.launch}], closes the browser and all of its pages (if any
* were opened).
* In case this browser is obtained using {@link BrowserType#launch BrowserType.launch()}, closes the browser and all of
* its pages (if any were opened).
*
* <p> In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from the
* browser server.
@@ -639,19 +586,51 @@ public interface Browser {
void close();
/**
* Returns an array of all open browser contexts. In a newly created browser, this will return zero browser contexts.
* <pre>{@code
* Browser browser = pw.webkit().launch();
* System.out.println(browser.contexts().size()); // prints "0"
* BrowserContext context = browser.newContext();
* System.out.println(browser.contexts().size()); // prints "1"
* }</pre>
*/
List<BrowserContext> contexts();
/**
* Indicates that the browser is connected.
*/
boolean isConnected();
/**
* Creates a new browser context. It won't share cookies/cache with other browser contexts.
* <pre>{@code
* Browser browser = playwright.firefox().launch(); // Or 'chromium' or 'webkit'.
* // Create a new incognito browser context.
* BrowserContext context = browser.newContext();
* // Create a new page in a pristine context.
* Page page = context.newPage();
* page.navigate('https://example.com');
* }</pre>
*/
default BrowserContext newContext() {
return newContext(null);
}
/**
* Creates a new browser context. It won't share cookies/cache with other browser contexts.
* <pre>{@code
* Browser browser = playwright.firefox().launch(); // Or 'chromium' or 'webkit'.
* // Create a new incognito browser context.
* BrowserContext context = browser.newContext();
* // Create a new page in a pristine context.
* Page page = context.newPage();
* page.navigate('https://example.com');
* }</pre>
*/
BrowserContext newContext(NewContextOptions options);
/**
* Creates a new page in a new browser context. Closing this page will close the context as well.
*
* <p> This is a convenience API that should only be used for the single-page scenarios and short snippets. Production code and
* testing frameworks should explicitly create {@link Browser#newContext Browser.newContext()} followed by the {@link
* BrowserContext#newPage BrowserContext.newPage()} to control their exact life times.
*/
default Page newPage() {
return newPage(null);
}
@@ -659,10 +638,63 @@ public interface Browser {
* Creates a new page in a new browser context. Closing this page will close the context as well.
*
* <p> This is a convenience API that should only be used for the single-page scenarios and short snippets. Production code and
* testing frameworks should explicitly create [{@code method: Browser.newContext}] followed by the
* [{@code method: BrowserContext.newPage}] to control their exact life times.
* testing frameworks should explicitly create {@link Browser#newContext Browser.newContext()} followed by the {@link
* BrowserContext#newPage BrowserContext.newPage()} to control their exact life times.
*/
Page newPage(NewPageOptions options);
/**
* <strong>NOTE:</strong> Tracing is only supported on Chromium-based browsers.
*
* <p> You can use {@link Browser#startTracing Browser.startTracing()} and {@link Browser#stopTracing Browser.stopTracing()} to
* create a trace file that can be opened in Chrome DevTools performance panel.
* <pre>{@code
* browser.startTracing(page, new Browser.StartTracingOptions()
* .setPath(Paths.get("trace.json")));
* page.goto('https://www.google.com');
* browser.stopTracing();
* }</pre>
*
* @param page Optional, if specified, tracing includes screenshots of the given page.
*/
default void startTracing(Page page) {
startTracing(page, null);
}
/**
* <strong>NOTE:</strong> Tracing is only supported on Chromium-based browsers.
*
* <p> You can use {@link Browser#startTracing Browser.startTracing()} and {@link Browser#stopTracing Browser.stopTracing()} to
* create a trace file that can be opened in Chrome DevTools performance panel.
* <pre>{@code
* browser.startTracing(page, new Browser.StartTracingOptions()
* .setPath(Paths.get("trace.json")));
* page.goto('https://www.google.com');
* browser.stopTracing();
* }</pre>
*/
default void startTracing() {
startTracing(null);
}
/**
* <strong>NOTE:</strong> Tracing is only supported on Chromium-based browsers.
*
* <p> You can use {@link Browser#startTracing Browser.startTracing()} and {@link Browser#stopTracing Browser.stopTracing()} to
* create a trace file that can be opened in Chrome DevTools performance panel.
* <pre>{@code
* browser.startTracing(page, new Browser.StartTracingOptions()
* .setPath(Paths.get("trace.json")));
* page.goto('https://www.google.com');
* browser.stopTracing();
* }</pre>
*
* @param page Optional, if specified, tracing includes screenshots of the given page.
*/
void startTracing(Page page, StartTracingOptions options);
/**
* <strong>NOTE:</strong> Tracing is only supported on Chromium-based browsers.
*
* <p> Returns the buffer with trace data.
*/
byte[] stopTracing();
/**
* Returns the browser version.
*/
File diff suppressed because it is too large Load Diff
@@ -16,62 +16,101 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.nio.file.Path;
import java.util.*;
/**
* BrowserType provides methods to launch a specific browser instance or connect to an existing one. The following is a
* typical example of using Playwright to drive automation:
* <pre>{@code
* import com.microsoft.playwright.*;
*
* public class Example {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType chromium = playwright.chromium();
* Browser browser = chromium.launch();
* Page page = browser.newPage();
* page.navigate("https://example.com");
* // other actions...
* browser.close();
* }
* }
* }
* }</pre>
*/
public interface BrowserType {
class LaunchOptions {
public class Proxy {
/**
* Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example {@code http://myproxy.com:3128} or
* {@code socks5://myproxy.com:3128}. Short form {@code myproxy.com:3128} is considered an HTTP proxy.
*/
public String server;
/**
* Optional coma-separated domains to bypass proxy, for example {@code ".com, chromium.org, .domain.com"}.
*/
public String bypass;
/**
* Optional username to use if HTTP proxy requires authentication.
*/
public String username;
/**
* Optional password to use if HTTP proxy requires authentication.
*/
public String password;
Proxy() {
}
public LaunchOptions done() {
return LaunchOptions.this;
}
public Proxy withServer(String server) {
this.server = server;
return this;
}
public Proxy withBypass(String bypass) {
this.bypass = bypass;
return this;
}
public Proxy withUsername(String username) {
this.username = username;
return this;
}
public Proxy withPassword(String password) {
this.password = password;
return this;
}
}
class ConnectOptions {
/**
* Additional arguments to pass to the browser instance. The list of Chromium flags can be found
* [here](http://peter.sh/experiments/chromium-command-line-switches/).
* Additional HTTP headers to be sent with web socket connect request. Optional.
*/
public Map<String, String> headers;
/**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0.
*/
public Double slowMo;
/**
* Maximum time in milliseconds to wait for the connection to be established. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to
* disable timeout.
*/
public Double timeout;
public ConnectOptions setHeaders(Map<String, String> headers) {
this.headers = headers;
return this;
}
public ConnectOptions setSlowMo(double slowMo) {
this.slowMo = slowMo;
return this;
}
public ConnectOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class ConnectOverCDPOptions {
/**
* Additional HTTP headers to be sent with connect request. Optional.
*/
public Map<String, String> headers;
/**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0.
*/
public Double slowMo;
/**
* Maximum time in milliseconds to wait for the connection to be established. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to
* disable timeout.
*/
public Double timeout;
public ConnectOverCDPOptions setHeaders(Map<String, String> headers) {
this.headers = headers;
return this;
}
public ConnectOverCDPOptions setSlowMo(double slowMo) {
this.slowMo = slowMo;
return this;
}
public ConnectOverCDPOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class LaunchOptions {
/**
* Additional arguments to pass to the browser instance. The list of Chromium flags can be found <a
* href="http://peter.sh/experiments/chromium-command-line-switches/">here</a>.
*/
public List<String> args;
/**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers/#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public Object channel;
/**
* Enable Chromium sandboxing. Defaults to {@code false}.
*/
@@ -97,8 +136,8 @@ public interface BrowserType {
*/
public Path executablePath;
/**
* Firefox user preferences. Learn more about the Firefox user preferences at
* [{@code about:config}](https://support.mozilla.org/en-US/kb/about-config-editor-firefox).
* Firefox user preferences. Learn more about the Firefox user preferences at <a
* href="https://support.mozilla.org/en-US/kb/about-config-editor-firefox">{@code about:config}</a>.
*/
public Map<String, Object> firefoxUserPrefs;
/**
@@ -114,18 +153,22 @@ public interface BrowserType {
*/
public Boolean handleSIGTERM;
/**
* Whether to run browser in headless mode. More details for
* [Chromium](https://developers.google.com/web/updates/2017/04/headless-chrome) and
* [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to {@code true} unless the
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true} unless the
* {@code devtools} option is {@code true}.
*/
public Boolean headless;
/**
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}. If an array is
* given, then filters out the given default arguments. Dangerous option; use with care. Defaults to {@code false}.
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}. Dangerous option;
* use with care. Defaults to {@code false}.
*/
public Boolean ignoreAllDefaultArgs;
/**
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}. Dangerous option;
* use with care.
*/
public List<String> ignoreDefaultArgs;
public Boolean ignoreAllDefaultArgs;
/**
* Network proxy settings.
*/
@@ -139,199 +182,100 @@ public interface BrowserType {
* disable timeout.
*/
public Double timeout;
/**
* If specified, traces are saved into this directory.
*/
public Path tracesDir;
public LaunchOptions withArgs(List<String> args) {
public LaunchOptions setArgs(List<String> args) {
this.args = args;
return this;
}
public LaunchOptions withChromiumSandbox(boolean chromiumSandbox) {
@Deprecated
public LaunchOptions setChannel(BrowserChannel channel) {
this.channel = channel;
return this;
}
public LaunchOptions setChannel(String channel) {
this.channel = channel;
return this;
}
public LaunchOptions setChromiumSandbox(boolean chromiumSandbox) {
this.chromiumSandbox = chromiumSandbox;
return this;
}
public LaunchOptions withDevtools(boolean devtools) {
public LaunchOptions setDevtools(boolean devtools) {
this.devtools = devtools;
return this;
}
public LaunchOptions withDownloadsPath(Path downloadsPath) {
public LaunchOptions setDownloadsPath(Path downloadsPath) {
this.downloadsPath = downloadsPath;
return this;
}
public LaunchOptions withEnv(Map<String, String> env) {
public LaunchOptions setEnv(Map<String, String> env) {
this.env = env;
return this;
}
public LaunchOptions withExecutablePath(Path executablePath) {
public LaunchOptions setExecutablePath(Path executablePath) {
this.executablePath = executablePath;
return this;
}
public LaunchOptions withFirefoxUserPrefs(Map<String, Object> firefoxUserPrefs) {
public LaunchOptions setFirefoxUserPrefs(Map<String, Object> firefoxUserPrefs) {
this.firefoxUserPrefs = firefoxUserPrefs;
return this;
}
public LaunchOptions withHandleSIGHUP(boolean handleSIGHUP) {
public LaunchOptions setHandleSIGHUP(boolean handleSIGHUP) {
this.handleSIGHUP = handleSIGHUP;
return this;
}
public LaunchOptions withHandleSIGINT(boolean handleSIGINT) {
public LaunchOptions setHandleSIGINT(boolean handleSIGINT) {
this.handleSIGINT = handleSIGINT;
return this;
}
public LaunchOptions withHandleSIGTERM(boolean handleSIGTERM) {
public LaunchOptions setHandleSIGTERM(boolean handleSIGTERM) {
this.handleSIGTERM = handleSIGTERM;
return this;
}
public LaunchOptions withHeadless(boolean headless) {
public LaunchOptions setHeadless(boolean headless) {
this.headless = headless;
return this;
}
public LaunchOptions withIgnoreDefaultArgs(List<String> argumentNames) {
this.ignoreDefaultArgs = argumentNames;
public LaunchOptions setIgnoreAllDefaultArgs(boolean ignoreAllDefaultArgs) {
this.ignoreAllDefaultArgs = ignoreAllDefaultArgs;
return this;
}
public LaunchOptions withIgnoreAllDefaultArgs(boolean ignore) {
this.ignoreAllDefaultArgs = ignore;
public LaunchOptions setIgnoreDefaultArgs(List<String> ignoreDefaultArgs) {
this.ignoreDefaultArgs = ignoreDefaultArgs;
return this;
}
public Proxy setProxy() {
this.proxy = new Proxy();
return this.proxy;
public LaunchOptions setProxy(String server) {
return setProxy(new Proxy(server));
}
public LaunchOptions withSlowMo(double slowMo) {
public LaunchOptions setProxy(Proxy proxy) {
this.proxy = proxy;
return this;
}
public LaunchOptions setSlowMo(double slowMo) {
this.slowMo = slowMo;
return this;
}
public LaunchOptions withTimeout(double timeout) {
public LaunchOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
public LaunchOptions setTracesDir(Path tracesDir) {
this.tracesDir = tracesDir;
return this;
}
}
class LaunchPersistentContextOptions {
public class Proxy {
/**
* Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example {@code http://myproxy.com:3128} or
* {@code socks5://myproxy.com:3128}. Short form {@code myproxy.com:3128} is considered an HTTP proxy.
*/
public String server;
/**
* Optional coma-separated domains to bypass proxy, for example {@code ".com, chromium.org, .domain.com"}.
*/
public String bypass;
/**
* Optional username to use if HTTP proxy requires authentication.
*/
public String username;
/**
* Optional password to use if HTTP proxy requires authentication.
*/
public String password;
Proxy() {
}
public LaunchPersistentContextOptions done() {
return LaunchPersistentContextOptions.this;
}
public Proxy withServer(String server) {
this.server = server;
return this;
}
public Proxy withBypass(String bypass) {
this.bypass = bypass;
return this;
}
public Proxy withUsername(String username) {
this.username = username;
return this;
}
public Proxy withPassword(String password) {
this.password = password;
return this;
}
}
public class RecordHar {
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
public Boolean omitContent;
/**
* Path on the filesystem to write the HAR file to.
*/
public Path path;
RecordHar() {
}
public LaunchPersistentContextOptions done() {
return LaunchPersistentContextOptions.this;
}
public RecordHar withOmitContent(boolean omitContent) {
this.omitContent = omitContent;
return this;
}
public RecordHar withPath(Path path) {
this.path = path;
return this;
}
}
public class RecordVideo {
public class Size {
/**
* Video frame width.
*/
public int width;
/**
* Video frame height.
*/
public int height;
Size() {
}
public RecordVideo done() {
return RecordVideo.this;
}
public Size withWidth(int width) {
this.width = width;
return this;
}
public Size withHeight(int height) {
this.height = height;
return this;
}
}
/**
* Path to the directory to put videos into.
*/
public Path dir;
/**
* Optional dimensions of the recorded videos. If not specified the size will be equal to {@code viewport}. If {@code viewport} is not
* configured explicitly the video size defaults to 1280x720. Actual picture of each page will be scaled down if necessary
* to fit the specified size.
*/
public Size size;
RecordVideo() {
}
public LaunchPersistentContextOptions done() {
return LaunchPersistentContextOptions.this;
}
public RecordVideo withDir(Path dir) {
this.dir = dir;
return this;
}
public Size setSize() {
this.size = new Size();
return this.size;
}
}
/**
* Whether to automatically download all the attachments. Defaults to {@code false} where all the downloads are canceled.
*/
public Boolean acceptDownloads;
/**
* Additional arguments to pass to the browser instance. The list of Chromium flags can be found
* [here](http://peter.sh/experiments/chromium-command-line-switches/).
* Additional arguments to pass to the browser instance. The list of Chromium flags can be found <a
* href="http://peter.sh/experiments/chromium-command-line-switches/">here</a>.
*/
public List<String> args;
/**
@@ -339,12 +283,18 @@ public interface BrowserType {
*/
public Boolean bypassCSP;
/**
* Enable Chromium sandboxing. Defaults to {@code true}.
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers/#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public Object channel;
/**
* Enable Chromium sandboxing. Defaults to {@code false}.
*/
public Boolean chromiumSandbox;
/**
* Emulates {@code 'prefers-colors-scheme'} media feature, supported values are {@code 'light'}, {@code 'dark'}, {@code 'no-preference'}. See
* [{@code method: Page.emulateMedia}] for more details. Defaults to '{@code light}'.
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code "no-preference"}. See
* {@link Page#emulateMedia Page.emulateMedia()} for more details. Defaults to {@code "light"}.
*/
public ColorScheme colorScheme;
/**
@@ -367,8 +317,8 @@ public interface BrowserType {
public Map<String, String> env;
/**
* Path to a browser executable to run instead of the bundled one. If {@code executablePath} is a relative path, then it is
* resolved relative to the current working directory. **BEWARE**: Playwright is only guaranteed to work with the bundled
* Chromium, Firefox or WebKit, use at your own risk.
* resolved relative to the current working directory. Note that Playwright only works with the bundled Chromium, Firefox
* or WebKit, use at your own risk.
*/
public Path executablePath;
/**
@@ -393,22 +343,26 @@ public interface BrowserType {
*/
public Boolean hasTouch;
/**
* Whether to run browser in headless mode. More details for
* [Chromium](https://developers.google.com/web/updates/2017/04/headless-chrome) and
* [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to {@code true} unless the
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true} unless the
* {@code devtools} option is {@code true}.
*/
public Boolean headless;
/**
* Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>.
*/
public BrowserContext.HTTPCredentials httpCredentials;
public HttpCredentials httpCredentials;
/**
* If {@code true}, then do not use any of the default arguments. If an array is given, then filter out the given default
* arguments. Dangerous option; use with care. Defaults to {@code false}.
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}. Dangerous option;
* use with care. Defaults to {@code false}.
*/
public Boolean ignoreAllDefaultArgs;
/**
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}. Dangerous option;
* use with care.
*/
public List<String> ignoreDefaultArgs;
public Boolean ignoreAllDefaultArgs;
/**
* Whether to ignore HTTPS errors during navigation. Defaults to {@code false}.
*/
@@ -432,8 +386,8 @@ public interface BrowserType {
*/
public Boolean offline;
/**
* A list of permissions to grant to all pages in this context. See [{@code method: BrowserContext.grantPermissions}] for more
* details.
* A list of permissions to grant to all pages in this context. See {@link BrowserContext#grantPermissions
* BrowserContext.grantPermissions()} for more details.
*/
public List<String> permissions;
/**
@@ -441,18 +395,38 @@ public interface BrowserType {
*/
public Proxy proxy;
/**
* Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into {@code recordHar.path} file. If not
* specified, the HAR is not recorded. Make sure to await [{@code method: BrowserContext.close}] for the HAR to be saved.
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
public RecordHar recordHar;
public Boolean recordHarOmitContent;
/**
* Enables video recording for all pages into {@code recordVideo.dir} directory. If not specified videos are not recorded. Make
* sure to await [{@code method: BrowserContext.close}] for videos to be saved.
* Enables <a href="http://www.softwareishard.com/blog/har-12-spec">HAR</a> recording for all pages into the specified HAR
* file on the filesystem. If not specified, the HAR is not recorded. Make sure to call {@link BrowserContext#close
* BrowserContext.close()} for the HAR to be saved.
*/
public RecordVideo recordVideo;
public Path recordHarPath;
/**
* Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure
* to call {@link BrowserContext#close BrowserContext.close()} for videos to be saved.
*/
public Path recordVideoDir;
/**
* Dimensions of the recorded videos. If not specified the size will be equal to {@code viewport} scaled down to fit into
* 800x800. If {@code viewport} is not configured explicitly the video size defaults to 800x450. Actual picture of each page will
* be scaled down if necessary to fit the specified size.
*/
public RecordVideoSize recordVideoSize;
/**
* Emulates {@code "prefers-reduced-motion"} media feature, supported values are {@code "reduce"}, {@code "no-preference"}. See {@link
* Page#emulateMedia Page.emulateMedia()} for more details. Defaults to {@code "no-preference"}.
*/
public ReducedMotion reducedMotion;
/**
* Emulates consistent window screen size available inside web page via {@code window.screen}. Is only used when the {@code viewport}
* is set.
*/
public ScreenSize screenSize;
/**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0.
*/
public Double slowMo;
/**
@@ -461,161 +435,279 @@ public interface BrowserType {
*/
public Double timeout;
/**
* Changes the timezone of the context. See
* [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1)
* for a list of supported timezone IDs.
* Changes the timezone of the context. See <a
* href="https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1">ICU's
* metaZones.txt</a> for a list of supported timezone IDs.
*/
public String timezoneId;
/**
* If specified, traces are saved into this directory.
*/
public Path tracesDir;
/**
* Specific user agent to use in this context.
*/
public String userAgent;
/**
* Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. {@code null} disables the default viewport.
* Emulates consistent viewport for each page. Defaults to an 1280x720 viewport. {@code null} disables the default viewport.
*/
public Page.Viewport viewport;
public Optional<ViewportSize> viewportSize;
public LaunchPersistentContextOptions withAcceptDownloads(boolean acceptDownloads) {
public LaunchPersistentContextOptions setAcceptDownloads(boolean acceptDownloads) {
this.acceptDownloads = acceptDownloads;
return this;
}
public LaunchPersistentContextOptions withArgs(List<String> args) {
public LaunchPersistentContextOptions setArgs(List<String> args) {
this.args = args;
return this;
}
public LaunchPersistentContextOptions withBypassCSP(boolean bypassCSP) {
public LaunchPersistentContextOptions setBypassCSP(boolean bypassCSP) {
this.bypassCSP = bypassCSP;
return this;
}
public LaunchPersistentContextOptions withChromiumSandbox(boolean chromiumSandbox) {
@Deprecated
public LaunchPersistentContextOptions setChannel(BrowserChannel channel) {
this.channel = channel;
return this;
}
public LaunchPersistentContextOptions setChannel(String channel) {
this.channel = channel;
return this;
}
public LaunchPersistentContextOptions setChromiumSandbox(boolean chromiumSandbox) {
this.chromiumSandbox = chromiumSandbox;
return this;
}
public LaunchPersistentContextOptions withColorScheme(ColorScheme colorScheme) {
public LaunchPersistentContextOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = colorScheme;
return this;
}
public LaunchPersistentContextOptions withDeviceScaleFactor(double deviceScaleFactor) {
public LaunchPersistentContextOptions setDeviceScaleFactor(double deviceScaleFactor) {
this.deviceScaleFactor = deviceScaleFactor;
return this;
}
public LaunchPersistentContextOptions withDevtools(boolean devtools) {
public LaunchPersistentContextOptions setDevtools(boolean devtools) {
this.devtools = devtools;
return this;
}
public LaunchPersistentContextOptions withDownloadsPath(Path downloadsPath) {
public LaunchPersistentContextOptions setDownloadsPath(Path downloadsPath) {
this.downloadsPath = downloadsPath;
return this;
}
public LaunchPersistentContextOptions withEnv(Map<String, String> env) {
public LaunchPersistentContextOptions setEnv(Map<String, String> env) {
this.env = env;
return this;
}
public LaunchPersistentContextOptions withExecutablePath(Path executablePath) {
public LaunchPersistentContextOptions setExecutablePath(Path executablePath) {
this.executablePath = executablePath;
return this;
}
public LaunchPersistentContextOptions withExtraHTTPHeaders(Map<String, String> extraHTTPHeaders) {
public LaunchPersistentContextOptions setExtraHTTPHeaders(Map<String, String> extraHTTPHeaders) {
this.extraHTTPHeaders = extraHTTPHeaders;
return this;
}
public LaunchPersistentContextOptions withGeolocation(Geolocation geolocation) {
public LaunchPersistentContextOptions setGeolocation(double latitude, double longitude) {
return setGeolocation(new Geolocation(latitude, longitude));
}
public LaunchPersistentContextOptions setGeolocation(Geolocation geolocation) {
this.geolocation = geolocation;
return this;
}
public LaunchPersistentContextOptions withHandleSIGHUP(boolean handleSIGHUP) {
public LaunchPersistentContextOptions setHandleSIGHUP(boolean handleSIGHUP) {
this.handleSIGHUP = handleSIGHUP;
return this;
}
public LaunchPersistentContextOptions withHandleSIGINT(boolean handleSIGINT) {
public LaunchPersistentContextOptions setHandleSIGINT(boolean handleSIGINT) {
this.handleSIGINT = handleSIGINT;
return this;
}
public LaunchPersistentContextOptions withHandleSIGTERM(boolean handleSIGTERM) {
public LaunchPersistentContextOptions setHandleSIGTERM(boolean handleSIGTERM) {
this.handleSIGTERM = handleSIGTERM;
return this;
}
public LaunchPersistentContextOptions withHasTouch(boolean hasTouch) {
public LaunchPersistentContextOptions setHasTouch(boolean hasTouch) {
this.hasTouch = hasTouch;
return this;
}
public LaunchPersistentContextOptions withHeadless(boolean headless) {
public LaunchPersistentContextOptions setHeadless(boolean headless) {
this.headless = headless;
return this;
}
public LaunchPersistentContextOptions withHttpCredentials(String username, String password) {
this.httpCredentials = new BrowserContext.HTTPCredentials(username, password);
public LaunchPersistentContextOptions setHttpCredentials(String username, String password) {
return setHttpCredentials(new HttpCredentials(username, password));
}
public LaunchPersistentContextOptions setHttpCredentials(HttpCredentials httpCredentials) {
this.httpCredentials = httpCredentials;
return this;
}
public LaunchPersistentContextOptions withIgnoreDefaultArgs(List<String> argumentNames) {
this.ignoreDefaultArgs = argumentNames;
public LaunchPersistentContextOptions setIgnoreAllDefaultArgs(boolean ignoreAllDefaultArgs) {
this.ignoreAllDefaultArgs = ignoreAllDefaultArgs;
return this;
}
public LaunchPersistentContextOptions withIgnoreAllDefaultArgs(boolean ignore) {
this.ignoreAllDefaultArgs = ignore;
public LaunchPersistentContextOptions setIgnoreDefaultArgs(List<String> ignoreDefaultArgs) {
this.ignoreDefaultArgs = ignoreDefaultArgs;
return this;
}
public LaunchPersistentContextOptions withIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) {
public LaunchPersistentContextOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) {
this.ignoreHTTPSErrors = ignoreHTTPSErrors;
return this;
}
public LaunchPersistentContextOptions withIsMobile(boolean isMobile) {
public LaunchPersistentContextOptions setIsMobile(boolean isMobile) {
this.isMobile = isMobile;
return this;
}
public LaunchPersistentContextOptions withJavaScriptEnabled(boolean javaScriptEnabled) {
public LaunchPersistentContextOptions setJavaScriptEnabled(boolean javaScriptEnabled) {
this.javaScriptEnabled = javaScriptEnabled;
return this;
}
public LaunchPersistentContextOptions withLocale(String locale) {
public LaunchPersistentContextOptions setLocale(String locale) {
this.locale = locale;
return this;
}
public LaunchPersistentContextOptions withOffline(boolean offline) {
public LaunchPersistentContextOptions setOffline(boolean offline) {
this.offline = offline;
return this;
}
public LaunchPersistentContextOptions withPermissions(List<String> permissions) {
public LaunchPersistentContextOptions setPermissions(List<String> permissions) {
this.permissions = permissions;
return this;
}
public Proxy setProxy() {
this.proxy = new Proxy();
return this.proxy;
public LaunchPersistentContextOptions setProxy(String server) {
return setProxy(new Proxy(server));
}
public RecordHar setRecordHar() {
this.recordHar = new RecordHar();
return this.recordHar;
public LaunchPersistentContextOptions setProxy(Proxy proxy) {
this.proxy = proxy;
return this;
}
public RecordVideo setRecordVideo() {
this.recordVideo = new RecordVideo();
return this.recordVideo;
public LaunchPersistentContextOptions setRecordHarOmitContent(boolean recordHarOmitContent) {
this.recordHarOmitContent = recordHarOmitContent;
return this;
}
public LaunchPersistentContextOptions withSlowMo(double slowMo) {
public LaunchPersistentContextOptions setRecordHarPath(Path recordHarPath) {
this.recordHarPath = recordHarPath;
return this;
}
public LaunchPersistentContextOptions setRecordVideoDir(Path recordVideoDir) {
this.recordVideoDir = recordVideoDir;
return this;
}
public LaunchPersistentContextOptions setRecordVideoSize(int width, int height) {
return setRecordVideoSize(new RecordVideoSize(width, height));
}
public LaunchPersistentContextOptions setRecordVideoSize(RecordVideoSize recordVideoSize) {
this.recordVideoSize = recordVideoSize;
return this;
}
public LaunchPersistentContextOptions setReducedMotion(ReducedMotion reducedMotion) {
this.reducedMotion = reducedMotion;
return this;
}
public LaunchPersistentContextOptions setScreenSize(int width, int height) {
return setScreenSize(new ScreenSize(width, height));
}
public LaunchPersistentContextOptions setScreenSize(ScreenSize screenSize) {
this.screenSize = screenSize;
return this;
}
public LaunchPersistentContextOptions setSlowMo(double slowMo) {
this.slowMo = slowMo;
return this;
}
public LaunchPersistentContextOptions withTimeout(double timeout) {
public LaunchPersistentContextOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
public LaunchPersistentContextOptions withTimezoneId(String timezoneId) {
public LaunchPersistentContextOptions setTimezoneId(String timezoneId) {
this.timezoneId = timezoneId;
return this;
}
public LaunchPersistentContextOptions withUserAgent(String userAgent) {
public LaunchPersistentContextOptions setTracesDir(Path tracesDir) {
this.tracesDir = tracesDir;
return this;
}
public LaunchPersistentContextOptions setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
public LaunchPersistentContextOptions withViewport(int width, int height) {
this.viewport = new Page.Viewport(width, height);
public LaunchPersistentContextOptions setViewportSize(int width, int height) {
return setViewportSize(new ViewportSize(width, height));
}
public LaunchPersistentContextOptions setViewportSize(ViewportSize viewportSize) {
this.viewportSize = Optional.ofNullable(viewportSize);
return this;
}
}
/**
* This methods attaches Playwright to an existing browser instance.
*
* @param wsEndpoint A browser websocket endpoint to connect to.
*/
default Browser connect(String wsEndpoint) {
return connect(wsEndpoint, null);
}
/**
* This methods attaches Playwright to an existing browser instance.
*
* @param wsEndpoint A browser websocket endpoint to connect to.
*/
Browser connect(String wsEndpoint, ConnectOptions options);
/**
* This methods attaches Playwright to an existing browser instance using the Chrome DevTools Protocol.
*
* <p> The default browser context is accessible via {@link Browser#contexts Browser.contexts()}.
*
* <p> <strong>NOTE:</strong> Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
*
* @param endpointURL A CDP websocket endpoint or http url to connect to. For example {@code http://localhost:9222/} or
* {@code ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4}.
*/
default Browser connectOverCDP(String endpointURL) {
return connectOverCDP(endpointURL, null);
}
/**
* This methods attaches Playwright to an existing browser instance using the Chrome DevTools Protocol.
*
* <p> The default browser context is accessible via {@link Browser#contexts Browser.contexts()}.
*
* <p> <strong>NOTE:</strong> Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
*
* @param endpointURL A CDP websocket endpoint or http url to connect to. For example {@code http://localhost:9222/} or
* {@code ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4}.
*/
Browser connectOverCDP(String endpointURL, ConnectOverCDPOptions options);
/**
* A path where Playwright expects to find a bundled browser executable.
*/
String executablePath();
/**
* Returns the browser instance.
*
* <p> You can use {@code ignoreDefaultArgs} to filter out {@code --mute-audio} from default arguments:
* <pre>{@code
* // Or "firefox" or "webkit".
* Browser browser = chromium.launch(new BrowserType.LaunchOptions()
* .setIgnoreDefaultArgs(Arrays.asList("--mute-audio")));
* }</pre>
*
* <p> > **Chromium-only** Playwright can also be used to control the Google Chrome or Microsoft Edge browsers, but it works
* best with the version of Chromium it is bundled with. There is no guarantee it will work with any other version. Use
* {@code executablePath} option with extreme caution.
*
* <p> >
*
* <p> > If Google Chrome (rather than Chromium) is preferred, a <a
* href="https://www.google.com/chrome/browser/canary.html">Chrome Canary</a> or <a
* href="https://www.chromium.org/getting-involved/dev-channel">Dev Channel</a> build is suggested.
*
* <p> >
*
* <p> > Stock browsers like Google Chrome and Microsoft Edge are suitable for tests that require proprietary media codecs for
* video playback. See <a
* href="https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/">this article</a> for
* other differences between Chromium and Chrome. <a
* href="https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md">This article</a>
* describes some differences for Linux users.
*/
default Browser launch() {
return launch(null);
}
@@ -623,23 +715,43 @@ public interface BrowserType {
* Returns the browser instance.
*
* <p> You can use {@code ignoreDefaultArgs} to filter out {@code --mute-audio} from default arguments:
* <pre>{@code
* // Or "firefox" or "webkit".
* Browser browser = chromium.launch(new BrowserType.LaunchOptions()
* .setIgnoreDefaultArgs(Arrays.asList("--mute-audio")));
* }</pre>
*
* <p> **Chromium-only** Playwright can also be used to control the Chrome browser, but it works best with the version of
* Chromium it is bundled with. There is no guarantee it will work with any other version. Use {@code executablePath} option with
* extreme caution.
* >
* If Google Chrome (rather than Chromium) is preferred, a
* [Chrome Canary](https://www.google.com/chrome/browser/canary.html) or
* [Dev Channel](https://www.chromium.org/getting-involved/dev-channel) build is suggested.
* >
* In [{@code method: BrowserType.launch}] above, any mention of Chromium also applies to Chrome.
* >
* See [{@code this article}](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for
* a description of the differences between Chromium and Chrome.
* [{@code This article}](https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md)
* <p> > **Chromium-only** Playwright can also be used to control the Google Chrome or Microsoft Edge browsers, but it works
* best with the version of Chromium it is bundled with. There is no guarantee it will work with any other version. Use
* {@code executablePath} option with extreme caution.
*
* <p> >
*
* <p> > If Google Chrome (rather than Chromium) is preferred, a <a
* href="https://www.google.com/chrome/browser/canary.html">Chrome Canary</a> or <a
* href="https://www.chromium.org/getting-involved/dev-channel">Dev Channel</a> build is suggested.
*
* <p> >
*
* <p> > Stock browsers like Google Chrome and Microsoft Edge are suitable for tests that require proprietary media codecs for
* video playback. See <a
* href="https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/">this article</a> for
* other differences between Chromium and Chrome. <a
* href="https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md">This article</a>
* describes some differences for Linux users.
*/
Browser launch(LaunchOptions options);
/**
* Returns the persistent browser context instance.
*
* <p> Launches browser that uses persistent storage located at {@code userDataDir} and returns the only context. Closing this
* context will automatically close the browser.
*
* @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for <a
* href="https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile">Firefox</a>. Note that
* Chromium's user data directory is the **parent** directory of the "Profile Path" seen at {@code chrome://version}.
*/
default BrowserContext launchPersistentContext(Path userDataDir) {
return launchPersistentContext(userDataDir, null);
}
@@ -649,13 +761,14 @@ public interface BrowserType {
* <p> Launches browser that uses persistent storage located at {@code userDataDir} and returns the only context. Closing this
* context will automatically close the browser.
*
* @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for
* [Chromium](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md) and
* [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile).
* @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for <a
* href="https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile">Firefox</a>. Note that
* Chromium's user data directory is the **parent** directory of the "Profile Path" seen at {@code chrome://version}.
*/
BrowserContext launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options);
/**
* Returns browser name. For example: {@code 'chromium'}, {@code 'webkit'} or {@code 'firefox'}.
* Returns browser name. For example: {@code "chromium"}, {@code "webkit"} or {@code "firefox"}.
*/
String name();
}
@@ -0,0 +1,41 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.impl.Driver;
import java.io.IOException;
import java.nio.file.Path;
import static java.util.Arrays.asList;
/**
* Use this class to launch playwright cli.
*/
public class CLI {
public static void main(String[] args) throws IOException, InterruptedException {
Path driver = Driver.ensureDriverInstalled();
ProcessBuilder pb = new ProcessBuilder(driver.toString());
pb.command().addAll(asList(args));
if (!pb.environment().containsKey("PW_CLI_TARGET_LANG")) {
pb.environment().put("PW_CLI_TARGET_LANG", "java");
}
pb.inheritIO();
Process process = pb.start();
System.exit(process.waitFor());
}
}
@@ -1,3 +0,0 @@
package com.microsoft.playwright;
public enum ColorScheme { DARK, LIGHT, NO_PREFERENCE }
@@ -19,40 +19,25 @@ package com.microsoft.playwright;
import java.util.*;
/**
* {@code ConsoleMessage} objects are dispatched by page via the [{@code event: Page.console}] event.
* {@code ConsoleMessage} objects are dispatched by page via the {@link Page#onConsole Page.onConsole()} event.
*/
public interface ConsoleMessage {
class Location {
/**
* URL of the resource.
*/
private String url;
/**
* 0-based line number in the resource.
*/
private int lineNumber;
/**
* 0-based column number in the resource.
*/
private int columnNumber;
public String url() {
return this.url;
}
public int lineNumber() {
return this.lineNumber;
}
public int columnNumber() {
return this.columnNumber;
}
}
/**
* List of arguments passed to a {@code console} function call. See also {@link Page#onConsole Page.onConsole()}.
*/
List<JSHandle> args();
Location location();
/**
* URL of the resource followed by 0-based line and column numbers in the resource formatted as {@code URL:line:column}.
*/
String location();
/**
* The text of the console message.
*/
String text();
/**
* One of the following values: {@code 'log'}, {@code 'debug'}, {@code 'info'}, {@code 'error'}, {@code 'warning'}, {@code 'dir'}, {@code 'dirxml'}, {@code 'table'},
* {@code 'trace'}, {@code 'clear'}, {@code 'startGroup'}, {@code 'startGroupCollapsed'}, {@code 'endGroup'}, {@code 'assert'}, {@code 'profile'}, {@code 'profileEnd'},
* {@code 'count'}, {@code 'timeEnd'}.
* One of the following values: {@code "log"}, {@code "debug"}, {@code "info"}, {@code "error"}, {@code "warning"}, {@code "dir"}, {@code "dirxml"}, {@code "table"},
* {@code "trace"}, {@code "clear"}, {@code "startGroup"}, {@code "startGroupCollapsed"}, {@code "endGroup"}, {@code "assert"}, {@code "profile"}, {@code "profileEnd"},
* {@code "count"}, {@code "timeEnd"}.
*/
String type();
}
@@ -19,11 +19,39 @@ package com.microsoft.playwright;
import java.util.*;
/**
* {@code Dialog} objects are dispatched by page via the [{@code event: Page.dialog}] event.
* {@code Dialog} objects are dispatched by page via the {@link Page#onDialog Page.onDialog()} event.
*
* <p> An example of using {@code Dialog} class:
* <pre>{@code
* import com.microsoft.playwright.*;
*
* public class Example {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType chromium = playwright.chromium();
* Browser browser = chromium.launch();
* Page page = browser.newPage();
* page.onDialog(dialog -> {
* System.out.println(dialog.message());
* dialog.dismiss();
* });
* page.evaluate("alert('1')");
* browser.close();
* }
* }
* }
* }</pre>
*
* <p> <strong>NOTE:</strong> Dialogs are dismissed automatically, unless there is a {@link Page#onDialog Page.onDialog()} listener. When listener is
* present, it **must** either {@link Dialog#accept Dialog.accept()} or {@link Dialog#dismiss Dialog.dismiss()} the dialog
* - otherwise the page will <a
* href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking">freeze</a> waiting for the
* dialog, and actions like click will never finish.
*/
public interface Dialog {
enum Type { ALERT, BEFOREUNLOAD, CONFIRM, PROMPT }
/**
* Returns when the dialog has been accepted.
*/
default void accept() {
accept(null);
}
@@ -48,6 +76,6 @@ public interface Dialog {
/**
* Returns dialog's type, can be one of {@code alert}, {@code beforeunload}, {@code confirm} or {@code prompt}.
*/
Type type();
String type();
}
@@ -21,16 +21,30 @@ import java.nio.file.Path;
import java.util.*;
/**
* {@code Download} objects are dispatched by page via the [{@code event: Page.download}] event.
* {@code Download} objects are dispatched by page via the {@link Page#onDownload Page.onDownload()} event.
*
* <p> All the downloaded files belonging to the browser context are deleted when the browser context is closed. All downloaded
* files are deleted when the browser closes.
* <p> If {@code downloadsPath} isn't specified, all the downloaded files belonging to the browser context are deleted when the
* browser context is closed. And all downloaded files are deleted when the browser closes.
*
* <p> Download event is emitted once the download starts. Download path becomes available once download completes:
* <pre>{@code
* // wait for download to start
* Download download = page.waitForDownload(() -> page.click("a"));
* // wait for download to complete
* Path path = download.path();
* }</pre>
* <pre>{@code
* // wait for download to start
* Download download = page.waitForDownload(() -> {
* page.click("a");
* });
* // wait for download to complete
* Path path = download.path();
* }</pre>
*
* <p> <strong>NOTE:</strong> Browser context **must** be created with the {@code acceptDownloads} set to {@code true} when user needs access to the
* downloaded content. If {@code acceptDownloads} is not set, download events are emitted, but the actual download is not
* performed and user has no access to the downloaded files.
* <p> <strong>NOTE:</strong> Browser context **must** be created with the {@code acceptDownloads} set to {@code true} when user needs access to the downloaded
* content. If {@code acceptDownloads} is not set, download events are emitted, but the actual download is not performed and user
* has no access to the downloaded files.
*/
public interface Download {
/**
@@ -38,28 +52,35 @@ public interface Download {
*/
InputStream createReadStream();
/**
* Deletes the downloaded file.
* Deletes the downloaded file. Will wait for the download to finish if necessary.
*/
void delete();
/**
* Returns download error if any.
* Returns download error if any. Will wait for the download to finish if necessary.
*/
String failure();
/**
* Returns path to the downloaded file in case of successful download.
* Get the page that the download belongs to.
*/
Page page();
/**
* Returns path to the downloaded file in case of successful download. The method will wait for the download to finish if
* necessary. The method throws when connected remotely.
*/
Path path();
/**
* Saves the download to a user-specified path.
* Copy the download to a user-specified path. It is safe to call this method while the download is still in progress. Will
* wait for the download to finish if necessary.
*
* @param path Path where the download should be saved.
* @param path Path where the download should be copied.
*/
void saveAs(Path path);
/**
* Returns suggested filename for this download. It is typically computed by the browser from the
* [{@code Content-Disposition}](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) response header
* or the {@code download} attribute. See the spec on [whatwg](https://html.spec.whatwg.org/#downloading-resources). Different
* browsers can use different logic for computing it.
* Returns suggested filename for this download. It is typically computed by the browser from the <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition">{@code Content-Disposition}</a> response
* header or the {@code download} attribute. See the spec on <a
* href="https://html.spec.whatwg.org/#downloading-resources">whatwg</a>. Different browsers can use different logic for
* computing it.
*/
String suggestedFilename();
/**
File diff suppressed because it is too large Load Diff
@@ -16,25 +16,18 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.nio.file.Path;
import java.util.*;
/**
* {@code FileChooser} objects are dispatched by the page in the [{@code event: Page.filechooser}] event.
* {@code FileChooser} objects are dispatched by the page in the {@link Page#onFileChooser Page.onFileChooser()} event.
* <pre>{@code
* FileChooser fileChooser = page.waitForFileChooser(() -> page.click("upload"));
* fileChooser.setFiles(Paths.get("myfile.pdf"));
* }</pre>
*/
public interface FileChooser {
class FilePayload {
public final String name;
public final String mimeType;
public final byte[] buffer;
public FilePayload(String name, String mimeType, byte[] buffer) {
this.name = name;
this.mimeType = mimeType;
this.buffer = buffer;
}
}
class SetFilesOptions {
/**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can
@@ -44,15 +37,16 @@ public interface FileChooser {
public Boolean noWaitAfter;
/**
* Maximum time in milliseconds, defaults to 30 seconds, pass {@code 0} to disable timeout. The default value can be changed by
* using the [{@code method: BrowserContext.setDefaultTimeout}] or [{@code method: Page.setDefaultTimeout}] methods.
* using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()} or {@link Page#setDefaultTimeout
* Page.setDefaultTimeout()} methods.
*/
public Double timeout;
public SetFilesOptions withNoWaitAfter(boolean noWaitAfter) {
public SetFilesOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter;
return this;
}
public SetFilesOptions withTimeout(double timeout) {
public SetFilesOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
@@ -69,17 +63,53 @@ public interface FileChooser {
* Returns page this file chooser belongs to.
*/
Page page();
default void setFiles(Path file) { setFiles(file, null); }
default void setFiles(Path file, SetFilesOptions options) { setFiles(new Path[]{ file }, options); }
default void setFiles(Path[] files) { setFiles(files, null); }
void setFiles(Path[] files, SetFilesOptions options);
default void setFiles(FileChooser.FilePayload file) { setFiles(file, null); }
default void setFiles(FileChooser.FilePayload file, SetFilesOptions options) { setFiles(new FileChooser.FilePayload[]{ file }, options); }
default void setFiles(FileChooser.FilePayload[] files) { setFiles(files, null); }
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
void setFiles(FileChooser.FilePayload[] files, SetFilesOptions options);
default void setFiles(Path files) {
setFiles(files, null);
}
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
void setFiles(Path files, SetFilesOptions options);
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
default void setFiles(Path[] files) {
setFiles(files, null);
}
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
void setFiles(Path[] files, SetFilesOptions options);
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
default void setFiles(FilePayload files) {
setFiles(files, null);
}
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
void setFiles(FilePayload files, SetFilesOptions options);
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
default void setFiles(FilePayload[] files) {
setFiles(files, null);
}
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
void setFiles(FilePayload[] files, SetFilesOptions options);
}
File diff suppressed because it is too large Load Diff
@@ -19,15 +19,19 @@ package com.microsoft.playwright;
import java.util.*;
/**
* JSHandle represents an in-page JavaScript object. JSHandles can be created with the [{@code method: Page.evaluateHandle}]
* method.
* JSHandle represents an in-page JavaScript object. JSHandles can be created with the {@link Page#evaluateHandle
* Page.evaluateHandle()} method.
* <pre>{@code
* JSHandle windowHandle = page.evaluateHandle("() => window");
* // ...
* }</pre>
*
* <p> JSHandle prevents the referenced JavaScript object being garbage collected unless the handle is exposed with
* [{@code method: JSHandle.dispose}]. JSHandles are auto-disposed when their origin frame gets navigated or the parent context
* gets destroyed.
* <p> JSHandle prevents the referenced JavaScript object being garbage collected unless the handle is exposed with {@link
* JSHandle#dispose JSHandle.dispose()}. JSHandles are auto-disposed when their origin frame gets navigated or the parent
* context gets destroyed.
*
* <p> JSHandle instances can be used as an argument in [{@code method: Page.$eval}], [{@code method: Page.evaluate}] and
* [{@code method: Page.evaluateHandle}] methods.
* <p> JSHandle instances can be used as an argument in {@link Page#evalOnSelector Page.evalOnSelector()}, {@link Page#evaluate
* Page.evaluate()} and {@link Page#evaluateHandle Page.evaluateHandle()} methods.
*/
public interface JSHandle {
/**
@@ -38,44 +42,95 @@ public interface JSHandle {
* The {@code jsHandle.dispose} method stops referencing the element handle.
*/
void dispose();
default Object evaluate(String pageFunction) {
return evaluate(pageFunction, null);
}
/**
* Returns the return value of {@code pageFunction}
* Returns the return value of {@code expression}.
*
* <p> This method passes this handle as the first argument to {@code pageFunction}.
* <p> This method passes this handle as the first argument to {@code expression}.
*
* <p> If {@code pageFunction} returns a [Promise], then {@code handle.evaluate} would wait for the promise to resolve and return its
* value.
* <p> If {@code expression} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then
* {@code handle.evaluate} would wait for the promise to resolve and return its value.
*
* <p> Examples:
* <pre>{@code
* ElementHandle tweetHandle = page.querySelector(".tweet .retweets");
* assertEquals("10 retweets", tweetHandle.evaluate("node => node.innerText"));
* }</pre>
*
* @param pageFunction Function to be evaluated in browser context
* @param arg Optional argument to pass to {@code pageFunction}
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
*/
Object evaluate(String pageFunction, Object arg);
default JSHandle evaluateHandle(String pageFunction) {
return evaluateHandle(pageFunction, null);
default Object evaluate(String expression) {
return evaluate(expression, null);
}
/**
* Returns the return value of {@code pageFunction} as in-page object (JSHandle).
* Returns the return value of {@code expression}.
*
* <p> This method passes this handle as the first argument to {@code pageFunction}.
* <p> This method passes this handle as the first argument to {@code expression}.
*
* <p> If {@code expression} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then
* {@code handle.evaluate} would wait for the promise to resolve and return its value.
*
* <p> Examples:
* <pre>{@code
* ElementHandle tweetHandle = page.querySelector(".tweet .retweets");
* assertEquals("10 retweets", tweetHandle.evaluate("node => node.innerText"));
* }</pre>
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param arg Optional argument to pass to {@code expression}.
*/
Object evaluate(String expression, Object arg);
/**
* Returns the return value of {@code expression} as a {@code JSHandle}.
*
* <p> This method passes this handle as the first argument to {@code expression}.
*
* <p> The only difference between {@code jsHandle.evaluate} and {@code jsHandle.evaluateHandle} is that {@code jsHandle.evaluateHandle} returns
* in-page object (JSHandle).
* {@code JSHandle}.
*
* <p> If the function passed to the {@code jsHandle.evaluateHandle} returns a [Promise], then {@code jsHandle.evaluateHandle} would wait
* for the promise to resolve and return its value.
* <p> If the function passed to the {@code jsHandle.evaluateHandle} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then
* {@code jsHandle.evaluateHandle} would wait for the promise to resolve and return its value.
*
* <p> See [{@code method: Page.evaluateHandle}] for more details.
* <p> See {@link Page#evaluateHandle Page.evaluateHandle()} for more details.
*
* @param pageFunction Function to be evaluated
* @param arg Optional argument to pass to {@code pageFunction}
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
*/
JSHandle evaluateHandle(String pageFunction, Object arg);
default JSHandle evaluateHandle(String expression) {
return evaluateHandle(expression, null);
}
/**
* Returns the return value of {@code expression} as a {@code JSHandle}.
*
* <p> This method passes this handle as the first argument to {@code expression}.
*
* <p> The only difference between {@code jsHandle.evaluate} and {@code jsHandle.evaluateHandle} is that {@code jsHandle.evaluateHandle} returns
* {@code JSHandle}.
*
* <p> If the function passed to the {@code jsHandle.evaluateHandle} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then
* {@code jsHandle.evaluateHandle} would wait for the promise to resolve and return its value.
*
* <p> See {@link Page#evaluateHandle Page.evaluateHandle()} for more details.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param arg Optional argument to pass to {@code expression}.
*/
JSHandle evaluateHandle(String expression, Object arg);
/**
* The method returns a map with **own property names** as keys and JSHandle instances for the property values.
* <pre>{@code
* JSHandle handle = page.evaluateHandle("() => ({window, document}"););
* Map<String, JSHandle> properties = handle.getProperties();
* JSHandle windowHandle = properties.get("window");
* JSHandle documentHandle = properties.get("document");
* handle.dispose();
* }</pre>
*/
Map<String, JSHandle> getProperties();
/**
@@ -87,8 +142,8 @@ public interface JSHandle {
/**
* Returns a JSON representation of the object. If the object has a {@code toJSON} function, it **will not be called**.
*
* <p> <strong>NOTE:</strong> The method will return an empty JSON object if the referenced object is not stringifiable. It will throw an
* error if the object has circular references.
* <p> <strong>NOTE:</strong> The method will return an empty JSON object if the referenced object is not stringifiable. It will throw an error if the
* object has circular references.
*/
Object jsonValue();
}
@@ -16,27 +16,51 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* Keyboard provides an api for managing a virtual keyboard. The high level api is [{@code method: Keyboard.type}], which takes
* raw characters and generates proper keydown, keypress/input, and keyup events on your page.
* Keyboard provides an api for managing a virtual keyboard. The high level api is {@link Keyboard#type Keyboard.type()},
* which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page.
*
* <p> For finer control, you can use [{@code method: Keyboard.down}], [{@code method: Keyboard.up}], and [{@code method: Keyboard.insertText}]
* to manually fire events as if they were generated from a real keyboard.
* <p> For finer control, you can use {@link Keyboard#down Keyboard.down()}, {@link Keyboard#up Keyboard.up()}, and {@link
* Keyboard#insertText Keyboard.insertText()} to manually fire events as if they were generated from a real keyboard.
*
* <p> An example of holding down {@code Shift} in order to select and delete some text:
* <pre>{@code
* page.keyboard().type("Hello World!");
* page.keyboard().press("ArrowLeft");
* page.keyboard().down("Shift");
* for (int i = 0; i < " World".length(); i++)
* page.keyboard().press("ArrowLeft");
* page.keyboard().up("Shift");
* page.keyboard().press("Backspace");
* // Result text will end up saying "Hello!"
* }</pre>
*
* <p> An example of pressing uppercase {@code A}
* <pre>{@code
* page.keyboard().press("Shift+KeyA");
* // or
* page.keyboard().press("Shift+A");
* }</pre>
*
* <p> An example to trigger select-all with the keyboard
* <pre>{@code
* // on Windows and Linux
* page.keyboard().press("Control+A");
* // on macOS
* page.keyboard().press("Meta+A");
* }</pre>
*/
public interface Keyboard {
enum Modifier { ALT, CONTROL, META, SHIFT }
class PressOptions {
/**
* Time to wait between {@code keydown} and {@code keyup} in milliseconds. Defaults to 0.
*/
public Double delay;
public PressOptions withDelay(double delay) {
public PressOptions setDelay(double delay) {
this.delay = delay;
return this;
}
@@ -47,7 +71,7 @@ public interface Keyboard {
*/
public Double delay;
public TypeOptions withDelay(double delay) {
public TypeOptions setDelay(double delay) {
this.delay = delay;
return this;
}
@@ -55,9 +79,10 @@ public interface Keyboard {
/**
* Dispatches a {@code keydown} event.
*
* <p> {@code key} can specify the intended [keyboardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key)
* value or a single character to generate the text for. A superset of the {@code key} values can be found
* [here](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values). Examples of the keys are:
* <p> {@code key} can specify the intended <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key">keyboardEvent.key</a> value or a single
* character to generate the text for. A superset of the {@code key} values can be found <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a>. Examples of the keys are:
*
* <p> {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus}, {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab},
* {@code Delete}, {@code Escape}, {@code ArrowDown}, {@code End}, {@code Enter}, {@code Home}, {@code Insert}, {@code PageDown}, {@code PageUp}, {@code ArrowRight}, {@code ArrowUp}, etc.
@@ -70,11 +95,11 @@ public interface Keyboard {
* texts.
*
* <p> If {@code key} is a modifier key, {@code Shift}, {@code Meta}, {@code Control}, or {@code Alt}, subsequent key presses will be sent with that modifier
* active. To release the modifier key, use [{@code method: Keyboard.up}].
* active. To release the modifier key, use {@link Keyboard#up Keyboard.up()}.
*
* <p> After the key is pressed once, subsequent calls to [{@code method: Keyboard.down}] will have
* [repeat](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat) set to true. To release the key, use
* [{@code method: Keyboard.up}].
* <p> After the key is pressed once, subsequent calls to {@link Keyboard#down Keyboard.down()} will have <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat">repeat</a> set to true. To release the key,
* use {@link Keyboard#up Keyboard.up()}.
*
* <p> <strong>NOTE:</strong> Modifier keys DO influence {@code keyboard.down}. Holding down {@code Shift} will type the text in upper case.
*
@@ -83,19 +108,20 @@ public interface Keyboard {
void down(String key);
/**
* Dispatches only {@code input} event, does not emit the {@code keydown}, {@code keyup} or {@code keypress} events.
* <pre>{@code
* page.keyboard().insertText("嗨");
* }</pre>
*
* <p> <strong>NOTE:</strong> Modifier keys DO NOT effect {@code keyboard.insertText}. Holding down {@code Shift} will not type the text in upper case.
*
* @param text Sets input to the specified text value.
*/
void insertText(String text);
default void press(String key) {
press(key, null);
}
/**
* {@code key} can specify the intended [keyboardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key)
* value or a single character to generate the text for. A superset of the {@code key} values can be found
* [here](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values). Examples of the keys are:
* {@code key} can specify the intended <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key">keyboardEvent.key</a> value or a single
* character to generate the text for. A superset of the {@code key} values can be found <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a>. Examples of the keys are:
*
* <p> {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus}, {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab},
* {@code Delete}, {@code Escape}, {@code ArrowDown}, {@code End}, {@code Enter}, {@code Home}, {@code Insert}, {@code PageDown}, {@code PageUp}, {@code ArrowRight}, {@code ArrowUp}, etc.
@@ -107,27 +133,100 @@ public interface Keyboard {
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate different respective
* texts.
*
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When speficied with the
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with the
* modifier, modifier is pressed and being held while the subsequent key is being pressed.
* <pre>{@code
* Page page = browser.newPage();
* page.navigate("https://keycode.info");
* page.keyboard().press("A");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png"));
* page.keyboard().press("ArrowLeft");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("ArrowLeft.png")));
* page.keyboard().press("Shift+O");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("O.png")));
* browser.close();
* }</pre>
*
* <p> Shortcut for [{@code method: Keyboard.down}] and [{@code method: Keyboard.up}].
* <p> Shortcut for {@link Keyboard#down Keyboard.down()} and {@link Keyboard#up Keyboard.up()}.
*
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
*/
void press(String key, PressOptions delay);
default void press(String key) {
press(key, null);
}
/**
* {@code key} can specify the intended <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key">keyboardEvent.key</a> value or a single
* character to generate the text for. A superset of the {@code key} values can be found <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a>. Examples of the keys are:
*
* <p> {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus}, {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab},
* {@code Delete}, {@code Escape}, {@code ArrowDown}, {@code End}, {@code Enter}, {@code Home}, {@code Insert}, {@code PageDown}, {@code PageUp}, {@code ArrowRight}, {@code ArrowUp}, etc.
*
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code ShiftLeft}.
*
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
*
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate different respective
* texts.
*
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with the
* modifier, modifier is pressed and being held while the subsequent key is being pressed.
* <pre>{@code
* Page page = browser.newPage();
* page.navigate("https://keycode.info");
* page.keyboard().press("A");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png"));
* page.keyboard().press("ArrowLeft");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("ArrowLeft.png")));
* page.keyboard().press("Shift+O");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("O.png")));
* browser.close();
* }</pre>
*
* <p> Shortcut for {@link Keyboard#down Keyboard.down()} and {@link Keyboard#up Keyboard.up()}.
*
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
*/
void press(String key, PressOptions options);
/**
* Sends a {@code keydown}, {@code keypress}/{@code input}, and {@code keyup} event for each character in the text.
*
* <p> To press a special key, like {@code Control} or {@code ArrowDown}, use {@link Keyboard#press Keyboard.press()}.
* <pre>{@code
* // Types instantly
* page.keyboard().type("Hello");
* // Types slower, like a user
* page.keyboard().type("World", new Keyboard.TypeOptions().setDelay(100));
* }</pre>
*
* <p> <strong>NOTE:</strong> Modifier keys DO NOT effect {@code keyboard.type}. Holding down {@code Shift} will not type the text in upper case.
*
* <p> <strong>NOTE:</strong> For characters that are not on a US keyboard, only an {@code input} event will be sent.
*
* @param text A text to type into a focused element.
*/
default void type(String text) {
type(text, null);
}
/**
* Sends a {@code keydown}, {@code keypress}/{@code input}, and {@code keyup} event for each character in the text.
*
* <p> To press a special key, like {@code Control} or {@code ArrowDown}, use [{@code method: Keyboard.press}].
* <p> To press a special key, like {@code Control} or {@code ArrowDown}, use {@link Keyboard#press Keyboard.press()}.
* <pre>{@code
* // Types instantly
* page.keyboard().type("Hello");
* // Types slower, like a user
* page.keyboard().type("World", new Keyboard.TypeOptions().setDelay(100));
* }</pre>
*
* <p> <strong>NOTE:</strong> Modifier keys DO NOT effect {@code keyboard.type}. Holding down {@code Shift} will not type the text in upper case.
*
* <p> <strong>NOTE:</strong> For characters that are not on a US keyboard, only an {@code input} event will be sent.
*
* @param text A text to type into a focused element.
*/
void type(String text, TypeOptions delay);
void type(String text, TypeOptions options);
/**
* Dispatches a {@code keyup} event.
*
@@ -16,21 +16,30 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* The Mouse class operates in main-frame CSS pixels relative to the top-left corner of the viewport.
*
* <p> Every {@code page} object has its own Mouse, accessible with [{@code property: Page.mouse}].
* <p> Every {@code page} object has its own Mouse, accessible with {@link Page#mouse Page.mouse()}.
* <pre>{@code
* // Using page.mouse to trace a 100x100 square.
* page.mouse().move(0, 0);
* page.mouse().down();
* page.mouse().move(0, 100);
* page.mouse().move(100, 100);
* page.mouse().move(100, 0);
* page.mouse().move(0, 0);
* page.mouse().up();
* }</pre>
*/
public interface Mouse {
enum Button { LEFT, MIDDLE, RIGHT }
class ClickOptions {
/**
* Defaults to {@code left}.
*/
public Button button;
public MouseButton button;
/**
* defaults to 1. See [UIEvent.detail].
*/
@@ -40,15 +49,15 @@ public interface Mouse {
*/
public Double delay;
public ClickOptions withButton(Button button) {
public ClickOptions setButton(MouseButton button) {
this.button = button;
return this;
}
public ClickOptions withClickCount(int clickCount) {
public ClickOptions setClickCount(int clickCount) {
this.clickCount = clickCount;
return this;
}
public ClickOptions withDelay(double delay) {
public ClickOptions setDelay(double delay) {
this.delay = delay;
return this;
}
@@ -57,17 +66,17 @@ public interface Mouse {
/**
* Defaults to {@code left}.
*/
public Button button;
public MouseButton button;
/**
* Time to wait between {@code mousedown} and {@code mouseup} in milliseconds. Defaults to 0.
*/
public Double delay;
public DblclickOptions withButton(Button button) {
public DblclickOptions setButton(MouseButton button) {
this.button = button;
return this;
}
public DblclickOptions withDelay(double delay) {
public DblclickOptions setDelay(double delay) {
this.delay = delay;
return this;
}
@@ -76,17 +85,17 @@ public interface Mouse {
/**
* Defaults to {@code left}.
*/
public Button button;
public MouseButton button;
/**
* defaults to 1. See [UIEvent.detail].
*/
public Integer clickCount;
public DownOptions withButton(Button button) {
public DownOptions setButton(MouseButton button) {
this.button = button;
return this;
}
public DownOptions withClickCount(int clickCount) {
public DownOptions setClickCount(int clickCount) {
this.clickCount = clickCount;
return this;
}
@@ -97,7 +106,7 @@ public interface Mouse {
*/
public Integer steps;
public MoveOptions withSteps(int steps) {
public MoveOptions setSteps(int steps) {
this.steps = steps;
return this;
}
@@ -106,36 +115,46 @@ public interface Mouse {
/**
* Defaults to {@code left}.
*/
public Button button;
public MouseButton button;
/**
* defaults to 1. See [UIEvent.detail].
*/
public Integer clickCount;
public UpOptions withButton(Button button) {
public UpOptions setButton(MouseButton button) {
this.button = button;
return this;
}
public UpOptions withClickCount(int clickCount) {
public UpOptions setClickCount(int clickCount) {
this.clickCount = clickCount;
return this;
}
}
/**
* Shortcut for {@link Mouse#move Mouse.move()}, {@link Mouse#down Mouse.down()}, {@link Mouse#up Mouse.up()}.
*/
default void click(double x, double y) {
click(x, y, null);
}
/**
* Shortcut for [{@code method: Mouse.move}], [{@code method: Mouse.down}], [{@code method: Mouse.up}].
* Shortcut for {@link Mouse#move Mouse.move()}, {@link Mouse#down Mouse.down()}, {@link Mouse#up Mouse.up()}.
*/
void click(double x, double y, ClickOptions options);
/**
* Shortcut for {@link Mouse#move Mouse.move()}, {@link Mouse#down Mouse.down()}, {@link Mouse#up Mouse.up()}, {@link
* Mouse#down Mouse.down()} and {@link Mouse#up Mouse.up()}.
*/
default void dblclick(double x, double y) {
dblclick(x, y, null);
}
/**
* Shortcut for [{@code method: Mouse.move}], [{@code method: Mouse.down}], [{@code method: Mouse.up}], [{@code method: Mouse.down}] and
* [{@code method: Mouse.up}].
* Shortcut for {@link Mouse#move Mouse.move()}, {@link Mouse#down Mouse.down()}, {@link Mouse#up Mouse.up()}, {@link
* Mouse#down Mouse.down()} and {@link Mouse#up Mouse.up()}.
*/
void dblclick(double x, double y, DblclickOptions options);
/**
* Dispatches a {@code mousedown} event.
*/
default void down() {
down(null);
}
@@ -143,6 +162,9 @@ public interface Mouse {
* Dispatches a {@code mousedown} event.
*/
void down(DownOptions options);
/**
* Dispatches a {@code mousemove} event.
*/
default void move(double x, double y) {
move(x, y, null);
}
@@ -150,6 +172,9 @@ public interface Mouse {
* Dispatches a {@code mousemove} event.
*/
void move(double x, double y, MoveOptions options);
/**
* Dispatches a {@code mouseup} event.
*/
default void up() {
up(null);
}
File diff suppressed because it is too large Load Diff
@@ -22,34 +22,58 @@ import java.util.*;
/**
* Playwright module provides a method to launch a browser instance. The following is a typical example of using Playwright
* to drive automation:
* <pre>{@code
* import com.microsoft.playwright.*;
*
* public class Example {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType chromium = playwright.chromium();
* Browser browser = chromium.launch();
* Page page = browser.newPage();
* page.navigate("http://example.com");
* // other actions...
* browser.close();
* }
* }
* }
* }</pre>
*/
public interface Playwright {
public interface Playwright extends AutoCloseable {
/**
* This object can be used to launch or connect to Chromium, returning instances of {@code ChromiumBrowser}.
* This object can be used to launch or connect to Chromium, returning instances of {@code Browser}.
*/
BrowserType chromium();
/**
* Returns a dictionary of devices to be used with [{@code method: Browser.newContext}] or [{@code method: Browser.newPage}].
*/
Map<String, DeviceDescriptor> devices();
/**
* This object can be used to launch or connect to Firefox, returning instances of {@code FirefoxBrowser}.
* This object can be used to launch or connect to Firefox, returning instances of {@code Browser}.
*/
BrowserType firefox();
/**
* Selectors can be used to install custom selector engines. See
* [Working with selectors](./selectors.md#working-with-selectors) for more information.
* Selectors can be used to install custom selector engines. See <a
* href="https://playwright.dev/java/docs/selectors/">Working with selectors</a> for more information.
*/
Selectors selectors();
/**
* This object can be used to launch or connect to WebKit, returning instances of {@code WebKitBrowser}.
* This object can be used to launch or connect to WebKit, returning instances of {@code Browser}.
*/
BrowserType webkit();
/**
* Terminates this instance of Playwright, will also close all created browsers if they are still running.
*/
void close();
/**
* Launches new Playwright driver process and connects to it. {@link Playwright#close Playwright.close()} should be called
* when the instance is no longer needed.
* <pre>{@code
* Playwright playwright = Playwright.create()) {
* Browser browser = playwright.webkit().launch();
* Page page = browser.newPage();
* page.navigate("https://www.w3.org/");
* playwright.close();
* }</pre>
*/
static Playwright create() {
return PlaywrightImpl.create();
}
void close() throws Exception;
}
@@ -16,6 +16,10 @@
package com.microsoft.playwright;
/**
* PlaywrightException is thrown whenever certain operations are terminated abnormally, e.g. browser closes while {@link
* Page#evaluate Page.evaluate()} is running. All Playwright exceptions inherit from this class.
*/
public class PlaywrightException extends RuntimeException {
public PlaywrightException(String message) {
super(message);
@@ -16,117 +16,39 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* Whenever the page sends a request for a network resource the following sequence of events are emitted by {@code Page}:
* - [{@code event: Page.request}] emitted when the request is issued by the page.
* - [{@code event: Page.response}] emitted when/if the response status and headers are received for the request.
* - [{@code event: Page.requestfinished}] emitted when the response body is downloaded and the request is complete.
* <ul>
* <li> {@link Page#onRequest Page.onRequest()} emitted when the request is issued by the page.</li>
* <li> {@link Page#onResponse Page.onResponse()} emitted when/if the response status and headers are received for the request.</li>
* <li> {@link Page#onRequestFinished Page.onRequestFinished()} emitted when the response body is downloaded and the request is
* complete.</li>
* </ul>
*
* <p> If request fails at some point, then instead of {@code 'requestfinished'} event (and possibly instead of 'response' event),
* the [{@code event: Page.requestfailed}] event is emitted.
* <p> If request fails at some point, then instead of {@code "requestfinished"} event (and possibly instead of 'response' event),
* the {@link Page#onRequestFailed Page.onRequestFailed()} event is emitted.
*
* <p> <strong>NOTE:</strong> HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request will
* complete with {@code 'requestfinished'} event.
* <p> <strong>NOTE:</strong> HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request will complete
* with {@code "requestfinished"} event.
*
* <p> If request gets a 'redirect' response, the request is successfully finished with the 'requestfinished' event, and a new
* request is issued to a redirected url.
*/
public interface Request {
class RequestFailure {
/**
* Human-readable error message, e.g. {@code 'net::ERR_FAILED'}.
*/
private String errorText;
public RequestFailure(String errorText) {
this.errorText = errorText;
}
public String errorText() {
return this.errorText;
}
}
class RequestTiming {
/**
* Request start time in milliseconds elapsed since January 1, 1970 00:00:00 UTC
*/
private double startTime;
/**
* Time immediately before the browser starts the domain name lookup for the resource. The value is given in milliseconds
* relative to {@code startTime}, -1 if not available.
*/
private double domainLookupStart;
/**
* Time immediately after the browser starts the domain name lookup for the resource. The value is given in milliseconds
* relative to {@code startTime}, -1 if not available.
*/
private double domainLookupEnd;
/**
* Time immediately before the user agent starts establishing the connection to the server to retrieve the resource. The
* value is given in milliseconds relative to {@code startTime}, -1 if not available.
*/
private double connectStart;
/**
* Time immediately before the browser starts the handshake process to secure the current connection. The value is given in
* milliseconds relative to {@code startTime}, -1 if not available.
*/
private double secureConnectionStart;
/**
* Time immediately before the user agent starts establishing the connection to the server to retrieve the resource. The
* value is given in milliseconds relative to {@code startTime}, -1 if not available.
*/
private double connectEnd;
/**
* Time immediately before the browser starts requesting the resource from the server, cache, or local resource. The value
* is given in milliseconds relative to {@code startTime}, -1 if not available.
*/
private double requestStart;
/**
* Time immediately after the browser starts requesting the resource from the server, cache, or local resource. The value
* is given in milliseconds relative to {@code startTime}, -1 if not available.
*/
private double responseStart;
/**
* Time immediately after the browser receives the last byte of the resource or immediately before the transport connection
* is closed, whichever comes first. The value is given in milliseconds relative to {@code startTime}, -1 if not available.
*/
private double responseEnd;
public double startTime() {
return this.startTime;
}
public double domainLookupStart() {
return this.domainLookupStart;
}
public double domainLookupEnd() {
return this.domainLookupEnd;
}
public double connectStart() {
return this.connectStart;
}
public double secureConnectionStart() {
return this.secureConnectionStart;
}
public double connectEnd() {
return this.connectEnd;
}
public double requestStart() {
return this.requestStart;
}
public double responseStart() {
return this.responseStart;
}
public double responseEnd() {
return this.responseEnd;
}
}
/**
* The method returns {@code null} unless this request has failed, as reported by {@code requestfailed} event.
*
* <p> Example of logging of all the failed requests:
* <pre>{@code
* page.onRequestFailed(request -> {
* System.out.println(request.url() + " " + request.failure());
* });
* }</pre>
*/
RequestFailure failure();
String failure();
/**
* Returns the {@code Frame} that initiated this request.
*/
@@ -159,14 +81,25 @@ public interface Request {
* construct the whole redirect chain by repeatedly calling {@code redirectedFrom()}.
*
* <p> For example, if the website {@code http://example.com} redirects to {@code https://example.com}:
* <pre>{@code
* Response response = page.navigate("http://example.com");
* System.out.println(response.request().redirectedFrom().url()); // "http://example.com"
* }</pre>
*
* <p> If the website {@code https://google.com} has no redirects:
* <pre>{@code
* Response response = page.navigate("https://google.com");
* System.out.println(response.request().redirectedFrom()); // null
* }</pre>
*/
Request redirectedFrom();
/**
* New request issued by the browser if the server responded with redirect.
*
* <p> This method is the opposite of [{@code method: Request.redirectedFrom}]:
* <p> This method is the opposite of {@link Request#redirectedFrom Request.redirectedFrom()}:
* <pre>{@code
* System.out.println(request.redirectedFrom().redirectedTo() == request); // true
* }</pre>
*/
Request redirectedTo();
/**
@@ -181,10 +114,17 @@ public interface Request {
Response response();
/**
* Returns resource timing information for given request. Most of the timing values become available upon the response,
* {@code responseEnd} becomes available when request finishes. Find more information at
* [Resource Timing API](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming).
* {@code responseEnd} becomes available when request finishes. Find more information at <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming">Resource Timing API</a>.
* <pre>{@code
* page.onRequestFinished(request -> {
* Timing timing = request.timing();
* System.out.println(timing.responseEnd - timing.startTime);
* });
* page.navigate("http://example.com");
* }</pre>
*/
RequestTiming timing();
Timing timing();
/**
* URL of the request.
*/
@@ -21,11 +21,11 @@ import java.nio.file.Path;
import java.util.*;
/**
* Whenever a network route is set up with [{@code method: Page.route}] or [{@code method: BrowserContext.route}], the {@code Route} object
* allows to handle the route.
* Whenever a network route is set up with {@link Page#route Page.route()} or {@link BrowserContext#route
* BrowserContext.route()}, the {@code Route} object allows to handle the route.
*/
public interface Route {
class ContinueOptions {
class ResumeOptions {
/**
* If set changes the request HTTP headers. Header values will be converted to a string.
*/
@@ -37,38 +37,41 @@ public interface Route {
/**
* If set changes the post data of request
*/
public byte[] postData;
public Object postData;
/**
* If set changes the request URL. New URL must have same protocol as original one.
*/
public String url;
public ContinueOptions withHeaders(Map<String, String> headers) {
public ResumeOptions setHeaders(Map<String, String> headers) {
this.headers = headers;
return this;
}
public ContinueOptions withMethod(String method) {
public ResumeOptions setMethod(String method) {
this.method = method;
return this;
}
public ContinueOptions withPostData(String postData) {
this.postData = postData.getBytes(StandardCharsets.UTF_8);
return this;
}
public ContinueOptions withPostData(byte[] postData) {
public ResumeOptions setPostData(String postData) {
this.postData = postData;
return this;
}
public ContinueOptions withUrl(String url) {
public ResumeOptions setPostData(byte[] postData) {
this.postData = postData;
return this;
}
public ResumeOptions setUrl(String url) {
this.url = url;
return this;
}
}
class FulfillOptions {
/**
* Response body.
* Optional response body as text.
*/
public String body;
/**
* Optional response body as raw bytes.
*/
public byte[] bodyBytes;
/**
* If set, equals to setting {@code Content-Type} response header.
@@ -88,31 +91,34 @@ public interface Route {
*/
public Integer status;
public FulfillOptions withBody(byte[] body) {
this.bodyBytes = body;
return this;
}
public FulfillOptions withBody(String body) {
public FulfillOptions setBody(String body) {
this.body = body;
return this;
}
public FulfillOptions withContentType(String contentType) {
public FulfillOptions setBodyBytes(byte[] bodyBytes) {
this.bodyBytes = bodyBytes;
return this;
}
public FulfillOptions setContentType(String contentType) {
this.contentType = contentType;
return this;
}
public FulfillOptions withHeaders(Map<String, String> headers) {
public FulfillOptions setHeaders(Map<String, String> headers) {
this.headers = headers;
return this;
}
public FulfillOptions withPath(Path path) {
public FulfillOptions setPath(Path path) {
this.path = path;
return this;
}
public FulfillOptions withStatus(int status) {
public FulfillOptions setStatus(int status) {
this.status = status;
return this;
}
}
/**
* Aborts the route's request.
*/
default void abort() {
abort(null);
}
@@ -120,36 +126,94 @@ public interface Route {
* Aborts the route's request.
*
* @param errorCode Optional error code. Defaults to {@code failed}, could be one of the following:
* - {@code 'aborted'} - An operation was aborted (due to user action)
* - {@code 'accessdenied'} - Permission to access a resource, other than the network, was denied
* - {@code 'addressunreachable'} - The IP address is unreachable. This usually means that there is no route to the specified
* host or network.
* - {@code 'blockedbyclient'} - The client chose to block the request.
* - {@code 'blockedbyresponse'} - The request failed because the response was delivered along with requirements which are not
* met ('X-Frame-Options' and 'Content-Security-Policy' ancestor checks, for instance).
* - {@code 'connectionaborted'} - A connection timed out as a result of not receiving an ACK for data sent.
* - {@code 'connectionclosed'} - A connection was closed (corresponding to a TCP FIN).
* - {@code 'connectionfailed'} - A connection attempt failed.
* - {@code 'connectionrefused'} - A connection attempt was refused.
* - {@code 'connectionreset'} - A connection was reset (corresponding to a TCP RST).
* - {@code 'internetdisconnected'} - The Internet connection has been lost.
* - {@code 'namenotresolved'} - The host name could not be resolved.
* - {@code 'timedout'} - An operation timed out.
* - {@code 'failed'} - A generic failure occurred.
* <ul>
* <li> {@code "aborted"} - An operation was aborted (due to user action)</li>
* <li> {@code "accessdenied"} - Permission to access a resource, other than the network, was denied</li>
* <li> {@code "addressunreachable"} - The IP address is unreachable. This usually means that there is no route to the specified host
* or network.</li>
* <li> {@code "blockedbyclient"} - The client chose to block the request.</li>
* <li> {@code "blockedbyresponse"} - The request failed because the response was delivered along with requirements which are not met
* ('X-Frame-Options' and 'Content-Security-Policy' ancestor checks, for instance).</li>
* <li> {@code "connectionaborted"} - A connection timed out as a result of not receiving an ACK for data sent.</li>
* <li> {@code "connectionclosed"} - A connection was closed (corresponding to a TCP FIN).</li>
* <li> {@code "connectionfailed"} - A connection attempt failed.</li>
* <li> {@code "connectionrefused"} - A connection attempt was refused.</li>
* <li> {@code "connectionreset"} - A connection was reset (corresponding to a TCP RST).</li>
* <li> {@code "internetdisconnected"} - The Internet connection has been lost.</li>
* <li> {@code "namenotresolved"} - The host name could not be resolved.</li>
* <li> {@code "timedout"} - An operation timed out.</li>
* <li> {@code "failed"} - A generic failure occurred.</li>
* </ul>
*/
void abort(String errorCode);
default void continue_() {
continue_(null);
/**
* Continues route's request with optional overrides.
* <pre>{@code
* page.route("**\/*", route -> {
* // Override headers
* Map<String, String> headers = new HashMap<>(route.request().headers());
* headers.put("foo", "bar"); // set "foo" header
* headers.remove("origin"); // remove "origin" header
* route.resume(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
*/
default void resume() {
resume(null);
}
/**
* Continues route's request with optional overrides.
* <pre>{@code
* page.route("**\/*", route -> {
* // Override headers
* Map<String, String> headers = new HashMap<>(route.request().headers());
* headers.put("foo", "bar"); // set "foo" header
* headers.remove("origin"); // remove "origin" header
* route.resume(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
*/
void resume(ResumeOptions options);
/**
* Fulfills route's request with given response.
*
* <p> An example of fulfilling all requests with 404 responses:
* <pre>{@code
* page.route("**\/*", route -> {
* route.fulfill(new Route.FulfillOptions()
* .setStatus(404)
* .setContentType("text/plain")
* .setBody("Not Found!"));
* });
* }</pre>
*
* <p> An example of serving static file:
* <pre>{@code
* page.route("**\/xhr_endpoint", route -> route.fulfill(
* new Route.FulfillOptions().setPath(Paths.get("mock_data.json")));
* }</pre>
*/
void continue_(ContinueOptions options);
default void fulfill() {
fulfill(null);
}
/**
* Fulfills route's request with given response.
*
* <p> An example of fulfilling all requests with 404 responses:
* <pre>{@code
* page.route("**\/*", route -> {
* route.fulfill(new Route.FulfillOptions()
* .setStatus(404)
* .setContentType("text/plain")
* .setBody("Not Found!"));
* });
* }</pre>
*
* <p> An example of serving static file:
* <pre>{@code
* page.route("**\/xhr_endpoint", route -> route.fulfill(
* new Route.FulfillOptions().setPath(Paths.get("mock_data.json")));
* }</pre>
*/
void fulfill(FulfillOptions options);
/**
@@ -20,8 +20,8 @@ import java.nio.file.Path;
import java.util.*;
/**
* Selectors can be used to install custom selector engines. See
* [Working with selectors](./selectors.md#working-with-selectors) for more information.
* Selectors can be used to install custom selector engines. See <a
* href="https://playwright.dev/java/docs/selectors/">Working with selectors</a> for more information.
*/
public interface Selectors {
class RegisterOptions {
@@ -32,22 +32,146 @@ public interface Selectors {
*/
public Boolean contentScript;
public RegisterOptions withContentScript(boolean contentScript) {
public RegisterOptions setContentScript(boolean contentScript) {
this.contentScript = contentScript;
return this;
}
}
default void register(String name, String script) { register(name, script, null); }
void register(String name, String script, RegisterOptions options);
default void register(String name, Path path) { register(name, path, null); }
/**
* An example of registering selector engine that queries elements based on a tag name:
*
* <pre>{@code
* // Script that evaluates to a selector engine instance.
* String createTagNameEngine = "{\n" +
* " // Returns the first element matching given selector in the root's subtree.\n" +
* " query(root, selector) {\n" +
* " return root.querySelector(selector);\n" +
* " },\n" +
* " // Returns all elements matching given selector in the root's subtree.\n" +
* " queryAll(root, selector) {\n" +
* " return Array.from(root.querySelectorAll(selector));\n" +
* " }\n" +
* "}";
* // Register the engine. Selectors will be prefixed with "tag=".
* playwright.selectors().register("tag", createTagNameEngine);
* Browser browser = playwright.firefox().launch();
* Page page = browser.newPage();
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* ElementHandle button = page.querySelector("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* browser.close();
* }</pre>
*
* @param name Name that is used in selectors as a prefix, e.g. {@code {name: 'foo'}} enables {@code foo=myselectorbody} selectors. May only
* contain {@code [a-zA-Z0-9_]} characters.
* @param script Script that evaluates to a selector engine instance.
*/
void register(String name, Path path, RegisterOptions options);
default void register(String name, String script) {
register(name, script, null);
}
/**
* An example of registering selector engine that queries elements based on a tag name:
* <pre>{@code
* // Script that evaluates to a selector engine instance.
* String createTagNameEngine = "{\n" +
* " // Returns the first element matching given selector in the root's subtree.\n" +
* " query(root, selector) {\n" +
* " return root.querySelector(selector);\n" +
* " },\n" +
* " // Returns all elements matching given selector in the root's subtree.\n" +
* " queryAll(root, selector) {\n" +
* " return Array.from(root.querySelectorAll(selector));\n" +
* " }\n" +
* "}";
* // Register the engine. Selectors will be prefixed with "tag=".
* playwright.selectors().register("tag", createTagNameEngine);
* Browser browser = playwright.firefox().launch();
* Page page = browser.newPage();
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* ElementHandle button = page.querySelector("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* browser.close();
* }</pre>
*
* @param name Name that is used in selectors as a prefix, e.g. {@code {name: 'foo'}} enables {@code foo=myselectorbody} selectors. May only
* contain {@code [a-zA-Z0-9_]} characters.
* @param script Script that evaluates to a selector engine instance.
*/
void register(String name, String script, RegisterOptions options);
/**
* An example of registering selector engine that queries elements based on a tag name:
* <pre>{@code
* // Script that evaluates to a selector engine instance.
* String createTagNameEngine = "{\n" +
* " // Returns the first element matching given selector in the root's subtree.\n" +
* " query(root, selector) {\n" +
* " return root.querySelector(selector);\n" +
* " },\n" +
* " // Returns all elements matching given selector in the root's subtree.\n" +
* " queryAll(root, selector) {\n" +
* " return Array.from(root.querySelectorAll(selector));\n" +
* " }\n" +
* "}";
* // Register the engine. Selectors will be prefixed with "tag=".
* playwright.selectors().register("tag", createTagNameEngine);
* Browser browser = playwright.firefox().launch();
* Page page = browser.newPage();
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* ElementHandle button = page.querySelector("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* browser.close();
* }</pre>
*
* @param name Name that is used in selectors as a prefix, e.g. {@code {name: 'foo'}} enables {@code foo=myselectorbody} selectors. May only
* contain {@code [a-zA-Z0-9_]} characters.
* @param script Script that evaluates to a selector engine instance.
*/
default void register(String name, Path script) {
register(name, script, null);
}
/**
* An example of registering selector engine that queries elements based on a tag name:
* <pre>{@code
* // Script that evaluates to a selector engine instance.
* String createTagNameEngine = "{\n" +
* " // Returns the first element matching given selector in the root's subtree.\n" +
* " query(root, selector) {\n" +
* " return root.querySelector(selector);\n" +
* " },\n" +
* " // Returns all elements matching given selector in the root's subtree.\n" +
* " queryAll(root, selector) {\n" +
* " return Array.from(root.querySelectorAll(selector));\n" +
* " }\n" +
* "}";
* // Register the engine. Selectors will be prefixed with "tag=".
* playwright.selectors().register("tag", createTagNameEngine);
* Browser browser = playwright.firefox().launch();
* Page page = browser.newPage();
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* ElementHandle button = page.querySelector("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* browser.close();
* }</pre>
*
* @param name Name that is used in selectors as a prefix, e.g. {@code {name: 'foo'}} enables {@code foo=myselectorbody} selectors. May only
* contain {@code [a-zA-Z0-9_]} characters.
* @param script Script that evaluates to a selector engine instance.
*/
void register(String name, Path script, RegisterOptions options);
}
@@ -16,14 +16,17 @@
package com.microsoft.playwright;
import java.util.*;
/**
* - extends: [Error]
*
* <p> TimeoutError is emitted whenever certain operations are terminated due to timeout, e.g. [{@code method: Page.waitForSelector}]
* or [{@code method: BrowserType.launch}].
* TimeoutError is emitted whenever certain operations are terminated due to timeout, e.g. {@link Page#waitForSelector
* Page.waitForSelector()} or {@link BrowserType#launch BrowserType.launch()}.
*/
public interface TimeoutError {
public class TimeoutError extends PlaywrightException {
public TimeoutError(String message) {
super(message);
}
public TimeoutError(String message, Throwable exception) {
super(message, exception);
}
}
@@ -20,7 +20,7 @@ import java.util.*;
/**
* The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the
* touchscreen can only be used in browser contexts that have been intialized with {@code hasTouch} set to true.
* touchscreen can only be used in browser contexts that have been initialized with {@code hasTouch} set to true.
*/
public interface Touchscreen {
/**
@@ -0,0 +1,118 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import java.nio.file.Path;
import java.util.*;
/**
* API for collecting and saving Playwright traces. Playwright traces can be opened using the Playwright CLI after
* Playwright script runs.
*
* <p> Start with specifying the folder traces will be stored in:
* <pre>{@code
* Browser browser = chromium.launch();
* BrowserContext context = browser.newContext();
* context.tracing.start(new Tracing.StartOptions()
* .setScreenshots(true)
* .setSnapshots(true);
* Page page = context.newPage();
* page.goto("https://playwright.dev");
* context.tracing.stop(new Tracing.StopOptions()
* .setPath(Paths.get("trace.zip")));
* }</pre>
*/
public interface Tracing {
class StartOptions {
/**
* If specified, the trace is going to be saved into the file with the given name inside the {@code tracesDir} folder specified
* in {@link BrowserType#launch BrowserType.launch()}.
*/
public String name;
/**
* Whether to capture screenshots during tracing. Screenshots are used to build a timeline preview.
*/
public Boolean screenshots;
/**
* Whether to capture DOM snapshot on every action.
*/
public Boolean snapshots;
public StartOptions setName(String name) {
this.name = name;
return this;
}
public StartOptions setScreenshots(boolean screenshots) {
this.screenshots = screenshots;
return this;
}
public StartOptions setSnapshots(boolean snapshots) {
this.snapshots = snapshots;
return this;
}
}
class StopOptions {
/**
* Export trace into the file with the given name.
*/
public Path path;
public StopOptions setPath(Path path) {
this.path = path;
return this;
}
}
/**
* Start tracing.
* <pre>{@code
* context.tracing.start(new Tracing.StartOptions()
* .setScreenshots(true)
* .setSnapshots(true);
* Page page = context.newPage();
* page.goto('https://playwright.dev');
* context.tracing.stop(new Tracing.StopOptions()
* .setPath(Paths.get("trace.zip")));
* }</pre>
*/
default void start() {
start(null);
}
/**
* Start tracing.
* <pre>{@code
* context.tracing.start(new Tracing.StartOptions()
* .setScreenshots(true)
* .setSnapshots(true);
* Page page = context.newPage();
* page.goto('https://playwright.dev');
* context.tracing.stop(new Tracing.StopOptions()
* .setPath(Paths.get("trace.zip")));
* }</pre>
*/
void start(StartOptions options);
/**
* Stop tracing.
*/
default void stop() {
stop(null);
}
/**
* Stop tracing.
*/
void stop(StopOptions options);
}
@@ -20,13 +20,27 @@ import java.nio.file.Path;
import java.util.*;
/**
* When browser context is created with the {@code videosPath} option, each page has a video object associated with it.
* When browser context is created with the {@code recordVideo} option, each page has a video object associated with it.
* <pre>{@code
* System.out.println(page.video().path());
* }</pre>
*/
public interface Video {
/**
* Deletes the video file. Will wait for the video to finish if necessary.
*/
void delete();
/**
* Returns the file system path this video will be recorded to. The video is guaranteed to be written to the filesystem
* upon closing the browser context.
* upon closing the browser context. This method throws when connected remotely.
*/
Path path();
/**
* Saves the video to a user-specified path. It is safe to call this method while the video is still in progress, or after
* the page has closed. This method waits until the page is closed and the video is fully saved.
*
* @param path Path where the video should be saved.
*/
void saveAs(Path path);
}
@@ -24,55 +24,83 @@ import java.util.function.Predicate;
* The {@code WebSocket} class represents websocket connections in the page.
*/
public interface WebSocket {
interface FrameData {
byte[] body();
String text();
}
/**
* Fired when the websocket closes.
*/
void onClose(Consumer<WebSocket> handler);
/**
* Removes handler that was previously added with {@link #onClose onClose(handler)}.
*/
void offClose(Consumer<WebSocket> handler);
void onFrameReceived(Consumer<FrameData> handler);
void offFrameReceived(Consumer<FrameData> handler);
/**
* Fired when the websocket receives a frame.
*/
void onFrameReceived(Consumer<WebSocketFrame> handler);
/**
* Removes handler that was previously added with {@link #onFrameReceived onFrameReceived(handler)}.
*/
void offFrameReceived(Consumer<WebSocketFrame> handler);
void onFrameSent(Consumer<FrameData> handler);
void offFrameSent(Consumer<FrameData> handler);
/**
* Fired when the websocket sends a frame.
*/
void onFrameSent(Consumer<WebSocketFrame> handler);
/**
* Removes handler that was previously added with {@link #onFrameSent onFrameSent(handler)}.
*/
void offFrameSent(Consumer<WebSocketFrame> handler);
/**
* Fired when the websocket has an error.
*/
void onSocketError(Consumer<String> handler);
/**
* Removes handler that was previously added with {@link #onSocketError onSocketError(handler)}.
*/
void offSocketError(Consumer<String> handler);
class WaitForFrameReceivedOptions {
/**
* Receives the {@code WebSocketFrame} object and resolves to truthy value when the waiting should resolve.
*/
public Predicate<WebSocketFrame> predicate;
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
public WaitForFrameReceivedOptions withTimeout(double timeout) {
public WaitForFrameReceivedOptions setPredicate(Predicate<WebSocketFrame> predicate) {
this.predicate = predicate;
return this;
}
public WaitForFrameReceivedOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
FrameData waitForFrameReceived(Runnable code, WaitForFrameReceivedOptions options);
default FrameData waitForFrameReceived(Runnable code) { return waitForFrameReceived(code, null); }
class WaitForFrameSentOptions {
/**
* Receives the {@code WebSocketFrame} object and resolves to truthy value when the waiting should resolve.
*/
public Predicate<WebSocketFrame> predicate;
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
public WaitForFrameSentOptions withTimeout(double timeout) {
public WaitForFrameSentOptions setPredicate(Predicate<WebSocketFrame> predicate) {
this.predicate = predicate;
return this;
}
public WaitForFrameSentOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
FrameData waitForFrameSent(Runnable code, WaitForFrameSentOptions options);
default FrameData waitForFrameSent(Runnable code) { return waitForFrameSent(code, null); }
class WaitForSocketErrorOptions {
public Double timeout;
public WaitForSocketErrorOptions withTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
String waitForSocketError(Runnable code, WaitForSocketErrorOptions options);
default String waitForSocketError(Runnable code) { return waitForSocketError(code, null); }
/**
* Indicates that the web socket has been closed.
*/
@@ -81,5 +109,41 @@ public interface WebSocket {
* Contains the URL of the WebSocket.
*/
String url();
/**
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into the
* {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an error if the
* WebSocket or Page is closed before the frame is received.
*
* @param callback Callback that performs the action triggering the event.
*/
default WebSocketFrame waitForFrameReceived(Runnable callback) {
return waitForFrameReceived(null, callback);
}
/**
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into the
* {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an error if the
* WebSocket or Page is closed before the frame is received.
*
* @param callback Callback that performs the action triggering the event.
*/
WebSocketFrame waitForFrameReceived(WaitForFrameReceivedOptions options, Runnable callback);
/**
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into the
* {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an error if the
* WebSocket or Page is closed before the frame is sent.
*
* @param callback Callback that performs the action triggering the event.
*/
default WebSocketFrame waitForFrameSent(Runnable callback) {
return waitForFrameSent(null, callback);
}
/**
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into the
* {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an error if the
* WebSocket or Page is closed before the frame is sent.
*
* @param callback Callback that performs the action triggering the event.
*/
WebSocketFrame waitForFrameSent(WaitForFrameSentOptions options, Runnable callback);
}
@@ -0,0 +1,36 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import java.util.*;
/**
* The {@code WebSocketFrame} class represents frames sent over {@code WebSocket} connections in the page. Frame payload is returned by
* either {@link WebSocketFrame#text WebSocketFrame.text()} or {@link WebSocketFrame#binary WebSocketFrame.binary()} method
* depending on the its type.
*/
public interface WebSocketFrame {
/**
* Returns binary payload.
*/
byte[] binary();
/**
* Returns text payload.
*/
String text();
}
@@ -20,59 +20,121 @@ import java.util.*;
import java.util.function.Consumer;
/**
* The Worker class represents a [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). {@code worker}
* event is emitted on the page object to signal a worker creation. {@code close} event is emitted on the worker object when the
* worker is gone.
* The Worker class represents a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">WebWorker</a>.
* {@code worker} event is emitted on the page object to signal a worker creation. {@code close} event is emitted on the worker object
* when the worker is gone.
* <pre>{@code
* page.onWorker(worker -> {
* System.out.println("Worker created: " + worker.url());
* worker.onClose(worker1 -> System.out.println("Worker destroyed: " + worker1.url()));
* });
* System.out.println("Current workers:");
* for (Worker worker : page.workers())
* System.out.println(" " + worker.url());
* }</pre>
*/
public interface Worker {
/**
* Emitted when this dedicated <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">WebWorker</a> is
* terminated.
*/
void onClose(Consumer<Worker> handler);
/**
* Removes handler that was previously added with {@link #onClose onClose(handler)}.
*/
void offClose(Consumer<Worker> handler);
class WaitForCloseOptions {
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
public WaitForCloseOptions withTimeout(double timeout) {
public WaitForCloseOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
Worker waitForClose(Runnable code, WaitForCloseOptions options);
default Worker waitForClose(Runnable code) { return waitForClose(code, null); }
default Object evaluate(String pageFunction) {
return evaluate(pageFunction, null);
/**
* Returns the return value of {@code expression}.
*
* <p> If the function passed to the {@link Worker#evaluate Worker.evaluate()} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@link
* Worker#evaluate Worker.evaluate()} would wait for the promise to resolve and return its value.
*
* <p> If the function passed to the {@link Worker#evaluate Worker.evaluate()} returns a non-[Serializable] value, then {@link
* Worker#evaluate Worker.evaluate()} returns {@code undefined}. Playwright also supports transferring some additional values
* that are not serializable by {@code JSON}: {@code -0}, {@code NaN}, {@code Infinity}, {@code -Infinity}.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
*/
default Object evaluate(String expression) {
return evaluate(expression, null);
}
/**
* Returns the return value of {@code pageFunction}
* Returns the return value of {@code expression}.
*
* <p> If the function passed to the {@code worker.evaluate} returns a [Promise], then {@code worker.evaluate} would wait for the promise
* to resolve and return its value.
* <p> If the function passed to the {@link Worker#evaluate Worker.evaluate()} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@link
* Worker#evaluate Worker.evaluate()} would wait for the promise to resolve and return its value.
*
* <p> If the function passed to the {@code worker.evaluate} returns a non-[Serializable] value, then {@code worker.evaluate} returns
* {@code undefined}. DevTools Protocol also supports transferring some additional values that are not serializable by {@code JSON}:
* {@code -0}, {@code NaN}, {@code Infinity}, {@code -Infinity}, and bigint literals.
* <p> If the function passed to the {@link Worker#evaluate Worker.evaluate()} returns a non-[Serializable] value, then {@link
* Worker#evaluate Worker.evaluate()} returns {@code undefined}. Playwright also supports transferring some additional values
* that are not serializable by {@code JSON}: {@code -0}, {@code NaN}, {@code Infinity}, {@code -Infinity}.
*
* @param pageFunction Function to be evaluated in the worker context
* @param arg Optional argument to pass to {@code pageFunction}
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param arg Optional argument to pass to {@code expression}.
*/
Object evaluate(String pageFunction, Object arg);
default JSHandle evaluateHandle(String pageFunction) {
return evaluateHandle(pageFunction, null);
Object evaluate(String expression, Object arg);
/**
* Returns the return value of {@code expression} as a {@code JSHandle}.
*
* <p> The only difference between {@link Worker#evaluate Worker.evaluate()} and {@link Worker#evaluateHandle
* Worker.evaluateHandle()} is that {@link Worker#evaluateHandle Worker.evaluateHandle()} returns {@code JSHandle}.
*
* <p> If the function passed to the {@link Worker#evaluateHandle Worker.evaluateHandle()} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@link
* Worker#evaluateHandle Worker.evaluateHandle()} would wait for the promise to resolve and return its value.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
*/
default JSHandle evaluateHandle(String expression) {
return evaluateHandle(expression, null);
}
/**
* Returns the return value of {@code pageFunction} as in-page object (JSHandle).
* Returns the return value of {@code expression} as a {@code JSHandle}.
*
* <p> The only difference between {@code worker.evaluate} and {@code worker.evaluateHandle} is that {@code worker.evaluateHandle} returns
* in-page object (JSHandle).
* <p> The only difference between {@link Worker#evaluate Worker.evaluate()} and {@link Worker#evaluateHandle
* Worker.evaluateHandle()} is that {@link Worker#evaluateHandle Worker.evaluateHandle()} returns {@code JSHandle}.
*
* <p> If the function passed to the {@code worker.evaluateHandle} returns a [Promise], then {@code worker.evaluateHandle} would wait for
* the promise to resolve and return its value.
* <p> If the function passed to the {@link Worker#evaluateHandle Worker.evaluateHandle()} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@link
* Worker#evaluateHandle Worker.evaluateHandle()} would wait for the promise to resolve and return its value.
*
* @param pageFunction Function to be evaluated in the page context
* @param arg Optional argument to pass to {@code pageFunction}
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param arg Optional argument to pass to {@code expression}.
*/
JSHandle evaluateHandle(String pageFunction, Object arg);
JSHandle evaluateHandle(String expression, Object arg);
String url();
/**
* Performs action and waits for the Worker to close.
*
* @param callback Callback that performs the action triggering the event.
*/
default Worker waitForClose(Runnable callback) {
return waitForClose(null, callback);
}
/**
* Performs action and waits for the Worker to close.
*
* @param callback Callback that performs the action triggering the event.
*/
Worker waitForClose(WaitForCloseOptions options, Runnable callback);
}
@@ -1,48 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Accessibility;
import com.microsoft.playwright.AccessibilityNode;
import static com.microsoft.playwright.impl.Serialization.gson;
class AccessibilityImpl implements Accessibility {
private final PageImpl page;
AccessibilityImpl(PageImpl page) {
this.page = page;
}
@Override
public AccessibilityNode snapshot(SnapshotOptions options) {
return page.withLogging("Accessibility.snapshot", () -> snapshotImpl(options));
}
private AccessibilityNode snapshotImpl(SnapshotOptions options) {
if (options == null) {
options = new SnapshotOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
JsonObject json = page.sendMessage("accessibilitySnapshot", params).getAsJsonObject();
if (!json.has("rootAXNode")) {
return null;
}
return new AccessibilityNodeImpl(json.getAsJsonObject("rootAXNode"));
}
}
@@ -1,258 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.AccessibilityNode;
import java.util.ArrayList;
import java.util.List;
class AccessibilityNodeImpl implements AccessibilityNode {
private final JsonObject json;
AccessibilityNodeImpl(JsonObject json) {
this.json = json;
}
@Override
public String role() {
return json.get("role").getAsString();
}
@Override
public String name() {
return json.get("name").getAsString();
}
@Override
public String valueString() {
if (!json.has("valueString")) {
return null;
}
return json.get("valueString").getAsString();
}
@Override
public Double valueNumber() {
if (!json.has("valueNumber")) {
return null;
}
return json.get("valueNumber").getAsDouble();
}
@Override
public String description() {
if (!json.has("description")) {
return null;
}
return json.get("description").getAsString();
}
@Override
public String keyshortcuts() {
if (!json.has("keyshortcuts")) {
return null;
}
return json.get("keyshortcuts").getAsString();
}
@Override
public String roledescription() {
if (!json.has("roledescription")) {
return null;
}
return json.get("roledescription").getAsString();
}
@Override
public String valuetext() {
if (!json.has("valuetext")) {
return null;
}
return json.get("valuetext").getAsString();
}
@Override
public Boolean disabled() {
if (!json.has("disabled")) {
return null;
}
return json.get("disabled").getAsBoolean();
}
@Override
public Boolean expanded() {
if (!json.has("expanded")) {
return null;
}
return json.get("expanded").getAsBoolean();
}
@Override
public Boolean focused() {
if (!json.has("focused")) {
return null;
}
return json.get("focused").getAsBoolean();
}
@Override
public Boolean modal() {
if (!json.has("modal")) {
return null;
}
return json.get("modal").getAsBoolean();
}
@Override
public Boolean multiline() {
if (!json.has("multiline")) {
return null;
}
return json.get("multiline").getAsBoolean();
}
@Override
public Boolean multiselectable() {
if (!json.has("multiselectable")) {
return null;
}
return json.get("multiselectable").getAsBoolean();
}
@Override
public Boolean readonly() {
if (!json.has("readonly")) {
return null;
}
return json.get("readonly").getAsBoolean();
}
@Override
public Boolean required() {
if (!json.has("required")) {
return null;
}
return json.get("required").getAsBoolean();
}
@Override
public Boolean selected() {
if (!json.has("selected")) {
return null;
}
return json.get("selected").getAsBoolean();
}
@Override
public CheckedState checked() {
if (!json.has("checked")) {
return null;
}
String value = json.get("checked").getAsString();
switch (value) {
case "checked": return CheckedState.CHECKED;
case "unchecked": return CheckedState.UNCHECKED;
case "mixed": return CheckedState.MIXED;
default: throw new IllegalStateException("Unexpected value: " + value);
}
}
@Override
public PressedState pressed() {
if (!json.has("pressed")) {
return null;
}
String value = json.get("pressed").getAsString();
switch (value) {
case "pressed": return PressedState.PRESSED;
case "released": return PressedState.RELEASED;
case "mixed": return PressedState.MIXED;
default: throw new IllegalStateException("Unexpected value: " + value);
}
}
@Override
public Integer level() {
if (!json.has("level")) {
return null;
}
return json.get("level").getAsInt();
}
@Override
public Double valuemin() {
if (!json.has("valuemin")) {
return null;
}
return json.get("valuemin").getAsDouble();
}
@Override
public Double valuemax() {
if (!json.has("valuemax")) {
return null;
}
return json.get("valuemax").getAsDouble();
}
@Override
public String autocomplete() {
if (!json.has("autocomplete")) {
return null;
}
return json.get("autocomplete").getAsString();
}
@Override
public String haspopup() {
if (!json.has("haspopup")) {
return null;
}
return json.get("haspopup").getAsString();
}
@Override
public String invalid() {
if (!json.has("invalid")) {
return null;
}
return json.get("invalid").getAsString();
}
@Override
public String orientation() {
if (!json.has("orientation")) {
return null;
}
return json.get("orientation").getAsString();
}
@Override
public List<AccessibilityNode> children() {
if (!json.has("children")) {
return null;
}
List<AccessibilityNode> result = new ArrayList<>();
for (JsonElement e : json.getAsJsonArray("children")) {
result.add(new AccessibilityNodeImpl(e.getAsJsonObject()));
}
return result;
}
}
@@ -0,0 +1,75 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import java.io.InputStream;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import static com.microsoft.playwright.impl.Utils.writeToFile;
class ArtifactImpl extends ChannelOwner {
boolean isRemote;
public ArtifactImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
public InputStream createReadStream() {
JsonObject result = sendMessage("stream").getAsJsonObject();
if (!result.has("stream")) {
return null;
}
Stream stream = connection.getExistingObject(result.getAsJsonObject("stream").get("guid").getAsString());
return stream.stream();
}
public void delete() {
sendMessage("delete");
}
public String failure() {
JsonObject result = sendMessage("failure").getAsJsonObject();
if (result.has("error")) {
return result.get("error").getAsString();
}
return null;
}
public Path pathAfterFinished() {
if (isRemote) {
throw new PlaywrightException("Path is not available when using browserType.connect(). Use download.saveAs() to save a local copy.");
}
JsonObject json = sendMessage("pathAfterFinished").getAsJsonObject();
return FileSystems.getDefault().getPath(json.get("value").getAsString());
}
public void saveAs(Path path) {
if (isRemote) {
JsonObject jsonObject = sendMessage("saveAsStream").getAsJsonObject();
Stream stream = connection.getExistingObject(jsonObject.getAsJsonObject("stream").get("guid").getAsString());
writeToFile(stream.stream(), path);
return;
}
JsonObject params = new JsonObject();
params.addProperty("path", path.toString());
sendMessage("saveAs", params);
}
}
@@ -22,6 +22,7 @@ import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.JSHandle;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.BindingCallback;
import java.util.ArrayList;
import java.util.List;
@@ -29,7 +30,7 @@ import java.util.List;
import static com.microsoft.playwright.impl.Serialization.*;
class BindingCall extends ChannelOwner {
private static class SourceImpl implements Page.Binding.Source {
private static class SourceImpl implements BindingCallback.Source {
private final Frame frame;
public SourceImpl(Frame frame) {
@@ -60,10 +61,10 @@ class BindingCall extends ChannelOwner {
return initializer.get("name").getAsString();
}
void call(Page.Binding binding) {
void call(BindingCallback binding) {
try {
Frame frame = connection.getExistingObject(initializer.getAsJsonObject("frame").get("guid").getAsString());
Page.Binding.Source source = new SourceImpl(frame);
BindingCallback.Source source = new SourceImpl(frame);
List<Object> args = new ArrayList<>();
if (initializer.has("handle")) {
JSHandle handle = connection.getExistingObject(initializer.getAsJsonObject("handle").get("guid").getAsString());
@@ -20,26 +20,35 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.BindingCallback;
import com.microsoft.playwright.options.Cookie;
import com.microsoft.playwright.options.FunctionCallback;
import com.microsoft.playwright.options.Geolocation;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.isFunctionBody;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes;
import static java.util.Arrays.asList;
class BrowserContextImpl extends ChannelOwner implements BrowserContext {
private final BrowserImpl browser;
private final TracingImpl tracing;
final List<PageImpl> pages = new ArrayList<>();
final Router routes = new Router();
private boolean isClosedOrClosing;
final Map<String, Page.Binding> bindings = new HashMap<>();
final Map<String, BindingCallback> bindings = new HashMap<>();
PageImpl ownerPage;
private final ListenerCollection<EventType> listeners = new ListenerCollection<>();
final TimeoutSettings timeoutSettings = new TimeoutSettings();
@@ -48,6 +57,10 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
enum EventType {
CLOSE,
PAGE,
REQUEST,
REQUESTFAILED,
REQUESTFINISHED,
RESPONSE,
}
BrowserContextImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -57,6 +70,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} else {
browser = null;
}
this.tracing = new TracingImpl(this);
}
@Override
@@ -79,6 +93,46 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
listeners.remove(EventType.PAGE, handler);
}
@Override
public void onRequest(Consumer<Request> handler) {
listeners.add(EventType.REQUEST, handler);
}
@Override
public void offRequest(Consumer<Request> handler) {
listeners.remove(EventType.REQUEST, handler);
}
@Override
public void onRequestFailed(Consumer<Request> handler) {
listeners.add(EventType.REQUESTFAILED, handler);
}
@Override
public void offRequestFailed(Consumer<Request> handler) {
listeners.remove(EventType.REQUESTFAILED, handler);
}
@Override
public void onRequestFinished(Consumer<Request> handler) {
listeners.add(EventType.REQUESTFINISHED, handler);
}
@Override
public void offRequestFinished(Consumer<Request> handler) {
listeners.remove(EventType.REQUESTFINISHED, handler);
}
@Override
public void onResponse(Consumer<Response> handler) {
listeners.add(EventType.RESPONSE, handler);
}
@Override
public void offResponse(Consumer<Response> handler) {
listeners.remove(EventType.RESPONSE, handler);
}
private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Double timeout) {
List<Waitable<T>> waitables = new ArrayList<>();
waitables.add(new WaitableEvent<>(listeners, eventType));
@@ -88,7 +142,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public Page waitForPage(Runnable code, WaitForPageOptions options) {
public Page waitForPage(WaitForPageOptions options, Runnable code) {
return withWaitLogging("BrowserContext.close", () -> waitForPageImpl(options, code));
}
private Page waitForPageImpl(WaitForPageOptions options, Runnable code) {
if (options == null) {
options = new WaitForPageOptions();
}
@@ -99,6 +157,12 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
public void close() {
withLogging("BrowserContext.close", () -> closeImpl());
}
@Override
public List<Cookie> cookies(String url) {
return cookies(url == null ? new ArrayList<>() : asList(url));
}
private void closeImpl() {
if (isClosedOrClosing) {
return;
@@ -114,7 +178,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public void addCookies(List<AddCookie> cookies) {
public void addCookies(List<Cookie> cookies) {
withLogging("BrowserContext.addCookies", () -> {
JsonObject params = new JsonObject();
params.add("cookies", gson().toJsonTree(cookies));
@@ -123,15 +187,24 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public void addInitScript(String script, Object arg) {
withLogging("BrowserContext.addInitScript", () -> addInitScriptImpl(script, arg));
public void addInitScript(String script) {
withLogging("BrowserContext.addInitScript", () -> addInitScriptImpl(script));
}
private void addInitScriptImpl(String script, Object arg) {
// TODO: serialize arg
@Override
public void addInitScript(Path path) {
withLogging("BrowserContext.addInitScript", () -> {
try {
byte[] bytes = readAllBytes(path);
addInitScriptImpl(new String(bytes, UTF_8));
} catch (IOException e) {
throw new PlaywrightException("Failed to read script from file", e);
}
});
}
private void addInitScriptImpl(String script) {
JsonObject params = new JsonObject();
if (isFunctionBody(script)) {
script = "(" + script + ")()";
}
params.addProperty("source", script);
sendMessage("addInitScript", params);
}
@@ -159,20 +232,20 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
private List<Cookie> cookiesImpl(List<String> urls) {
JsonObject params = new JsonObject();
if (urls == null) {
urls = Collections.emptyList();
urls = new ArrayList<>();
}
params.add("urls", gson().toJsonTree(urls));
JsonObject json = sendMessage("cookies", params).getAsJsonObject();
Cookie[] cookies = gson().fromJson(json.getAsJsonArray("cookies"), Cookie[].class);
return Arrays.asList(cookies);
return asList(cookies);
}
@Override
public void exposeBinding(String name, Page.Binding playwrightBinding, ExposeBindingOptions options) {
public void exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
withLogging("BrowserContext.exposeBinding", () -> exposeBindingImpl(name, playwrightBinding, options));
}
private void exposeBindingImpl(String name, Page.Binding playwrightBinding, ExposeBindingOptions options) {
private void exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
if (bindings.containsKey(name)) {
throw new PlaywrightException("Function \"" + name + "\" has been already registered");
}
@@ -192,9 +265,9 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public void exposeFunction(String name, Page.Function playwrightFunction) {
public void exposeFunction(String name, FunctionCallback playwrightFunction) {
withLogging("BrowserContext.exposeFunction",
() -> exposeBindingImpl(name, (Page.Binding.Source source, Object... args) -> playwrightFunction.call(args), null));
() -> exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args), null));
}
@Override
@@ -207,7 +280,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
options = new GrantPermissionsOptions();
}
if (permissions == null) {
permissions = Collections.emptyList();
permissions = new ArrayList<>();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.add("permissions", gson().toJsonTree(permissions));
@@ -315,24 +388,22 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public StorageState storageState(StorageStateOptions options) {
public String storageState(StorageStateOptions options) {
return withLogging("BrowserContext.storageState", () -> {
JsonElement json = sendMessage("storageState");
StorageState storageState = gson().fromJson(json, StorageState.class);
String storageState = json.toString();
if (options != null && options.path != null) {
try {
Files.createDirectories(options.path.getParent());
try (FileWriter writer = new FileWriter(options.path.toFile())) {
writer.write(json.toString());
}
} catch (IOException e) {
throw new PlaywrightException("Failed to write storage state to file", e);
}
Utils.writeToFile(storageState.getBytes(StandardCharsets.UTF_8), options.path);
}
return storageState;
});
}
@Override
public Tracing tracing() {
return tracing;
}
@Override
public void unroute(String url, Consumer<Route> handler) {
unroute(new UrlMatcher(url), handler);
@@ -370,30 +441,82 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
});
}
void pause() {
sendMessage("pause");
}
@Override
protected void handleEvent(String event, JsonObject params) {
if ("route".equals(event)) {
Route route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
boolean handled = routes.handle(route);
if (!handled) {
route.continue_();
route.resume();
}
} else if ("page".equals(event)) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
listeners.notify(EventType.PAGE, page);
pages.add(page);
listeners.notify(EventType.PAGE, page);
if (page.opener() != null && !page.opener().isClosed()) {
page.opener().notifyPopup(page);
}
} else if ("bindingCall".equals(event)) {
BindingCall bindingCall = connection.getExistingObject(params.getAsJsonObject("binding").get("guid").getAsString());
Page.Binding binding = bindings.get(bindingCall.name());
BindingCallback binding = bindings.get(bindingCall.name());
if (binding != null) {
bindingCall.call(binding);
}
} else if ("close".equals(event)) {
isClosedOrClosing = true;
if (browser != null) {
browser.contexts.remove(this);
} else if ("request".equals(event)) {
String guid = params.getAsJsonObject("request").get("guid").getAsString();
RequestImpl request = connection.getExistingObject(guid);
listeners.notify(EventType.REQUEST, request);
if (params.has("page")) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
page.listeners.notify(PageImpl.EventType.REQUEST, request);
}
listeners.notify(EventType.CLOSE, this);
} else if ("requestFailed".equals(event)) {
String guid = params.getAsJsonObject("request").get("guid").getAsString();
RequestImpl request = connection.getExistingObject(guid);
if (params.has("failureText")) {
request.failure = params.get("failureText").getAsString();
}
if (request.timing != null) {
request.timing.responseEnd = params.get("responseEndTiming").getAsDouble();
}
listeners.notify(EventType.REQUESTFAILED, request);
if (params.has("page")) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
page.listeners.notify(PageImpl.EventType.REQUESTFAILED, request);
}
} else if ("requestFinished".equals(event)) {
String guid = params.getAsJsonObject("request").get("guid").getAsString();
RequestImpl request = connection.getExistingObject(guid);
if (request.timing != null) {
request.timing.responseEnd = params.get("responseEndTiming").getAsDouble();
}
listeners.notify(EventType.REQUESTFINISHED, request);
if (params.has("page")) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
page.listeners.notify(PageImpl.EventType.REQUESTFINISHED, request);
}
} else if ("response".equals(event)) {
String guid = params.getAsJsonObject("response").get("guid").getAsString();
Response response = connection.getExistingObject(guid);
listeners.notify(EventType.RESPONSE, response);
if (params.has("page")) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
page.listeners.notify(PageImpl.EventType.RESPONSE, response);
}
} else if ("close".equals(event)) {
didClose();
}
}
void didClose() {
isClosedOrClosing = true;
if (browser != null) {
browser.contexts.remove(this);
}
listeners.notify(EventType.CLOSE, this);
}
}
@@ -16,17 +16,18 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.PlaywrightException;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.function.Consumer;
import static com.microsoft.playwright.impl.Serialization.gson;
@@ -34,8 +35,10 @@ import static com.microsoft.playwright.impl.Utils.convertViaJson;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
class BrowserImpl extends ChannelOwner implements Browser {
final Set<BrowserContext> contexts = new HashSet<>();
final Set<BrowserContextImpl> contexts = new HashSet<>();
private final ListenerCollection<EventType> listeners = new ListenerCollection<>();
boolean isRemote;
boolean isConnectedOverWebSocket;
private boolean isConnected = true;
enum EventType {
@@ -60,7 +63,17 @@ class BrowserImpl extends ChannelOwner implements Browser {
public void close() {
withLogging("Browser.close", () -> closeImpl());
}
private void closeImpl() {
if (isConnectedOverWebSocket) {
try {
connection.close();
} catch (IOException e) {
throw new PlaywrightException("Failed to close browser connection", e);
}
notifyRemoteClosed();
return;
}
try {
sendMessage("close");
} catch (PlaywrightException e) {
@@ -70,6 +83,17 @@ class BrowserImpl extends ChannelOwner implements Browser {
}
}
void notifyRemoteClosed() {
// Emulate all pages, contexts and the browser closing upon disconnect.
for (BrowserContextImpl context : new ArrayList<>(contexts)) {
for (PageImpl page : new ArrayList<>(context.pages)) {
page.didClose();
}
context.didClose();
}
didClose();
}
@Override
public List<BrowserContext> contexts() {
return new ArrayList<>(contexts);
@@ -90,18 +114,62 @@ class BrowserImpl extends ChannelOwner implements Browser {
options = new NewContextOptions();
}
if (options.storageStatePath != null) {
try (FileReader reader = new FileReader(options.storageStatePath.toFile())) {
options.storageState = gson().fromJson(reader, BrowserContext.StorageState.class);
try {
byte[] bytes = Files.readAllBytes(options.storageStatePath);
options.storageState = new String(bytes, StandardCharsets.UTF_8);
options.storageStatePath = null;
} catch (IOException e) {
throw new PlaywrightException("Failed to read storage state from file", e);
}
}
JsonObject storageState = null;
if (options.storageState != null) {
storageState = new Gson().fromJson(options.storageState, JsonObject.class);
options.storageState = null;
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
if (storageState != null) {
params.add("storageState", storageState);
}
if (options.recordHarPath != null) {
JsonObject recordHar = new JsonObject();
recordHar.addProperty("path", options.recordHarPath.toString());
if (options.recordHarOmitContent != null) {
recordHar.addProperty("omitContent", true);
}
params.remove("recordHarPath");
params.remove("recordHarOmitContent");
params.add("recordHar", recordHar);
} else if (options.recordHarOmitContent != null) {
throw new PlaywrightException("recordHarOmitContent is set but recordHarPath is null");
}
if (options.recordVideoDir != null) {
JsonObject recordVideo = new JsonObject();
recordVideo.addProperty("dir", options.recordVideoDir.toString());
if (options.recordVideoSize != null) {
recordVideo.add("size", gson().toJsonTree(options.recordVideoSize));
}
params.remove("recordVideoDir");
params.remove("recordVideoSize");
params.add("recordVideo", recordVideo);
} else if (options.recordVideoSize != null) {
throw new PlaywrightException("recordVideoSize is set but recordVideoDir is null");
}
if (options.viewportSize != null) {
if (options.viewportSize.isPresent()) {
JsonElement size = params.get("viewportSize");
params.remove("viewportSize");
params.add("viewport", size);
} else {
params.remove("viewportSize");
params.addProperty("noDefaultViewport", true);
}
}
params.addProperty("sdkLanguage", "java");
JsonElement result = sendMessage("newContext", params);
BrowserContextImpl context = connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("context").get("guid").getAsString());
if (options.recordVideo != null) {
context.videosDir = options.recordVideo.dir;
if (options.recordVideoDir != null) {
context.videosDir = options.recordVideoDir;
}
contexts.add(context);
return context;
@@ -112,6 +180,34 @@ class BrowserImpl extends ChannelOwner implements Browser {
return withLogging("Browser.newPage", () -> newPageImpl(options));
}
@Override
public void startTracing(Page page, StartTracingOptions options) {
withLogging("Browser.startTracing", () -> startTracingImpl(page, options));
}
private void startTracingImpl(Page page, StartTracingOptions options) {
if (options == null) {
options = new StartTracingOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
if (page != null) {
JsonObject jsonPage = new JsonObject();
jsonPage.addProperty("guid", ((PageImpl) page).guid);
params.add("page", jsonPage);
}
sendMessage("startTracing", params);
}
@Override
public byte[] stopTracing() {
return withLogging("Browser.stopTracing", () -> stopTracingImpl());
}
private byte[] stopTracingImpl() {
JsonObject json = sendMessage("stopTracing").getAsJsonObject();
return Base64.getDecoder().decode(json.get("binary").getAsString());
}
private Page newPageImpl(NewPageOptions options) {
BrowserContextImpl context = newContext(convertViaJson(options, NewContextOptions.class));
PageImpl page = context.newPage();
@@ -136,8 +232,12 @@ class BrowserImpl extends ChannelOwner implements Browser {
@Override
void handleEvent(String event, JsonObject parameters) {
if ("close".equals(event)) {
isConnected = false;
listeners.notify(EventType.DISCONNECTED, this);
didClose();
}
}
private void didClose() {
isConnected = false;
listeners.notify(EventType.DISCONNECTED, this);
}
}
@@ -18,13 +18,20 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.PlaywrightException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Serialization.toProtocol;
class BrowserTypeImpl extends ChannelOwner implements BrowserType {
BrowserTypeImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -45,6 +52,87 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
return connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("browser").get("guid").getAsString());
}
@Override
public Browser connect(String wsEndpoint, ConnectOptions options) {
return withLogging("BrowserType.connect", () -> connectImpl(wsEndpoint, options));
}
private Browser connectImpl(String wsEndpoint, ConnectOptions options) {
try {
Duration timeout = Duration.ofDays(1);
Map<String, String> headers = Collections.emptyMap();
Duration slowMo = null;
if (options != null) {
if (options.timeout != null) {
timeout = Duration.ofMillis(Math.round(options.timeout));
}
if (options.headers != null) {
headers = options.headers;
}
if (options.slowMo != null) {
slowMo = Duration.ofMillis(options.slowMo.intValue());
}
}
WebSocketTransport transport = new WebSocketTransport(new URI(wsEndpoint), headers, timeout, slowMo);
Connection connection = new Connection(transport);
PlaywrightImpl playwright = (PlaywrightImpl) connection.waitForObjectWithKnownName("Playwright");
if (!playwright.initializer.has("preLaunchedBrowser")) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace(System.err);
}
throw new PlaywrightException("Malformed endpoint. Did you use launchServer method?");
}
playwright.initSharedSelectors(this.connection.getExistingObject("Playwright"));
BrowserImpl browser = connection.getExistingObject(playwright.initializer.getAsJsonObject("preLaunchedBrowser").get("guid").getAsString());
browser.isRemote = true;
browser.isConnectedOverWebSocket = true;
Consumer<WebSocketTransport> connectionCloseListener = t -> browser.notifyRemoteClosed();
transport.onClose(connectionCloseListener);
browser.onDisconnected(b -> {
playwright.unregisterSelectors();
transport.offClose(connectionCloseListener);
try {
connection.close();
} catch (IOException e) {
e.printStackTrace(System.err);
}
});
return browser;
} catch (URISyntaxException e) {
throw new PlaywrightException("Failed to connect", e);
}
}
@Override
public Browser connectOverCDP(String endpointURL, ConnectOverCDPOptions options) {
if (!"chromium".equals(name())) {
throw new PlaywrightException("Connecting over CDP is only supported in Chromium.");
}
return withLogging("BrowserType.connectOverCDP", () -> connectOverCDPImpl(endpointURL, options));
}
private Browser connectOverCDPImpl(String endpointURL, ConnectOverCDPOptions options) {
if (options == null) {
options = new ConnectOverCDPOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("sdkLanguage", "java");
params.addProperty("endpointURL", endpointURL);
JsonObject json = sendMessage("connectOverCDP", params).getAsJsonObject();
BrowserImpl browser = connection.getExistingObject(json.getAsJsonObject("browser").get("guid").getAsString());
browser.isRemote = true;
if (json.has("defaultContext")) {
String contextId = json.getAsJsonObject("defaultContext").get("guid").getAsString();
BrowserContextImpl defaultContext = connection.getExistingObject(contextId);
browser.contexts.add(defaultContext);
}
return browser;
}
public String executablePath() {
return initializer.get("executablePath").getAsString();
}
@@ -61,10 +149,45 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("userDataDir", userDataDir.toString());
if (options.recordHarPath != null) {
JsonObject recordHar = new JsonObject();
recordHar.addProperty("path", options.recordHarPath.toString());
if (options.recordHarOmitContent != null) {
recordHar.addProperty("omitContent", true);
}
params.remove("recordHarPath");
params.remove("recordHarOmitContent");
params.add("recordHar", recordHar);
} else if (options.recordHarOmitContent != null) {
throw new PlaywrightException("recordHarOmitContent is set but recordHarPath is null");
}
if (options.recordVideoDir != null) {
JsonObject recordVideo = new JsonObject();
recordVideo.addProperty("dir", options.recordVideoDir.toString());
if (options.recordVideoSize != null) {
recordVideo.add("size", gson().toJsonTree(options.recordVideoSize));
}
params.remove("recordVideoDir");
params.remove("recordVideoSize");
params.add("recordVideo", recordVideo);
} else if (options.recordVideoSize != null) {
throw new PlaywrightException("recordVideoSize is set but recordVideoDir is null");
}
if (options.viewportSize != null) {
if (options.viewportSize.isPresent()) {
JsonElement size = params.get("viewportSize");
params.remove("viewportSize");
params.add("viewport", size);
} else {
params.remove("viewportSize");
params.addProperty("noDefaultViewport", true);
}
}
params.addProperty("sdkLanguage", "java");
JsonObject json = sendMessage("launchPersistentContext", params).getAsJsonObject();
BrowserContextImpl context = connection.getExistingObject(json.getAsJsonObject("context").get("guid").getAsString());
if (options.recordVideo != null) {
context.videosDir = options.recordVideo.dir;
if (options.recordVideoDir != null) {
context.videosDir = options.recordVideoDir;
}
return context;
}
@@ -22,6 +22,7 @@ import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
class ChannelOwner extends LoggingSupport {
final Connection connection;
@@ -67,6 +68,11 @@ class ChannelOwner extends LoggingSupport {
objects.clear();
}
<T> T withWaitLogging(String apiName, Supplier<T> code) {
return super.withLogging(apiName, new WaitForEventLogger<>(this, apiName, code));
}
WaitableResult<JsonElement> sendMessageAsync(String method, JsonObject params) {
return connection.sendMessageAsync(guid, method, params);
}
@@ -16,14 +16,27 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.TimeoutError;
import java.io.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.*;
import java.util.HashMap;
import java.util.Map;
import static com.microsoft.playwright.impl.LoggingSupport.logWithTimestamp;
import static com.microsoft.playwright.impl.Serialization.gson;
class Message {
@@ -53,7 +66,13 @@ public class Connection {
private final Map<String, ChannelOwner> objects = new HashMap<>();
private final Root root;
private int lastId = 0;
private final Path srcDir;
private final Map<Integer, WaitableResult<JsonElement>> callbacks = new HashMap<>();
private static final boolean isLogging;
static {
String debug = System.getenv("DEBUG");
isLogging = (debug != null) && debug.contains("pw:channel");
}
class Root extends ChannelOwner {
Root(Connection connection) {
@@ -61,9 +80,18 @@ public class Connection {
}
}
public Connection(InputStream in, OutputStream out) {
transport = new Transport(in, out);
Connection(Transport transport) {
this.transport = transport;
root = new Root(this);
String srcRoot = System.getenv("PLAYWRIGHT_JAVA_SRC");
if (srcRoot == null) {
srcDir = null;
} else {
srcDir = Paths.get(srcRoot);
if (!Files.exists(srcDir)) {
throw new PlaywrightException("PLAYWRIGHT_JAVA_SRC environment variable points to non-existing location: '" + srcRoot + "'");
}
}
}
void close() throws IOException {
@@ -78,6 +106,45 @@ public class Connection {
return internalSendMessage(guid, method, params);
}
private String sourceFile(StackTraceElement frame) {
String pkg = frame.getClassName();
int lastDot = pkg.lastIndexOf('.');
if (lastDot == -1) {
pkg = "";
} else {
pkg = frame.getClassName().substring(0, lastDot + 1);
}
pkg = pkg.replace('.', File.separatorChar);
return srcDir.resolve(pkg).resolve(frame.getFileName()).toString();
}
private JsonArray currentStackTrace() {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
int index = 0;
while (index < stack.length && !stack[index].getClassName().equals(getClass().getName())) {
index++;
};
// Find Playwright API call
while (index < stack.length && stack[index].getClassName().startsWith("com.microsoft.playwright.")) {
// hack for tests
if (stack[index].getClassName().startsWith("com.microsoft.playwright.Test")) {
break;
}
index++;
}
JsonArray jsonStack = new JsonArray();
for (; index < stack.length; index++) {
StackTraceElement frame = stack[index];
JsonObject jsonFrame = new JsonObject();
jsonFrame.addProperty("file", sourceFile(frame));
jsonFrame.addProperty("line", frame.getLineNumber());
jsonFrame.addProperty("function", frame.getClassName() + "." + frame.getMethodName());
jsonStack.add(jsonFrame);
}
return jsonStack;
}
private WaitableResult<JsonElement> internalSendMessage(String guid, String method, JsonObject params) {
int id = ++lastId;
WaitableResult<JsonElement> result = new WaitableResult<>();
@@ -87,7 +154,16 @@ public class Connection {
message.addProperty("guid", guid);
message.addProperty("method", method);
message.add("params", params);
transport.send(gson().toJson(message));
if (srcDir != null) {
JsonObject metadata = new JsonObject();
metadata.add("stack", currentStackTrace());
message.add("metadata", metadata);
}
String messageString = gson().toJson(message);
if (isLogging) {
logWithTimestamp("SEND ► " + messageString);
}
transport.send(messageString);
return result;
}
@@ -118,6 +194,9 @@ public class Connection {
if (messageString == null) {
return;
}
if (isLogging) {
logWithTimestamp("◀ RECV " + messageString);
}
Gson gson = gson();
Message message = gson.fromJson(messageString, Message.class);
dispatch(message);
@@ -135,10 +214,12 @@ public class Connection {
if (message.error == null) {
callback.complete(message.result);
} else {
if (message.error.error != null) {
callback.completeExceptionally(new ServerException(message.error.error));
} else {
if (message.error.error == null) {
callback.completeExceptionally(new PlaywrightException(message.error.toString()));
} else if ("TimeoutError".equals(message.error.error.name)) {
callback.completeExceptionally(new TimeoutError(message.error.error.toString()));
} else {
callback.completeExceptionally(new DriverException(message.error.error));
}
}
return;
@@ -187,6 +268,9 @@ public class Connection {
case "AndroidDevice":
// result = new AndroidDevice(parent, type, guid, initializer);
break;
case "Artifact":
result = new ArtifactImpl(parent, type, guid, initializer);
break;
case "BindingCall":
result = new BindingCall(parent, type, guid, initializer);
break;
@@ -205,9 +289,6 @@ public class Connection {
case "Dialog":
result = new DialogImpl(parent, type, guid, initializer);
break;
case "Download":
result = new DownloadImpl(parent, type, guid, initializer);
break;
case "Electron":
// result = new Playwright(parent, type, guid, initializer);
break;
@@ -48,7 +48,11 @@ public class ConsoleMessageImpl extends ChannelOwner implements ConsoleMessage {
return result;
}
public Location location() {
return gson().fromJson(initializer.get("location"), Location.class);
@Override
public String location() {
JsonObject location = initializer.getAsJsonObject("location");
return location.get("url").getAsString() + ":" +
location.get("lineNumber").getAsNumber() + ":" +
location.get("columnNumber").getAsNumber();
}
}
@@ -1,80 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.DeviceDescriptor;
import com.microsoft.playwright.PlaywrightException;
class DeviceDescriptorImpl implements DeviceDescriptor {
PlaywrightImpl playwright;
private static class ViewportImpl implements Viewport{
private int width;
private int height;
@Override
public int width() {
return width;
}
@Override
public int height() {
return height;
}
}
private ViewportImpl viewport;
private String userAgent;
private double deviceScaleFactor;
private boolean isMobile;
private boolean hasTouch;
private String defaultBrowserType;
@Override
public Viewport viewport() {
return viewport;
}
@Override
public String userAgent() {
return userAgent;
}
@Override
public double deviceScaleFactor() {
return deviceScaleFactor;
}
@Override
public boolean isMobile() {
return isMobile;
}
@Override
public boolean hasTouch() {
return hasTouch;
}
@Override
public BrowserType defaultBrowserType() {
switch (defaultBrowserType) {
case "chromium": return playwright.chromium();
case "firefox": return playwright.firefox();
case "webkit": return playwright.webkit();
default: throw new PlaywrightException("Unknown type: " + defaultBrowserType);
}
}
}
@@ -18,7 +18,6 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Dialog;
import com.microsoft.playwright.PlaywrightException;
class DialogImpl extends ChannelOwner implements Dialog {
DialogImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -52,13 +51,7 @@ class DialogImpl extends ChannelOwner implements Dialog {
}
@Override
public Type type() {
switch (initializer.get("type").getAsString()) {
case "alert": return Type.ALERT;
case "beforeunload": return Type.BEFOREUNLOAD;
case "confirm": return Type.CONFIRM;
case "prompt": return Type.PROMPT;
default: throw new PlaywrightException("Unexpected dialog type: " + initializer.get("type").getAsString());
}
public String type() {
return initializer.get("type").getAsString();
}
}
@@ -18,14 +18,20 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Download;
import com.microsoft.playwright.Page;
import java.io.InputStream;
import java.nio.file.FileSystems;
import java.nio.file.Path;
public class DownloadImpl extends ChannelOwner implements Download {
public DownloadImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
class DownloadImpl extends LoggingSupport implements Download {
private final PageImpl page;
private final ArtifactImpl artifact;
private final JsonObject initializer;
DownloadImpl(PageImpl page, ArtifactImpl artifact, JsonObject initializer) {
this.page = page;
this.artifact = artifact;
this.initializer = initializer;
}
@Override
@@ -40,48 +46,31 @@ public class DownloadImpl extends ChannelOwner implements Download {
@Override
public InputStream createReadStream() {
return withLogging("Download.createReadStream", () -> {
JsonObject result = sendMessage("stream").getAsJsonObject();
if (!result.has("stream")) {
return null;
}
Stream stream = connection.getExistingObject(result.getAsJsonObject("stream").get("guid").getAsString());
return stream.stream();
});
return withLogging("Download.createReadStream", () -> artifact.createReadStream());
}
@Override
public void delete() {
withLogging("Download.delete", () -> {
sendMessage("delete");
});
withLogging("Download.delete", () -> artifact.delete());
}
@Override
public String failure() {
return withLogging("Download.failure", () -> {
JsonObject result = sendMessage("failure").getAsJsonObject();
if (result.has("error")) {
return result.get("error").getAsString();
}
return null;
});
return withLogging("Download.failure", () -> artifact.failure());
}
@Override
public Page page() {
return page;
}
@Override
public Path path() {
return withLogging("Download.path", () -> {
JsonObject json = sendMessage("path").getAsJsonObject();
return FileSystems.getDefault().getPath(json.get("value").getAsString());
});
return withLogging("Download.path", () -> artifact.pathAfterFinished());
}
@Override
public void saveAs(Path path) {
withLogging("Download.saveAs", () -> {
JsonObject params = new JsonObject();
params.addProperty("path", path.toString());
sendMessage("saveAs", params);
});
withLogging("Download.saveAs", () -> artifact.saveAs(path));
}
}
@@ -1,12 +1,12 @@
/*
* Copyright (c) Microsoft Corporation.
* <p>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
*
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -14,17 +14,15 @@
* limitations under the License.
*/
package com.microsoft.playwright;
package com.microsoft.playwright.impl;
public interface DeviceDescriptor {
interface Viewport {
int width();
int height();
import com.microsoft.playwright.PlaywrightException;
import java.io.PrintStream;
import java.io.PrintWriter;
class DriverException extends PlaywrightException {
DriverException(SerializedError.Error error) {
super(error.toString());
}
Viewport viewport();
String userAgent();
double deviceScaleFactor();
boolean isMobile();
boolean hasTouch();
BrowserType defaultBrowserType();
}
@@ -22,14 +22,20 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.ElementHandle;
import com.microsoft.playwright.FileChooser;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.options.BoundingBox;
import com.microsoft.playwright.options.ElementState;
import com.microsoft.playwright.options.FilePayload;
import com.microsoft.playwright.options.SelectOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.isFunctionBody;
import static com.microsoft.playwright.options.ScreenshotType.JPEG;
import static com.microsoft.playwright.options.ScreenshotType.PNG;
public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
ElementHandleImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -79,7 +85,6 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evalOnSelector", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
@@ -93,7 +98,6 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evalOnSelectorAll", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
@@ -135,16 +139,6 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
options = new ClickOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("button");
if (options.button != null) {
params.addProperty("button", Serialization.toProtocol(options.button));
}
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
sendMessage("click", params);
}
@@ -171,16 +165,6 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
options = new DblclickOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("button");
if (options.button != null) {
params.addProperty("button", Serialization.toProtocol(options.button));
}
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
sendMessage("dblclick", params);
}
@@ -227,10 +211,6 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
public void hover(HoverOptions options) {
withLogging("ElementHandle.hover", () -> {
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
sendMessage("hover", params);
});
}
@@ -323,10 +303,6 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
sendMessage("press", params);
}
private static String toProtocol(ScreenshotOptions.Type type) {
return type.toString().toLowerCase();
}
@Override
public byte[] screenshot(ScreenshotOptions options) {
return withLogging("ElementHandle.screenshot", () -> screenshotImpl(options));
@@ -337,21 +313,19 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
options = new ScreenshotOptions();
}
if (options.type == null) {
options.type = ScreenshotOptions.Type.PNG;
options.type = PNG;
if (options.path != null) {
String fileName = options.path.getFileName().toString();
int extStart = fileName.lastIndexOf('.');
if (extStart != -1) {
String extension = fileName.substring(extStart).toLowerCase();
if (".jpeg".equals(extension) || ".jpg".equals(extension)) {
options.type = ScreenshotOptions.Type.JPEG;
options.type = JPEG;
}
}
}
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("type");
params.addProperty("type", toProtocol(options.type));
params.remove("path");
JsonObject json = sendMessage("screenshot", params).getAsJsonObject();
@@ -367,6 +341,33 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
withLogging("ElementHandle.scrollIntoViewIfNeeded", () -> scrollIntoViewIfNeededImpl(options));
}
@Override
public List<String> selectOption(String value, SelectOptionOptions options) {
String[] values = value == null ? null : new String[]{ value };
return selectOption(values, options);
}
@Override
public List<String> selectOption(ElementHandle value, SelectOptionOptions options) {
ElementHandle[] values = value == null ? null : new ElementHandle[]{ value };
return selectOption(values, options);
}
@Override
public List<String> selectOption(String[] values, SelectOptionOptions options) {
if (values == null) {
return selectOption(new SelectOption[0], options);
}
return selectOption(Arrays.asList(values).stream().map(
v -> new SelectOption().setValue(v)).toArray(SelectOption[]::new), options);
}
@Override
public List<String> selectOption(SelectOption value, SelectOptionOptions options) {
SelectOption[] values = value == null ? null : new SelectOption[]{ value };
return selectOption(values, options);
}
private void scrollIntoViewIfNeededImpl(ScrollIntoViewIfNeededOptions options) {
if (options == null) {
options = new ScrollIntoViewIfNeededOptions();
@@ -400,7 +401,7 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
}
private List<String> selectOption(JsonObject params) {
return withLogging("ElementHandle.selectOption", () -> {
return withLogging("SelectOption", () -> {
JsonObject json = sendMessage("selectOption", params).getAsJsonObject();
return parseStringList(json.getAsJsonArray("values"));
});
@@ -411,6 +412,11 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
withLogging("ElementHandle.selectText", () -> selectTextImpl(options));
}
@Override
public void setInputFiles(Path files, SetInputFilesOptions options) {
setInputFiles(new Path[]{files}, options);
}
private void selectTextImpl(SelectTextOptions options) {
if (options == null) {
options = new SelectTextOptions();
@@ -425,11 +431,16 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
}
@Override
public void setInputFiles(FileChooser.FilePayload[] files, SetInputFilesOptions options) {
public void setInputFiles(FilePayload files, SetInputFilesOptions options) {
setInputFiles(new FilePayload[]{files}, options);
}
@Override
public void setInputFiles(FilePayload[] files, SetInputFilesOptions options) {
withLogging("ElementHandle.setInputFiles", () -> setInputFilesImpl(files, options));
}
void setInputFilesImpl(FileChooser.FilePayload[] files, SetInputFilesOptions options) {
void setInputFilesImpl(FilePayload[] files, SetInputFilesOptions options) {
if (options == null) {
options = new SetInputFilesOptions();
}
@@ -448,10 +459,6 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
options = new TapOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
if (options.modifiers != null) {
params.remove("modifiers");
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
sendMessage("tap", params);
}
@@ -503,15 +510,15 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
if (options == null) {
options = new WaitForElementStateOptions();
}
if (state == null) {
throw new IllegalArgumentException("State cannot be null");
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("state", toProtocol(state));
sendMessage("waitForElementState", params);
}
private static String toProtocol(ElementState state) {
if (state == null) {
throw new IllegalArgumentException("State cannot by null");
}
return state.toString().toLowerCase();
}
@@ -525,8 +532,6 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
options = new WaitForSelectorOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("state");
params.addProperty("state", toProtocol(options.state));
params.addProperty("selector", selector);
JsonElement json = sendMessage("waitForSelector", params);
JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
@@ -535,11 +540,4 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
}
return connection.getExistingObject(element.get("guid").getAsString());
}
private static String toProtocol(WaitForSelectorOptions.State state) {
if (state == null) {
state = WaitForSelectorOptions.State.VISIBLE;
}
return state.toString().toLowerCase();
}
}
@@ -19,6 +19,7 @@ package com.microsoft.playwright.impl;
import com.microsoft.playwright.ElementHandle;
import com.microsoft.playwright.FileChooser;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.FilePayload;
import java.nio.file.Path;
@@ -50,11 +51,21 @@ class FileChooserImpl implements FileChooser {
return page;
}
@Override
public void setFiles(Path files, SetFilesOptions options) {
setFiles(new Path[]{files}, options);
}
@Override
public void setFiles(Path[] files, SetFilesOptions options) {
setFiles(Utils.toFilePayloads(files), options);
}
@Override
public void setFiles(FilePayload files, SetFilesOptions options) {
setFiles(new FilePayload[]{files}, options);
}
@Override
public void setFiles(FilePayload[] files, SetFilesOptions options) {
page.withLogging("FileChooser.setInputFiles",
@@ -20,6 +20,7 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -27,11 +28,12 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.Frame.LoadState.*;
import static com.microsoft.playwright.options.LoadState.*;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
import static com.microsoft.playwright.impl.Utils.isFunctionBody;
public class FrameImpl extends ChannelOwner implements Frame {
private String name;
@@ -88,6 +90,33 @@ public class FrameImpl extends ChannelOwner implements Frame {
return withLogging("Frame.querySelectorAll", () -> querySelectorAllImpl(selector));
}
@Override
public List<String> selectOption(String selector, String value, SelectOptionOptions options) {
String[] values = value == null ? null : new String[]{ value };
return selectOption(selector, values, options);
}
@Override
public List<String> selectOption(String selector, ElementHandle value, SelectOptionOptions options) {
ElementHandle[] values = value == null ? null : new ElementHandle[]{value};
return selectOption(selector, values, options);
}
@Override
public List<String> selectOption(String selector, String[] values, SelectOptionOptions options) {
if (values == null) {
return selectOption(selector, new SelectOption[0], options);
}
return selectOption(selector, Arrays.asList(values).stream().map(
v -> new SelectOption().setValue(v)).toArray(SelectOption[]::new), options);
}
@Override
public List<String> selectOption(String selector, SelectOption value, SelectOptionOptions options) {
SelectOption[] values = value == null ? null : new SelectOption[]{value};
return selectOption(selector, values, options);
}
List<ElementHandle> querySelectorAllImpl(String selector) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
@@ -112,7 +141,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evalOnSelector", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
@@ -128,7 +156,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evalOnSelectorAll", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
@@ -217,17 +244,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
params.remove("button");
if (options.button != null) {
params.addProperty("button", Serialization.toProtocol(options.button));
}
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
sendMessage("click", params);
}
@@ -251,17 +267,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
params.remove("button");
if (options.button != null) {
params.addProperty("button", Serialization.toProtocol(options.button));
}
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
sendMessage("dblclick", params);
}
@@ -290,7 +295,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
JsonObject params = new JsonObject();
params.addProperty("expression", expression);
params.addProperty("world", "main");
params.addProperty("isFunction", isFunctionBody(expression));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpression", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
@@ -306,7 +310,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("world", "main");
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpressionHandle", params);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("handle").get("guid").getAsString());
@@ -382,10 +385,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("url", url);
if (options.waitUntil != null) {
params.remove("waitUntil");
params.addProperty("waitUntil", toProtocol(options.waitUntil));
}
JsonElement result = sendMessage("goto", params);
JsonObject jsonResponse = result.getAsJsonObject().getAsJsonObject("response");
if (jsonResponse == null) {
@@ -564,11 +563,11 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public List<String> selectOption(String selector, ElementHandle.SelectOption[] values, SelectOptionOptions options) {
public List<String> selectOption(String selector, SelectOption[] values, SelectOptionOptions options) {
return withLogging("Frame.selectOption", () -> selectOptionImpl(selector, values, options));
}
List<String> selectOptionImpl(String selector, ElementHandle.SelectOption[] values, SelectOptionOptions options) {
List<String> selectOptionImpl(String selector, SelectOption[] values, SelectOptionOptions options) {
if (options == null) {
options = new SelectOptionOptions();
}
@@ -602,31 +601,22 @@ public class FrameImpl extends ChannelOwner implements Frame {
return parseStringList(json.getAsJsonArray("values"));
}
static String toProtocol(LoadState waitUntil) {
if (waitUntil == null) {
waitUntil = LoadState.LOAD;
}
switch (waitUntil) {
case DOMCONTENTLOADED: return "domcontentloaded";
case LOAD: return "load";
case NETWORKIDLE: return "networkidle";
default: throw new PlaywrightException("Unexpected value: " + waitUntil);
}
}
@Override
public void setContent(String html, SetContentOptions options) {
withLogging("Frame.setContent", () -> setContentImpl(html, options));
}
@Override
public void setInputFiles(String selector, Path files, SetInputFilesOptions options) {
setInputFiles(selector, new Path[] {files}, options);
}
void setContentImpl(String html, SetContentOptions options) {
if (options == null) {
options = new SetContentOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("html", html);
params.remove("waitUntil");
params.addProperty("waitUntil", toProtocol(options.waitUntil));
sendMessage("setContent", params);
}
@@ -635,16 +625,21 @@ public class FrameImpl extends ChannelOwner implements Frame {
withLogging("Frame.setInputFiles", () -> setInputFilesImpl(selector, files, options));
}
@Override
public void setInputFiles(String selector, FilePayload files, SetInputFilesOptions options) {
setInputFiles(selector, new FilePayload[]{files}, options);
}
void setInputFilesImpl(String selector, Path[] files, SetInputFilesOptions options) {
setInputFiles(selector, Utils.toFilePayloads(files), options);
}
@Override
public void setInputFiles(String selector, FileChooser.FilePayload[] files, SetInputFilesOptions options) {
public void setInputFiles(String selector, FilePayload[] files, SetInputFilesOptions options) {
withLogging("Frame.setInputFiles", () -> setInputFilesImpl(selector, files, options));
}
void setInputFilesImpl(String selector, FileChooser.FilePayload[] files, SetInputFilesOptions options) {
void setInputFilesImpl(String selector, FilePayload[] files, SetInputFilesOptions options) {
if (options == null) {
options = new SetInputFilesOptions();
}
@@ -663,10 +658,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
options = new TapOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
params.addProperty("selector", selector);
sendMessage("tap", params);
}
@@ -740,7 +731,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("waitForFunction", params);
JsonObject element = json.getAsJsonObject().getAsJsonObject("handle");
@@ -867,31 +857,33 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public Response waitForNavigation(Runnable code, WaitForNavigationOptions options) {
return withLogging("Frame.waitForNavigation", () -> waitForNavigationImpl(code, options));
public Response waitForNavigation(WaitForNavigationOptions options, Runnable code) {
return withLogging("Frame.waitForNavigation", () -> waitForNavigationImpl(code, options, null));
}
Response waitForNavigationImpl(Runnable code, WaitForNavigationOptions options) {
return waitForNavigationImpl(code, options, null);
}
private Response waitForNavigationImpl(Runnable code, WaitForNavigationOptions options, UrlMatcher matcher) {
if (options == null) {
options = new WaitForNavigationOptions();
}
if (options.waitUntil == null) {
options.waitUntil = LOAD;
options.waitUntil = WaitUntilState.LOAD;
}
List<Waitable<Response>> waitables = new ArrayList<>();
UrlMatcher matcher = UrlMatcher.forOneOf(options.glob, options.pattern, options.predicate);
waitables.add(new WaitForNavigationHelper(matcher, options.waitUntil));
if (matcher == null) {
matcher = UrlMatcher.forOneOf(options.url);
}
waitables.add(new WaitForNavigationHelper(matcher, convertViaJson(options.waitUntil, LoadState.class)));
waitables.add(page.createWaitForCloseHelper());
waitables.add(page.createWaitableFrameDetach(this));
waitables.add(page.createWaitableNavigationTimeout(options.timeout));
return runUntil(code, new WaitableRace<>(waitables));
}
private static String toProtocol(WaitForSelectorOptions.State state) {
return state.toString().toLowerCase();
}
@Override
public ElementHandle waitForSelector(String selector, WaitForSelectorOptions options) {
return withLogging("Frame.waitForSelector", () -> waitForSelectorImpl(selector, options));
@@ -903,10 +895,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
if (options.state != null) {
params.remove("state");
params.addProperty("state", toProtocol(options.state));
}
JsonElement json = sendMessage("waitForSelector", params);
JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
if (element == null) {
@@ -930,6 +918,37 @@ public class FrameImpl extends ChannelOwner implements Frame {
});
}
@Override
public void waitForURL(String url, WaitForURLOptions options) {
waitForURL(new UrlMatcher(url), options);
}
@Override
public void waitForURL(Pattern url, WaitForURLOptions options) {
waitForURL(new UrlMatcher(url), options);
}
@Override
public void waitForURL(Predicate<String> url, WaitForURLOptions options) {
waitForURL(new UrlMatcher(url), options);
}
private void waitForURL(UrlMatcher matcher, WaitForURLOptions options) {
withLogging("Frame.waitForURL", () -> waitForURLImpl(matcher, options));
}
void waitForURLImpl(UrlMatcher matcher, WaitForURLOptions options) {
if (options == null) {
options = new WaitForURLOptions();
}
if (matcher.test(url())) {
waitForLoadStateImpl(convertViaJson(options.waitUntil, LoadState.class),
convertViaJson(options, WaitForLoadStateOptions.class));
return;
}
waitForNavigationImpl(() -> {}, convertViaJson(options, WaitForNavigationOptions.class), matcher);
}
protected void handleEvent(String event, JsonObject params) {
if ("loadstate".equals(event)) {
JsonElement add = params.get("add");
@@ -25,7 +25,6 @@ import java.util.HashMap;
import java.util.Map;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.isFunctionBody;
public class JSHandleImpl extends ChannelOwner implements JSHandle {
private String preview;
@@ -51,7 +50,6 @@ public class JSHandleImpl extends ChannelOwner implements JSHandle {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("world", "main");
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpression", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
@@ -65,7 +63,6 @@ public class JSHandleImpl extends ChannelOwner implements JSHandle {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("world", "main");
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpressionHandle", params);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("handle").get("guid").getAsString());
@@ -16,9 +16,9 @@
package com.microsoft.playwright.impl;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.function.Supplier;
class LoggingSupport {
@@ -28,12 +28,8 @@ class LoggingSupport {
isEnabled = (debug != null) && debug.contains("pw:api");
}
private static final SimpleDateFormat timestampFormat;
static {
timestampFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
timestampFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private static final DateTimeFormatter timestampFormat = DateTimeFormatter.ofPattern(
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX").withZone(ZoneId.of("UTC"));
void withLogging(String apiName, Runnable code) {
withLogging(apiName, () -> {
@@ -58,9 +54,14 @@ class LoggingSupport {
}
}
static void logWithTimestamp(String message) {
// This matches log format produced by the server.
String timestamp = ZonedDateTime.now().format(timestampFormat);
System.err.println(timestamp + " " + message);
}
private void logApi(String message) {
// This matches log format produced by the server.
String timestamp = timestampFormat.format(new Date());
System.err.println(timestamp + " pw:api " + message);
logWithTimestamp("pw:api " + message);
}
}
@@ -16,12 +16,10 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Mouse;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Serialization.toProtocol;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
class MouseImpl implements Mouse {
@@ -43,10 +41,6 @@ class MouseImpl implements Mouse {
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("x", x);
params.addProperty("y", y);
if (options.button != null) {
params.remove("button");
params.addProperty("button", toProtocol(options.button));
}
page.sendMessage("mouseClick", params);
}
@@ -19,16 +19,22 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.options.ScreenshotType.JPEG;
import static com.microsoft.playwright.options.ScreenshotType.PNG;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes;
import static java.util.Arrays.asList;
@@ -37,12 +43,12 @@ public class PageImpl extends ChannelOwner implements Page {
private final FrameImpl mainFrame;
private final KeyboardImpl keyboard;
private final MouseImpl mouse;
private final AccessibilityImpl accessibility;
private final TouchscreenImpl touchscreen;
private Viewport viewport;
final Waitable<?> waitableClosedOrCrashed;
private ViewportSize viewport;
private final Router routes = new Router();
private final Set<FrameImpl> frames = new LinkedHashSet<>();
private final ListenerCollection<EventType> listeners = new ListenerCollection<EventType>() {
final ListenerCollection<EventType> listeners = new ListenerCollection<EventType>() {
@Override
void add(EventType eventType, Consumer<?> listener) {
if (eventType == EventType.FILECHOOSER) {
@@ -59,12 +65,13 @@ public class PageImpl extends ChannelOwner implements Page {
}
}
};
final Map<String, Binding> bindings = new HashMap<>();
final Map<String, BindingCallback> bindings = new HashMap<>();
BrowserContextImpl ownedContext;
private boolean isClosed;
final Set<Worker> workers = new HashSet<>();
private final TimeoutSettings timeoutSettings;
private VideoImpl video;
private final PageImpl opener;
enum EventType {
CLOSE,
@@ -95,14 +102,19 @@ public class PageImpl extends ChannelOwner implements Page {
mainFrame.page = this;
isClosed = initializer.get("isClosed").getAsBoolean();
if (initializer.has("viewportSize")) {
viewport = gson().fromJson(initializer.get("viewportSize"), Viewport.class);
viewport = gson().fromJson(initializer.get("viewportSize"), ViewportSize.class);
}
keyboard = new KeyboardImpl(this);
mouse = new MouseImpl(this);
touchscreen = new TouchscreenImpl(this);
accessibility = new AccessibilityImpl(this);
frames.add(mainFrame);
timeoutSettings = new TimeoutSettings(browserContext.timeoutSettings);
waitableClosedOrCrashed = createWaitForCloseHelper();
if (initializer.has("opener")) {
opener = connection.getExistingObject(initializer.getAsJsonObject("opener").get("guid").getAsString());
} else {
opener = null;
}
}
@Override
@@ -110,14 +122,11 @@ public class PageImpl extends ChannelOwner implements Page {
if ("dialog".equals(event)) {
String guid = params.getAsJsonObject("dialog").get("guid").getAsString();
DialogImpl dialog = connection.getExistingObject(guid);
// If no action taken the dialog will stay open and execution will hang. We
// could automatically dismiss the dialog if there are no listeners but we want
// the behavior to match upstream one.
listeners.notify(EventType.DIALOG, dialog);
} else if ("popup".equals(event)) {
String guid = params.getAsJsonObject("page").get("guid").getAsString();
PageImpl popup = connection.getExistingObject(guid);
listeners.notify(EventType.POPUP, popup);
if (listeners.hasListeners(EventType.DIALOG)) {
listeners.notify(EventType.DIALOG, dialog);
} else {
dialog.dismiss();
}
} else if ("worker".equals(event)) {
String guid = params.getAsJsonObject("worker").get("guid").getAsString();
WorkerImpl worker = connection.getExistingObject(guid);
@@ -133,8 +142,10 @@ public class PageImpl extends ChannelOwner implements Page {
ConsoleMessageImpl message = connection.getExistingObject(guid);
listeners.notify(EventType.CONSOLE, message);
} else if ("download".equals(event)) {
String guid = params.getAsJsonObject("download").get("guid").getAsString();
DownloadImpl download = connection.getExistingObject(guid);
String artifactGuid = params.getAsJsonObject("artifact").get("guid").getAsString();
ArtifactImpl artifact = connection.getExistingObject(artifactGuid);
artifact.isRemote = browserContext.browser() != null && browserContext.browser().isRemote;
DownloadImpl download = new DownloadImpl(this, artifact, params);
listeners.notify(EventType.DOWNLOAD, download);
} else if ("fileChooser".equals(event)) {
String guid = params.getAsJsonObject("element").get("guid").getAsString();
@@ -144,7 +155,7 @@ public class PageImpl extends ChannelOwner implements Page {
} else if ("bindingCall".equals(event)) {
String guid = params.getAsJsonObject("binding").get("guid").getAsString();
BindingCall bindingCall = connection.getExistingObject(guid);
Binding binding = bindings.get(bindingCall.name());
BindingCallback binding = bindings.get(bindingCall.name());
if (binding == null) {
binding = browserContext.bindings.get(bindingCall.name());
}
@@ -159,25 +170,6 @@ public class PageImpl extends ChannelOwner implements Page {
listeners.notify(EventType.LOAD, this);
} else if ("domcontentloaded".equals(event)) {
listeners.notify(EventType.DOMCONTENTLOADED, this);
} else if ("request".equals(event)) {
String guid = params.getAsJsonObject("request").get("guid").getAsString();
Request request = connection.getExistingObject(guid);
listeners.notify(EventType.REQUEST, request);
} else if ("requestFailed".equals(event)) {
String guid = params.getAsJsonObject("request").get("guid").getAsString();
RequestImpl request = connection.getExistingObject(guid);
if (params.has("failureText")) {
request.failure = new Request.RequestFailure(params.get("failureText").getAsString());
}
listeners.notify(EventType.REQUESTFAILED, request);
} else if ("requestFinished".equals(event)) {
String guid = params.getAsJsonObject("request").get("guid").getAsString();
Request request = connection.getExistingObject(guid);
listeners.notify(EventType.REQUESTFINISHED, request);
} else if ("response".equals(event)) {
String guid = params.getAsJsonObject("response").get("guid").getAsString();
Response response = connection.getExistingObject(guid);
listeners.notify(EventType.RESPONSE, response);
} else if ("frameAttached".equals(event)) {
String guid = params.getAsJsonObject("frame").get("guid").getAsString();
FrameImpl frame = connection.getExistingObject(guid);
@@ -203,22 +195,39 @@ public class PageImpl extends ChannelOwner implements Page {
handled = browserContext.routes.handle(route);
}
if (!handled) {
route.continue_();
route.resume();
}
} else if ("video".equals(event)) {
video().setRelativePath(params.get("relativePath").getAsString());
String artifactGuid = params.getAsJsonObject("artifact").get("guid").getAsString();
ArtifactImpl artifact = connection.getExistingObject(artifactGuid);
forceVideo().setArtifact(artifact);
} else if ("pageError".equals(event)) {
SerializedError error = gson().fromJson(params.getAsJsonObject("error"), SerializedError.class);
listeners.notify(EventType.PAGEERROR, new ErrorImpl(error));
String errorStr = "";
if (error.error != null) {
errorStr = error.error.name + ": " + error.error.message;
if (error.error.stack != null && !error.error.stack.isEmpty()) {
errorStr += "\n" + error.error.stack;
}
}
listeners.notify(EventType.PAGEERROR, errorStr);
} else if ("crash".equals(event)) {
listeners.notify(EventType.CRASH, this);
} else if ("close".equals(event)) {
isClosed = true;
browserContext.pages.remove(this);
listeners.notify(EventType.CLOSE, this);
didClose();
}
}
void notifyPopup(PageImpl popup) {
listeners.notify(EventType.POPUP, popup);
}
void didClose() {
isClosed = true;
browserContext.pages.remove(this);
listeners.notify(EventType.CLOSE, this);
}
private void willAddFileChooserListener() {
if (!listeners.hasListeners(EventType.FILECHOOSER)) {
updateFileChooserInterception(true);
@@ -248,12 +257,12 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public void onConsole(Consumer<ConsoleMessage> handler) {
public void onConsoleMessage(Consumer<ConsoleMessage> handler) {
listeners.add(EventType.CONSOLE, handler);
}
@Override
public void offConsole(Consumer<ConsoleMessage> handler) {
public void offConsoleMessage(Consumer<ConsoleMessage> handler) {
listeners.remove(EventType.CONSOLE, handler);
}
@@ -348,12 +357,12 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public void onPageError(Consumer<Error> handler) {
public void onPageError(Consumer<String> handler) {
listeners.add(EventType.PAGEERROR, handler);
}
@Override
public void offPageError(Consumer<Error> handler) {
public void offPageError(Consumer<String> handler) {
listeners.remove(EventType.PAGEERROR, handler);
}
@@ -428,7 +437,11 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public Page waitForClose(Runnable code, WaitForCloseOptions options) {
public Page waitForClose(WaitForCloseOptions options, Runnable code) {
return withWaitLogging("Page.waitForClose", () -> waitForCloseImpl(options, code));
}
private Page waitForCloseImpl(WaitForCloseOptions options, Runnable code) {
if (options == null) {
options = new WaitForCloseOptions();
}
@@ -436,23 +449,23 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public ConsoleMessage waitForConsole(Runnable code, WaitForConsoleOptions options) {
public ConsoleMessage waitForConsoleMessage(WaitForConsoleMessageOptions options, Runnable code) {
return withWaitLogging("Page.waitForConsoleMessage", () -> waitForConsoleMessageImpl(options, code));
}
private ConsoleMessage waitForConsoleMessageImpl(WaitForConsoleMessageOptions options, Runnable code) {
if (options == null) {
options = new WaitForConsoleOptions();
options = new WaitForConsoleMessageOptions();
}
return waitForEventWithTimeout(EventType.CONSOLE, code, options.timeout);
}
private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Double timeout) {
List<Waitable<T>> waitables = new ArrayList<>();
waitables.add(new WaitableEvent<>(listeners, eventType));
waitables.add(createWaitForCloseHelper());
waitables.add(createWaitableTimeout(timeout));
return runUntil(code, new WaitableRace<>(waitables));
@Override
public Download waitForDownload(WaitForDownloadOptions options, Runnable code) {
return withWaitLogging("Page.waitForDownload", () -> waitForDownloadImpl(options, code));
}
@Override
public Download waitForDownload(Runnable code, WaitForDownloadOptions options) {
private Download waitForDownloadImpl(WaitForDownloadOptions options, Runnable code) {
if (options == null) {
options = new WaitForDownloadOptions();
}
@@ -460,7 +473,11 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public FileChooser waitForFileChooser(Runnable code, WaitForFileChooserOptions options) {
public FileChooser waitForFileChooser(WaitForFileChooserOptions options, Runnable code) {
return withWaitLogging("Page.waitForFileChooser", () -> waitForFileChooserImpl(options, code));
}
private FileChooser waitForFileChooserImpl(WaitForFileChooserOptions options, Runnable code) {
// TODO: enable/disable file chooser interception
if (options == null) {
options = new WaitForFileChooserOptions();
@@ -469,39 +486,11 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public Frame waitForFrameAttached(Runnable code, WaitForFrameAttachedOptions options) {
if (options == null) {
options = new WaitForFrameAttachedOptions();
}
return waitForEventWithTimeout(EventType.FRAMEATTACHED, code, options.timeout);
public Page waitForPopup(WaitForPopupOptions options, Runnable code) {
return withWaitLogging("Page.waitForPopup", () -> waitForPopupImpl(options, code));
}
@Override
public Frame waitForFrameDetached(Runnable code, WaitForFrameDetachedOptions options) {
if (options == null) {
options = new WaitForFrameDetachedOptions();
}
return waitForEventWithTimeout(EventType.FRAMEDETACHED, code, options.timeout);
}
@Override
public Frame waitForFrameNavigated(Runnable code, WaitForFrameNavigatedOptions options) {
if (options == null) {
options = new WaitForFrameNavigatedOptions();
}
return waitForEventWithTimeout(EventType.FRAMENAVIGATED, code, options.timeout);
}
@Override
public Error waitForPageError(Runnable code, WaitForPageErrorOptions options) {
if (options == null) {
options = new WaitForPageErrorOptions();
}
return waitForEventWithTimeout(EventType.PAGEERROR, code, options.timeout);
}
@Override
public Page waitForPopup(Runnable code, WaitForPopupOptions options) {
private Page waitForPopupImpl(WaitForPopupOptions options, Runnable code) {
if (options == null) {
options = new WaitForPopupOptions();
}
@@ -509,23 +498,11 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public Request waitForRequestFailed(Runnable code, WaitForRequestFailedOptions options) {
if (options == null) {
options = new WaitForRequestFailedOptions();
}
return waitForEventWithTimeout(EventType.REQUESTFAILED, code, options.timeout);
public WebSocket waitForWebSocket(WaitForWebSocketOptions options, Runnable code) {
return withWaitLogging("Page.waitForWebSocket", () -> waitForWebSocketImpl(options, code));
}
@Override
public Request waitForRequestFinished(Runnable code, WaitForRequestFinishedOptions options) {
if (options == null) {
options = new WaitForRequestFinishedOptions();
}
return waitForEventWithTimeout(EventType.REQUESTFINISHED, code, options.timeout);
}
@Override
public WebSocket waitForWebSocket(Runnable code, WaitForWebSocketOptions options) {
private WebSocket waitForWebSocketImpl(WaitForWebSocketOptions options, Runnable code) {
if (options == null) {
options = new WaitForWebSocketOptions();
}
@@ -533,13 +510,25 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public Worker waitForWorker(Runnable code, WaitForWorkerOptions options) {
public Worker waitForWorker(WaitForWorkerOptions options, Runnable code) {
return withWaitLogging("Page.waitForWorker", () -> waitForWorkerImpl(options, code));
}
private Worker waitForWorkerImpl(WaitForWorkerOptions options, Runnable code) {
if (options == null) {
options = new WaitForWorkerOptions();
}
return waitForEventWithTimeout(EventType.WORKER, code, options.timeout);
}
private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Double timeout) {
List<Waitable<T>> waitables = new ArrayList<>();
waitables.add(new WaitableEvent<>(listeners, eventType));
waitables.add(createWaitForCloseHelper());
waitables.add(createWaitableTimeout(timeout));
return runUntil(code, new WaitableRace<>(waitables));
}
@Override
public void close(CloseOptions options) {
if (isClosed) {
@@ -579,20 +568,28 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public Accessibility accessibility() {
return accessibility;
public void addInitScript(String script) {
withLogging("Page.addInitScript", () -> addInitScriptImpl(script));
}
@Override
public void addInitScript(String script, Object arg) {
public void addInitScript(Path path) {
withLogging("Page.addInitScript", () -> {
JsonObject params = new JsonObject();
// TODO: support or drop arg
params.addProperty("source", script);
sendMessage("addInitScript", params);
try {
byte[] bytes = readAllBytes(path);
addInitScriptImpl(new String(bytes, UTF_8));
} catch (IOException e) {
throw new PlaywrightException("Failed to read script from file", e);
}
});
}
private void addInitScriptImpl(String script) {
JsonObject params = new JsonObject();
params.addProperty("source", script);
sendMessage("addInitScript", params);
}
@Override
public ElementHandle addScriptTag(AddScriptTagOptions options) {
return withLogging("Page.addScriptTag",
@@ -645,13 +642,13 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public void emulateMedia(EmulateMediaParams options) {
public void emulateMedia(EmulateMediaOptions options) {
withLogging("Page.emulateMedia", () -> emulateMediaImpl(options));
}
private void emulateMediaImpl(EmulateMediaParams options) {
private void emulateMediaImpl(EmulateMediaOptions options) {
if (options == null) {
options = new EmulateMediaParams();
options = new EmulateMediaOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("emulateMedia", params);
@@ -668,11 +665,11 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public void exposeBinding(String name, Binding playwrightBinding, ExposeBindingOptions options) {
public void exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
withLogging("Page.exposeBinding", () -> exposeBindingImpl(name, playwrightBinding, options));
}
private void exposeBindingImpl(String name, Binding playwrightBinding, ExposeBindingOptions options) {
private void exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
if (bindings.containsKey(name)) {
throw new PlaywrightException("Function \"" + name + "\" has been already registered");
}
@@ -690,9 +687,9 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public void exposeFunction(String name, Function playwrightFunction) {
public void exposeFunction(String name, FunctionCallback playwrightFunction) {
withLogging("Page.exposeFunction",
() -> exposeBindingImpl(name, (Binding.Source source, Object... args) -> playwrightFunction.call(args), null));
() -> exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args), null));
}
@Override
@@ -708,7 +705,7 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public Frame frameByName(String name) {
public Frame frame(String name) {
for (Frame frame : frames) {
if (name.equals(frame.name())) {
return frame;
@@ -762,8 +759,6 @@ public class PageImpl extends ChannelOwner implements Page {
options = new GoBackOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("waitUntil");
params.addProperty("waitUntil", FrameImpl.toProtocol(options.waitUntil));
JsonObject json = sendMessage("goBack", params).getAsJsonObject();
if (json.has("response")) {
return connection.getExistingObject(json.getAsJsonObject("response").get("guid").getAsString());
@@ -781,8 +776,6 @@ public class PageImpl extends ChannelOwner implements Page {
options = new GoForwardOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("waitUntil");
params.addProperty("waitUntil", FrameImpl.toProtocol(options.waitUntil));
JsonObject json = sendMessage("goForward", params).getAsJsonObject();
if (json.has("response")) {
return connection.getExistingObject(json.getAsJsonObject("response").get("guid").getAsString());
@@ -871,13 +864,17 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public Page opener() {
return withLogging("Page.opener", () -> {
JsonObject result = sendMessage("opener").getAsJsonObject();
if (!result.has("page")) {
return null;
}
return connection.getExistingObject(result.getAsJsonObject("page").get("guid").getAsString());
public PageImpl opener() {
if (opener == null || opener.isClosed()) {
return null;
}
return opener;
}
@Override
public void pause() {
withLogging("BrowserContext.pause", () -> {
context().pause();
});
}
@@ -919,8 +916,6 @@ public class PageImpl extends ChannelOwner implements Page {
options = new ReloadOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("waitUntil");
params.addProperty("waitUntil", FrameImpl.toProtocol(options.waitUntil));
JsonObject json = sendMessage("reload", params).getAsJsonObject();
if (json.has("response")) {
return connection.getExistingObject(json.getAsJsonObject("response").get("guid").getAsString());
@@ -954,35 +949,58 @@ public class PageImpl extends ChannelOwner implements Page {
});
}
private static String toProtocol(ScreenshotOptions.Type type) {
return type.toString().toLowerCase();
}
@Override
public byte[] screenshot(ScreenshotOptions options) {
return withLogging("Page.screenshot", () -> screenshotImpl(options));
}
@Override
public List<String> selectOption(String selector, String value, SelectOptionOptions options) {
String[] values = value == null ? null : new String[]{ value };
return selectOption(selector, values, options);
}
@Override
public List<String> selectOption(String selector, ElementHandle value, SelectOptionOptions options) {
ElementHandle[] values = value == null ? null : new ElementHandle[]{ value };
return selectOption(selector, values, options);
}
@Override
public List<String> selectOption(String selector, String[] values, SelectOptionOptions options) {
if (values == null) {
return selectOption(selector, new SelectOption[0], options);
}
return selectOption(selector, Arrays.asList(values).stream().map(
v -> new SelectOption().setValue(v)).toArray(SelectOption[]::new), options);
}
@Override
public List<String> selectOption(String selector, SelectOption value, SelectOptionOptions options) {
SelectOption[] values = value == null ? null : new SelectOption[]{value};
return selectOption(selector, values, options);
}
private byte[] screenshotImpl(ScreenshotOptions options) {
if (options == null) {
options = new ScreenshotOptions();
}
if (options.type == null) {
options.type = ScreenshotOptions.Type.PNG;
options.type = PNG;
if (options.path != null) {
String fileName = options.path.getFileName().toString();
int extStart = fileName.lastIndexOf('.');
if (extStart != -1) {
String extension = fileName.substring(extStart).toLowerCase();
if (".jpeg".equals(extension) || ".jpg".equals(extension)) {
options.type = ScreenshotOptions.Type.JPEG;
options.type = JPEG;
}
}
}
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("type");
params.addProperty("type", toProtocol(options.type));
params.remove("path");
JsonObject json = sendMessage("screenshot", params).getAsJsonObject();
@@ -994,7 +1012,7 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public List<String> selectOption(String selector, ElementHandle.SelectOption[] values, SelectOptionOptions options) {
public List<String> selectOption(String selector, SelectOption[] values, SelectOptionOptions options) {
return withLogging("Page.selectOption",
() -> mainFrame.selectOptionImpl(selector, values, convertViaJson(options, Frame.SelectOptionOptions.class)));
}
@@ -1047,6 +1065,11 @@ public class PageImpl extends ChannelOwner implements Page {
});
}
@Override
public void setInputFiles(String selector, Path files, SetInputFilesOptions options) {
setInputFiles(selector, new Path[]{files}, options);
}
@Override
public void setInputFiles(String selector, Path[] files, SetInputFilesOptions options) {
withLogging("Page.setInputFiles",
@@ -1054,7 +1077,12 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public void setInputFiles(String selector, FileChooser.FilePayload[] files, SetInputFilesOptions options) {
public void setInputFiles(String selector, FilePayload files, SetInputFilesOptions options) {
setInputFiles(selector, new FilePayload[]{files}, options);
}
@Override
public void setInputFiles(String selector, FilePayload[] files, SetInputFilesOptions options) {
withLogging("Page.setInputFiles",
() -> mainFrame.setInputFilesImpl(selector, files, convertViaJson(options, Frame.SetInputFilesOptions.class)));
}
@@ -1062,7 +1090,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void setViewportSize(int width, int height) {
withLogging("Page.setViewportSize", () -> {
viewport = new Viewport(width, height);
viewport = new ViewportSize(width, height);
JsonObject params = new JsonObject();
params.add("viewportSize", gson().toJsonTree(viewport));
sendMessage("setViewportSize", params);
@@ -1134,24 +1162,27 @@ public class PageImpl extends ChannelOwner implements Page {
return mainFrame.url();
}
@Override
public VideoImpl video() {
if (video != null) {
return video;
}
if (browserContext.videosDir == null) {
return null;
}
video = new VideoImpl(this);
// In case of persistent profile, we already have it.
if (initializer.has("videoRelativePath")) {
video.setRelativePath(initializer.get("videoRelativePath").getAsString());
private VideoImpl forceVideo() {
if (video == null) {
video = new VideoImpl(this);
}
return video;
}
@Override
public Viewport viewportSize() {
public VideoImpl video() {
// Note: we are creating Video object lazily, because we do not know
// BrowserContextOptions when constructing the page - it is assigned
// too late during launchPersistentContext.
if (browserContext.videosDir == null) {
return null;
}
return forceVideo();
}
@Override
public ViewportSize viewportSize() {
return viewport;
}
@@ -1172,11 +1203,11 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void waitForLoadState(LoadState state, WaitForLoadStateOptions options) {
withLogging("Page.waitForLoadState",
() -> mainFrame.waitForLoadStateImpl(convertViaJson(state, Frame.LoadState.class), convertViaJson(options, Frame.WaitForLoadStateOptions.class)));
() -> mainFrame.waitForLoadStateImpl(state, convertViaJson(options, Frame.WaitForLoadStateOptions.class)));
}
@Override
public Response waitForNavigation(Runnable code, WaitForNavigationOptions options) {
public Response waitForNavigation(WaitForNavigationOptions options, Runnable code) {
return withLogging("Page.waitForNavigation", () -> waitForNavigationImpl(code, options));
}
@@ -1185,9 +1216,7 @@ public class PageImpl extends ChannelOwner implements Page {
if (options != null) {
frameOptions.timeout = options.timeout;
frameOptions.waitUntil = options.waitUntil;
frameOptions.glob = options.glob;
frameOptions.pattern = options.pattern;
frameOptions.predicate = options.predicate;
frameOptions.url = options.url;
}
return mainFrame.waitForNavigationImpl(code, frameOptions);
}
@@ -1196,29 +1225,6 @@ public class PageImpl extends ChannelOwner implements Page {
listeners.notify(EventType.FRAMENAVIGATED, frame);
}
private static class ErrorImpl implements Error {
private final SerializedError error;
ErrorImpl(SerializedError error) {
this.error = error;
}
@Override
public String message() {
return error.error.message;
}
@Override
public String name() {
return error.error.name;
}
@Override
public String stack() {
return error.error.stack;
}
}
private class WaitableFrameDetach extends WaitableEvent<EventType, Frame> {
WaitableFrameDetach(Frame frameArg) {
super(PageImpl.this.listeners, EventType.FRAMEDETACHED, detachedFrame -> frameArg.equals(detachedFrame));
@@ -1263,25 +1269,25 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public Request waitForRequest(Runnable code, String urlGlob, WaitForRequestOptions options) {
return waitForRequest(code, toRequestPredicate(new UrlMatcher(urlGlob)), options);
public Request waitForRequest(String urlGlob, WaitForRequestOptions options, Runnable code) {
return waitForRequest(toRequestPredicate(new UrlMatcher(urlGlob)), options, code);
}
@Override
public Request waitForRequest(Runnable code, Pattern urlPattern, WaitForRequestOptions options) {
return waitForRequest(code, toRequestPredicate(new UrlMatcher(urlPattern)), options);
public Request waitForRequest(Pattern urlPattern, WaitForRequestOptions options, Runnable code) {
return waitForRequest(toRequestPredicate(new UrlMatcher(urlPattern)), options, code);
}
@Override
public Request waitForRequest(Runnable code, Predicate<Request> predicate, WaitForRequestOptions options) {
return withLogging("Page.waitForRequest", () -> waitForRequestImpl(code, predicate, options));
public Request waitForRequest(Predicate<Request> predicate, WaitForRequestOptions options, Runnable code) {
return withWaitLogging("Page.waitForRequest", () -> waitForRequestImpl(predicate, options, code));
}
private static Predicate<Request> toRequestPredicate(UrlMatcher matcher) {
return request -> matcher.test(request.url());
}
private Request waitForRequestImpl(Runnable code, Predicate<Request> predicate, WaitForRequestOptions options) {
private Request waitForRequestImpl(Predicate<Request> predicate, WaitForRequestOptions options, Runnable code) {
if (options == null) {
options = new WaitForRequestOptions();
}
@@ -1294,25 +1300,43 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public Response waitForResponse(Runnable code, String urlGlob, WaitForResponseOptions options) {
return waitForResponse(code, toResponsePredicate(new UrlMatcher(urlGlob)), options);
public Request waitForRequestFinished(WaitForRequestFinishedOptions options, Runnable code) {
return withWaitLogging("Page.waitForRequestFinished", () -> waitForRequestFinishedImpl(options, code));
}
private Request waitForRequestFinishedImpl(WaitForRequestFinishedOptions options, Runnable code) {
if (options == null) {
options = new WaitForRequestFinishedOptions();
}
List<Waitable<Request>> waitables = new ArrayList<>();
Predicate<Request> predicate = options.predicate;
waitables.add(new WaitableEvent<>(listeners, EventType.REQUESTFINISHED,
request -> predicate == null || predicate.test(request)));
waitables.add(createWaitForCloseHelper());
waitables.add(createWaitableTimeout(options.timeout));
return runUntil(code, new WaitableRace<>(waitables));
}
@Override
public Response waitForResponse(Runnable code, Pattern urlPattern, WaitForResponseOptions options) {
return waitForResponse(code, toResponsePredicate(new UrlMatcher(urlPattern)), options);
public Response waitForResponse(String urlGlob, WaitForResponseOptions options, Runnable code) {
return waitForResponse(toResponsePredicate(new UrlMatcher(urlGlob)), options, code);
}
@Override
public Response waitForResponse(Runnable code, Predicate<Response> predicate, WaitForResponseOptions options) {
return withLogging("Page.waitForResponse", () -> waitForResponseImpl(code, predicate, options));
public Response waitForResponse(Pattern urlPattern, WaitForResponseOptions options, Runnable code) {
return waitForResponse(toResponsePredicate(new UrlMatcher(urlPattern)), options, code);
}
@Override
public Response waitForResponse(Predicate<Response> predicate, WaitForResponseOptions options, Runnable code) {
return withLogging("Page.waitForResponse", () -> waitForResponseImpl(predicate, options, code));
}
private static Predicate<Response> toResponsePredicate(UrlMatcher matcher) {
return response -> matcher.test(response.url());
}
private Response waitForResponseImpl(Runnable code, Predicate<Response> predicate, WaitForResponseOptions options) {
private Response waitForResponseImpl(Predicate<Response> predicate, WaitForResponseOptions options, Runnable code) {
if (options == null) {
options = new WaitForResponseOptions();
}
@@ -1335,8 +1359,41 @@ public class PageImpl extends ChannelOwner implements Page {
withLogging("Page.waitForTimeout", () -> mainFrame.waitForTimeoutImpl(timeout));
}
@Override
public void waitForURL(String url, WaitForURLOptions options) {
waitForURL(new UrlMatcher(url), options);
}
@Override
public void waitForURL(Pattern url, WaitForURLOptions options) {
waitForURL(new UrlMatcher(url), options);
}
@Override
public void waitForURL(Predicate<String> url, WaitForURLOptions options) {
waitForURL(new UrlMatcher(url), options);
}
private void waitForURL(UrlMatcher matcher, WaitForURLOptions options) {
withLogging("Page.waitForURL", () -> mainFrame.waitForURLImpl(matcher, convertViaJson(options, Frame.WaitForURLOptions.class)));
}
@Override
public List<Worker> workers() {
return new ArrayList<>(workers);
}
@Override
public void onceDialog(Consumer<Dialog> handler) {
onDialog(new Consumer<Dialog>() {
@Override
public void accept(Dialog dialog) {
try {
handler.accept(dialog);
} finally {
offDialog(this);
}
}
});
}
}
@@ -0,0 +1,177 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class PipeTransport implements Transport {
private final BlockingQueue<String> incoming = new ArrayBlockingQueue<>(1000);
private final BlockingQueue<String> outgoing= new ArrayBlockingQueue<>(1000);
private final ReaderThread readerThread;
private final WriterThread writerThread;
private boolean isClosed;
PipeTransport(InputStream input, OutputStream output) {
DataInputStream in = new DataInputStream(new BufferedInputStream(input));
readerThread = new ReaderThread(in, incoming);
readerThread.start();
writerThread = new WriterThread(output, outgoing);
writerThread.start();
}
@Override
public void send(String message) {
if (isClosed) {
throw new PlaywrightException("Playwright connection closed");
}
try {
outgoing.put(message);
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to send message", e);
}
}
@Override
public String poll(Duration timeout) {
if (isClosed) {
throw new PlaywrightException("Playwright connection closed");
}
try {
String message = incoming.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
if (message == null && readerThread.exception != null) {
try {
close();
} catch (IOException e) {
e.printStackTrace(System.err);
}
throw new PlaywrightException("Failed to read message from driver, pipe closed.", readerThread.exception);
}
return message;
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to read message", e);
}
}
@Override
public void close() throws IOException {
if (isClosed) {
return;
}
isClosed = true;
// We interrupt only the outgoing pipe and keep reader thread running as
// otherwise child process may block on writing to its stdout and never
// exit (observed on Windows).
readerThread.isClosing = true;
writerThread.out.close();
writerThread.interrupt();
}
}
class ReaderThread extends Thread {
private final DataInputStream in;
private final BlockingQueue<String> queue;
volatile boolean isClosing;
volatile Exception exception;
private static int readIntLE(DataInputStream in) throws IOException {
int ch1 = in.read();
int ch2 = in.read();
int ch3 = in.read();
int ch4 = in.read();
if ((ch1 | ch2 | ch3 | ch4) < 0) {
throw new EOFException();
} else {
return (ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0);
}
}
ReaderThread(DataInputStream in, BlockingQueue<String> queue) {
this.in = in;
this.queue = queue;
}
@Override
public void run() {
while (!isInterrupted()) {
try {
queue.put(readMessage());
} catch (IOException e) {
if (!isInterrupted() && !isClosing) {
exception = e;
}
break;
} catch (InterruptedException e) {
break;
}
}
}
private String readMessage() throws IOException {
int len = readIntLE(in);
byte[] raw = new byte[len];
in.readFully(raw, 0, len);
return new String(raw, StandardCharsets.UTF_8);
}
}
class WriterThread extends Thread {
final OutputStream out;
private final BlockingQueue<String> queue;
private static void writeIntLE(OutputStream out, int v) throws IOException {
out.write(v >>> 0 & 255);
out.write(v >>> 8 & 255);
out.write(v >>> 16 & 255);
out.write(v >>> 24 & 255);
}
WriterThread(OutputStream out, BlockingQueue<String> queue) {
this.out = out;
this.queue = queue;
}
@Override
public void run() {
while (!isInterrupted()) {
try {
if (queue.isEmpty())
out.flush();
sendMessage(queue.take());
} catch (IOException e) {
if (!isInterrupted())
e.printStackTrace();
break;
} catch (InterruptedException e) {
break;
}
}
}
private void sendMessage(String message) throws IOException {
byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
writeIntLE(out, bytes.length);
out.write(bytes);
}
}
@@ -16,20 +16,13 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.DeviceDescriptor;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Selectors;
import java.io.*;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class PlaywrightImpl extends ChannelOwner implements Playwright {
@@ -42,9 +35,10 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
// pb.environment().put("DEBUG", "pw:pro*");
Process p = pb.start();
Connection connection = new Connection(p.getInputStream(), p.getOutputStream());
Connection connection = new Connection(new PipeTransport(p.getInputStream(), p.getOutputStream()));
PlaywrightImpl result = (PlaywrightImpl) connection.waitForObjectWithKnownName("Playwright");
result.driverProcess = p;
result.initSharedSelectors(null);
return result;
} catch (IOException e) {
throw new PlaywrightException("Failed to launch driver", e);
@@ -54,24 +48,30 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
private final BrowserTypeImpl chromium;
private final BrowserTypeImpl firefox;
private final BrowserTypeImpl webkit;
private final Selectors selectors;
private final Map<String, DeviceDescriptor> devices = new HashMap<>();
private final SelectorsImpl selectors;
private SharedSelectors sharedSelectors;;
PlaywrightImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
chromium = parent.connection.getExistingObject(initializer.getAsJsonObject("chromium").get("guid").getAsString());
firefox = parent.connection.getExistingObject(initializer.getAsJsonObject("firefox").get("guid").getAsString());
webkit = parent.connection.getExistingObject(initializer.getAsJsonObject("webkit").get("guid").getAsString());
selectors = parent.connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString());
Gson gson = Serialization.gson();
for (JsonElement item : initializer.getAsJsonArray("deviceDescriptors")) {
JsonObject o = item.getAsJsonObject();
String name = o.get("name").getAsString();
DeviceDescriptorImpl descriptor = gson.fromJson(o.get("descriptor"), DeviceDescriptorImpl.class);
descriptor.playwright = this;
devices.put(name, descriptor);
selectors = connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString());
}
void initSharedSelectors(PlaywrightImpl parent) {
assert sharedSelectors == null;
if (parent == null) {
sharedSelectors = new SharedSelectors();;
} else {
sharedSelectors = parent.sharedSelectors;
}
sharedSelectors.addChannel(selectors);
}
void unregisterSelectors() {
sharedSelectors.removeChannel(selectors);
}
@Override
@@ -89,23 +89,25 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
return webkit;
}
@Override
public Map<String, DeviceDescriptor> devices() {
return devices;
}
@Override
public Selectors selectors() {
return selectors;
return sharedSelectors;
}
@Override
public void close() throws Exception {
connection.close();
// playwright-cli will exit when its stdin is closed, we wait for that.
boolean didClose = driverProcess.waitFor(30, TimeUnit.SECONDS);
if (!didClose) {
System.err.println("WARNING: Timed out while waiting for driver process to exit");
public void close() {
try {
connection.close();
// playwright-cli will exit when its stdin is closed, we wait for that.
boolean didClose = driverProcess.waitFor(30, TimeUnit.SECONDS);
if (!didClose) {
System.err.println("WARNING: Timed out while waiting for driver process to exit");
}
} catch (IOException e) {
throw new PlaywrightException("Failed to terminate", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new PlaywrightException("Operation interrupted", e);
}
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
public class RemoteBrowser extends ChannelOwner {
RemoteBrowser(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
BrowserImpl browser() {
return connection.getExistingObject(initializer.getAsJsonObject("browser").get("guid").getAsString());
}
SelectorsImpl selectors() {
return connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString());
}
}
@@ -21,6 +21,7 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.Request;
import com.microsoft.playwright.Response;
import com.microsoft.playwright.options.Timing;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@@ -32,8 +33,8 @@ public class RequestImpl extends ChannelOwner implements Request {
private RequestImpl redirectedFrom;
private RequestImpl redirectedTo;
final Map<String, String> headers = new HashMap<>();
RequestFailure failure;
RequestTiming timing;
String failure;
Timing timing;
RequestImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
@@ -54,7 +55,7 @@ public class RequestImpl extends ChannelOwner implements Request {
}
@Override
public RequestFailure failure() {
public String failure() {
return failure;
}
@@ -118,8 +119,8 @@ public class RequestImpl extends ChannelOwner implements Request {
}
@Override
public RequestTiming timing() {
return null;
public Timing timing() {
return timing;
}
@Override
@@ -21,6 +21,7 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.Request;
import com.microsoft.playwright.Response;
import com.microsoft.playwright.options.Timing;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@@ -45,7 +46,7 @@ public class ResponseImpl extends ChannelOwner implements Response {
JsonObject item = e.getAsJsonObject();
request.headers.put(item.get("name").getAsString().toLowerCase(), item.get("value").getAsString());
}
request.timing = Serialization.gson().fromJson(initializer.get("timing"), Request.RequestTiming.class);
request.timing = Serialization.gson().fromJson(initializer.get("timing"), Timing.class);
}
@Override
@@ -16,16 +16,15 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Request;
import com.microsoft.playwright.Route;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -44,13 +43,13 @@ public class RouteImpl extends ChannelOwner implements Route {
}
@Override
public void continue_(ContinueOptions options) {
withLogging("Route.continue", () -> continueImpl(options));
public void resume(ResumeOptions options) {
withLogging("Route.resume", () -> resumeImpl(options));
}
private void continueImpl(ContinueOptions options) {
private void resumeImpl(ResumeOptions options) {
if (options == null) {
options = new ContinueOptions();
options = new ResumeOptions();
}
JsonObject params = new JsonObject();
if (options.url != null) {
@@ -63,7 +62,15 @@ public class RouteImpl extends ChannelOwner implements Route {
params.add("headers", Serialization.toProtocol(options.headers));
}
if (options.postData != null) {
String base64 = Base64.getEncoder().encodeToString(options.postData);
byte[] bytes = null;
if (options.postData instanceof byte[]) {
bytes = (byte[]) options.postData;
} else if (options.postData instanceof String) {
bytes = ((String) options.postData).getBytes(StandardCharsets.UTF_8);
} else {
throw new PlaywrightException("postData must be either String or byte[], found: " + options.postData.getClass().getName());
}
String base64 = Base64.getEncoder().encodeToString(bytes);
params.addProperty("postData", base64);
}
sendMessage("continue", params);
@@ -16,12 +16,10 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.Request;
import com.microsoft.playwright.Route;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -27,34 +27,18 @@ import java.nio.file.Path;
import static com.microsoft.playwright.impl.Serialization.gson;
import static java.nio.charset.StandardCharsets.UTF_8;
class SelectorsImpl extends ChannelOwner implements Selectors {
class SelectorsImpl extends ChannelOwner {
SelectorsImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@Override
public void register(String name, String script, RegisterOptions options) {
withLogging("Selectors.register", () -> registerImpl(name, script, options));
}
private void registerImpl(String name, String script, RegisterOptions options) {
void registerImpl(String name, String script, Selectors.RegisterOptions options) {
if (options == null) {
options = new RegisterOptions();
options = new Selectors.RegisterOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("name", name);
params.addProperty("source", script);
sendMessage("register", params);
}
@Override
public void register(String name, Path path, RegisterOptions options) {
byte[] buffer;
try {
buffer = Files.readAllBytes(path);
} catch (IOException e) {
throw new PlaywrightException("Failed to read selector from file: " + path, e);
}
register(name, new String(buffer, UTF_8), options);
}
}
@@ -20,7 +20,9 @@ import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.microsoft.playwright.*;
import com.microsoft.playwright.ElementHandle;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -36,13 +38,21 @@ class Serialization {
static Gson gson() {
if (gson == null) {
gson = new GsonBuilder()
.registerTypeAdapter(BrowserContext.SameSite.class, new SameSiteAdapter().nullSafe())
.registerTypeAdapter(SameSiteAttribute.class, new SameSiteAdapter().nullSafe())
.registerTypeAdapter(BrowserChannel.class, new BrowserChannelSerializer())
.registerTypeAdapter(ColorScheme.class, new ColorSchemeAdapter().nullSafe())
.registerTypeAdapter(Page.EmulateMediaParams.Media.class, new MediaSerializer())
.registerTypeAdapter(Media.class, new MediaSerializer())
.registerTypeAdapter(ScreenshotType.class, new ToLowerCaseSerializer<ScreenshotType>())
.registerTypeAdapter(MouseButton.class, new ToLowerCaseSerializer<MouseButton>())
.registerTypeAdapter(LoadState.class, new ToLowerCaseSerializer<LoadState>())
.registerTypeAdapter(WaitUntilState.class, new ToLowerCaseSerializer<WaitUntilState>())
.registerTypeAdapter(WaitForSelectorState.class, new ToLowerCaseSerializer<WaitForSelectorState>())
.registerTypeAdapter((new TypeToken<List<KeyboardModifier>>(){}).getType(), new KeyboardModifiersSerializer())
.registerTypeAdapter(Optional.class, new OptionalSerializer())
.registerTypeHierarchyAdapter(JSHandleImpl.class, new HandleSerializer())
.registerTypeHierarchyAdapter(Map.class, new StringMapSerializer())
.registerTypeAdapter(Path.class, new PathSerializer()).create();
.registerTypeAdapter((new TypeToken<Map<String, String>>(){}).getType(), new StringMapSerializer())
.registerTypeAdapter((new TypeToken<Map<String, Object>>(){}).getType(), new FirefoxUserPrefsSerializer())
.registerTypeHierarchyAdapter(Path.class, new PathSerializer()).create();
}
return gson;
}
@@ -180,39 +190,29 @@ class Serialization {
throw new PlaywrightException("Unexpected result: " + gson().toJson(value));
}
static String toProtocol(Mouse.Button button) {
switch (button) {
case LEFT:
return "left";
case RIGHT:
return "right";
case MIDDLE:
return "middle";
default:
throw new PlaywrightException("Unexpected value: " + button);
private static class KeyboardModifiersSerializer implements JsonSerializer<List<KeyboardModifier>> {
@Override
public JsonArray serialize(List<KeyboardModifier> modifiers, Type typeOfSrc, JsonSerializationContext context) {
JsonArray result = new JsonArray();
if (modifiers.contains(KeyboardModifier.ALT)) {
result.add("Alt");
}
if (modifiers.contains(KeyboardModifier.CONTROL)) {
result.add("Control");
}
if (modifiers.contains(KeyboardModifier.META)) {
result.add("Meta");
}
if (modifiers.contains(KeyboardModifier.SHIFT)) {
result.add("Shift");
}
return result;
}
}
static JsonArray toProtocol(Set<Keyboard.Modifier> modifiers) {
JsonArray result = new JsonArray();
if (modifiers.contains(Keyboard.Modifier.ALT)) {
result.add("Alt");
}
if (modifiers.contains(Keyboard.Modifier.CONTROL)) {
result.add("Control");
}
if (modifiers.contains(Keyboard.Modifier.META)) {
result.add("Meta");
}
if (modifiers.contains(Keyboard.Modifier.SHIFT)) {
result.add("Shift");
}
return result;
}
static JsonArray toJsonArray(FileChooser.FilePayload[] files) {
static JsonArray toJsonArray(FilePayload[] files) {
JsonArray jsonFiles = new JsonArray();
for (FileChooser.FilePayload p : files) {
for (FilePayload p : files) {
JsonObject jsonFile = new JsonObject();
jsonFile.addProperty("name", p.name);
jsonFile.addProperty("mimeType", p.mimeType);
@@ -253,13 +253,14 @@ class Serialization {
private static class OptionalSerializer implements JsonSerializer<Optional<?>> {
private static boolean isSupported(Type type) {
return new TypeToken<Optional<Page.EmulateMediaParams.Media>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
new TypeToken<Optional<ColorScheme>>() {}.getType().getTypeName().equals(type.getTypeName());
return new TypeToken<Optional<Media>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
new TypeToken<Optional<ColorScheme>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
new TypeToken<Optional<ViewportSize>>() {}.getType().getTypeName().equals(type.getTypeName());
}
@Override
public JsonElement serialize(Optional<?> src, Type typeOfSrc, JsonSerializationContext context) {
assert isSupported(typeOfSrc);
assert isSupported(typeOfSrc) : "Unexpected optional type: " + typeOfSrc.getTypeName();
if (!src.isPresent()) {
return new JsonPrimitive("null");
}
@@ -276,6 +277,16 @@ class Serialization {
}
}
private static class FirefoxUserPrefsSerializer implements JsonSerializer<Map<String, Object>> {
@Override
public JsonElement serialize(Map<String, Object> src, Type typeOfSrc, JsonSerializationContext context) {
if (!"java.util.Map<java.lang.String, java.lang.Object>".equals(typeOfSrc.getTypeName())) {
throw new PlaywrightException("Unexpected map type: " + typeOfSrc);
}
return context.serialize(src, Map.class);
}
}
private static class StringMapSerializer implements JsonSerializer<Map<String, String>> {
@Override
public JsonElement serialize(Map<String, String> src, Type typeOfSrc, JsonSerializationContext context) {
@@ -286,9 +297,16 @@ class Serialization {
}
}
private static class MediaSerializer implements JsonSerializer<Page.EmulateMediaParams.Media> {
private static class BrowserChannelSerializer implements JsonSerializer<BrowserChannel> {
@Override
public JsonElement serialize(Page.EmulateMediaParams.Media src, Type typeOfSrc, JsonSerializationContext context) {
public JsonElement serialize(BrowserChannel src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString().toLowerCase().replace('_', '-'));
}
}
private static class MediaSerializer implements JsonSerializer<Media> {
@Override
public JsonElement serialize(Media src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString().toLowerCase());
}
}
@@ -300,9 +318,16 @@ class Serialization {
}
}
private static class SameSiteAdapter extends TypeAdapter<BrowserContext.SameSite> {
private static class ToLowerCaseSerializer<E extends Enum<E>> implements JsonSerializer<E> {
@Override
public void write(JsonWriter out, BrowserContext.SameSite value) throws IOException {
public JsonElement serialize(E src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString().toLowerCase());
}
}
private static class SameSiteAdapter extends TypeAdapter<SameSiteAttribute> {
@Override
public void write(JsonWriter out, SameSiteAttribute value) throws IOException {
String stringValue;
switch (value) {
case STRICT:
@@ -321,9 +346,9 @@ class Serialization {
}
@Override
public BrowserContext.SameSite read(JsonReader in) throws IOException {
public SameSiteAttribute read(JsonReader in) throws IOException {
String value = in.nextString();
return BrowserContext.SameSite.valueOf(value.toUpperCase());
return SameSiteAttribute.valueOf(value.toUpperCase());
}
}
@@ -1,47 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import java.io.PrintStream;
import java.io.PrintWriter;
class ServerException extends PlaywrightException {
private final SerializedError.Error error;
ServerException(SerializedError.Error error) {
super(error.name + ": " + error.message);
this.error = error;
}
@Override
public void printStackTrace(PrintWriter s) {
super.printStackTrace(s);
s.println("Caused by Playwright server error:");
s.println(getMessage());
s.println(error.stack);
}
@Override
public void printStackTrace(PrintStream s) {
super.printStackTrace(s);
s.println("Caused by Playwright server error:");
s.println(getMessage());
s.println(error.stack);
}
}
@@ -0,0 +1,77 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Selectors;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static java.nio.charset.StandardCharsets.UTF_8;
public class SharedSelectors extends LoggingSupport implements Selectors {
private final List<SelectorsImpl> channels = new ArrayList<>();
private final List<Registration> registrations = new ArrayList<>();
private static class Registration {
final String name;
final String script;
final RegisterOptions options;
Registration(String name, String script, RegisterOptions options) {
this.name = name;
this.script = script;
this.options = options;
}
}
@Override
public void register(String name, String script, RegisterOptions options) {
withLogging("Selectors.register", () -> registerImpl(name, script, options));
}
@Override
public void register(String name, Path path, RegisterOptions options) {
withLogging("Selectors.register", () -> {
byte[] buffer;
try {
buffer = Files.readAllBytes(path);
} catch (IOException e) {
throw new PlaywrightException("Failed to read selector from file: " + path, e);
}
registerImpl(name, new String(buffer, UTF_8), options);
});
}
void addChannel(SelectorsImpl channel) {
registrations.forEach(r -> channel.registerImpl(r.name, r.script, r.options));
channels.add(channel);
}
void removeChannel(SelectorsImpl channel) {
channels.remove(channel);
}
private void registerImpl(String name, String script, RegisterOptions options) {
channels.forEach(impl -> impl.registerImpl(name, script, options));
registrations.add(new Registration(name, script, options));
}
}
@@ -0,0 +1,66 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Tracing;
import java.nio.file.Path;
import static com.microsoft.playwright.impl.Serialization.gson;
class TracingImpl implements Tracing {
private final BrowserContextImpl context;
TracingImpl(BrowserContextImpl context) {
this.context = context;
}
private void export(Path path) {
JsonObject json = context.sendMessage("tracingExport").getAsJsonObject();
ArtifactImpl artifact = context.connection.getExistingObject(json.getAsJsonObject("artifact").get("guid").getAsString());
if (context.browser().isRemote) {
artifact.isRemote = true;
}
artifact.saveAs(path);
artifact.delete();
}
@Override
public void start(StartOptions options) {
context.withLogging("Tracing.start", () -> startImpl(options));
}
private void startImpl(StartOptions options) {
if (options == null) {
options = new StartOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
context.sendMessage("tracingStart", params);
}
@Override
public void stop(StopOptions options) {
context.withLogging("Tracing.stop", () -> {
context.sendMessage("tracingStop");
if (options != null && options.path != null) {
export(options.path);
}
});
}
}
@@ -13,152 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class Transport {
private final BlockingQueue<String> incoming = new ArrayBlockingQueue<>(1000);
private final BlockingQueue<String> outgoing= new ArrayBlockingQueue<>(1000);
private final ReaderThread readerThread;
private final WriterThread writerThread;
private boolean isClosed;
Transport(InputStream input, OutputStream output) {
DataInputStream in = new DataInputStream(new BufferedInputStream(input));
readerThread = new ReaderThread(in, incoming);
readerThread.start();
writerThread = new WriterThread(output, outgoing);
writerThread.start();
}
public void send(String message) {
if (isClosed) {
throw new PlaywrightException("Playwright connection closed");
}
try {
outgoing.put(message);
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to send message", e);
}
}
public String poll(Duration timeout) {
if (isClosed) {
throw new PlaywrightException("Playwright connection closed");
}
try {
return incoming.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to read message", e);
}
}
void close() throws IOException {
if (isClosed) {
return;
}
isClosed = true;
// We interrupt only the outgoing pipe and keep reader thread running as
// otherwise child process may block on writing to its stdout and never
// exit (observed on Windows).
readerThread.isClosing = true;
writerThread.out.close();
writerThread.interrupt();
}
}
class ReaderThread extends Thread {
private final DataInputStream in;
private final BlockingQueue<String> queue;
volatile boolean isClosing;
private static int readIntLE(DataInputStream in) throws IOException {
int ch1 = in.read();
int ch2 = in.read();
int ch3 = in.read();
int ch4 = in.read();
if ((ch1 | ch2 | ch3 | ch4) < 0) {
throw new EOFException();
} else {
return (ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0);
}
}
ReaderThread(DataInputStream in, BlockingQueue<String> queue) {
this.in = in;
this.queue = queue;
}
@Override
public void run() {
while (!isInterrupted()) {
try {
queue.put(readMessage());
} catch (IOException e) {
if (!isInterrupted() && !isClosing) {
e.printStackTrace();
}
break;
} catch (InterruptedException e) {
break;
}
}
}
private String readMessage() throws IOException {
int len = readIntLE(in);
byte[] raw = new byte[len];
in.readFully(raw, 0, len);
return new String(raw, StandardCharsets.UTF_8);
}
}
class WriterThread extends Thread {
final OutputStream out;
private final BlockingQueue<String> queue;
private static void writeIntLE(OutputStream out, int v) throws IOException {
out.write(v >>> 0 & 255);
out.write(v >>> 8 & 255);
out.write(v >>> 16 & 255);
out.write(v >>> 24 & 255);
}
WriterThread(OutputStream out, BlockingQueue<String> queue) {
this.out = out;
this.queue = queue;
}
@Override
public void run() {
while (!isInterrupted()) {
try {
if (queue.isEmpty())
out.flush();
sendMessage(queue.take());
} catch (IOException e) {
if (!isInterrupted())
e.printStackTrace();
break;
} catch (InterruptedException e) {
break;
}
}
}
private void sendMessage(String message) throws IOException {
byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
writeIntLE(out, bytes.length);
out.write(bytes);
}
public interface Transport {
void send(String message);
String poll(Duration timeout);
void close() throws IOException;
}
@@ -16,6 +16,8 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.regex.Pattern;
@@ -34,25 +36,20 @@ class UrlMatcher {
return new UrlMatcher(null, null);
}
static UrlMatcher forOneOf(String glob, Pattern pattern, Predicate<String> predicate) {
UrlMatcher result = UrlMatcher.any();
int conditionCount = 0;
if (glob != null) {
conditionCount += 1;
result = new UrlMatcher(glob);
static UrlMatcher forOneOf(Object object) {
if (object == null) {
return UrlMatcher.any();
}
if (pattern != null) {
conditionCount += 1;
result = new UrlMatcher(pattern);
if (object instanceof String) {
return new UrlMatcher((String) object);
}
if (predicate != null) {
conditionCount += 1;
result = new UrlMatcher(predicate);
if (object instanceof Pattern) {
return new UrlMatcher((Pattern) object);
}
if (conditionCount > 1) {
throw new IllegalArgumentException("Only one of glob, pattern and predicate can be specified");
if (object instanceof Predicate) {
return new UrlMatcher((Predicate<String>) object);
}
return result;
throw new PlaywrightException("Url must be String, Pattern or Predicate<String>, found: " + object.getClass().getTypeName());
}
UrlMatcher(String url) {
@@ -16,32 +16,40 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.microsoft.playwright.FileChooser;
import com.microsoft.playwright.Playwright;
import com.google.gson.*;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.FilePayload;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
class Utils {
// TODO: generate converter.
static <F, T> T convertViaJson(F f, Class<T> t) {
Gson gson = new Gson();
Gson gson = new GsonBuilder()
// Necessary to avoid access to private fields/classes,
// see https://github.com/microsoft/playwright-java/issues/423
.registerTypeAdapter(Optional.class, new OptionalSerializer())
.create();
String json = gson.toJson(f);
return gson.fromJson(json, t);
}
static boolean isFunctionBody(String expression) {
expression = expression.trim();
return expression.startsWith("function") ||
expression.startsWith("async ") ||
expression.contains("=>");
private static class OptionalSerializer implements JsonSerializer<Optional> {
@Override
public JsonElement serialize(Optional src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject result = new JsonObject();
if (src.isPresent()) {
result.add("value", context.serialize(src.get()));
}
return result;
}
}
static Set<Character> escapeGlobChars = new HashSet<>(Arrays.asList('/', '$', '^', '+', '.', '(', ')', '=', '!', '|'));
@@ -114,8 +122,8 @@ class Utils {
return mimeType;
}
static FileChooser.FilePayload[] toFilePayloads(Path[] files) {
List<FileChooser.FilePayload> payloads = new ArrayList<>();
static FilePayload[] toFilePayloads(Path[] files) {
List<FilePayload> payloads = new ArrayList<>();
for (Path file : files) {
byte[] buffer;
try {
@@ -123,13 +131,13 @@ class Utils {
} catch (IOException e) {
throw new PlaywrightException("Failed to read from file", e);
}
payloads.add(new FileChooser.FilePayload(file.getFileName().toString(), mimeType(file), buffer));
payloads.add(new FilePayload(file.getFileName().toString(), mimeType(file), buffer));
}
return payloads.toArray(new FileChooser.FilePayload[0]);
return payloads.toArray(new FilePayload[0]);
}
static void writeToFile(byte[] buffer, Path path) {
Path dir = path.getParent();
static void mkParentDirs(Path file) {
Path dir = file.getParent();
if (dir != null) {
if (!Files.exists(dir)) {
try {
@@ -139,13 +147,30 @@ class Utils {
}
}
}
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(path.toFile()));) {
}
static void writeToFile(byte[] buffer, Path path) {
mkParentDirs(path);
try (FileOutputStream out = new FileOutputStream(path.toFile())) {
out.write(buffer);
} catch (IOException e) {
throw new PlaywrightException("Failed to write to file", e);
}
}
static void writeToFile(InputStream inputStream, Path path) {
mkParentDirs(path);
try (FileOutputStream out = new FileOutputStream(path.toFile())) {
byte[] buf = new byte[8192];
int length;
while ((length = inputStream.read(buf)) > 0) {
out.write(buf, 0, length);
}
} catch (IOException e) {
throw new PlaywrightException("Failed to write to file", e);
}
}
static boolean isSafeCloseError(PlaywrightException exception) {
return isSafeCloseError(exception.getMessage());
}
@@ -154,4 +179,12 @@ class Utils {
return error.endsWith("Browser has been closed") || error.endsWith("Target page, context or browser has been closed");
}
static String createGuid() {
StringBuffer result = new StringBuffer();
Random random = new Random();
for (int i = 0; i < 4; i++) {
result.append(Integer.toHexString(random.nextInt()));
}
return result.toString();
}
}
@@ -16,27 +16,67 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Video;
import java.nio.file.Path;
import java.nio.file.Paths;
class VideoImpl implements Video {
import static java.util.Arrays.asList;
class VideoImpl extends LoggingSupport implements Video {
private final PageImpl page;
private Path fullPath;
private final WaitableResult<ArtifactImpl> waitableArtifact = new WaitableResult<>();
private final boolean isRemote;
VideoImpl(PageImpl page) {
this.page = page;
BrowserImpl browser = page.context().browser();
isRemote = browser != null && browser.isRemote;
}
void setRelativePath(String path) {
fullPath = page.context().videosDir.resolve(path);
void setArtifact(ArtifactImpl artifact) {
artifact.isRemote = isRemote;
waitableArtifact.complete(artifact);
}
private ArtifactImpl waitForArtifact() {
Waitable<ArtifactImpl> waitable = new WaitableRace<>(asList(waitableArtifact, (Waitable<ArtifactImpl>) page.waitableClosedOrCrashed));
return page.runUntil(() -> {}, waitable);
}
@Override
public void delete() {
withLogging("Video.delete", () -> {
try {
waitForArtifact().delete();
} catch (PlaywrightException e) {
}
});
}
@Override
public Path path() {
while (fullPath == null) {
page.connection.processOneMessage();
}
return fullPath;
return withLogging("Video.path", () -> {
if (isRemote) {
throw new PlaywrightException("Path is not available when using browserType.connect(). Use saveAs() to save a local copy.");
}
try {
return Paths.get(waitForArtifact().initializer.get("absolutePath").getAsString());
} catch (PlaywrightException e) {
throw new PlaywrightException("Page did not produce any video frames", e);
}
});
}
@Override
public void saveAs(Path path) {
withLogging("Video.saveAs", () -> {
try {
waitForArtifact().saveAs(path);
} catch (PlaywrightException e) {
throw new PlaywrightException("Page did not produce any video frames", e);
}
});
}
}
@@ -0,0 +1,62 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import java.util.function.Supplier;
import static com.microsoft.playwright.impl.Utils.createGuid;
public class WaitForEventLogger<T> implements Supplier<T> {
private final Supplier<T> supplier;
private final ChannelOwner channel;
private final String waitId;
private final String apiName;
WaitForEventLogger(ChannelOwner channelOwner, String apiName, Supplier<T> supplier) {
this.supplier = supplier;
this.channel = channelOwner;
this.apiName = apiName;
this.waitId = createGuid();
JsonObject info = new JsonObject();
info.addProperty("phase", "before");
sendWaitForEventInfo(info);
}
@Override
public T get() {
JsonObject info = new JsonObject();
info.addProperty("phase", "after");
try {
return supplier.get();
} catch (RuntimeException e) {
info.addProperty("error", e.getMessage());
throw e;
} finally {
sendWaitForEventInfo(info);
}
}
private void sendWaitForEventInfo(JsonObject info) {
info.addProperty("apiName", apiName);
info.addProperty("waitId", waitId);
JsonObject params = new JsonObject();
params.add("info", info);
channel.sendMessageAsync("waitForEventInfo", params);
}
}
@@ -16,8 +16,6 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.Event;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -17,6 +17,7 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.TimeoutError;
class WaitableResult<T> implements Waitable<T> {
private T result;
@@ -47,6 +48,9 @@ class WaitableResult<T> implements Waitable<T> {
@Override
public T get() {
if (exception != null) {
if (exception instanceof TimeoutError) {
throw new TimeoutError(exception.getMessage(), exception);
}
throw new PlaywrightException(exception.getMessage(), exception);
}
return result;
@@ -16,7 +16,7 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.TimeoutError;
class WaitableTimeout<T> implements Waitable<T> {
private final long deadline;
@@ -38,7 +38,7 @@ class WaitableTimeout<T> implements Waitable<T> {
if (timeoutStr.endsWith(".0")) {
timeoutStr = timeoutStr.substring(0, timeoutStr.length() - 2);
}
throw new PlaywrightException("Timeout " + timeoutStr + "ms exceeded");
throw new TimeoutError("Timeout " + timeoutStr + "ms exceeded");
}
@Override
@@ -17,11 +17,12 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.WebSocket;
import com.microsoft.playwright.WebSocketFrame;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.function.Consumer;
@@ -54,22 +55,22 @@ class WebSocketImpl extends ChannelOwner implements WebSocket {
}
@Override
public void onFrameReceived(Consumer<FrameData> handler) {
public void onFrameReceived(Consumer<WebSocketFrame> handler) {
listeners.add(EventType.FRAMERECEIVED, handler);
}
@Override
public void offFrameReceived(Consumer<FrameData> handler) {
public void offFrameReceived(Consumer<WebSocketFrame> handler) {
listeners.remove(EventType.FRAMERECEIVED, handler);
}
@Override
public void onFrameSent(Consumer<FrameData> handler) {
public void onFrameSent(Consumer<WebSocketFrame> handler) {
listeners.add(EventType.FRAMESENT, handler);
}
@Override
public void offFrameSent(Consumer<FrameData> handler) {
public void offFrameSent(Consumer<WebSocketFrame> handler) {
listeners.remove(EventType.FRAMESENT, handler);
}
@@ -84,7 +85,11 @@ class WebSocketImpl extends ChannelOwner implements WebSocket {
}
@Override
public FrameData waitForFrameReceived(Runnable code, WaitForFrameReceivedOptions options) {
public WebSocketFrame waitForFrameReceived(WaitForFrameReceivedOptions options, Runnable code) {
return withWaitLogging("WebSocket.waitForFrameReceived", () -> waitForFrameReceivedImpl(options, code));
}
private WebSocketFrame waitForFrameReceivedImpl(WaitForFrameReceivedOptions options, Runnable code) {
if (options == null) {
options = new WaitForFrameReceivedOptions();
}
@@ -92,21 +97,17 @@ class WebSocketImpl extends ChannelOwner implements WebSocket {
}
@Override
public FrameData waitForFrameSent(Runnable code, WaitForFrameSentOptions options) {
public WebSocketFrame waitForFrameSent(WaitForFrameSentOptions options, Runnable code) {
return withWaitLogging("WebSocket.waitForFrameSent", () -> waitForFrameSentImpl(options, code));
}
private WebSocketFrame waitForFrameSentImpl(WaitForFrameSentOptions options, Runnable code) {
if (options == null) {
options = new WaitForFrameSentOptions();
}
return waitForEventWithTimeout(EventType.FRAMESENT, code, options.timeout);
}
@Override
public String waitForSocketError(Runnable code, WaitForSocketErrorOptions options) {
if (options == null) {
options = new WaitForSocketErrorOptions();
}
return waitForEventWithTimeout(EventType.SOCKETERROR, code, options.timeout);
}
@Override
public boolean isClosed() {
return isClosed;
@@ -149,10 +150,10 @@ class WebSocketImpl extends ChannelOwner implements WebSocket {
return runUntil(code, new WaitableRace<>(waitables));
}
private static class FrameDataImpl implements FrameData {
private static class WebSocketFrameImpl implements WebSocketFrame {
private final byte[] bytes;
FrameDataImpl(String payload, boolean isBase64) {
WebSocketFrameImpl(String payload, boolean isBase64) {
if (isBase64) {
bytes = Base64.getDecoder().decode(payload);
} else {
@@ -161,7 +162,7 @@ class WebSocketImpl extends ChannelOwner implements WebSocket {
}
@Override
public byte[] body() {
public byte[] binary() {
return bytes;
}
@@ -175,15 +176,15 @@ class WebSocketImpl extends ChannelOwner implements WebSocket {
void handleEvent(String event, JsonObject parameters) {
switch (event) {
case "frameSent": {
FrameDataImpl frameData = new FrameDataImpl(
WebSocketFrameImpl WebSocketFrame = new WebSocketFrameImpl(
parameters.get("data").getAsString(), parameters.get("opcode").getAsInt() == 2);
listeners.notify(EventType.FRAMESENT, frameData);
listeners.notify(EventType.FRAMESENT, WebSocketFrame);
break;
}
case "frameReceived": {
FrameDataImpl frameData = new FrameDataImpl(
WebSocketFrameImpl WebSocketFrame = new WebSocketFrameImpl(
parameters.get("data").getAsString(), parameters.get("opcode").getAsInt() == 2);
listeners.notify(EventType.FRAMERECEIVED, frameData);
listeners.notify(EventType.FRAMERECEIVED, WebSocketFrame);
break;
}
case "socketError": {
@@ -0,0 +1,129 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
class WebSocketTransport implements Transport {
private final BlockingQueue<String> incoming = new LinkedBlockingQueue<>();
private final ClientConnection clientConnection;
private final Duration slowMo;
private boolean isClosed;
private volatile Exception lastError;
ListenerCollection<EventType> listeners = new ListenerCollection<>();
private enum EventType { CLOSE }
private class ClientConnection extends WebSocketClient {
ClientConnection(URI serverUri) {
super(serverUri);
}
@Override
public void onOpen(ServerHandshake handshakedata) {
}
@Override
public void onMessage(String message) {
incoming.add(message);
}
@Override
public void onClose(int code, String reason, boolean remote) {
}
@Override
public void onError(Exception ex) {
lastError = ex;
}
}
WebSocketTransport(URI uri, Map<String, String> headers, Duration timeout, Duration slowMo) {
clientConnection = new ClientConnection(uri);
for (Map.Entry<String, String> entry : headers.entrySet()) {
clientConnection.addHeader(entry.getKey(), entry.getValue());
}
try {
if (!clientConnection.connectBlocking(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
throw new PlaywrightException("Failed to connect", lastError);
}
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to connect", e);
}
this.slowMo = slowMo;
}
@Override
public void send(String message) {
checkIfClosed();
clientConnection.send(message);
}
@Override
public String poll(Duration timeout) {
checkIfClosed();
try {
String message = incoming.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
if (slowMo != null && message != null) {
Thread.sleep(slowMo.toMillis());
}
return message;
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to read message", e);
}
}
@Override
public void close() throws IOException {
if (isClosed) {
return;
}
isClosed = true;
clientConnection.close();
}
void onClose(Consumer<WebSocketTransport> handler) {
listeners.add(EventType.CLOSE, handler);
}
void offClose(Consumer<WebSocketTransport> handler) {
listeners.remove(EventType.CLOSE, handler);
}
private void checkIfClosed() {
if (isClosed) {
throw new PlaywrightException("Playwright connection closed");
}
if (clientConnection.isClosed()) {
isClosed = true;
listeners.notify(EventType.CLOSE, this);
throw new PlaywrightException("Playwright connection closed");
}
}
}
@@ -16,17 +16,16 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.JSHandle;
import com.microsoft.playwright.Worker;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.isFunctionBody;
class WorkerImpl extends ChannelOwner implements Worker {
private final ListenerCollection<EventType> listeners = new ListenerCollection<>();
@@ -59,7 +58,11 @@ class WorkerImpl extends ChannelOwner implements Worker {
}
@Override
public Worker waitForClose(Runnable code, WaitForCloseOptions options) {
public Worker waitForClose(WaitForCloseOptions options, Runnable code) {
return withWaitLogging("Worker.waitForClose", () -> waitForCloseImpl(options, code));
}
private Worker waitForCloseImpl(WaitForCloseOptions options, Runnable code) {
if (options == null) {
options = new WaitForCloseOptions();
}
@@ -71,7 +74,6 @@ class WorkerImpl extends ChannelOwner implements Worker {
return withLogging("Worker.evaluate", () -> {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpression", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
@@ -84,7 +86,6 @@ class WorkerImpl extends ChannelOwner implements Worker {
return withLogging("Worker.evaluateHandle", () -> {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpressionHandle", params);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("handle").get("guid").getAsString());
@@ -0,0 +1,31 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.Page;
public interface BindingCallback {
interface Source {
BrowserContext context();
Page page();
Frame frame();
}
Object call(Source source, Object... args);
}
@@ -0,0 +1,37 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class BoundingBox {
/**
* the x coordinate of the element in pixels.
*/
public double x;
/**
* the y coordinate of the element in pixels.
*/
public double y;
/**
* the width of the element in pixels.
*/
public double width;
/**
* the height of the element in pixels.
*/
public double height;
}
@@ -0,0 +1,30 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
@Deprecated
public enum BrowserChannel {
CHROME,
CHROME_BETA,
CHROME_DEV,
CHROME_CANARY,
MSEDGE,
MSEDGE_BETA,
MSEDGE_DEV,
MSEDGE_CANARY,
@Deprecated FIREFOX_STABLE
}
@@ -0,0 +1,43 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class Clip {
/**
* x-coordinate of top-left corner of clip area
*/
public double x;
/**
* y-coordinate of top-left corner of clip area
*/
public double y;
/**
* width of clipping area
*/
public double width;
/**
* height of clipping area
*/
public double height;
public Clip(double x, double y, double width, double height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
}
@@ -0,0 +1,23 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public enum ColorScheme {
LIGHT,
DARK,
NO_PREFERENCE
}
@@ -0,0 +1,83 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class Cookie {
public String name;
public String value;
/**
* either url or domain / path are required. Optional.
*/
public String url;
/**
* either url or domain / path are required Optional.
*/
public String domain;
/**
* either url or domain / path are required Optional.
*/
public String path;
/**
* Unix time in seconds. Optional.
*/
public Double expires;
/**
* Optional.
*/
public Boolean httpOnly;
/**
* Optional.
*/
public Boolean secure;
/**
* Optional.
*/
public SameSiteAttribute sameSite;
public Cookie(String name, String value) {
this.name = name;
this.value = value;
}
public Cookie setUrl(String url) {
this.url = url;
return this;
}
public Cookie setDomain(String domain) {
this.domain = domain;
return this;
}
public Cookie setPath(String path) {
this.path = path;
return this;
}
public Cookie setExpires(double expires) {
this.expires = expires;
return this;
}
public Cookie setHttpOnly(boolean httpOnly) {
this.httpOnly = httpOnly;
return this;
}
public Cookie setSecure(boolean secure) {
this.secure = secure;
return this;
}
public Cookie setSameSite(SameSiteAttribute sameSite) {
this.sameSite = sameSite;
return this;
}
}

Some files were not shown because too many files have changed in this diff Show More