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

Compare commits

...

142 Commits

Author SHA1 Message Date
Yury Semikhatsky c34a35d6a7 chore: mark 1.26.0 (#1072) (#1074) 2022-09-20 14:34:23 -07:00
Yury Semikhatsky 6ef0cc29dd chore: roll driver to 1.26.0 (#1071) 2022-09-20 14:09:20 -07:00
pranesh517 8dfe959cce Added example in README.md for Gradle project (#1064) 2022-09-16 09:08:49 -07:00
Yury Semikhatsky 5979077403 fix: unflake TestPageRequestFallback.shouldChainOnce on win (#1058) 2022-09-12 16:44:14 -07:00
Yury Semikhatsky 020618d83d feat: roll beta driver (#1057) 2022-09-12 15:34:50 -07:00
Yury Semikhatsky 6bef6bb4a7 feat(docker): bump jdk version to 17 (#1052) 2022-09-08 15:03:48 -07:00
Yury Semikhatsky 734d56f5d5 test: make installation tests pass in docker (#1051) 2022-09-08 13:22:42 -07:00
Yury Semikhatsky 6e66861db3 feat: roll driver, support parameter for isEnabled/isEditable (#1049) 2022-09-08 13:05:33 -07:00
Yury Semikhatsky cf73a8d525 fix: serialize LocalDateTime parameters as Date (#1048) 2022-09-07 16:41:37 -07:00
Yury Semikhatsky 4a0ff6ef5c fix: log url for waitFor* methods (#1047) 2022-09-07 15:10:38 -07:00
Meir Blachman 398c979378 tests: remove duplicate cookies test (#1046) 2022-09-07 09:21:15 -07:00
Meir Blachman 39be690062 tests: unflake TestBrowserContextAddCookies.shouldRoundtripCookie (#1045) 2022-09-06 11:01:17 -07:00
Meir Blachman b5b0a983bd test: use assertThrows instead of try-catch - remaining tests (#1042) 2022-09-01 16:26:48 -07:00
Yury Semikhatsky 6b842e0d50 chore: roll driver, support new hasAttribute(name) (#1041) 2022-08-31 12:37:31 -07:00
Meir Blachman 321673407a tests: use assertThrows instead of try-catch (#1038) 2022-08-30 14:47:33 -07:00
Yury Semikhatsky 4d278c391e feat: allow using preinstalled node.js (#1030) 2022-08-15 20:51:07 -07:00
Yury Semikhatsky 5c47cfb1d5 chore: roll driver to 1.26.0-alpha-1660591492000 (#1031) 2022-08-15 13:07:43 -07:00
Yury Semikhatsky f036b3d38a chore: bump dev version to 1.26.0-SNAPSHOT (#1028) 2022-08-12 13:55:06 -07:00
Yury Semikhatsky 385131e51b chore: roll driver to 1.25.0 (#1026) 2022-08-11 13:40:57 -07:00
Yury Semikhatsky 6cb9f60988 chore: roll driver to 1.25.0-alpha-1659998098000 (#1025) 2022-08-09 14:07:53 -07:00
Yury Semikhatsky aef9badd64 feat: assertions default timeout (#1023) 2022-08-04 17:29:02 -07:00
Yury Semikhatsky 64f7a059af fix: prevent video.saveAs() from hanging (#1020) 2022-08-01 15:01:43 -07:00
Yury Semikhatsky 436fc12609 feat: roll driver to 1.25.0-alpha-jul-28-2022 (#1015) 2022-07-28 14:23:59 -07:00
Yury Semikhatsky 560575a9b5 test: unflake wheel test (#1014) 2022-07-27 16:39:47 -07:00
Yury Semikhatsky 0afaf6c561 fix: no split packages for compatibility with modules (#1013) 2022-07-27 13:32:25 -07:00
Yury Semikhatsky 202371b5d7 test: locator has with unicode symbols (#1012) 2022-07-26 14:12:47 -07:00
Yury Semikhatsky 2093bba554 fix: do not download browsers if selenium url is set (#1008) 2022-07-25 16:20:38 -07:00
Yury Semikhatsky 77538dcf7f chore: bump dev version to 1.25 (#1007) 2022-07-25 15:56:22 -07:00
Yury Semikhatsky f759222755 fix: do not escape html symbols when serializing strings to json (#1005) 2022-07-25 15:19:48 -07:00
Max Schmitt d87c6b24ca chore(roll): roll 1.24.0 add regex, date, url serializers (#1003) 2022-07-25 22:53:53 +02:00
Max Schmitt 41355bd059 chore: add 'gpg' package to Docker images (#1004) 2022-07-25 12:49:19 +02:00
Yury Semikhatsky e372513fa4 test: unflake playwrightDriverAlternativeImpl (#986) 2022-07-08 16:22:48 -07:00
Yury Semikhatsky b8e1e1d935 chore: remove isolated tests (#984) 2022-06-30 14:09:09 -07:00
Yury Semikhatsky a0745735d9 feat: roll driver to 1.23.1-beta, implement routeFromHar.update (#982) 2022-06-30 12:04:22 -07:00
Yury Semikhatsky b90de26d23 feat: accept PLAYWRIGHT_JAVA_SRC in Playwright.create (#980) 2022-06-29 15:27:11 -07:00
Yury Semikhatsky adfdf92eaa test: use only 127.0.0.1 for loopback (#978) 2022-06-28 17:26:33 -07:00
Yury Semikhatsky 60cb6ea7b3 chore: remove extractZip logs from tests (#979) 2022-06-28 17:26:22 -07:00
Yury Semikhatsky 44cb76d92c chore: only log har when pw:api is enabled (#977) 2022-06-28 16:23:04 -07:00
Yury Semikhatsky 9fac877892 test: unflake TestWebSocket.shouldEmitError (#976) 2022-06-28 16:09:17 -07:00
Yury Semikhatsky ec8fb9f191 fix: use same eol setting in gitattributes as upstream (#973) 2022-06-28 12:29:27 -07:00
Yury Semikhatsky 844d1665b2 chore: set dev version to 1.24.0-SNAPSHOT (#972) 2022-06-28 09:14:57 -07:00
Yury Semikhatsky 484a255ec7 feat: support ignoreCase option (#969) 2022-06-28 08:43:20 -07:00
Yury Semikhatsky 3f60144e0f chore: update driver to 1.23.0 (#968) 2022-06-27 15:33:44 -07:00
Yury Semikhatsky 8004e5d0ff test: 403 response still has postBody (#967) 2022-06-27 14:13:40 -07:00
Yury Semikhatsky 3604aab710 chore: store LocalUtils on Connection (#963) 2022-06-24 16:25:58 -07:00
Yury Semikhatsky 2fdb89c94e fix: match against updated url (#962) 2022-06-24 15:13:41 -07:00
Yury Semikhatsky 4fee61a655 test: unflake TestHar.shouldAttachContent (#961) 2022-06-24 14:50:24 -07:00
Yury Semikhatsky efb281e016 feat: implement routeFromHAR (#960) 2022-06-24 13:44:57 -07:00
Yury Semikhatsky fdec32c650 chore: simplify handler result (#959) 2022-06-23 18:59:04 -07:00
Yury Semikhatsky 7e285ffe44 feat: route.fallback with overrides (#958) 2022-06-23 16:48:32 -07:00
Yury Semikhatsky edf0e45fb4 feat: roll to 1.23.0-beta-1655926399000 (#956) 2022-06-23 10:11:38 -07:00
Yury Semikhatsky c8eb4f9eeb feat: route chaining (#950) 2022-06-11 11:24:30 -07:00
dependabot[bot] e4ec9b8dbe chore(deps): bump gson from 2.8.6 to 2.8.9 in /tools/api-generator (#937) 2022-06-11 09:37:35 -07:00
dependabot[bot] ef13ab86b8 chore(deps): bump gson from 2.8.6 to 2.8.9 in /tools/test-local-installation (#938) 2022-06-11 09:37:09 -07:00
dependabot[bot] a48fef6b01 chore(deps): bump gson from 2.8.6 to 2.8.9 (#936) 2022-06-11 09:36:35 -07:00
Yury Semikhatsky 1c1f3d43ac docs: update docs link (#945) 2022-06-06 09:42:53 -07:00
Max Schmitt 8f59cd73f5 chore: fix examples which could not be executed (#941) 2022-05-31 09:33:33 +02:00
Yury Semikhatsky e04ef2132c test: make install test not skip browser downloads (#931) 2022-05-13 15:31:37 -07:00
Yury Semikhatsky 34d23a833e devops: bump version to 1.23.0-SNAPSHOT (#930) 2022-05-13 11:14:15 -07:00
Yury Semikhatsky 15eefc54af feat: roll beta driver, remove layout selectors (#926) 2022-05-13 00:49:59 -07:00
Yury Semikhatsky abf245ccc7 feat: implement Locator.filter (#925) 2022-05-12 09:39:10 -07:00
Gabriel Gavrilov 10592ce5c7 Added Selectors and Keyboard Manipulation Example (#920) 2022-05-11 09:20:51 -07:00
Yury Semikhatsky ef7f50c48a feat: roll driver, implement locator filters (#921) 2022-05-10 10:43:30 -07:00
Andrey Lushnikov 473b1ce794 devops: mark docker image as playwright official (#889) 2022-05-06 19:07:44 -07:00
Yury Semikhatsky 98ecb7e0a0 test: unflake TestInstall.shouldThrowWhenBrowserPathIsInvalid (#916) 2022-05-03 13:50:20 -07:00
Yury Semikhatsky 04eb228813 devops: add workflow triggering internal tests (#915) 2022-05-02 18:01:40 -07:00
Max Schmitt 298e01ee80 chore: add arm64 Docker image (#890) 2022-05-02 14:46:05 +01:00
Yury Semikhatsky b6b54af13c Revert "Revert "Add Linux/arm64 support (#883)" (#892)" (#905)
This reverts commit 536af6b3d8.
2022-04-27 17:24:12 -07:00
Yury Semikhatsky b8d2ccae08 fix: respect isChecked options (#911) 2022-04-26 17:01:50 -07:00
Yury Semikhatsky 59e7c0cc94 fix: convert file path to absolute in setInputFiles (#903) 2022-04-15 12:11:52 -07:00
Yury Semikhatsky 54d0366b9e fix: get docker version from pom.xml (#899) (#901) 2022-04-12 12:15:06 -07:00
Andrey Lushnikov 9845a05544 devops: fix docker publish workflow (#898) 2022-04-12 11:40:21 -07:00
Yury Semikhatsky 41fd9a6f75 chore: bump dev version to 1.22 (#897) 2022-04-12 10:46:31 -07:00
Yury Semikhatsky 483cf0d473 chore: revert some inadvertent changes (#894) 2022-04-12 08:55:05 -07:00
Yury Semikhatsky 1681c410dd feat: large file uploads (#891) 2022-04-12 08:33:40 -07:00
Yury Semikhatsky 9f6860539a fix: links to documentation (#893) 2022-04-11 19:20:29 -07:00
Yury Semikhatsky 536af6b3d8 Revert "Add Linux/arm64 support (#883)" (#892) 2022-04-11 17:25:13 -07:00
Yury Semikhatsky 8ce193d144 chore: roll to 1.21 beta driver (#888) 2022-04-11 13:04:57 -07:00
Michael S. Fischer 7eddd2d2b2 Add Linux/arm64 support (#883) 2022-04-06 18:42:39 -07:00
uchagani 447578c582 fix(driver): set driver instance to null if an exception is thrown du… (#879) 2022-04-04 12:25:37 -07:00
Leonard Brünings 58013adfac fix(tracing): support explicit StartOptions().setSources(false) (#866) 2022-03-30 10:42:54 -07:00
Alex (Huy Tran) 43d12a7662 feat: enable loading alternative driver via system properties (#873) 2022-03-30 10:42:18 -07:00
Yury Semikhatsky 1deccbb55d chore: add logging to driver installation (#865) 2022-03-25 12:02:47 -07:00
Yury Semikhatsky 1b2d33402e docs: add driver update instructions 2022-03-18 09:00:34 -07:00
Yury Semikhatsky d315e7b5bf fix: docker publishing (#851) (#852) 2022-03-14 20:31:16 -07:00
Yury Semikhatsky 7dc22aa08a devops: do not cache mvn packages (#848) 2022-03-14 19:01:28 -07:00
Yury Semikhatsky 5b0ef8b7bf chore: update current version to 1.21.0-SNAPSHOT (#847) 2022-03-14 17:16:37 -07:00
Yury Semikhatsky 43ba37817b fix: send x-playwright-browser (#844) 2022-03-14 15:31:19 -07:00
Yury Semikhatsky 4916ba22af chore: roll driver (#838) 2022-03-14 12:04:20 -07:00
uchagani 94f72694f1 chore: move chmod command to after browsers are installed and consolidate co… (#835)
Co-authored-by: Max Schmitt <max@schmitt.mx>
2022-03-09 18:22:00 +01:00
Max Schmitt be167a161b devops: trigger Docker tests on Dockerfile changes (#839) 2022-03-09 09:08:36 -08:00
Yury Semikhatsky 187d2ad6c6 devops: restore test docker trigger (#840) 2022-03-09 09:07:07 -08:00
Max Schmitt 68a7dbc1e3 devops: publish no arm Docker for now (#837) 2022-03-08 22:01:54 +01:00
Yury Semikhatsky 1153d473fb feat: roll driver to 3/3/22 (#832) 2022-03-04 08:31:13 -08:00
Max Schmitt 00d53bd1ea devops: fix Docker publishing 2 2022-03-04 00:05:14 +01:00
Max Schmitt d423733d9d devops: fix Docker publishing 2022-03-04 00:00:16 +01:00
Max Schmitt e127abb68e chore: align Docker with upstream Docker infra (#831) 2022-03-03 22:55:21 +01:00
Yury Semikhatsky 5ee8f23380 fix(docs): drop trailing slash from link path (#818) 2022-02-17 15:26:10 -08:00
Yury Semikhatsky b448a1789a chore: roll 1.19.0 driver (#813) 2022-02-15 14:06:06 -08:00
Yury Semikhatsky 79b2c70513 fix: catch up with recent upstream changes (#812) 2022-02-15 08:39:27 -08:00
Max Schmitt d476d0a98c devops: trigger Java publish workflow on release (#811) 2022-02-14 22:44:03 +01:00
Yury Semikhatsky 2122c5690a fix: support multiple source dirs (#809) 2022-02-14 09:52:29 -08:00
Yury Semikhatsky 3119102b10 fix(assertions): include expected/actual values into error message (#808) 2022-02-11 13:02:27 -08:00
Yury Semikhatsky 838e7a40b3 feat: roll driver, support "has" option (#805) 2022-02-10 14:11:39 -08:00
Yury Semikhatsky ea6ede4670 fix: skip syntetic fields when converting options (#804) 2022-02-09 11:44:46 -08:00
Yury Semikhatsky 3cdefc2931 fix: normalize whitespaces in hasTitle (#803) 2022-02-09 09:54:46 -08:00
Yury Semikhatsky 119700a678 feat: add response param to route.fulfill (#797) 2022-01-28 08:38:24 -08:00
Yury Semikhatsky 17a4143a83 fix: throw if route is handled twice (#796) 2022-01-27 14:02:11 -08:00
Max Schmitt c03f4a9384 fix: CLI don't download browers automatically and set env accordingly (#790) 2022-01-24 12:54:49 -08:00
Andrey Lushnikov 85b671328e chore: roll to latest 1.18 (#785) 2022-01-19 16:42:07 -08:00
Yury Semikhatsky 11f898ca7f test: route.resume does not throw if page is closed (#781) 2022-01-19 13:19:33 -08:00
Yury Semikhatsky 2aef5c6742 fix: hide internal call from inspector log (#783) 2022-01-19 13:18:47 -08:00
Yury Semikhatsky 897d441c02 fix: include var name into tracing error message (#779) 2022-01-19 10:42:47 -08:00
Andrey Lushnikov f4c69faad3 chore: cut v1.18.0 (#777) 2022-01-19 08:42:53 -08:00
Andrey Lushnikov 6b30c0b3d2 fix: pass required env variables for new driver (#774)
Fixes #772
2022-01-19 05:15:37 -08:00
Yury Semikhatsky a006d51872 chore: merge assertions module into playwright (#773) 2022-01-18 12:22:15 -08:00
Yury Semikhatsky f411bf4194 chore: roll driver to 01/13/22 (#771) 2022-01-13 23:55:50 -08:00
Yury Semikhatsky 25a0927056 feat: roll driver, comoute count in util world (#769) 2022-01-10 13:20:20 -08:00
Yury Semikhatsky e9b379f5ed test: unroute predicate (#768) 2022-01-10 12:41:10 -08:00
Yury Semikhatsky 0c1d491c14 feat: roll driver, implement APIResponse assertions (#764) 2022-01-06 17:16:19 -08:00
Yury Semikhatsky b09b9aecfb feat: custom temp dir for driver via playwright.driver.tmpdir property (#763) 2022-01-06 14:36:39 -08:00
Yury Semikhatsky e926c1ae82 feat(tracing): collect sources for remote tracing (#755) 2021-12-20 12:20:57 -08:00
Yury Semikhatsky 86e91590cb feat: include source files in trace (#754) 2021-12-17 16:58:00 -08:00
Yury Semikhatsky 963afac983 feat: make --version return java package version (#748) 2021-12-16 14:38:15 -08:00
Yury Semikhatsky c230bed27e feat: roll driver, implement hasText locator option (#747) 2021-12-16 13:03:26 -08:00
Yury Semikhatsky a4348f250f feat: inclide Implementation-Version into manifest (#743) 2021-12-09 17:17:52 -08:00
Yury Semikhatsky 52d31a173e devops: run tests on Java 17 (#740) 2021-12-08 09:34:52 -08:00
Yury Semikhatsky cf534a0586 feat: roll to 1.18.0-alpha-nov-29-2021 (#725) 2021-11-30 15:05:05 -08:00
Yury Semikhatsky fa3bdebcbb chore: remove unused script (#724) 2021-11-30 12:21:29 -08:00
Bruno Borges 0467aa7d4a chore: No more need for jbang-catalog.json (#721) 2021-11-29 08:45:59 -08:00
codeboyzhou 8d84afccec fix(scripts): install_local_driver.sh can't download driver successfully (#722) 2021-11-29 08:41:12 -08:00
Yury Semikhatsky 7d1026ea9c feat: request API with shared RequestOptions and FormData (#718) 2021-11-19 17:35:59 -08:00
Yury Semikhatsky 0076b8f8a9 chore: bump version in examples (#716) 2021-11-19 08:58:07 -08:00
Yury Semikhatsky 4d379726e5 test: fix WARNING: sendResponseHeaders:... (#713) 2021-11-18 17:18:55 -08:00
Yury Semikhatsky b4100a4d68 chore: roll driver to 1.18.0-alpha-1637257604000 (#711) 2021-11-18 16:52:36 -08:00
Andrey Lushnikov 3b4d8dc955 chore: proper driver URL detection (#709)
Since Nov 16, 2021, we have the following conventions:

- Drivers published from tip-of-tree have an `-alpha` version and are
  stored at `/next` subfolder on Azure Storage.
- Drivers auto-published for each commit of the release branch have a `-beta` version and are
  stored at `/next` subfolder on Azure Storage.
- Drivers published due to a release might have `-rc` as part of the
  version, and are stored in root subfolder on Azure Storage.

We no longer have driver versions that include "next" as part of the
version. I kept it for backwards compatibility.
2021-11-17 18:32:01 -08:00
Yury Semikhatsky d71795801c chore: roll driver to 1.18.0-alpha-1637178126000 (#708) 2021-11-17 14:04:33 -08:00
Yury Semikhatsky 16c7b7f25e chore: use try resource in auth tests (#707) 2021-11-17 11:52:24 -08:00
Yury Semikhatsky f739ae28e8 chore: convert using reflection instead of Gson (#705) 2021-11-16 22:33:38 -08:00
Yury Semikhatsky 9f8ff0e7a1 feat: APIRequest & co (#704) 2021-11-15 17:30:09 -08:00
Yury Semikhatsky f782ca6339 chore: bump dev version to 1.18 (#703) 2021-11-15 15:10:23 -08:00
Yury Semikhatsky 90ccaa195f devops: clean old build before updating readme (#702) 2021-11-15 15:08:08 -08:00
231 changed files with 14235 additions and 4216 deletions
+4 -2
View File
@@ -1,3 +1,5 @@
# text files must be lf for golden file tests to work
*.txt eol=lf
*.json eol=lf
* text=auto eol=lf
# make project show as TS on GitHub
*.js linguist-detectable=false
+2 -1
View File
@@ -1,6 +1,7 @@
name: Publish
on:
workflow_dispatch:
release:
types: [published]
push:
branches:
- main
+6 -7
View File
@@ -17,10 +17,9 @@ jobs:
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 }}
- name: Set up Docker QEMU for arm64 docker builds
uses: docker/setup-qemu-action@v1
with:
platforms: arm64
- name: publish docker canary
run: ./utils/docker/publish_docker.sh canary
+13 -16
View File
@@ -3,6 +3,11 @@ on:
release:
types: [published]
workflow_dispatch:
inputs:
is_release:
required: true
type: boolean
description: "Is this a release image?"
branches:
- release-*
jobs:
@@ -17,20 +22,12 @@ jobs:
login-server: playwright.azurecr.io
username: playwright
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up Docker QEMU for arm64 docker builds
uses: docker/setup-qemu-action@v1
with:
platforms: arm64
- 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
- run: ./utils/docker/publish_docker.sh stable
if: (github.event_name != 'workflow_dispatch' && !github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && github.event.inputs.is_release == 'true')
- run: ./utils/docker/publish_docker.sh canary
if: (github.event_name != 'workflow_dispatch' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && github.event.inputs.is_release != 'true')
+50 -23
View File
@@ -19,32 +19,31 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1.5.0
- uses: microsoft/playwright-github-action@v1
- name: Set up JDK 1.8
uses: actions/setup-java@v1
uses: actions/setup-java@v2
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
distribution: zulu
java-version: 8
- 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: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress --fail-at-end
env:
BROWSER: ${{ matrix.browser }}
- name: Run tracing tests w/ sources
run: mvn test --no-transfer-progress --fail-at-end -D test=*TestTracing*
env:
BROWSER: ${{ matrix.browser }}
PLAYWRIGHT_JAVA_SRC: src/test/java
- 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
@@ -64,28 +63,56 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1.5.0
- 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
uses: actions/setup-java@v2
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
distribution: zulu
java-version: 8
- 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: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress --fail-at-end
env:
BROWSER: chromium
BROWSER_CHANNEL: ${{ matrix.browser-channel }}
Java_17:
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
distribution: adopt
java-version: 17
- name: Download drivers
shell: bash
run: scripts/download_driver_for_all_platforms.sh
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress --fail-at-end
env:
BROWSER: ${{ matrix.browser }}
- name: Test Spring Boot Starter
shell: bash
env:
BROWSER: ${{ matrix.browser }}
run: |
cd tools/test-spring-boot-starter
mvn package -D skipTests --no-transfer-progress
java -jar target/test-spring-boot*.jar
+3
View File
@@ -26,3 +26,6 @@ jobs:
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
- name: Test CLI version
shell: bash
run: tools/test-cli-version/test.sh
+3 -3
View File
@@ -3,14 +3,14 @@ on:
push:
paths:
- '.github/workflows/test_docker.yml'
- 'Dockerfile*'
- '**/Dockerfile*'
branches:
- main
- release-*
pull_request:
paths:
- .github/workflows/test_docker.yml
- Dockerfile.*
- '**/Dockerfile*'
- scripts/CLI_VERSION
- '**/pom.xml'
branches:
@@ -23,7 +23,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Build Docker image
run: docker build -t playwright-java:localbuild-focal -f Dockerfile.focal .
run: bash utils/docker/build.sh --amd64 focal playwright-java:localbuild-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)"
@@ -0,0 +1,21 @@
name: "Internal Tests"
on:
push:
branches:
- main
- release-*
jobs:
trigger:
name: "trigger"
runs-on: ubuntu-20.04
steps:
- run: |
curl -X POST \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${GH_TOKEN}" \
--data "{\"event_type\": \"playwright_tests_java\", \"client_payload\": {\"ref\": \"${GITHUB_SHA}\"}}" \
https://api.github.com/repos/microsoft/playwright-internal/dispatches
env:
GH_TOKEN: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
+1 -7
View File
@@ -20,13 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1.5.0
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- uses: microsoft/playwright-github-action@v1
- name: Download drivers
run: scripts/download_driver_for_all_platforms.sh
- name: Regenerate APIs
+14 -1
View File
@@ -8,7 +8,7 @@ Install git, Java JDK (version >= 8), Maven (tested with version 3.6.3), on Ubun
just run the following command:
```sh
sudo apt-get install git openjdk-11-jdk maven
sudo apt-get install git openjdk-11-jdk maven unzip
```
### Getting the Code
@@ -49,6 +49,19 @@ Java interfaces for the current driver run the following commands:
./scripts/generate_api.sh
```
#### Updating driver version
Driver version is read from [scripts/CLI_VERSION](https://github.com/microsoft/playwright-java/blob/main/scripts/CLI_VERSION) and can be found in the upstream [GHA build](https://github.com/microsoft/playwright/actions/workflows/publish_canary.yml) logs. To update the driver to a particular version run the following commands:
```bash
cat > scripts/CLI_VERSION
<paste new version>
^D
./scripts/download_driver_for_all_platforms.sh -f
./scripts/generate_api.sh
./scripts/update_readme.sh
```
### Code Style
- We try to follow [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)
-32
View File
@@ -1,32 +0,0 @@
FROM ubuntu:focal
# === INSTALL JDK and Maven ===
RUN apt-get update && apt-get install -y --no-install-recommends \
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 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 && \
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
+13 -5
View File
@@ -11,9 +11,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->98.0.4695.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->15.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->94.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->106.0.5249.30<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->16.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->104.0<!-- 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/java/docs/next/intro/#system-requirements) for details.
@@ -43,10 +43,18 @@ To run Playwright simply add following dependency to your Maven project:
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.16.0</version>
<version>1.17.0</version>
</dependency>
```
To run Playwright using Gradle add following dependency to your build.gradle file:
```json lines
dependencies {
implementation group: 'com.microsoft.playwright', name: 'playwright', version: '1.25.0'
}
```
#### 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 multiple Playwright instances each on its own thread.
@@ -173,7 +181,7 @@ public class InterceptNetworkRequests {
## Documentation
Check out our [new documentation site](https://playwright.dev/java)!.
Check out our official [documentation site](https://playwright.dev/java).
You can also browse [javadoc online](https://www.javadoc.io/doc/com.microsoft.playwright/playwright/latest/index.html).
-65
View File
@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.17.0-SNAPSHOT</version>
</parent>
<artifactId>assertions</artifactId>
<name>Playwright - Assertions</name>
<description>
This module provides Playwright assertions that will wait until the expected condition is met.
</description>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<subpackages>com.microsoft.playwright.assertions</subpackages>
<excludePackageNames>com.microsoft.playwright.impl</excludePackageNames>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.opentest4j</groupId>
<artifactId>opentest4j</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -1,826 +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 com.microsoft.playwright.assertions.LocatorAssertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;
import java.util.regex.Pattern;
import static com.microsoft.playwright.Utils.mapOf;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
public class TestLocatorAssertions extends TestBase {
@Test
void containsTextWRegexPass() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).containsText(Pattern.compile("ex"));
// Should not normalize whitespace.
assertThat(locator).containsText(Pattern.compile("ext cont"));
}
@Test
void containsTextWRegexCaseInsensitivePass() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).containsText(Pattern.compile("text", Pattern.CASE_INSENSITIVE));
}
@Test
void containsTextWRegexMultilinePass() {
page.setContent("<div id=node>Text \nContent</div>");
Locator locator = page.locator("#node");
assertThat(locator).containsText(Pattern.compile("^Content", Pattern.MULTILINE));
}
@Test
void containsTextWRegexDotAllPass() {
page.setContent("<div id=node>foo\nbar</div>");
Locator locator = page.locator("#node");
assertThat(locator).containsText(Pattern.compile("foo.bar", Pattern.DOTALL));
}
@Test
void containsTextWRegexFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).containsText(Pattern.compile("ex2"), new LocatorAssertions.ContainsTextOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("ex2", e.getExpected().getStringRepresentation());
assertEquals("Text content", e.getActual().getValue());
assertTrue(e.getMessage().contains("Locator expected to contain regex"), e.getMessage());
}
}
@Test
void hasTextWRegexPass() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).hasText(Pattern.compile("Te.t"));
// Should not normalize whitespace.
assertThat(locator).hasText(Pattern.compile("Text.+content"));
}
@Test
void hasTextWRegexFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasText(Pattern.compile("Text 2"), new LocatorAssertions.HasTextOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("Text 2", e.getExpected().getStringRepresentation());
assertEquals("Text content", e.getActual().getValue());
assertTrue(e.getMessage().contains("Locator expected to have text matching regex"), e.getMessage());
}
}
@Test
void hasTextWTextPass() {
page.setContent("<div id=node><span></span>Text \ncontent&nbsp; </div>");
Locator locator = page.locator("#node");
// Should normalize whitespace.
assertThat(locator).hasText("Text content");
}
@Test
void hasTextWTextFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
// Should normalize whitespace.
try {
assertThat(locator).hasText("Text", new LocatorAssertions.HasTextOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("Text", e.getExpected().getStringRepresentation());
assertEquals("Text content", e.getActual().getValue());
assertTrue(e.getMessage().contains("Locator expected to have text"), e.getMessage());
}
}
@Test
void hasTextWTextArrayPass() {
page.setContent("<div>Text \n1</div><div>Text 2a</div>");
Locator locator = page.locator("div");
// Should normalize whitespace.
assertThat(locator).hasText(new String[] {"Text 1", "Text 2a"});
}
@Test
void hasTextWTextArrayPassEmpty() {
page.setContent("<div></div>");
Locator locator = page.locator("p");
// Should normalize whitespace.
assertThat(locator).hasText(new String[] {});
}
@Test
void hasTextWTextArrayPassNotEmpty() {
page.setContent("<div><p>Test</p></div>");
Locator locator = page.locator("div");
// Should normalize whitespace.
assertThat(locator).not().hasText(new String[] {});
}
@Test
void hasTextWTextArrayPassOnEmpty() {
page.setContent("<div></div>");
Locator locator = page.locator("p");
// Should normalize whitespace.
assertThat(locator).not().hasText(new String[] {"Test"});
}
@Test
void hasTextWTextArrayFailOnNotEmpty() {
page.setContent("<div></div>");
Locator locator = page.locator("p");
// Should normalize whitespace.
try {
assertThat(locator).not().hasText(new String[] {}, new LocatorAssertions.HasTextOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("[]", e.getExpected().getStringRepresentation());
assertEquals("null", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected not to have text"), e.getMessage());
}
}
@Test
void hasTextWTextArrayPassLazyPass() {
page.setContent("<div id=div></div>");
Locator locator = page.locator("p");
page.evaluate("setTimeout(() => {\n" +
" div.innerHTML = \"<p>Text 1</p><p>Text 2</p>\";\n" +
"}, 100);");
// Should normalize whitespace.
assertThat(locator).hasText(new String[] {"Text 1", "Text 2"}, new LocatorAssertions.HasTextOptions().setTimeout(1000));
}
@Test
void hasTextWTextArrayFail() {
page.setContent("<div>Text 1</div><div>Text 3</div>");
Locator locator = page.locator("div");
page.evaluate("setTimeout(() => {\n" +
" div.innerHTML = \"<p>Text 1</p><p>Text 2</p>\";\n" +
"}, 100);");
try {
// Should normalize whitespace.
assertThat(locator).hasText(new String[] {"Text 1", "Text 3", "Extra"}, new LocatorAssertions.HasTextOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("[Text 1, Text 3, Extra]", e.getExpected().getStringRepresentation());
assertEquals("[Text 1, Text 3]", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have text"), e.getMessage());
}
}
@Test
void hasTextWRegExArrayPass() {
page.setContent("<div>Text \n1</div><div>Text 2a</div>");
Locator locator = page.locator("div");
// Should normalize whitespace.
assertThat(locator).hasText(new Pattern[] {Pattern.compile( "Text \n1"), Pattern.compile("Text \\d+a")});
}
@Test
void hasTextWRegExArrayFail() {
page.setContent("<div>Text 1</div><div>Text 3</div>");
Locator locator = page.locator("div");
try {
// Should normalize whitespace.
assertThat(locator).hasText(new Pattern[] {Pattern.compile( "Text 1"), Pattern.compile("Text \\d"), Pattern.compile("Extra")}, new LocatorAssertions.HasTextOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("[Text 1, Text \\d, Extra]", e.getExpected().getStringRepresentation());
assertEquals("[Text 1, Text 3]", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have text"), e.getMessage());
}
}
@Test
void hasAttributeTextPass() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).hasAttribute("id", "node");
}
@Test
void hasAttributeTextFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasAttribute("id", "foo", new LocatorAssertions.HasAttributeOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("foo", e.getExpected().getStringRepresentation());
assertEquals("node", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have attribute 'id'"), e.getMessage());
}
}
@Test
void hasAttributeRegExpPass() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).hasAttribute("id", Pattern.compile("n..e"));
}
@Test
void hasAttributeRegExpFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasAttribute("id", Pattern.compile(".Nod.."), new LocatorAssertions.HasAttributeOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals(".Nod..", e.getExpected().getStringRepresentation());
assertEquals("node", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have attribute 'id' matching regex"), e.getMessage());
}
}
@Test
void hasClassTextPass() {
page.setContent("<div class=\"foo bar baz\"></div>");
Locator locator = page.locator("div");
assertThat(locator).hasClass("foo bar baz");
}
@Test
void hasClassTextFail() {
page.setContent("<div class=\"bar baz\"></div>");
Locator locator = page.locator("div");
try {
assertThat(locator).hasClass("foo bar baz", new LocatorAssertions.HasClassOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("foo bar baz", e.getExpected().getStringRepresentation());
assertEquals("bar baz", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have class"), e.getMessage());
}
}
@Test
void hasClassRegExpPass() {
page.setContent("<div class=\"foo bar baz\"></div>");
Locator locator = page.locator("div");
assertThat(locator).hasClass(Pattern.compile("foo.* baz"));
}
@Test
void hasClassRegExpFail() {
page.setContent("<div class=\"bar baz\"></div>");
Locator locator = page.locator("div");
try {
assertThat(locator).hasClass(Pattern.compile("foo Z.*"), new LocatorAssertions.HasClassOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("foo Z.*", e.getExpected().getStringRepresentation());
assertEquals("bar baz", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have class matching regex"), e.getMessage());
}
}
@Test
void hasClassTextArrayPass() {
page.setContent("<div class=\"foo\"></div><div class=\"bar\"></div><div class=\"baz\"></div>");
Locator locator = page.locator("div");
assertThat(locator).hasClass(new String[] {"foo", "bar", "baz"});
}
@Test
void hasClassTextArrayFail() {
page.setContent("<div class=\"foo\"></div><div class=\"bar\"></div><div class=\"baz\"></div>");
Locator locator = page.locator("div");
try {
assertThat(locator).hasClass(new String[] {"foo", "bar", "missing"}, new LocatorAssertions.HasClassOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("[foo, bar, missing]", e.getExpected().getStringRepresentation());
assertEquals("[foo, bar, baz]", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have class"), e.getMessage());
}
}
@Test
void hasClassRegExpArrayPass() {
page.setContent("<div class=\"foo\"></div><div class=\"bar\"></div><div class=\"baz\"></div>");
Locator locator = page.locator("div");
assertThat(locator).hasClass(new Pattern[] {Pattern.compile("fo.*"), Pattern.compile(".ar"), Pattern.compile("baz")});
}
@Test
void hasClassRegExpArrayFail() {
page.setContent("<div class=\"foo\"></div><div class=\"bar\"></div><div class=\"baz\"></div>");
Locator locator = page.locator("div");
try {
assertThat(locator).hasClass(new Pattern[] {Pattern.compile("fo.*"), Pattern.compile(".ar"), Pattern.compile("baz"), Pattern.compile("extra")}, new LocatorAssertions.HasClassOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("[fo.*, .ar, baz, extra]", e.getExpected().getStringRepresentation());
assertEquals("[foo, bar, baz]", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have class matching regex"), e.getMessage());
}
}
@Test
void hasCountPass() {
page.setContent("<select><option>One</option><option>Two</option></select>");
Locator locator = page.locator("option");
assertThat(locator).hasCount(2);
}
@Test
void hasCountFail() {
page.setContent("<select><option>One</option><option>Two</option></select>");
Locator locator = page.locator("option");
try {
assertThat(locator).hasCount(1, new LocatorAssertions.HasCountOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("1", e.getExpected().getStringRepresentation());
assertEquals("2", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have count"), e.getMessage());
}
}
@Test
void hasCountPassZero() {
page.setContent("<div></div>");
Locator locator = page.locator("span");
assertThat(locator).hasCount(0);
assertThat(locator).not().hasCount(1);
}
@Test
void hasCSSPass() {
page.setContent("<div id=node style='color: rgb(255, 0, 0)'>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).hasCSS("color", "rgb(255, 0, 0)");
}
@Test
void hasCSSFail() {
page.setContent("<div id=node style='color: rgb(255, 0, 0)'>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasCSS("color", "red", new LocatorAssertions.HasCSSOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("red", e.getExpected().getStringRepresentation());
assertEquals("rgb(255, 0, 0)", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have CSS property 'color'"), e.getMessage());
}
}
@Test
void hasCSSRegExPass() {
page.setContent("<div id=node style='color: rgb(255, 0, 0)'>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).hasCSS("color", Pattern.compile("rgb.*"));
}
@Test
void hasCSSRegExFail() {
page.setContent("<div id=node style='color: rgb(255, 0, 0)'>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasCSS("color", Pattern.compile("red"), new LocatorAssertions.HasCSSOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("red", e.getExpected().getStringRepresentation());
assertEquals("rgb(255, 0, 0)", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have CSS property 'color' matching regex"), e.getMessage());
}
}
@Test
void hasIdPass() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).hasId("node");
}
@Test
void hasIdFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasId("foo", new LocatorAssertions.HasIdOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("foo", e.getExpected().getStringRepresentation());
assertEquals("node", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have ID"), e.getMessage());
}
}
@Test
void hasJSPropertyPass() {
page.setContent("<div></div>");
page.evalOnSelector("div", "e => e.foo = { a: 1, b: 'string' }");
Locator locator = page.locator("div");
assertThat(locator).hasJSProperty("foo", mapOf("a", 1, "b", "string"));
}
@Test
void hasJSPropertyNumberFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
page.evalOnSelector("div", "e => e.foo = 2021");
try {
assertThat(locator).hasJSProperty("foo", 1, new LocatorAssertions.HasJSPropertyOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("1", e.getExpected().getStringRepresentation());
assertEquals("2021", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have JavaScript property 'foo'"), e.getMessage());
}
}
@Test
void hasJSPropertyObjectFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
page.evalOnSelector("div", "e => e.foo = { a: 1, b: 'string' }");
try {
assertThat(locator).hasJSProperty("foo", mapOf("a", 2), new LocatorAssertions.HasJSPropertyOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("{a=2}", e.getExpected().getStringRepresentation());
assertEquals("{a=1, b=string}", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have JavaScript property 'foo'"), e.getMessage());
}
}
@Test
void hasJSPropertyStringFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasJSProperty("id", "foo", new LocatorAssertions.HasJSPropertyOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("foo", e.getExpected().getStringRepresentation());
assertEquals("node", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have JavaScript property 'id'"), e.getMessage());
}
}
@Test
void hasValueTextPass() {
page.setContent("<input id=node></input>");
Locator locator = page.locator("#node");
locator.fill("Text content");
assertThat(locator).hasValue("Text content");
}
@Test
void hasValueTextFail() {
page.setContent("<input id=node></input>");
Locator locator = page.locator("#node");
locator.fill("Text content");
try {
assertThat(locator).hasValue("Text2", new LocatorAssertions.HasValueOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("Text2", e.getExpected().getStringRepresentation());
assertEquals("Text content", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have value"), e.getMessage());
}
}
@Test
void hasValueRegExpPass() {
page.setContent("<input id=node></input>");
Locator locator = page.locator("#node");
locator.fill("Text content");
assertThat(locator).hasValue(Pattern.compile("Text"));
}
@Test
void hasValueRegExpPassWithNot() {
page.setContent("<input id=node></input>");
Locator locator = page.locator("#node");
locator.fill("Text content");
assertThat(locator).not().hasValue(Pattern.compile("Text2"));
}
@Test
void hasValueRegExpFail() {
page.setContent("<input id=node></input>");
Locator locator = page.locator("#node");
locator.fill("Text content");
try {
assertThat(locator).hasValue(Pattern.compile("Text2"), new LocatorAssertions.HasValueOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("Text2", e.getExpected().getStringRepresentation());
assertEquals("Text content", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have value matching regex"), e.getMessage());
}
}
@Test
void isCheckedPass() {
page.setContent("<input type=checkbox checked></input>");
Locator locator = page.locator("input");
assertThat(locator).isChecked();
}
@Test
void isCheckedFail() {
page.setContent("<input type=checkbox></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).isChecked(new LocatorAssertions.IsCheckedOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be checked"), e.getMessage());
}
}
@Test
void notIsCheckedFail() {
page.setContent("<input type=checkbox checked></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).not().isChecked(new LocatorAssertions.IsCheckedOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be checked"), e.getMessage());
}
}
@Test
void isDisabledPass() {
page.setContent("<button disabled>Text</button>");
Locator locator = page.locator("button");
assertThat(locator).isDisabled();
}
@Test
void isDisabledFail() {
page.setContent("<button>Text</button>");
Locator locator = page.locator("button");
try {
assertThat(locator).isDisabled(new LocatorAssertions.IsDisabledOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be disabled"), e.getMessage());
}
}
@Test
void notIsDisabledFail() {
page.setContent("<button disabled>Text</button>");
Locator locator = page.locator("button");
try {
assertThat(locator).not().isDisabled(new LocatorAssertions.IsDisabledOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be disabled"), e.getMessage());
}
}
@Test
void isEditablePass() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
assertThat(locator).isEditable();
}
@Test
void isEditableFail() {
page.setContent("<input disabled></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).isEditable(new LocatorAssertions.IsEditableOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be editable"), e.getMessage());
}
}
@Test
void notIsEditableFail() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).not().isEditable(new LocatorAssertions.IsEditableOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be editable"), e.getMessage());
}
}
@Test
void isEmptyPass() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
assertThat(locator).isEmpty();
}
@Test
void isEmptyFail() {
page.setContent("<input value=text></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).isEmpty(new LocatorAssertions.IsEmptyOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be empty"), e.getMessage());
}
}
@Test
void notIsEmptyFail() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).not().isEmpty(new LocatorAssertions.IsEmptyOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be empty"), e.getMessage());
}
}
@Test
void isEnabledPass() {
page.setContent("<button>Text</button>");
Locator locator = page.locator("button");
assertThat(locator).isEnabled();
}
@Test
void isEnabledFail() {
page.setContent("<button disabled>Text</button>");
Locator locator = page.locator("button");
try {
assertThat(locator).isEnabled(new LocatorAssertions.IsEnabledOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be enabled"), e.getMessage());
}
}
@Test
void notIsEnabledFail() {
page.setContent("<button>Text</button>");
Locator locator = page.locator("button");
try {
assertThat(locator).not().isEnabled(new LocatorAssertions.IsEnabledOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be enabled"), e.getMessage());
}
}
@Test
void isFocusedPass() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
locator.focus();
assertThat(locator).isFocused();
}
@Test
void isFocusedFail() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).isFocused(new LocatorAssertions.IsFocusedOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be focused"), e.getMessage());
}
}
@Test
void notIsFocusedFail() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
locator.focus();
try {
assertThat(locator).not().isFocused(new LocatorAssertions.IsFocusedOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be focused"), e.getMessage());
}
}
@Test
void isHiddenPass() {
page.setContent("<button style='display: none'></button>");
Locator locator = page.locator("button");
assertThat(locator).isHidden();
}
@Test
void isHiddenFail() {
page.setContent("<button></button>");
Locator locator = page.locator("button");
try {
assertThat(locator).isHidden(new LocatorAssertions.IsHiddenOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be hidden"), e.getMessage());
}
}
@Test
void notIsHiddenFail() {
page.setContent("<button style='display: none'></button>");
Locator locator = page.locator("button");
try {
assertThat(locator).not().isHidden(new LocatorAssertions.IsHiddenOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be hidden"), e.getMessage());
}
}
@Test
void isVisiblePass() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
assertThat(locator).isVisible();
}
@Test
void isVisibleFail() {
page.setContent("<input style='display: none'></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).isVisible(new LocatorAssertions.IsVisibleOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be visible"), e.getMessage());
}
}
@Test
void notIsVisibleFail() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).not().isVisible(new LocatorAssertions.IsVisibleOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be visible"), e.getMessage());
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.17.0-SNAPSHOT</version>
<version>1.26.0</version>
</parent>
<artifactId>driver-bundle</artifactId>
@@ -14,7 +14,9 @@
* limitations under the License.
*/
package com.microsoft.playwright.impl;
package com.microsoft.playwright.impl.driver.jar;
import com.microsoft.playwright.impl.driver.Driver;
import java.io.IOException;
import java.net.URI;
@@ -26,17 +28,45 @@ import java.util.concurrent.TimeUnit;
public class DriverJar extends Driver {
private static final String PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD";
private static final String SELENIUM_REMOTE_URL = "SELENIUM_REMOTE_URL";
static final String PLAYWRIGHT_NODEJS_PATH = "PLAYWRIGHT_NODEJS_PATH";
private final Path driverTempDir;
private Path preinstalledNodePath;
DriverJar() throws IOException, URISyntaxException, InterruptedException {
driverTempDir = Files.createTempDirectory("playwright-java-");
public DriverJar() throws IOException {
// Allow specifying custom path for the driver installation
// See https://github.com/microsoft/playwright-java/issues/728
String alternativeTmpdir = System.getProperty("playwright.driver.tmpdir");
String prefix = "playwright-java-";
driverTempDir = alternativeTmpdir == null
? Files.createTempDirectory(prefix)
: Files.createTempDirectory(Paths.get(alternativeTmpdir), prefix);
driverTempDir.toFile().deleteOnExit();
String nodePath = System.getProperty("playwright.nodejs.path");
if (nodePath != null) {
preinstalledNodePath = Paths.get(nodePath);
if (!Files.exists(preinstalledNodePath)) {
throw new RuntimeException("Invalid Node.js path specified: " + nodePath);
}
}
logMessage("created DriverJar: " + driverTempDir);
}
@Override
protected void initialize(Map<String, String> env) throws Exception {
protected void initialize(Boolean installBrowsers) throws Exception {
if (preinstalledNodePath == null && env.containsKey(PLAYWRIGHT_NODEJS_PATH)) {
preinstalledNodePath = Paths.get(env.get(PLAYWRIGHT_NODEJS_PATH));
if (!Files.exists(preinstalledNodePath)) {
throw new RuntimeException("Invalid Node.js path specified: " + preinstalledNodePath);
}
} else if (preinstalledNodePath != null) {
// Pass the env variable to the driver process.
env.put(PLAYWRIGHT_NODEJS_PATH, preinstalledNodePath.toString());
}
extractDriverToTempDir();
installBrowsers(env);
logMessage("extracted driver from jar to " + driverPath());
if (installBrowsers)
installBrowsers(env);
}
private void installBrowsers(Map<String, String> env) throws IOException, InterruptedException {
@@ -48,13 +78,16 @@ public class DriverJar extends Driver {
System.out.println("Skipping browsers download because `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` env variable is set");
return;
}
String cliFileName = super.cliFileName();
Path driver = driverTempDir.resolve(cliFileName);
if (!Files.exists(driver)) {
throw new RuntimeException("Failed to find " + cliFileName + " at " + driver);
if (env.get(SELENIUM_REMOTE_URL) != null || System.getenv(SELENIUM_REMOTE_URL) != null) {
logMessage("Skipping browsers download because `SELENIUM_REMOTE_URL` env variable is set");
return;
}
ProcessBuilder pb = new ProcessBuilder(driver.toString(), "install");
pb.environment().putAll(env);
Path driver = driverPath();
if (!Files.exists(driver)) {
throw new RuntimeException("Failed to find driver: " + driver);
}
ProcessBuilder pb = createProcessBuilder();
pb.command().add("install");
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
Process p = pb.start();
@@ -73,9 +106,10 @@ public class DriverJar extends Driver {
return name.endsWith(".sh") || name.endsWith(".exe") || !name.contains(".");
}
private void extractDriverToTempDir() throws URISyntaxException, IOException {
void extractDriverToTempDir() throws URISyntaxException, IOException {
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
URI originalUri = classloader.getResource("driver/" + platformDir()).toURI();
URI originalUri = classloader.getResource(
"driver/" + platformDir()).toURI();
URI uri = maybeExtractNestedJar(originalUri);
// Create zip filesystem if loading from jar.
@@ -87,6 +121,12 @@ public class DriverJar extends Driver {
// See https://github.com/microsoft/playwright-java/issues/306
Path srcRootDefaultFs = Paths.get(srcRoot.toString());
Files.walk(srcRoot).forEach(fromPath -> {
if (preinstalledNodePath != null) {
String fileName = fromPath.getFileName().toString();
if ("node.exe".equals(fileName) || "node".equals(fileName)) {
return;
}
}
Path relative = srcRootDefaultFs.relativize(Paths.get(fromPath.toString()));
Path toPath = driverTempDir.resolve(relative.toString());
try {
@@ -130,11 +170,17 @@ public class DriverJar extends Driver {
private static String platformDir() {
String name = System.getProperty("os.name").toLowerCase();
String arch = System.getProperty("os.arch").toLowerCase();
if (name.contains("windows")) {
return "win32_x64";
}
if (name.contains("linux")) {
return "linux";
if (arch.equals("aarch64")) {
return "linux-arm64";
} else {
return "linux";
}
}
if (name.contains("mac os x")) {
return "mac";
@@ -143,7 +189,7 @@ public class DriverJar extends Driver {
}
@Override
Path driverDir() {
protected Path driverDir() {
return driverTempDir;
}
}
@@ -1,45 +0,0 @@
/*
* 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.
* 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 org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestInstall {
@Test
void playwrightCliInstalled() throws Exception {
// Clear system property to ensure that the driver is loaded from jar.
System.clearProperty("playwright.cli.dir");
Path cli = Driver.ensureDriverInstalled(Collections.emptyMap());
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");
}
}
@@ -0,0 +1,166 @@
/*
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl.driver.jar;
import com.microsoft.playwright.impl.driver.Driver;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.ServerSocket;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.microsoft.playwright.impl.driver.jar.DriverJar.PLAYWRIGHT_NODEJS_PATH;
import static java.util.Collections.singletonMap;
import static org.junit.jupiter.api.Assertions.*;
public class TestInstall {
private static boolean isPortAvailable(int port) {
try (ServerSocket ignored = new ServerSocket(port)) {
return true;
} catch (IOException ignored) {
return false;
}
}
private static int unusedPort() {
for (int i = 10000; i < 11000; i++) {
if (isPortAvailable(i)) {
return i;
}
}
throw new RuntimeException("Cannot find unused local port");
}
@BeforeEach
void clearSystemProperties() {
// Clear system property to ensure that the driver is loaded from jar.
System.clearProperty("playwright.cli.dir");
System.clearProperty("playwright.driver.tmpdir");
System.clearProperty("playwright.nodejs.path");
// Clear system property to ensure that the default driver is loaded.
System.clearProperty("playwright.driver.impl");
}
@Test
void shouldThrowWhenBrowserPathIsInvalid(@TempDir Path tmpDir) throws NoSuchFieldException, IllegalAccessException {
Map<String,String> env = new HashMap<>();
// On macOS we can only use 127.0.0.1, so pick unused port instead.
// https://superuser.com/questions/458875/how-do-you-get-loopback-addresses-other-than-127-0-0-1-to-work-on-os-x
env.put("PLAYWRIGHT_DOWNLOAD_HOST", "https://127.0.0.1:" + unusedPort());
// Make sure the browsers are not installed yet by pointing at an empty dir.
env.put("PLAYWRIGHT_BROWSERS_PATH", tmpDir.toString());
env.put("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "false");
RuntimeException exception = assertThrows(RuntimeException.class, () -> Driver.createAndInstall(env, true));
String message = exception.getMessage();
assertTrue(message.contains("Failed to create driver"), message);
}
@Test
void playwrightCliInstalled() throws Exception {
Driver driver = Driver.createAndInstall(Collections.emptyMap(), false);
assertTrue(Files.exists(driver.driverPath()));
ProcessBuilder pb = driver.createProcessBuilder();
pb.command().add("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");
}
@Test
void playwrightDriverInAlternativeTmpdir(@TempDir Path tmpdir) throws Exception {
System.setProperty("playwright.driver.tmpdir", tmpdir.toString());
DriverJar driver = new DriverJar();
assertTrue(driver.driverPath().startsWith(tmpdir), "Driver path: " + driver.driverPath() + " tmp: " + tmpdir);
}
@Test
void playwrightDriverDefaultImpl() {
assertDoesNotThrow(() -> Driver.createAndInstall(Collections.emptyMap(), false));
}
@Test
void playwrightDriverAlternativeImpl() throws NoSuchFieldException, IllegalAccessException {
System.setProperty("playwright.driver.impl", "com.microsoft.playwright.impl.AlternativeDriver");
RuntimeException thrown =
assertThrows(
RuntimeException.class,
() -> Driver.createAndInstall(Collections.emptyMap(), false));
assertEquals("Failed to create driver", thrown.getMessage());
}
@Test
void canPassPreinstalledNodeJsAsSystemProperty(@TempDir Path tmpDir) throws IOException, URISyntaxException, InterruptedException {
String nodePath = extractNodeJsToTemp();
System.setProperty("playwright.nodejs.path", nodePath);
Driver driver = Driver.createAndInstall(Collections.emptyMap(), false);
canSpecifyPreinstalledNodeJsShared(driver, tmpDir);
}
@Test
void canSpecifyPreinstalledNodeJsAsEnv(@TempDir Path tmpDir) throws IOException, URISyntaxException, InterruptedException {
String nodePath = extractNodeJsToTemp();
Driver driver = Driver.createAndInstall(singletonMap(PLAYWRIGHT_NODEJS_PATH, nodePath), false);
canSpecifyPreinstalledNodeJsShared(driver, tmpDir);
}
private static String extractNodeJsToTemp() throws URISyntaxException, IOException {
DriverJar auxDriver = new DriverJar();
auxDriver.extractDriverToTempDir();
String nodePath = auxDriver.driverPath().getParent().resolve(isWindows() ? "node.exe" : "node").toString();
return nodePath;
}
private static boolean isWindows() {
String name = System.getProperty("os.name").toLowerCase();
return name.contains("win");
}
private static void canSpecifyPreinstalledNodeJsShared(Driver driver, Path tmpDir) throws IOException, URISyntaxException, InterruptedException {
Path builtinNode = driver.driverPath().getParent().resolve("node");
assertFalse(Files.exists(builtinNode), builtinNode.toString());
Path builtinNodeExe = driver.driverPath().getParent().resolve("node.exe");
assertFalse(Files.exists(builtinNodeExe), builtinNodeExe.toString());
ProcessBuilder pb = driver.createProcessBuilder();
pb.command().add("--version");
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
Path out = tmpDir.resolve("out.txt");
pb.redirectOutput(out.toFile());
Process p = pb.start();
boolean result = p.waitFor(1, TimeUnit.MINUTES);
assertTrue(result, "Timed out waiting for version to be printed");
String stdout = new String(Files.readAllBytes(out), StandardCharsets.UTF_8);
assertTrue(stdout.contains("Version "), stdout);
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.17.0-SNAPSHOT</version>
<version>1.26.0</version>
</parent>
<artifactId>driver</artifactId>
@@ -1,79 +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 java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
/**
* This class provides access to playwright-cli. It can be either preinstalled
* in the host system and its path is passed as a system property or it can be
* loaded from the driver-bundle module if that module is in the classpath.
*/
public abstract class Driver {
private static Driver instance;
private static class PreinstalledDriver extends Driver {
private final Path driverDir;
PreinstalledDriver(Path driverDir) {
this.driverDir = driverDir;
}
@Override
protected void initialize(Map<String, String> env) {
// no-op
}
@Override
Path driverDir() {
return driverDir;
}
}
public static synchronized Path ensureDriverInstalled(Map<String, String> env) {
if (instance == null) {
try {
instance = createDriver();
instance.initialize(env);
} catch (Exception exception) {
throw new RuntimeException("Failed to create driver", exception);
}
}
String name = instance.cliFileName();
return instance.driverDir().resolve(name);
}
protected abstract void initialize(Map<String, String> env) throws Exception;
protected String cliFileName() {
return System.getProperty("os.name").toLowerCase().contains("windows") ?
"playwright.cmd" : "playwright.sh";
}
private static Driver createDriver() throws Exception {
String pathFromProperty = System.getProperty("playwright.cli.dir");
if (pathFromProperty != null) {
return new PreinstalledDriver(Paths.get(pathFromProperty));
}
Class<?> jarDriver = Class.forName("com.microsoft.playwright.impl.DriverJar");
return (Driver) jarDriver.getDeclaredConstructor().newInstance();
}
abstract Path driverDir();
}
@@ -0,0 +1,127 @@
/*
* 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.driver;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import static com.microsoft.playwright.impl.driver.DriverLogging.logWithTimestamp;
/**
* This class provides access to playwright-cli. It can be either preinstalled
* in the host system and its path is passed as a system property or it can be
* loaded from the driver-bundle module if that module is in the classpath.
*/
public abstract class Driver {
protected final Map<String, String> env = new LinkedHashMap<>();
private static Driver instance;
private static class PreinstalledDriver extends Driver {
private final Path driverDir;
PreinstalledDriver(Path driverDir) {
logMessage("created PreinstalledDriver: " + driverDir);
this.driverDir = driverDir;
}
@Override
protected void initialize(Boolean installBrowsers) {
// no-op
}
@Override
protected Path driverDir() {
return driverDir;
}
}
public static synchronized Driver ensureDriverInstalled(Map<String, String> env, Boolean installBrowsers) {
if (instance == null) {
instance = createAndInstall(env, installBrowsers);
}
return instance;
}
private void initialize(Map<String, String> env, Boolean installBrowsers) throws Exception {
this.env.putAll(env);
initialize(installBrowsers);
}
protected abstract void initialize(Boolean installBrowsers) throws Exception;
public Path driverPath() {
String cliFileName = System.getProperty("os.name").toLowerCase().contains("windows") ?
"playwright.cmd" : "playwright.sh";
return driverDir().resolve(cliFileName);
}
public ProcessBuilder createProcessBuilder() {
ProcessBuilder pb = new ProcessBuilder(driverPath().toString());
pb.environment().putAll(env);
pb.environment().put("PW_LANG_NAME", "java");
pb.environment().put("PW_LANG_NAME_VERSION", getMajorJavaVersion());
String version = Driver.class.getPackage().getImplementationVersion();
if (version != null) {
pb.environment().put("PW_CLI_DISPLAY_VERSION", version);
}
return pb;
}
private static String getMajorJavaVersion() {
String version = System.getProperty("java.version");
if (version.startsWith("1.")) {
return version.substring(2, 3);
}
int dot = version.indexOf(".");
if (dot != -1) {
return version.substring(0, dot);
}
return version;
}
public static Driver createAndInstall(Map<String, String> env, Boolean installBrowsers) {
try {
Driver instance = newInstance();
logMessage("initializing driver");
instance.initialize(env, installBrowsers);
logMessage("driver initialized.");
return instance;
} catch (Exception exception) {
throw new RuntimeException("Failed to create driver", exception);
}
}
private static Driver newInstance() throws Exception {
String pathFromProperty = System.getProperty("playwright.cli.dir");
if (pathFromProperty != null) {
return new PreinstalledDriver(Paths.get(pathFromProperty));
}
String driverImpl =
System.getProperty("playwright.driver.impl", "com.microsoft.playwright.impl.driver.jar.DriverJar");
Class<?> jarDriver = Class.forName(driverImpl);
return (Driver) jarDriver.getDeclaredConstructor().newInstance();
}
protected abstract Path driverDir();
protected static void logMessage(String message) {
// This matches log format produced by the server.
logWithTimestamp("pw:install " + message);
}
}
@@ -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.impl.driver;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
class DriverLogging {
private static final boolean isEnabled;
static {
String debug = System.getenv("DEBUG");
isEnabled = (debug != null) && debug.contains("pw:install");
}
private static final DateTimeFormatter timestampFormat = DateTimeFormatter.ofPattern(
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX").withZone(ZoneId.of("UTC"));
static void logWithTimestamp(String message) {
if (!isEnabled) {
return;
}
// This matches log format produced by the server.
String timestamp = ZonedDateTime.now().format(timestampFormat);
System.err.println(timestamp + " " + message);
}
}
+2 -2
View File
@@ -6,7 +6,7 @@
<groupId>org.example</groupId>
<artifactId>examples</artifactId>
<version>1.17.0-SNAPSHOT</version>
<version>1.26.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>1.16.0</version>
<version>1.22.0</version>
</dependency>
</dependencies>
<build>
@@ -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 org.example;
import java.nio.file.Paths;
import com.microsoft.playwright.*;
public class SelectorsAndKeyboardManipulation {
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://playwright.dev/java/");
page.locator("text=SearchK").click();
page.locator("[placeholder=\"Search docs\"]").fill("getting started");
page.locator("div[role=\"button\"]:has-text(\"CancelIntroductionGetting startedInstallationGetting startedUsageGetting start\")").click();
page.waitForSelector("h1:has-text(\"Getting started\")"); // Waits for the new page to load before screenshotting.
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("Screenshot.png")));
}
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"catalogs": {},
"aliases": {
"playwright": {
"script-ref": "scripts/playwright.java",
"description": "Playwright lets you automate Chromium, Firefox and Webkit with a single API. \nWith this cli you can install, trace, generate pdf and screenshots and more.\nExample on how to record and run a script:\n```\n jbang playwright@microsoft/playwright-java codegen -o Example.java`\n jbang --deps com.microsoft.playwright:playwright:RELEASE Example.java\n```"
}
},
"templates": {}
}
+5 -2
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.17.0-SNAPSHOT</version>
<version>1.26.0</version>
</parent>
<artifactId>playwright</artifactId>
@@ -44,7 +44,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<goals>
@@ -74,6 +73,10 @@
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</dependency>
<dependency>
<groupId>org.opentest4j</groupId>
<artifactId>opentest4j</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>driver</artifactId>
@@ -0,0 +1,184 @@
/*
* 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.options.*;
import java.nio.file.Path;
import java.util.*;
/**
* Exposes API that can be used for the Web API testing. This class is used for creating {@code APIRequestContext} instance which
* in turn can be used for sending web requests. An instance of this class can be obtained via {@link Playwright#request
* Playwright.request()}. For more information see {@code APIRequestContext}.
*/
public interface APIRequest {
class NewContextOptions {
/**
* Methods like {@link APIRequestContext#get APIRequestContext.get()} take the base URL into consideration by using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code URL()}</a> constructor for building the corresponding
* URL. Examples:
* <ul>
* <li> baseURL: {@code http://localhost:3000} and sending request to {@code /bar.html} results in {@code http://localhost:3000/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo/} and sending request to {@code ./bar.html} results in
* {@code http://localhost:3000/foo/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo} (without trailing slash) and navigating to {@code ./bar.html} results in
* {@code http://localhost:3000/bar.html}</li>
* </ul>
*/
public String baseURL;
/**
* An object containing additional HTTP headers to be sent with every request.
*/
public Map<String, String> extraHTTPHeaders;
/**
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>.
*/
public HttpCredentials httpCredentials;
/**
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
*/
public Boolean ignoreHTTPSErrors;
/**
* Network proxy settings.
*/
public Proxy proxy;
/**
* 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()} or {@link APIRequestContext#storageState
* APIRequestContext.storageState()}. Either a path to the file with saved storage, or the value returned by one of {@link
* BrowserContext#storageState BrowserContext.storageState()} or {@link APIRequestContext#storageState
* APIRequestContext.storageState()} methods.
*/
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 Path storageStatePath;
/**
* Maximum time in milliseconds to wait for the response. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout.
*/
public Double timeout;
/**
* Specific user agent to use in this context.
*/
public String userAgent;
/**
* Methods like {@link APIRequestContext#get APIRequestContext.get()} take the base URL into consideration by using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code URL()}</a> constructor for building the corresponding
* URL. Examples:
* <ul>
* <li> baseURL: {@code http://localhost:3000} and sending request to {@code /bar.html} results in {@code http://localhost:3000/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo/} and sending request to {@code ./bar.html} results in
* {@code http://localhost:3000/foo/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo} (without trailing slash) and navigating to {@code ./bar.html} results in
* {@code http://localhost:3000/bar.html}</li>
* </ul>
*/
public NewContextOptions setBaseURL(String baseURL) {
this.baseURL = baseURL;
return this;
}
/**
* An object containing additional HTTP headers to be sent with every request.
*/
public NewContextOptions setExtraHTTPHeaders(Map<String, String> extraHTTPHeaders) {
this.extraHTTPHeaders = extraHTTPHeaders;
return this;
}
/**
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>.
*/
public NewContextOptions setHttpCredentials(String username, String password) {
return setHttpCredentials(new HttpCredentials(username, password));
}
/**
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>.
*/
public NewContextOptions setHttpCredentials(HttpCredentials httpCredentials) {
this.httpCredentials = httpCredentials;
return this;
}
/**
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
*/
public NewContextOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) {
this.ignoreHTTPSErrors = ignoreHTTPSErrors;
return this;
}
/**
* Network proxy settings.
*/
public NewContextOptions setProxy(String server) {
return setProxy(new Proxy(server));
}
/**
* Network proxy settings.
*/
public NewContextOptions setProxy(Proxy proxy) {
this.proxy = proxy;
return this;
}
/**
* 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()} or {@link APIRequestContext#storageState
* APIRequestContext.storageState()}. Either a path to the file with saved storage, or the value returned by one of {@link
* BrowserContext#storageState BrowserContext.storageState()} or {@link APIRequestContext#storageState
* APIRequestContext.storageState()} methods.
*/
public NewContextOptions setStorageState(String storageState) {
this.storageState = storageState;
return this;
}
/**
* 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 NewContextOptions setStorageStatePath(Path storageStatePath) {
this.storageStatePath = storageStatePath;
return this;
}
/**
* Maximum time in milliseconds to wait for the response. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout.
*/
public NewContextOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
/**
* Specific user agent to use in this context.
*/
public NewContextOptions setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
}
/**
* Creates new instances of {@code APIRequestContext}.
*/
default APIRequestContext newContext() {
return newContext(null);
}
/**
* Creates new instances of {@code APIRequestContext}.
*/
APIRequestContext newContext(NewContextOptions options);
}
@@ -0,0 +1,227 @@
/*
* 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.options.*;
import java.nio.file.Path;
/**
* This API is used for the Web API testing. You can use it to trigger API endpoints, configure micro-services, prepare
* environment or the service to your e2e test.
*
* <p> Each Playwright browser context has associated with it {@code APIRequestContext} instance which shares cookie storage with the
* browser context and can be accessed via {@link BrowserContext#request BrowserContext.request()} or {@link Page#request
* Page.request()}. It is also possible to create a new APIRequestContext instance manually by calling {@link
* APIRequest#newContext APIRequest.newContext()}.
*
* <p> **Cookie management**
*
* <p> {@code APIRequestContext} returned by {@link BrowserContext#request BrowserContext.request()} and {@link Page#request
* Page.request()} shares cookie storage with the corresponding {@code BrowserContext}. Each API request will have {@code Cookie}
* header populated with the values from the browser context. If the API response contains {@code Set-Cookie} header it will
* automatically update {@code BrowserContext} cookies and requests made from the page will pick them up. This means that if you
* log in using this API, your e2e test will be logged in and vice versa.
*
* <p> If you want API requests to not interfere with the browser cookies you should create a new {@code APIRequestContext} by
* calling {@link APIRequest#newContext APIRequest.newContext()}. Such {@code APIRequestContext} object will have its own
* isolated cookie storage.
*/
public interface APIRequestContext {
class StorageStateOptions {
/**
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
*/
public Path path;
/**
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
*/
public StorageStateOptions setPath(Path path) {
this.path = path;
return this;
}
}
/**
* Sends HTTP(S) <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE">DELETE</a> request and returns
* its response. The method will populate request cookies from the context and update context cookies from the response.
* The method will automatically follow redirects.
*
* @param url Target URL.
*/
default APIResponse delete(String url) {
return delete(url, null);
}
/**
* Sends HTTP(S) <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE">DELETE</a> request and returns
* its response. The method will populate request cookies from the context and update context cookies from the response.
* The method will automatically follow redirects.
*
* @param url Target URL.
* @param params Optional request parameters.
*/
APIResponse delete(String url, RequestOptions params);
/**
* All responses returned by {@link APIRequestContext#get APIRequestContext.get()} and similar methods are stored in the
* memory, so that you can later call {@link APIResponse#body APIResponse.body()}. This method discards all stored
* responses, and makes {@link APIResponse#body APIResponse.body()} throw "Response disposed" error.
*/
void dispose();
/**
* Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
* context cookies from the response. The method will automatically follow redirects.
*
* @param urlOrRequest Target URL or Request to get all parameters from.
*/
default APIResponse fetch(String urlOrRequest) {
return fetch(urlOrRequest, null);
}
/**
* Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
* context cookies from the response. The method will automatically follow redirects.
*
* @param urlOrRequest Target URL or Request to get all parameters from.
* @param params Optional request parameters.
*/
APIResponse fetch(String urlOrRequest, RequestOptions params);
/**
* Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
* context cookies from the response. The method will automatically follow redirects.
*
* @param urlOrRequest Target URL or Request to get all parameters from.
*/
default APIResponse fetch(Request urlOrRequest) {
return fetch(urlOrRequest, null);
}
/**
* Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update
* context cookies from the response. The method will automatically follow redirects.
*
* @param urlOrRequest Target URL or Request to get all parameters from.
* @param params Optional request parameters.
*/
APIResponse fetch(Request urlOrRequest, RequestOptions params);
/**
* Sends HTTP(S) <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET">GET</a> request and returns its
* response. The method will populate request cookies from the context and update context cookies from the response. The
* method will automatically follow redirects.
*
* @param url Target URL.
*/
default APIResponse get(String url) {
return get(url, null);
}
/**
* Sends HTTP(S) <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET">GET</a> request and returns its
* response. The method will populate request cookies from the context and update context cookies from the response. The
* method will automatically follow redirects.
*
* @param url Target URL.
* @param params Optional request parameters.
*/
APIResponse get(String url, RequestOptions params);
/**
* Sends HTTP(S) <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD">HEAD</a> request and returns its
* response. The method will populate request cookies from the context and update context cookies from the response. The
* method will automatically follow redirects.
*
* @param url Target URL.
*/
default APIResponse head(String url) {
return head(url, null);
}
/**
* Sends HTTP(S) <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD">HEAD</a> request and returns its
* response. The method will populate request cookies from the context and update context cookies from the response. The
* method will automatically follow redirects.
*
* @param url Target URL.
* @param params Optional request parameters.
*/
APIResponse head(String url, RequestOptions params);
/**
* Sends HTTP(S) <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH">PATCH</a> request and returns
* its response. The method will populate request cookies from the context and update context cookies from the response.
* The method will automatically follow redirects.
*
* @param url Target URL.
*/
default APIResponse patch(String url) {
return patch(url, null);
}
/**
* Sends HTTP(S) <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH">PATCH</a> request and returns
* its response. The method will populate request cookies from the context and update context cookies from the response.
* The method will automatically follow redirects.
*
* @param url Target URL.
* @param params Optional request parameters.
*/
APIResponse patch(String url, RequestOptions params);
/**
* Sends HTTP(S) <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST">POST</a> request and returns its
* response. The method will populate request cookies from the context and update context cookies from the response. The
* method will automatically follow redirects.
*
* @param url Target URL.
*/
default APIResponse post(String url) {
return post(url, null);
}
/**
* Sends HTTP(S) <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST">POST</a> request and returns its
* response. The method will populate request cookies from the context and update context cookies from the response. The
* method will automatically follow redirects.
*
* @param url Target URL.
* @param params Optional request parameters.
*/
APIResponse post(String url, RequestOptions params);
/**
* Sends HTTP(S) <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT">PUT</a> request and returns its
* response. The method will populate request cookies from the context and update context cookies from the response. The
* method will automatically follow redirects.
*
* @param url Target URL.
*/
default APIResponse put(String url) {
return put(url, null);
}
/**
* Sends HTTP(S) <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT">PUT</a> request and returns its
* response. The method will populate request cookies from the context and update context cookies from the response. The
* method will automatically follow redirects.
*
* @param url Target URL.
* @param params Optional request parameters.
*/
APIResponse put(String url, RequestOptions params);
/**
* Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to
* the constructor.
*/
default String storageState() {
return storageState(null);
}
/**
* Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to
* the constructor.
*/
String storageState(StorageStateOptions options);
}
@@ -0,0 +1,65 @@
/*
* 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.options.*;
import java.util.*;
/**
* {@code APIResponse} class represents responses returned by {@link APIRequestContext#get APIRequestContext.get()} and similar
* methods.
*/
public interface APIResponse {
/**
* Returns the buffer with response body.
*/
byte[] body();
/**
* Disposes the body of this response. If not called then the body will stay in memory until the context closes.
*/
void dispose();
/**
* An object with all the response HTTP headers associated with this response.
*/
Map<String, String> headers();
/**
* An array with all the request HTTP headers associated with this response. Header names are not lower-cased. Headers with
* multiple entries, such as {@code Set-Cookie}, appear in the array multiple times.
*/
List<HttpHeader> headersArray();
/**
* Contains a boolean stating whether the response was successful (status in the range 200-299) or not.
*/
boolean ok();
/**
* Contains the status code of the response (e.g., 200 for a success).
*/
int status();
/**
* Contains the status text of the response (e.g. usually an "OK" for a success).
*/
String statusText();
/**
* Returns the text representation of response body.
*/
String text();
/**
* Contains the URL of the response.
*/
String url();
}
@@ -20,6 +20,7 @@ import com.microsoft.playwright.options.*;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.regex.Pattern;
/**
* A Browser is created via {@link BrowserType#launch BrowserType.launch()}. An example of using a {@code Browser} to create a
@@ -57,7 +58,7 @@ public interface Browser extends AutoCloseable {
class NewContextOptions {
/**
* Whether to automatically download all the attachments. Defaults to {@code false} where all the downloads are canceled.
* Whether to automatically download all the attachments. Defaults to {@code true} where all the downloads are accepted.
*/
public Boolean acceptDownloads;
/**
@@ -69,6 +70,8 @@ public interface Browser extends AutoCloseable {
* <ul>
* <li> baseURL: {@code http://localhost:3000} and navigating to {@code /bar.html} results in {@code http://localhost:3000/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo/} and navigating to {@code ./bar.html} results in {@code http://localhost:3000/foo/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo} (without trailing slash) and navigating to {@code ./bar.html} results in
* {@code http://localhost:3000/bar.html}</li>
* </ul>
*/
public String baseURL;
@@ -92,9 +95,6 @@ public interface Browser extends AutoCloseable {
/**
* Emulates {@code "forced-colors"} media feature, supported values are {@code "active"}, {@code "none"}. See {@link Page#emulateMedia
* Page.emulateMedia()} for more details. Defaults to {@code "none"}.
*
* <p> <strong>NOTE:</strong> It's not supported in WebKit, see <a href="https://bugs.webkit.org/show_bug.cgi?id=225281">here</a> in their issue
* tracker.
*/
public ForcedColors forcedColors;
public Geolocation geolocation;
@@ -141,6 +141,17 @@ public interface Browser extends AutoCloseable {
* 'http://per-context' } })}.
*/
public Proxy proxy;
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
*/
public HarContentPolicy recordHarContent;
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
*/
public HarMode recordHarMode;
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
@@ -151,6 +162,7 @@ public interface Browser extends AutoCloseable {
* BrowserContext.close()} for the HAR to be saved.
*/
public Path recordHarPath;
public Object recordHarUrlFilter;
/**
* 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.
@@ -172,6 +184,15 @@ public interface Browser extends AutoCloseable {
* is set.
*/
public ScreenSize screenSize;
/**
* Whether to allow sites to register Service workers. Defaults to {@code "allow"}.
* <ul>
* <li> {@code "allow"}: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a> can be
* registered.</li>
* <li> {@code "block"}: Playwright will block all registration of Service Workers.</li>
* </ul>
*/
public ServiceWorkerPolicy serviceWorkers;
/**
* 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()}.
@@ -184,7 +205,7 @@ public interface Browser extends AutoCloseable {
*/
public Path storageStatePath;
/**
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* that imply single target DOM element will throw when more than one element matches the selector. See {@code Locator} to learn
* more about the strict mode.
*/
@@ -205,7 +226,7 @@ public interface Browser extends AutoCloseable {
public Optional<ViewportSize> viewportSize;
/**
* Whether to automatically download all the attachments. Defaults to {@code false} where all the downloads are canceled.
* Whether to automatically download all the attachments. Defaults to {@code true} where all the downloads are accepted.
*/
public NewContextOptions setAcceptDownloads(boolean acceptDownloads) {
this.acceptDownloads = acceptDownloads;
@@ -220,6 +241,8 @@ public interface Browser extends AutoCloseable {
* <ul>
* <li> baseURL: {@code http://localhost:3000} and navigating to {@code /bar.html} results in {@code http://localhost:3000/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo/} and navigating to {@code ./bar.html} results in {@code http://localhost:3000/foo/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo} (without trailing slash) and navigating to {@code ./bar.html} results in
* {@code http://localhost:3000/bar.html}</li>
* </ul>
*/
public NewContextOptions setBaseURL(String baseURL) {
@@ -258,9 +281,6 @@ public interface Browser extends AutoCloseable {
/**
* Emulates {@code "forced-colors"} media feature, supported values are {@code "active"}, {@code "none"}. See {@link Page#emulateMedia
* Page.emulateMedia()} for more details. Defaults to {@code "none"}.
*
* <p> <strong>NOTE:</strong> It's not supported in WebKit, see <a href="https://bugs.webkit.org/show_bug.cgi?id=225281">here</a> in their issue
* tracker.
*/
public NewContextOptions setForcedColors(ForcedColors forcedColors) {
this.forcedColors = forcedColors;
@@ -359,6 +379,23 @@ public interface Browser extends AutoCloseable {
this.proxy = proxy;
return this;
}
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
*/
public NewContextOptions setRecordHarContent(HarContentPolicy recordHarContent) {
this.recordHarContent = recordHarContent;
return this;
}
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
*/
public NewContextOptions setRecordHarMode(HarMode recordHarMode) {
this.recordHarMode = recordHarMode;
return this;
}
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
@@ -375,6 +412,14 @@ public interface Browser extends AutoCloseable {
this.recordHarPath = recordHarPath;
return this;
}
public NewContextOptions setRecordHarUrlFilter(String recordHarUrlFilter) {
this.recordHarUrlFilter = recordHarUrlFilter;
return this;
}
public NewContextOptions setRecordHarUrlFilter(Pattern recordHarUrlFilter) {
this.recordHarUrlFilter = recordHarUrlFilter;
return this;
}
/**
* 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.
@@ -423,6 +468,18 @@ public interface Browser extends AutoCloseable {
this.screenSize = screenSize;
return this;
}
/**
* Whether to allow sites to register Service workers. Defaults to {@code "allow"}.
* <ul>
* <li> {@code "allow"}: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a> can be
* registered.</li>
* <li> {@code "block"}: Playwright will block all registration of Service Workers.</li>
* </ul>
*/
public NewContextOptions setServiceWorkers(ServiceWorkerPolicy serviceWorkers) {
this.serviceWorkers = serviceWorkers;
return this;
}
/**
* 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()}.
@@ -441,7 +498,7 @@ public interface Browser extends AutoCloseable {
return this;
}
/**
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* that imply single target DOM element will throw when more than one element matches the selector. See {@code Locator} to learn
* more about the strict mode.
*/
@@ -481,7 +538,7 @@ public interface Browser extends AutoCloseable {
}
class NewPageOptions {
/**
* Whether to automatically download all the attachments. Defaults to {@code false} where all the downloads are canceled.
* Whether to automatically download all the attachments. Defaults to {@code true} where all the downloads are accepted.
*/
public Boolean acceptDownloads;
/**
@@ -493,6 +550,8 @@ public interface Browser extends AutoCloseable {
* <ul>
* <li> baseURL: {@code http://localhost:3000} and navigating to {@code /bar.html} results in {@code http://localhost:3000/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo/} and navigating to {@code ./bar.html} results in {@code http://localhost:3000/foo/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo} (without trailing slash) and navigating to {@code ./bar.html} results in
* {@code http://localhost:3000/bar.html}</li>
* </ul>
*/
public String baseURL;
@@ -516,9 +575,6 @@ public interface Browser extends AutoCloseable {
/**
* Emulates {@code "forced-colors"} media feature, supported values are {@code "active"}, {@code "none"}. See {@link Page#emulateMedia
* Page.emulateMedia()} for more details. Defaults to {@code "none"}.
*
* <p> <strong>NOTE:</strong> It's not supported in WebKit, see <a href="https://bugs.webkit.org/show_bug.cgi?id=225281">here</a> in their issue
* tracker.
*/
public ForcedColors forcedColors;
public Geolocation geolocation;
@@ -565,6 +621,17 @@ public interface Browser extends AutoCloseable {
* 'http://per-context' } })}.
*/
public Proxy proxy;
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
*/
public HarContentPolicy recordHarContent;
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
*/
public HarMode recordHarMode;
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
@@ -575,6 +642,7 @@ public interface Browser extends AutoCloseable {
* BrowserContext.close()} for the HAR to be saved.
*/
public Path recordHarPath;
public Object recordHarUrlFilter;
/**
* 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.
@@ -596,6 +664,15 @@ public interface Browser extends AutoCloseable {
* is set.
*/
public ScreenSize screenSize;
/**
* Whether to allow sites to register Service workers. Defaults to {@code "allow"}.
* <ul>
* <li> {@code "allow"}: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a> can be
* registered.</li>
* <li> {@code "block"}: Playwright will block all registration of Service Workers.</li>
* </ul>
*/
public ServiceWorkerPolicy serviceWorkers;
/**
* 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()}.
@@ -608,7 +685,7 @@ public interface Browser extends AutoCloseable {
*/
public Path storageStatePath;
/**
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* that imply single target DOM element will throw when more than one element matches the selector. See {@code Locator} to learn
* more about the strict mode.
*/
@@ -629,7 +706,7 @@ public interface Browser extends AutoCloseable {
public Optional<ViewportSize> viewportSize;
/**
* Whether to automatically download all the attachments. Defaults to {@code false} where all the downloads are canceled.
* Whether to automatically download all the attachments. Defaults to {@code true} where all the downloads are accepted.
*/
public NewPageOptions setAcceptDownloads(boolean acceptDownloads) {
this.acceptDownloads = acceptDownloads;
@@ -644,6 +721,8 @@ public interface Browser extends AutoCloseable {
* <ul>
* <li> baseURL: {@code http://localhost:3000} and navigating to {@code /bar.html} results in {@code http://localhost:3000/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo/} and navigating to {@code ./bar.html} results in {@code http://localhost:3000/foo/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo} (without trailing slash) and navigating to {@code ./bar.html} results in
* {@code http://localhost:3000/bar.html}</li>
* </ul>
*/
public NewPageOptions setBaseURL(String baseURL) {
@@ -682,9 +761,6 @@ public interface Browser extends AutoCloseable {
/**
* Emulates {@code "forced-colors"} media feature, supported values are {@code "active"}, {@code "none"}. See {@link Page#emulateMedia
* Page.emulateMedia()} for more details. Defaults to {@code "none"}.
*
* <p> <strong>NOTE:</strong> It's not supported in WebKit, see <a href="https://bugs.webkit.org/show_bug.cgi?id=225281">here</a> in their issue
* tracker.
*/
public NewPageOptions setForcedColors(ForcedColors forcedColors) {
this.forcedColors = forcedColors;
@@ -783,6 +859,23 @@ public interface Browser extends AutoCloseable {
this.proxy = proxy;
return this;
}
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
*/
public NewPageOptions setRecordHarContent(HarContentPolicy recordHarContent) {
this.recordHarContent = recordHarContent;
return this;
}
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
*/
public NewPageOptions setRecordHarMode(HarMode recordHarMode) {
this.recordHarMode = recordHarMode;
return this;
}
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
@@ -799,6 +892,14 @@ public interface Browser extends AutoCloseable {
this.recordHarPath = recordHarPath;
return this;
}
public NewPageOptions setRecordHarUrlFilter(String recordHarUrlFilter) {
this.recordHarUrlFilter = recordHarUrlFilter;
return this;
}
public NewPageOptions setRecordHarUrlFilter(Pattern recordHarUrlFilter) {
this.recordHarUrlFilter = recordHarUrlFilter;
return this;
}
/**
* 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.
@@ -847,6 +948,18 @@ public interface Browser extends AutoCloseable {
this.screenSize = screenSize;
return this;
}
/**
* Whether to allow sites to register Service workers. Defaults to {@code "allow"}.
* <ul>
* <li> {@code "allow"}: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a> can be
* registered.</li>
* <li> {@code "block"}: Playwright will block all registration of Service Workers.</li>
* </ul>
*/
public NewPageOptions setServiceWorkers(ServiceWorkerPolicy serviceWorkers) {
this.serviceWorkers = serviceWorkers;
return this;
}
/**
* 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()}.
@@ -865,7 +978,7 @@ public interface Browser extends AutoCloseable {
return this;
}
/**
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* that imply single target DOM element will throw when more than one element matches the selector. See {@code Locator} to learn
* more about the strict mode.
*/
@@ -939,6 +1052,10 @@ public interface Browser extends AutoCloseable {
return this;
}
}
/**
* Get the browser type (chromium, firefox or webkit) that the browser belongs to.
*/
BrowserType browserType();
/**
* In case this browser is obtained using {@link BrowserType#launch BrowserType.launch()}, closes the browser and all of
* its pages (if any were opened).
@@ -946,6 +1063,10 @@ public interface Browser extends AutoCloseable {
* <p> In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from the
* browser server.
*
* <p> <strong>NOTE:</strong> This is similar to force quitting the browser. Therefore, you should call {@link BrowserContext#close
* BrowserContext.close()} on any {@code BrowserContext}'s you explicitly created earlier with {@link Browser#newContext
* Browser.newContext()} **before** calling {@link Browser#close Browser.close()}.
*
* <p> The {@code Browser} object itself is considered to be disposed and cannot be used anymore.
*/
void close();
@@ -965,6 +1086,11 @@ public interface Browser extends AutoCloseable {
boolean isConnected();
/**
* Creates a new browser context. It won't share cookies/cache with other browser contexts.
*
* <p> <strong>NOTE:</strong> If directly using this method to create {@code BrowserContext}s, it is best practice to explicitly close the returned context
* via {@link BrowserContext#close BrowserContext.close()} when your code is done with the {@code BrowserContext}, and before
* calling {@link Browser#close Browser.close()}. This will ensure the {@code context} is closed gracefully and any
* artifacts—like HARs and videos—are fully flushed and saved.
* <pre>{@code
* Browser browser = playwright.firefox().launch(); // Or 'chromium' or 'webkit'.
* // Create a new incognito browser context.
@@ -972,6 +1098,10 @@ public interface Browser extends AutoCloseable {
* // Create a new page in a pristine context.
* Page page = context.newPage();
* page.navigate('https://example.com');
*
* // Graceful close up everything
* context.close();
* browser.close();
* }</pre>
*/
default BrowserContext newContext() {
@@ -979,6 +1109,11 @@ public interface Browser extends AutoCloseable {
}
/**
* Creates a new browser context. It won't share cookies/cache with other browser contexts.
*
* <p> <strong>NOTE:</strong> If directly using this method to create {@code BrowserContext}s, it is best practice to explicitly close the returned context
* via {@link BrowserContext#close BrowserContext.close()} when your code is done with the {@code BrowserContext}, and before
* calling {@link Browser#close Browser.close()}. This will ensure the {@code context} is closed gracefully and any
* artifacts—like HARs and videos—are fully flushed and saved.
* <pre>{@code
* Browser browser = playwright.firefox().launch(); // Or 'chromium' or 'webkit'.
* // Create a new incognito browser context.
@@ -986,6 +1121,10 @@ public interface Browser extends AutoCloseable {
* // Create a new page in a pristine context.
* Page page = context.newPage();
* page.navigate('https://example.com');
*
* // Graceful close up everything
* context.close();
* browser.close();
* }</pre>
*/
BrowserContext newContext(NewContextOptions options);
@@ -1009,8 +1148,9 @@ public interface Browser extends AutoCloseable {
Page newPage(NewPageOptions options);
/**
* <strong>NOTE:</strong> This API controls <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">Chromium Tracing</a>
* which is a low-level chromium-specific debugging tool. API to control <a href="../trace-viewer">Playwright Tracing</a>
* could be found <a href="https://playwright.dev/java/docs/class-tracing">here</a>.
* which is a low-level chromium-specific debugging tool. API to control <a
* href="https://playwright.dev/java/docs/trace-viewer">Playwright Tracing</a> could be found <a
* href="https://playwright.dev/java/docs/api/class-tracing">here</a>.
*
* <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.
@@ -1028,8 +1168,9 @@ public interface Browser extends AutoCloseable {
}
/**
* <strong>NOTE:</strong> This API controls <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">Chromium Tracing</a>
* which is a low-level chromium-specific debugging tool. API to control <a href="../trace-viewer">Playwright Tracing</a>
* could be found <a href="https://playwright.dev/java/docs/class-tracing">here</a>.
* which is a low-level chromium-specific debugging tool. API to control <a
* href="https://playwright.dev/java/docs/trace-viewer">Playwright Tracing</a> could be found <a
* href="https://playwright.dev/java/docs/api/class-tracing">here</a>.
*
* <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.
@@ -1045,8 +1186,9 @@ public interface Browser extends AutoCloseable {
}
/**
* <strong>NOTE:</strong> This API controls <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">Chromium Tracing</a>
* which is a low-level chromium-specific debugging tool. API to control <a href="../trace-viewer">Playwright Tracing</a>
* could be found <a href="https://playwright.dev/java/docs/class-tracing">here</a>.
* which is a low-level chromium-specific debugging tool. API to control <a
* href="https://playwright.dev/java/docs/trace-viewer">Playwright Tracing</a> could be found <a
* href="https://playwright.dev/java/docs/api/class-tracing">here</a>.
*
* <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.
@@ -1062,8 +1204,9 @@ public interface Browser extends AutoCloseable {
void startTracing(Page page, StartTracingOptions options);
/**
* <strong>NOTE:</strong> This API controls <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">Chromium Tracing</a>
* which is a low-level chromium-specific debugging tool. API to control <a href="../trace-viewer">Playwright Tracing</a>
* could be found <a href="https://playwright.dev/java/docs/class-tracing">here</a>.
* which is a low-level chromium-specific debugging tool. API to control <a
* href="https://playwright.dev/java/docs/trace-viewer">Playwright Tracing</a> could be found <a
* href="https://playwright.dev/java/docs/api/class-tracing">here</a>.
*
* <p> Returns the buffer with trace data.
*/
@@ -67,7 +67,7 @@ public interface BrowserContext extends AutoCloseable {
* done and its response has started loading in the popup.
* <pre>{@code
* Page newPage = context.waitForPage(() -> {
* page.click("a[target=_blank]");
* page.locator("a[target=_blank]").click();
* });
* System.out.println(newPage.evaluate("location.href"));
* }</pre>
@@ -174,6 +174,62 @@ public interface BrowserContext extends AutoCloseable {
return this;
}
}
class RouteFromHAROptions {
/**
* <ul>
* <li> If set to 'abort' any request not found in the HAR file will be aborted.</li>
* <li> If set to 'fallback' falls through to the next route handler in the handler chain.</li>
* </ul>
*
* <p> Defaults to abort.
*/
public HarNotFound notFound;
/**
* If specified, updates the given HAR with the actual network information instead of serving from file.
*/
public Boolean update;
/**
* A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
* will be served from the HAR file. If not specified, all requests are served from the HAR file.
*/
public Object url;
/**
* <ul>
* <li> If set to 'abort' any request not found in the HAR file will be aborted.</li>
* <li> If set to 'fallback' falls through to the next route handler in the handler chain.</li>
* </ul>
*
* <p> Defaults to abort.
*/
public RouteFromHAROptions setNotFound(HarNotFound notFound) {
this.notFound = notFound;
return this;
}
/**
* If specified, updates the given HAR with the actual network information instead of serving from file.
*/
public RouteFromHAROptions setUpdate(boolean update) {
this.update = update;
return this;
}
/**
* A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
* will be served from the HAR file. If not specified, all requests are served from the HAR file.
*/
public RouteFromHAROptions setUrl(String url) {
this.url = url;
return this;
}
/**
* A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
* will be served from the HAR file. If not specified, all requests are served from the HAR file.
*/
public RouteFromHAROptions setUrl(Pattern url) {
this.url = url;
return this;
}
}
class StorageStateOptions {
/**
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
@@ -348,7 +404,7 @@ public interface BrowserContext extends AutoCloseable {
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>");
* page.click("button");
* page.locator("button").click();
* }
* }
* }
@@ -407,7 +463,7 @@ public interface BrowserContext extends AutoCloseable {
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>");
* page.click("button");
* page.locator("button").click();
* }
* }
* }
@@ -477,7 +533,7 @@ public interface BrowserContext extends AutoCloseable {
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>\n");
* page.click("button");
* page.locator("button").click();
* }
* }
* }
@@ -497,7 +553,6 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "midi"}</li>
* <li> {@code "midi-sysex"} (system-exclusive midi)</li>
* <li> {@code "notifications"}</li>
* <li> {@code "push"}</li>
* <li> {@code "camera"}</li>
* <li> {@code "microphone"}</li>
* <li> {@code "background-sync"}</li>
@@ -524,7 +579,6 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "midi"}</li>
* <li> {@code "midi-sysex"} (system-exclusive midi)</li>
* <li> {@code "notifications"}</li>
* <li> {@code "push"}</li>
* <li> {@code "camera"}</li>
* <li> {@code "microphone"}</li>
* <li> {@code "background-sync"}</li>
@@ -547,13 +601,17 @@ public interface BrowserContext extends AutoCloseable {
* Returns all open pages in the context.
*/
List<Page> pages();
/**
* API testing helper associated with this context. Requests made with this API will use context cookies.
*/
APIRequestContext request();
/**
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.route()} will not intercept requests intercepted by Service Worker. See <a
* <p> <strong>NOTE:</strong> {@link BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by Service Worker. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception. Via {@code await context.addInitScript(() => delete window.navigator.serviceWorker);}
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -603,9 +661,9 @@ public interface BrowserContext extends AutoCloseable {
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.route()} will not intercept requests intercepted by Service Worker. See <a
* <p> <strong>NOTE:</strong> {@link BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by Service Worker. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception. Via {@code await context.addInitScript(() => delete window.navigator.serviceWorker);}
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -653,9 +711,9 @@ public interface BrowserContext extends AutoCloseable {
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.route()} will not intercept requests intercepted by Service Worker. See <a
* <p> <strong>NOTE:</strong> {@link BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by Service Worker. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception. Via {@code await context.addInitScript(() => delete window.navigator.serviceWorker);}
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -705,9 +763,9 @@ public interface BrowserContext extends AutoCloseable {
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.route()} will not intercept requests intercepted by Service Worker. See <a
* <p> <strong>NOTE:</strong> {@link BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by Service Worker. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception. Via {@code await context.addInitScript(() => delete window.navigator.serviceWorker);}
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -755,9 +813,9 @@ public interface BrowserContext extends AutoCloseable {
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.route()} will not intercept requests intercepted by Service Worker. See <a
* <p> <strong>NOTE:</strong> {@link BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by Service Worker. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception. Via {@code await context.addInitScript(() => delete window.navigator.serviceWorker);}
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -807,9 +865,9 @@ public interface BrowserContext extends AutoCloseable {
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.route()} will not intercept requests intercepted by Service Worker. See <a
* <p> <strong>NOTE:</strong> {@link BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by Service Worker. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception. Via {@code await context.addInitScript(() => delete window.navigator.serviceWorker);}
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -853,6 +911,32 @@ public interface BrowserContext extends AutoCloseable {
* @param handler handler function to route the request.
*/
void route(Predicate<String> url, Consumer<Route> handler, RouteOptions options);
/**
* If specified the network requests that are made in the context will be served from the HAR file. Read more about <a
* href="https://playwright.dev/java/docs/network#replaying-from-har">Replaying from HAR</a>.
*
* <p> Playwright will not serve requests intercepted by Service Worker from the HAR file. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* @param har Path to a <a href="http://www.softwareishard.com/blog/har-12-spec">HAR</a> file with prerecorded network data. If {@code path}
* is a relative path, then it is resolved relative to the current working directory.
*/
default void routeFromHAR(Path har) {
routeFromHAR(har, null);
}
/**
* If specified the network requests that are made in the context will be served from the HAR file. Read more about <a
* href="https://playwright.dev/java/docs/network#replaying-from-har">Replaying from HAR</a>.
*
* <p> Playwright will not serve requests intercepted by Service Worker from the HAR file. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* @param har Path to a <a href="http://www.softwareishard.com/blog/har-12-spec">HAR</a> file with prerecorded network data. If {@code path}
* is a relative path, then it is resolved relative to the current working directory.
*/
void routeFromHAR(Path har, RouteFromHAROptions options);
/**
* This setting will change the default maximum navigation time for the following methods and related shortcuts:
* <ul>
@@ -19,6 +19,7 @@ package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
/**
* BrowserType provides methods to launch a specific browser instance or connect to an existing one. The following is a
@@ -52,8 +53,7 @@ public interface BrowserType {
*/
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.
* Maximum time in milliseconds to wait for the connection to be established. Defaults to {@code 0} (no timeout).
*/
public Double timeout;
@@ -73,8 +73,7 @@ public interface BrowserType {
return this;
}
/**
* Maximum time in milliseconds to wait for the connection to be established. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to
* disable timeout.
* Maximum time in milliseconds to wait for the connection to be established. Defaults to {@code 0} (no timeout).
*/
public ConnectOptions setTimeout(double timeout) {
this.timeout = timeout;
@@ -130,7 +129,7 @@ public interface BrowserType {
/**
* 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>.
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public Object channel;
/**
@@ -222,7 +221,7 @@ public interface BrowserType {
/**
* 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>.
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public LaunchOptions setChannel(BrowserChannel channel) {
this.channel = channel;
@@ -231,7 +230,7 @@ public interface BrowserType {
/**
* 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>.
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public LaunchOptions setChannel(String channel) {
this.channel = channel;
@@ -370,7 +369,7 @@ public interface BrowserType {
}
class LaunchPersistentContextOptions {
/**
* Whether to automatically download all the attachments. Defaults to {@code false} where all the downloads are canceled.
* Whether to automatically download all the attachments. Defaults to {@code true} where all the downloads are accepted.
*/
public Boolean acceptDownloads;
/**
@@ -387,6 +386,8 @@ public interface BrowserType {
* <ul>
* <li> baseURL: {@code http://localhost:3000} and navigating to {@code /bar.html} results in {@code http://localhost:3000/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo/} and navigating to {@code ./bar.html} results in {@code http://localhost:3000/foo/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo} (without trailing slash) and navigating to {@code ./bar.html} results in
* {@code http://localhost:3000/bar.html}</li>
* </ul>
*/
public String baseURL;
@@ -397,7 +398,7 @@ public interface BrowserType {
/**
* 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>.
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public Object channel;
/**
@@ -441,9 +442,6 @@ public interface BrowserType {
/**
* Emulates {@code "forced-colors"} media feature, supported values are {@code "active"}, {@code "none"}. See {@link Page#emulateMedia
* Page.emulateMedia()} for more details. Defaults to {@code "none"}.
*
* <p> <strong>NOTE:</strong> It's not supported in WebKit, see <a href="https://bugs.webkit.org/show_bug.cgi?id=225281">here</a> in their issue
* tracker.
*/
public ForcedColors forcedColors;
public Geolocation geolocation;
@@ -515,6 +513,17 @@ public interface BrowserType {
* Network proxy settings.
*/
public Proxy proxy;
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
*/
public HarContentPolicy recordHarContent;
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
*/
public HarMode recordHarMode;
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
@@ -525,6 +534,7 @@ public interface BrowserType {
* BrowserContext.close()} for the HAR to be saved.
*/
public Path recordHarPath;
public Object recordHarUrlFilter;
/**
* 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.
@@ -546,12 +556,21 @@ public interface BrowserType {
* is set.
*/
public ScreenSize screenSize;
/**
* Whether to allow sites to register Service workers. Defaults to {@code "allow"}.
* <ul>
* <li> {@code "allow"}: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a> can be
* registered.</li>
* <li> {@code "block"}: Playwright will block all registration of Service Workers.</li>
* </ul>
*/
public ServiceWorkerPolicy serviceWorkers;
/**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
*/
public Double slowMo;
/**
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* that imply single target DOM element will throw when more than one element matches the selector. See {@code Locator} to learn
* more about the strict mode.
*/
@@ -581,7 +600,7 @@ public interface BrowserType {
public Optional<ViewportSize> viewportSize;
/**
* Whether to automatically download all the attachments. Defaults to {@code false} where all the downloads are canceled.
* Whether to automatically download all the attachments. Defaults to {@code true} where all the downloads are accepted.
*/
public LaunchPersistentContextOptions setAcceptDownloads(boolean acceptDownloads) {
this.acceptDownloads = acceptDownloads;
@@ -604,6 +623,8 @@ public interface BrowserType {
* <ul>
* <li> baseURL: {@code http://localhost:3000} and navigating to {@code /bar.html} results in {@code http://localhost:3000/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo/} and navigating to {@code ./bar.html} results in {@code http://localhost:3000/foo/bar.html}</li>
* <li> baseURL: {@code http://localhost:3000/foo} (without trailing slash) and navigating to {@code ./bar.html} results in
* {@code http://localhost:3000/bar.html}</li>
* </ul>
*/
public LaunchPersistentContextOptions setBaseURL(String baseURL) {
@@ -621,7 +642,7 @@ public interface BrowserType {
/**
* 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>.
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public LaunchPersistentContextOptions setChannel(BrowserChannel channel) {
this.channel = channel;
@@ -630,7 +651,7 @@ public interface BrowserType {
/**
* 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>.
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public LaunchPersistentContextOptions setChannel(String channel) {
this.channel = channel;
@@ -701,9 +722,6 @@ public interface BrowserType {
/**
* Emulates {@code "forced-colors"} media feature, supported values are {@code "active"}, {@code "none"}. See {@link Page#emulateMedia
* Page.emulateMedia()} for more details. Defaults to {@code "none"}.
*
* <p> <strong>NOTE:</strong> It's not supported in WebKit, see <a href="https://bugs.webkit.org/show_bug.cgi?id=225281">here</a> in their issue
* tracker.
*/
public LaunchPersistentContextOptions setForcedColors(ForcedColors forcedColors) {
this.forcedColors = forcedColors;
@@ -841,6 +859,23 @@ public interface BrowserType {
this.proxy = proxy;
return this;
}
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
*/
public LaunchPersistentContextOptions setRecordHarContent(HarContentPolicy recordHarContent) {
this.recordHarContent = recordHarContent;
return this;
}
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
*/
public LaunchPersistentContextOptions setRecordHarMode(HarMode recordHarMode) {
this.recordHarMode = recordHarMode;
return this;
}
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
@@ -857,6 +892,14 @@ public interface BrowserType {
this.recordHarPath = recordHarPath;
return this;
}
public LaunchPersistentContextOptions setRecordHarUrlFilter(String recordHarUrlFilter) {
this.recordHarUrlFilter = recordHarUrlFilter;
return this;
}
public LaunchPersistentContextOptions setRecordHarUrlFilter(Pattern recordHarUrlFilter) {
this.recordHarUrlFilter = recordHarUrlFilter;
return this;
}
/**
* 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.
@@ -905,6 +948,18 @@ public interface BrowserType {
this.screenSize = screenSize;
return this;
}
/**
* Whether to allow sites to register Service workers. Defaults to {@code "allow"}.
* <ul>
* <li> {@code "allow"}: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a> can be
* registered.</li>
* <li> {@code "block"}: Playwright will block all registration of Service Workers.</li>
* </ul>
*/
public LaunchPersistentContextOptions setServiceWorkers(ServiceWorkerPolicy serviceWorkers) {
this.serviceWorkers = serviceWorkers;
return this;
}
/**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
*/
@@ -913,7 +968,7 @@ public interface BrowserType {
return this;
}
/**
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* that imply single target DOM element will throw when more than one element matches the selector. See {@code Locator} to learn
* more about the strict mode.
*/
@@ -967,7 +1022,9 @@ public interface BrowserType {
}
}
/**
* This methods attaches Playwright to an existing browser instance.
* This method attaches Playwright to an existing browser instance. When connecting to another browser launched via
* {@code BrowserType.launchServer} in Node.js, the major and minor version needs to match the client version (1.2.3 → is
* compatible with 1.2.x).
*
* @param wsEndpoint A browser websocket endpoint to connect to.
*/
@@ -975,17 +1032,24 @@ public interface BrowserType {
return connect(wsEndpoint, null);
}
/**
* This methods attaches Playwright to an existing browser instance.
* This method attaches Playwright to an existing browser instance. When connecting to another browser launched via
* {@code BrowserType.launchServer} in Node.js, the major and minor version needs to match the client version (1.2.3 → is
* compatible with 1.2.x).
*
* @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.
* This method 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.
* <pre>{@code
* Browser browser = playwright.chromium().connectOverCDP("http://localhost:9222");
* BrowserContext defaultContext = browser.contexts().get(0);
* Page page = defaultContext.pages().get(0);
* }</pre>
*
* @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}.
@@ -994,11 +1058,16 @@ public interface BrowserType {
return connectOverCDP(endpointURL, null);
}
/**
* This methods attaches Playwright to an existing browser instance using the Chrome DevTools Protocol.
* This method 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.
* <pre>{@code
* Browser browser = playwright.chromium().connectOverCDP("http://localhost:9222");
* BrowserContext defaultContext = browser.contexts().get(0);
* Page page = defaultContext.pages().get(0);
* }</pre>
*
* @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}.
@@ -16,7 +16,7 @@
package com.microsoft.playwright;
import com.microsoft.playwright.impl.Driver;
import com.microsoft.playwright.impl.driver.Driver;
import java.io.IOException;
import java.nio.file.Path;
@@ -29,11 +29,12 @@ import static java.util.Arrays.asList;
*/
public class CLI {
public static void main(String[] args) throws IOException, InterruptedException {
Path driver = Driver.ensureDriverInstalled(Collections.emptyMap());
ProcessBuilder pb = new ProcessBuilder(driver.toString());
Driver driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
ProcessBuilder pb = driver.createProcessBuilder();
pb.command().addAll(asList(args));
if (!pb.environment().containsKey("PW_CLI_TARGET_LANG")) {
pb.environment().put("PW_CLI_TARGET_LANG", "java");
String version = Playwright.class.getPackage().getImplementationVersion();
if (version != null) {
pb.environment().put("PW_CLI_DISPLAY_VERSION", version);
}
pb.inheritIO();
Process process = pb.start();
@@ -19,7 +19,28 @@ package com.microsoft.playwright;
import java.util.*;
/**
* {@code ConsoleMessage} objects are dispatched by page via the {@link Page#onConsoleMessage Page.onConsoleMessage()} event.
* {@code ConsoleMessage} objects are dispatched by page via the {@link Page#onConsoleMessage Page.onConsoleMessage()} event. For
* each console messages logged in the page there will be corresponding event in the Playwright context.
* <pre>{@code
* // Listen for all System.out.printlns
* page.onConsoleMessage(msg -> System.out.println(msg.text()));
*
* // Listen for all console events and handle errors
* page.onConsoleMessage(msg -> {
* if ("error".equals(msg.type()))
* System.out.println("Error text: " + msg.text());
* });
*
* // Get the next System.out.println
* ConsoleMessage msg = page.waitForConsoleMessage(() -> {
* // Issue console.log inside the page
* page.evaluate("console.log('hello', 42, { foo: 'bar' });");
* });
*
* // Deconstruct console.log arguments
* msg.args().get(0).jsonValue() // hello
* msg.args().get(1).jsonValue() // 42
* }</pre>
*/
public interface ConsoleMessage {
/**
@@ -16,7 +16,6 @@
package com.microsoft.playwright;
import java.util.*;
/**
* {@code Dialog} objects are dispatched by page via the {@link Page#onDialog Page.onDialog()} event.
@@ -18,7 +18,6 @@ package com.microsoft.playwright;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.*;
/**
* {@code Download} objects are dispatched by page via the {@link Page#onDownload Page.onDownload()} event.
@@ -28,22 +27,18 @@ import java.util.*;
* <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"));
* Download download = page.waitForDownload(() -> page.locator("a").click());
* // wait for download to complete
* Path path = download.path();
* }</pre>
* <pre>{@code
* // wait for download to start
* Download download = page.waitForDownload(() -> {
* page.click("a");
* page.locator("a").click();
* });
* // 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.
*/
public interface Download {
/**
File diff suppressed because it is too large Load Diff
@@ -18,12 +18,11 @@ 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 {@link Page#onFileChooser Page.onFileChooser()} event.
* <pre>{@code
* FileChooser fileChooser = page.waitForFileChooser(() -> page.click("upload"));
* FileChooser fileChooser = page.waitForFileChooser(() -> page.locator("upload").click());
* fileChooser.setFiles(Paths.get("myfile.pdf"));
* }</pre>
*/
@@ -75,50 +74,50 @@ public interface FileChooser {
Page page();
/**
* 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.
* they are resolved relative to 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.
* they are resolved relative to 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.
* they are resolved relative to 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.
* they are resolved relative to 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.
* they are resolved relative to 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.
* they are resolved relative to 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.
* they are resolved relative to 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.
* they are resolved relative to 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
@@ -16,7 +16,7 @@
package com.microsoft.playwright;
import java.util.*;
import java.util.regex.Pattern;
/**
* FrameLocator represents a view to the {@code iframe} on the page. It captures the logic sufficient to retrieve the {@code iframe}
@@ -30,7 +30,7 @@ import java.util.*;
* <p> **Strictness**
*
* <p> Frame locators are strict. This means that all operations on frame locators will throw if more than one element matches
* given selector.
* a given selector.
* <pre>{@code
* // Throws if there are several frames in DOM:
* page.frame_locator(".result-frame").locator("button").click();
@@ -38,8 +38,60 @@ import java.util.*;
* // Works because we explicitly tell locator to pick the first frame:
* page.frame_locator(".result-frame").first().locator("button").click();
* }</pre>
*
* <p> **Converting Locator to FrameLocator**
*
* <p> If you have a {@code Locator} object pointing to an {@code iframe} it can be converted to {@code FrameLocator} using <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/:scope">{@code :scope}</a> CSS selector:
* <pre>{@code
* Locator frameLocator = locator.frameLocator(':scope');
* }</pre>
*/
public interface FrameLocator {
class LocatorOptions {
/**
* Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer one.
* For example, {@code article} that has {@code text=Playwright} matches {@code <article><div>Playwright</div></article>}.
*
* <p> Note that outer and inner locators must belong to the same frame. Inner locator must not contain {@code FrameLocator}s.
*/
public Locator has;
/**
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a
* [string], matching is case-insensitive and searches for a substring. For example, {@code "Playwright"} matches
* {@code <article><div>Playwright</div></article>}.
*/
public Object hasText;
/**
* Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer one.
* For example, {@code article} that has {@code text=Playwright} matches {@code <article><div>Playwright</div></article>}.
*
* <p> Note that outer and inner locators must belong to the same frame. Inner locator must not contain {@code FrameLocator}s.
*/
public LocatorOptions setHas(Locator has) {
this.has = has;
return this;
}
/**
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a
* [string], matching is case-insensitive and searches for a substring. For example, {@code "Playwright"} matches
* {@code <article><div>Playwright</div></article>}.
*/
public LocatorOptions setHasText(String hasText) {
this.hasText = hasText;
return this;
}
/**
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a
* [string], matching is case-insensitive and searches for a substring. For example, {@code "Playwright"} matches
* {@code <article><div>Playwright</div></article>}.
*/
public LocatorOptions setHasText(Pattern hasText) {
this.hasText = hasText;
return this;
}
}
/**
* Returns locator to the first matching frame.
*/
@@ -48,7 +100,7 @@ public interface FrameLocator {
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
* that iframe.
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors/">working with
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors">working with
* selectors</a> for more details.
*/
FrameLocator frameLocator(String selector);
@@ -59,12 +111,21 @@ public interface FrameLocator {
/**
* The method finds an element matching the specified selector in the FrameLocator's subtree.
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors/">working with
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors">working with
* selectors</a> for more details.
*/
Locator locator(String selector);
default Locator locator(String selector) {
return locator(selector, null);
}
/**
* Returns locator to the n-th matching frame.
* The method finds an element matching the specified selector in the FrameLocator's subtree.
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors">working with
* selectors</a> for more details.
*/
Locator locator(String selector, LocatorOptions options);
/**
* Returns locator to the n-th matching frame. It's zero based, {@code nth(0)} selects the first frame.
*/
FrameLocator nth(int index);
}
@@ -17,7 +17,6 @@
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 {@link Keyboard#type Keyboard.type()},
File diff suppressed because it is too large Load Diff
@@ -17,7 +17,6 @@
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.
@@ -123,12 +122,12 @@ public interface Mouse {
}
class MoveOptions {
/**
* defaults to 1. Sends intermediate {@code mousemove} events.
* Defaults to 1. Sends intermediate {@code mousemove} events.
*/
public Integer steps;
/**
* defaults to 1. Sends intermediate {@code mousemove} events.
* Defaults to 1. Sends intermediate {@code mousemove} events.
*/
public MoveOptions setSteps(int steps) {
this.steps = steps;
File diff suppressed because it is too large Load Diff
@@ -64,9 +64,13 @@ public interface Playwright extends AutoCloseable {
* This object can be used to launch or connect to Firefox, returning instances of {@code Browser}.
*/
BrowserType firefox();
/**
* Exposes API that can be used for the Web API testing.
*/
APIRequest request();
/**
* 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.
* href="https://playwright.dev/java/docs/selectors">Working with selectors</a> for more information.
*/
Selectors selectors();
/**
@@ -58,8 +58,9 @@ public interface Request {
*/
Frame frame();
/**
* **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use {@link Request#allHeaders
* Request.allHeaders()} instead.
* An object with the request HTTP headers. The header names are lower-cased. Note that this method does not return
* security-related headers, including cookie-related ones. You can use {@link Request#allHeaders Request.allHeaders()} for
* complete list of headers that include {@code cookie} information.
*/
Map<String, String> headers();
/**
@@ -40,8 +40,14 @@ public interface Response {
*/
Frame frame();
/**
* **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use {@link Response#allHeaders
* Response.allHeaders()} instead.
* Indicates whether this Response was fulfilled by a Service Worker's Fetch Handler (i.e. via <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith">FetchEvent.respondWith</a>).
*/
boolean fromServiceWorker();
/**
* An object with the response HTTP headers. The header names are lower-cased. Note that this method does not return
* security-related headers, including cookie-related ones. You can use {@link Response#allHeaders Response.allHeaders()}
* for complete list of headers that include {@code cookie} information.
*/
Map<String, String> headers();
/**
@@ -16,13 +16,14 @@
package com.microsoft.playwright;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
/**
* 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.
*
* <p> Learn more about <a href="https://playwright.dev/java/docs/network">networking</a>.
*/
public interface Route {
class ResumeOptions {
@@ -79,6 +80,62 @@ public interface Route {
return this;
}
}
class FallbackOptions {
/**
* If set changes the request HTTP headers. Header values will be converted to a string.
*/
public Map<String, String> headers;
/**
* If set changes the request method (e.g. GET or POST)
*/
public String method;
/**
* If set changes the post data of request
*/
public Object postData;
/**
* If set changes the request URL. New URL must have same protocol as original one. Changing the URL won't affect the route
* matching, all the routes are matched using the original request URL.
*/
public String url;
/**
* If set changes the request HTTP headers. Header values will be converted to a string.
*/
public FallbackOptions setHeaders(Map<String, String> headers) {
this.headers = headers;
return this;
}
/**
* If set changes the request method (e.g. GET or POST)
*/
public FallbackOptions setMethod(String method) {
this.method = method;
return this;
}
/**
* If set changes the post data of request
*/
public FallbackOptions setPostData(String postData) {
this.postData = postData;
return this;
}
/**
* If set changes the post data of request
*/
public FallbackOptions setPostData(byte[] postData) {
this.postData = postData;
return this;
}
/**
* If set changes the request URL. New URL must have same protocol as original one. Changing the URL won't affect the route
* matching, all the routes are matched using the original request URL.
*/
public FallbackOptions setUrl(String url) {
this.url = url;
return this;
}
}
class FulfillOptions {
/**
* Optional response body as text.
@@ -101,6 +158,11 @@ public interface Route {
* is resolved relative to the current working directory.
*/
public Path path;
/**
* {@code APIResponse} to fulfill route's request with. Individual fields of the response (such as headers) can be overridden
* using fulfill options.
*/
public APIResponse response;
/**
* Response status code, defaults to {@code 200}.
*/
@@ -142,6 +204,14 @@ public interface Route {
this.path = path;
return this;
}
/**
* {@code APIResponse} to fulfill route's request with. Individual fields of the response (such as headers) can be overridden
* using fulfill options.
*/
public FulfillOptions setResponse(APIResponse response) {
this.response = response;
return this;
}
/**
* Response status code, defaults to {@code 200}.
*/
@@ -186,8 +256,8 @@ public interface Route {
* 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
* headers.put("foo", "foo-value"); // set "foo" header
* headers.remove("bar"); // remove "bar" header
* route.resume(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
@@ -201,13 +271,133 @@ public interface Route {
* 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
* headers.put("foo", "foo-value"); // set "foo" header
* headers.remove("bar"); // remove "bar" header
* route.resume(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
*/
void resume(ResumeOptions options);
/**
* When several routes match the given pattern, they run in the order opposite to their registration. That way the last
* registered route can always override all the previous ones. In the example below, request will be handled by the
* bottom-most handler first, then it'll fall back to the previous one and in the end will be aborted by the first
* registered route.
* <pre>{@code
* page.route("**\/*", route -> {
* // Runs last.
* route.abort();
* });
*
* page.route("**\/*", route -> {
* // Runs second.
* route.fallback();
* });
*
* page.route("**\/*", route -> {
* // Runs first.
* route.fallback();
* });
* }</pre>
*
* <p> Registering multiple routes is useful when you want separate handlers to handle different kinds of requests, for example
* API calls vs page resources or GET requests vs POST requests as in the example below.
* <pre>{@code
* // Handle GET requests.
* page.route("**\/*", route -> {
* if (!route.request().method().equals("GET")) {
* route.fallback();
* return;
* }
* // Handling GET only.
* // ...
* });
*
* // Handle POST requests.
* page.route("**\/*", route -> {
* if (!route.request().method().equals("POST")) {
* route.fallback();
* return;
* }
* // Handling POST only.
* // ...
* });
* }</pre>
*
* <p> One can also modify request while falling back to the subsequent handler, that way intermediate route handler can modify
* url, method, headers and postData of the request.
* <pre>{@code
* page.route("**\/*", route -> {
* // Override headers
* Map<String, String> headers = new HashMap<>(route.request().headers());
* headers.put("foo", "foo-value"); // set "foo" header
* headers.remove("bar"); // remove "bar" header
* route.fallback(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
*/
default void fallback() {
fallback(null);
}
/**
* When several routes match the given pattern, they run in the order opposite to their registration. That way the last
* registered route can always override all the previous ones. In the example below, request will be handled by the
* bottom-most handler first, then it'll fall back to the previous one and in the end will be aborted by the first
* registered route.
* <pre>{@code
* page.route("**\/*", route -> {
* // Runs last.
* route.abort();
* });
*
* page.route("**\/*", route -> {
* // Runs second.
* route.fallback();
* });
*
* page.route("**\/*", route -> {
* // Runs first.
* route.fallback();
* });
* }</pre>
*
* <p> Registering multiple routes is useful when you want separate handlers to handle different kinds of requests, for example
* API calls vs page resources or GET requests vs POST requests as in the example below.
* <pre>{@code
* // Handle GET requests.
* page.route("**\/*", route -> {
* if (!route.request().method().equals("GET")) {
* route.fallback();
* return;
* }
* // Handling GET only.
* // ...
* });
*
* // Handle POST requests.
* page.route("**\/*", route -> {
* if (!route.request().method().equals("POST")) {
* route.fallback();
* return;
* }
* // Handling POST only.
* // ...
* });
* }</pre>
*
* <p> One can also modify request while falling back to the subsequent handler, that way intermediate route handler can modify
* url, method, headers and postData of the request.
* <pre>{@code
* page.route("**\/*", route -> {
* // Override headers
* Map<String, String> headers = new HashMap<>(route.request().headers());
* headers.put("foo", "foo-value"); // set "foo" header
* headers.remove("bar"); // remove "bar" header
* route.fallback(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
*/
void fallback(FallbackOptions options);
/**
* Fulfills route's request with given response.
*
@@ -17,11 +17,10 @@
package com.microsoft.playwright;
import java.nio.file.Path;
import java.util.*;
/**
* 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.
* href="https://playwright.dev/java/docs/selectors">Working with selectors</a> for more information.
*/
public interface Selectors {
class RegisterOptions {
@@ -45,7 +44,7 @@ public interface Selectors {
/**
* An example of registering selector engine that queries elements based on a tag name:
* <pre>{@code
* // Script that evaluates to a selector engine instance.
* // Script that evaluates to a selector engine instance. The script is evaluated in the page context.
* String createTagNameEngine = "{\n" +
* " // Returns the first element matching given selector in the root's subtree.\n" +
* " query(root, selector) {\n" +
@@ -62,17 +61,17 @@ public interface Selectors {
* 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");
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* page.locator("tag=div >> text=\"Click me\"").click();
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* int buttonCount = (int) page.locator("tag=button").count();
* 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.
* @param script Script that evaluates to a selector engine instance. The script is evaluated in the page context.
*/
default void register(String name, String script) {
register(name, script, null);
@@ -80,7 +79,7 @@ public interface Selectors {
/**
* An example of registering selector engine that queries elements based on a tag name:
* <pre>{@code
* // Script that evaluates to a selector engine instance.
* // Script that evaluates to a selector engine instance. The script is evaluated in the page context.
* String createTagNameEngine = "{\n" +
* " // Returns the first element matching given selector in the root's subtree.\n" +
* " query(root, selector) {\n" +
@@ -97,23 +96,23 @@ public interface Selectors {
* 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");
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* page.locator("tag=div >> text=\"Click me\"").click();
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* int buttonCount = (int) page.locator("tag=button").count();
* 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.
* @param script Script that evaluates to a selector engine instance. The script is evaluated in the page context.
*/
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.
* // Script that evaluates to a selector engine instance. The script is evaluated in the page context.
* String createTagNameEngine = "{\n" +
* " // Returns the first element matching given selector in the root's subtree.\n" +
* " query(root, selector) {\n" +
@@ -130,17 +129,17 @@ public interface Selectors {
* 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");
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* page.locator("tag=div >> text=\"Click me\"").click();
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* int buttonCount = (int) page.locator("tag=button").count();
* 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.
* @param script Script that evaluates to a selector engine instance. The script is evaluated in the page context.
*/
default void register(String name, Path script) {
register(name, script, null);
@@ -148,7 +147,7 @@ public interface Selectors {
/**
* An example of registering selector engine that queries elements based on a tag name:
* <pre>{@code
* // Script that evaluates to a selector engine instance.
* // Script that evaluates to a selector engine instance. The script is evaluated in the page context.
* String createTagNameEngine = "{\n" +
* " // Returns the first element matching given selector in the root's subtree.\n" +
* " query(root, selector) {\n" +
@@ -165,17 +164,17 @@ public interface Selectors {
* 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");
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* page.locator("tag=div >> text=\"Click me\"").click();
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* int buttonCount = (int) page.locator("tag=button").count();
* 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.
* @param script Script that evaluates to a selector engine instance. The script is evaluated in the page context.
*/
void register(String name, Path script, RegisterOptions options);
}
@@ -16,7 +16,6 @@
package com.microsoft.playwright;
import java.util.*;
/**
* The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the
@@ -17,11 +17,10 @@
package com.microsoft.playwright;
import java.nio.file.Path;
import java.util.*;
/**
* API for collecting and saving Playwright traces. Playwright traces can be opened in <a
* href="https://playwright.dev/java/docs/trace-viewer/">Trace Viewer</a> after Playwright script runs.
* href="https://playwright.dev/java/docs/trace-viewer">Trace Viewer</a> after Playwright script runs.
*
* <p> Start recording a trace before performing actions. At the end, stop tracing and save it to a file.
* <pre>{@code
@@ -48,9 +47,19 @@ public interface Tracing {
*/
public Boolean screenshots;
/**
* Whether to capture DOM snapshot on every action.
* If this option is true tracing will
* <ul>
* <li> capture DOM snapshot on every action</li>
* <li> record network activity</li>
* </ul>
*/
public Boolean snapshots;
/**
* Whether to include source files for trace actions. List of the directories with source code for the application must be
* provided via {@code PLAYWRIGHT_JAVA_SRC} environment variable (the paths should be separated by ';' on Windows and by ':' on
* other platforms).
*/
public Boolean sources;
/**
* Trace name to be shown in the Trace Viewer.
*/
@@ -72,12 +81,25 @@ public interface Tracing {
return this;
}
/**
* Whether to capture DOM snapshot on every action.
* If this option is true tracing will
* <ul>
* <li> capture DOM snapshot on every action</li>
* <li> record network activity</li>
* </ul>
*/
public StartOptions setSnapshots(boolean snapshots) {
this.snapshots = snapshots;
return this;
}
/**
* Whether to include source files for trace actions. List of the directories with source code for the application must be
* provided via {@code PLAYWRIGHT_JAVA_SRC} environment variable (the paths should be separated by ';' on Windows and by ':' on
* other platforms).
*/
public StartOptions setSources(boolean sources) {
this.sources = sources;
return this;
}
/**
* Trace name to be shown in the Trace Viewer.
*/
@@ -170,7 +192,7 @@ public interface Tracing {
* page.navigate("https://playwright.dev");
*
* context.tracing().startChunk();
* page.click("text=Get Started");
* page.locator("text=Get Started").click();
* // Everything between startChunk and stopChunk will be recorded in the trace.
* context.tracing().stopChunk(new Tracing.StopChunkOptions()
* .setPath(Paths.get("trace1.zip")));
@@ -197,7 +219,7 @@ public interface Tracing {
* page.navigate("https://playwright.dev");
*
* context.tracing().startChunk();
* page.click("text=Get Started");
* page.locator("text=Get Started").click();
* // Everything between startChunk and stopChunk will be recorded in the trace.
* context.tracing().stopChunk(new Tracing.StopChunkOptions()
* .setPath(Paths.get("trace1.zip")));
@@ -17,7 +17,6 @@
package com.microsoft.playwright;
import java.nio.file.Path;
import java.util.*;
/**
* When browser context is created with the {@code recordVideo} option, each page has a video object associated with it.
@@ -16,7 +16,6 @@
package com.microsoft.playwright;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -16,7 +16,6 @@
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
@@ -16,7 +16,6 @@
package com.microsoft.playwright;
import java.util.*;
import java.util.function.Consumer;
/**
@@ -0,0 +1,56 @@
/*
* 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.assertions;
/**
* The {@code APIResponseAssertions} class provides assertion methods that can be used to make assertions about the {@code APIResponse}
* in the tests. A new instance of {@code APIResponseAssertions} is created by calling {@link PlaywrightAssertions#assertThat
* PlaywrightAssertions.assertThat()}:
* <pre>{@code
* ...
* import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
*
* public class TestPage {
* ...
* @Test
* void navigatesToLoginPage() {
* ...
* APIResponse response = page.request().get('https://playwright.dev');
* assertThat(response).isOK();
* }
* }
* }</pre>
*/
public interface APIResponseAssertions {
/**
* Makes the assertion check for the opposite condition. For example, this code tests that the response status is not
* successful:
* <pre>{@code
* assertThat(response).not().isOK();
* }</pre>
*/
APIResponseAssertions not();
/**
* Ensures the response status code is within {@code 200..299} range.
* <pre>{@code
* assertThat(response).isOK();
* }</pre>
*/
void isOK();
}
@@ -16,13 +16,11 @@
package com.microsoft.playwright.assertions;
import java.util.*;
import java.util.regex.Pattern;
import com.microsoft.playwright.Page;
/**
* The {@code PageAssertions} class provides assertion methods that can be used to make assertions about the {@code Page} state in the
* tests. A new instance of {@code LocatorAssertions} is created by calling {@link PlaywrightAssertions#assertThat
* tests. A new instance of {@code PageAssertions} is created by calling {@link PlaywrightAssertions#assertThat
* PlaywrightAssertions.assertThat()}:
* <pre>{@code
* ...
@@ -33,7 +31,7 @@ import com.microsoft.playwright.Page;
* @Test
* void navigatesToLoginPage() {
* ...
* page.click("#login");
* page.locator("#login").click();
* assertThat(page).hasURL(Pattern.compile(".*\/login"));
* }
* }
@@ -68,6 +66,14 @@ public interface PageAssertions {
return this;
}
}
/**
* Makes the assertion check for the opposite condition. For example, this code tests that the page URL doesn't contain
* {@code "error"}:
* <pre>{@code
* assertThat(page).not().hasURL("error");
* }</pre>
*/
PageAssertions not();
/**
* Ensures the page has the given title.
* <pre>{@code
@@ -114,7 +120,7 @@ public interface PageAssertions {
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
* @param urlOrRegExp Expected URL string or RegExp.
*/
default void hasURL(String urlOrRegExp) {
hasURL(urlOrRegExp, null);
@@ -125,7 +131,7 @@ public interface PageAssertions {
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
* @param urlOrRegExp Expected URL string or RegExp.
*/
void hasURL(String urlOrRegExp, HasURLOptions options);
/**
@@ -134,7 +140,7 @@ public interface PageAssertions {
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
* @param urlOrRegExp Expected URL string or RegExp.
*/
default void hasURL(Pattern urlOrRegExp) {
hasURL(urlOrRegExp, null);
@@ -145,16 +151,8 @@ public interface PageAssertions {
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
* @param urlOrRegExp Expected URL string or RegExp.
*/
void hasURL(Pattern urlOrRegExp, HasURLOptions options);
/**
* Makes the assertion check for the opposite condition. For example, this code tests that the page URL doesn't contain
* {@code "error"}:
* <pre>{@code
* assertThat(page).not().hasURL("error");
* }</pre>
*/
PageAssertions not();
}
@@ -16,15 +16,17 @@
package com.microsoft.playwright.assertions;
import java.util.*;
import com.microsoft.playwright.APIResponse;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.impl.APIResponseAssertionsImpl;
import com.microsoft.playwright.impl.AssertionsTimeout;
import com.microsoft.playwright.impl.LocatorAssertionsImpl;
import com.microsoft.playwright.impl.PageAssertionsImpl;
/**
* The {@code PlaywrightAssertions} class provides convenience methods for creating assertions that will wait until the expected
* condition is met.
* Playwright gives you Web-First Assertions with convenience methods for creating assertions that will wait and retry
* until the expected condition is met.
*
* <p> Consider the following example:
* <pre>{@code
@@ -36,7 +38,7 @@ import com.microsoft.playwright.impl.PageAssertionsImpl;
* @Test
* void statusBecomesSubmitted() {
* ...
* page.click("#submit-button");
* page.locator("#submit-button").click();
* assertThat(page.locator(".status")).hasText("Submitted");
* }
* }
@@ -47,10 +49,20 @@ import com.microsoft.playwright.impl.PageAssertionsImpl;
* You can pass this timeout as an option.
*
* <p> By default, the timeout for assertions is set to 5 seconds.
*
* <p> To use Playwright assertions add the following dependency into the {@code pom.xml} of your Maven project:
*/
public interface PlaywrightAssertions {
/**
* Creates a {@code APIResponseAssertions} object for the given {@code APIResponse}.
* <pre>{@code
* PlaywrightAssertions.assertThat(response).isOK();
* }</pre>
*
* @param response {@code APIResponse} object to use for assertions.
*/
static APIResponseAssertions assertThat(APIResponse response) {
return new APIResponseAssertionsImpl(response);
}
/**
* Creates a {@code LocatorAssertions} object for the given {@code Locator}.
* <pre>{@code
@@ -75,5 +87,17 @@ public interface PlaywrightAssertions {
return new PageAssertionsImpl(page);
}
/**
* Changes default timeout for Playwright assertions from 5 seconds to the specified value.
* <pre>{@code
* PlaywrightAssertions.setDefaultAssertionTimeout(30_000);
* }</pre>
*
* @param timeout Timeout in milliseconds.
*/
static void setDefaultAssertionTimeout(double milliseconds) {
AssertionsTimeout.setDefaultTimeout(milliseconds);
}
}
@@ -0,0 +1,205 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.APIRequestContext;
import com.microsoft.playwright.APIResponse;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Request;
import com.microsoft.playwright.options.FilePayload;
import com.microsoft.playwright.options.RequestOptions;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.toFilePayload;
class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
private final TracingImpl tracing;
APIRequestContextImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
this.tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString());
}
@Override
public APIResponse delete(String url, RequestOptions options) {
return fetch(url, ensureOptions(options, "DELETE"));
}
@Override
public void dispose() {
withLogging("APIRequestContext.dispose", () -> sendMessage("dispose"));
}
@Override
public APIResponse fetch(String urlOrRequest, RequestOptions options) {
return withLogging("APIRequestContext.fetch", () -> fetchImpl(urlOrRequest, (RequestOptionsImpl) options));
}
@Override
public APIResponse fetch(Request request, RequestOptions optionsArg) {
RequestOptionsImpl options = (RequestOptionsImpl) optionsArg;
if (options == null) {
options = new RequestOptionsImpl();
}
if (options.method == null) {
options.method = request.method();
}
if (options.headers == null) {
options.headers = request.headers();
}
if (options.data == null && options.form == null && options.multipart == null) {
options.data = request.postDataBuffer();
}
return fetch(request.url(), options);
}
private APIResponse fetchImpl(String url, RequestOptionsImpl options) {
if (options == null) {
options = new RequestOptionsImpl();
}
JsonObject params = new JsonObject();
params.addProperty("url", url);
if (options.params != null) {
Map<String, String> queryParams = new LinkedHashMap<>();
for (Map.Entry<String, ?> e : options.params.entrySet()) {
queryParams.put(e.getKey(), "" + e.getValue());
}
params.add("params", toNameValueArray(queryParams));
}
if (options.method != null) {
params.addProperty("method", options.method);
}
if (options.headers != null) {
params.add("headers", toProtocol(options.headers));
}
if (options.data != null) {
byte[] bytes = null;
if (options.data instanceof byte[]) {
bytes = (byte[]) options.data;
} else if (options.data instanceof String && !isJsonContentType(options.headers)) {
bytes = ((String) options.data).getBytes(StandardCharsets.UTF_8);
}
if (bytes == null) {
params.add("jsonData", gson().toJsonTree(options.data));
} else {
String base64 = Base64.getEncoder().encodeToString(bytes);
params.addProperty("postData", base64);
}
}
if (options.form != null) {
params.add("formData", toNameValueArray(options.form.fields));
}
if (options.multipart != null) {
params.add("multipartData", serializeMultipartData(options.multipart.fields));
}
if (options.timeout != null) {
params.addProperty("timeout", options.timeout);
}
if (options.failOnStatusCode != null) {
params.addProperty("failOnStatusCode", options.failOnStatusCode);
}
if (options.ignoreHTTPSErrors != null) {
params.addProperty("ignoreHTTPSErrors", options.ignoreHTTPSErrors);
}
if (options.maxRedirects != null) {
if (options.maxRedirects < 0) {
throw new PlaywrightException("'maxRedirects' should be greater than or equal to '0'");
}
params.addProperty("maxRedirects", options.maxRedirects);
}
JsonObject json = sendMessage("fetch", params).getAsJsonObject();
return new APIResponseImpl(this, json.getAsJsonObject("response"));
}
private static boolean isJsonContentType(Map<String, String> headers) {
if (headers == null) {
return false;
}
for (Map.Entry<String, String> e : headers.entrySet()) {
if ("content-type".equalsIgnoreCase(e.getKey())) {
return "application/json".equals(e.getValue());
}
}
return false;
}
private static JsonArray serializeMultipartData(Map<String, Object> data) {
JsonArray result = new JsonArray();
for (Map.Entry<String, Object> e : data.entrySet()) {
FilePayload filePayload = null;
if (e.getValue() instanceof FilePayload) {
filePayload = (FilePayload) e.getValue();
} else if (e.getValue() instanceof Path) {
filePayload = toFilePayload((Path) e.getValue());
} else if (e.getValue() instanceof File) {
filePayload = toFilePayload(((File) e.getValue()).toPath());
}
JsonObject item = new JsonObject();
item.addProperty("name", e.getKey());
if (filePayload == null) {
item.addProperty("value", "" + e.getValue());
} else {
item.add("file", toProtocol(filePayload));
}
result.add(item);
}
return result;
}
@Override
public APIResponse get(String url, RequestOptions options) {
return fetch(url, ensureOptions(options, "GET"));
}
@Override
public APIResponse head(String url, RequestOptions options) {
return fetch(url, ensureOptions(options, "HEAD"));
}
@Override
public APIResponse patch(String url, RequestOptions options) {
return fetch(url, ensureOptions(options, "PATCH"));
}
@Override
public APIResponse post(String url, RequestOptions options) {
return fetch(url, ensureOptions(options, "POST"));
}
@Override
public APIResponse put(String url, RequestOptions options) {
return fetch(url, ensureOptions(options, "PUT"));
}
@Override
public String storageState(StorageStateOptions options) {
return withLogging("APIRequestContext.storageState", () -> {
JsonElement json = sendMessage("storageState");
String storageState = json.toString();
if (options != null && options.path != null) {
Utils.writeToFile(storageState.getBytes(StandardCharsets.UTF_8), options.path);
}
return storageState;
});
}
private static RequestOptionsImpl ensureOptions(RequestOptions options, String method) {
RequestOptionsImpl impl = (RequestOptionsImpl) options;
if (impl == null) {
impl = new RequestOptionsImpl();
}
if (impl.method == null) {
impl.method = method;
}
return impl;
}
}
@@ -0,0 +1,53 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.microsoft.playwright.APIRequest;
import com.microsoft.playwright.PlaywrightException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import static com.microsoft.playwright.impl.Serialization.gson;
class APIRequestImpl implements APIRequest {
private final PlaywrightImpl playwright;
APIRequestImpl(PlaywrightImpl playwright) {
this.playwright = playwright;
}
@Override
public APIRequestContextImpl newContext(NewContextOptions options) {
return playwright.withLogging("APIRequest.newContext", () -> newContextImpl(options));
}
private APIRequestContextImpl newContextImpl(NewContextOptions options) {
if (options == null) {
options = new NewContextOptions();
}
if (options.storageStatePath != null) {
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);
}
JsonObject result = playwright.sendMessage("newRequest", params).getAsJsonObject();
APIRequestContextImpl context = playwright.connection.getExistingObject(result.getAsJsonObject("request").get("guid").getAsString());
return context;
}
}
@@ -0,0 +1,74 @@
/*
* 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.APIResponse;
import com.microsoft.playwright.assertions.APIResponseAssertions;
import org.opentest4j.AssertionFailedError;
import java.util.List;
import java.util.regex.Pattern;
public class APIResponseAssertionsImpl implements APIResponseAssertions {
private final APIResponse actual;
private final boolean isNot;
APIResponseAssertionsImpl(APIResponse response, boolean isNot) {
this.actual = response;
this.isNot = isNot;
}
public APIResponseAssertionsImpl(APIResponse response) {
this(response, false);
}
@Override
public APIResponseAssertions not() {
return new APIResponseAssertionsImpl(actual, !isNot);
}
@Override
public void isOK() {
if (actual.ok() == !isNot) {
return;
}
String message = "Response status expected to be within [200..299] range, was " + actual.status();
if (isNot) {
message = message.replace("expected to", "expected not to");
}
List<String> logList = ((APIResponseImpl) actual).fetchLog();
String log = String.join("\n", logList);
if (!log.isEmpty()) {
log = "\nCall log:\n" + log;
}
String contentType = actual.headers().get("content-type");
boolean isTextEncoding = contentType == null ? false : isTextualMimeType(contentType);
String responseText = "";
if (isTextEncoding) {
String text = actual.text();
if (text != null) {
responseText = "\nResponse text:\n" + (text.length() > 1000 ? text.substring(0, 1000) : text);
}
}
throw new AssertionFailedError(message + log + responseText);
}
static boolean isTextualMimeType(String mimeType) {
return Pattern.matches("^(text/.*?|application/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image/svg(\\+xml)?|application/.*?(\\+json|\\+xml))(;\\s*charset=.*)?$", mimeType);
}
}
@@ -0,0 +1,124 @@
/*
* 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.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import com.microsoft.playwright.APIResponse;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.HttpHeader;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
import static java.util.Arrays.asList;
class APIResponseImpl implements APIResponse {
final APIRequestContextImpl context;
private final JsonObject initializer;
private final RawHeaders headers;
APIResponseImpl(APIRequestContextImpl apiRequestContext, JsonObject response) {
context = apiRequestContext;
initializer = response;
headers = new RawHeaders(asList(gson().fromJson(initializer.getAsJsonArray("headers"), HttpHeader[].class)));
}
@Override
public byte[] body() {
return context.withLogging("APIResponse.body", () -> {
try {
JsonObject params = new JsonObject();
params.addProperty("fetchUid", fetchUid());
JsonObject json = context.sendMessage("fetchResponseBody", params).getAsJsonObject();
if (!json.has("binary")) {
throw new PlaywrightException("Response has been disposed");
}
return Base64.getDecoder().decode(json.get("binary").getAsString());
} catch (PlaywrightException e) {
if (isSafeCloseError(e)) {
throw new PlaywrightException("Response has been disposed");
}
throw e;
}
});
}
@Override
public void dispose() {
context.withLogging("APIResponse.dispose", () -> {
JsonObject params = new JsonObject();
params.addProperty("fetchUid", fetchUid());
context.sendMessage("disposeAPIResponse", params);
});
}
@Override
public Map<String, String> headers() {
return headers.headers();
}
@Override
public List<HttpHeader> headersArray() {
return headers.headersArray();
}
@Override
public boolean ok() {
int status = status();
return status == 0 || (status >= 200 && status <= 299);
}
@Override
public int status() {
return initializer.get("status").getAsInt();
}
@Override
public String statusText() {
return initializer.get("statusText").getAsString();
}
@Override
public String text() {
return new String(body(), StandardCharsets.UTF_8);
}
@Override
public String url() {
return initializer.get("url").getAsString();
}
String fetchUid() {
return initializer.get("fetchUid").getAsString();
}
List<String> fetchLog() {
JsonObject params = new JsonObject();
params.addProperty("fetchUid", fetchUid());
JsonObject json = context.sendMessage("fetchLog", params).getAsJsonObject();
JsonArray log = json.get("log").getAsJsonArray();
return gson().fromJson(log, new TypeToken<List<String>>() {}.getType());
}
}
@@ -25,6 +25,7 @@ import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
import static java.util.Arrays.asList;
class AssertionsBase {
@@ -51,7 +52,7 @@ class AssertionsBase {
void expectImpl(String expression, FrameExpectOptions expectOptions, Object expected, String message) {
if (expectOptions.timeout == null) {
expectOptions.timeout = 5_000.0;
expectOptions.timeout = AssertionsTimeout.defaultTimeout;
}
if (expectOptions.isNot) {
message = message.replace("expected to", "expected not to");
@@ -66,7 +67,10 @@ class AssertionsBase {
if (expected == null) {
throw new AssertionFailedError(message + log);
}
throw new AssertionFailedError(message + log, formatValue(expected), formatValue(actual));
ValueWrapper expectedValue = formatValue(expected);
ValueWrapper actualValue = formatValue(actual);
message += ": " + expectedValue.getStringRepresentation() + "\nReceived: " + actualValue.getStringRepresentation() + "\n";
throw new AssertionFailedError(message + log, expectedValue, actualValue);
}
}
@@ -83,22 +87,7 @@ class AssertionsBase {
ExpectedTextValue expected = new ExpectedTextValue();
expected.regexSource = pattern.pattern();
if (pattern.flags() != 0) {
expected.regexFlags = "";
if ((pattern.flags() & Pattern.CASE_INSENSITIVE) != 0) {
// Case-insensitive search.
expected.regexFlags += "i";
}
if ((pattern.flags() & Pattern.DOTALL) != 0) {
// Allows . to match newline characters.
expected.regexFlags += "s";
}
if ((pattern.flags() & Pattern.MULTILINE) != 0) {
// Multi-line search.
expected.regexFlags += "m";
}
if ((pattern.flags() & ~(Pattern.MULTILINE | Pattern.CASE_INSENSITIVE | Pattern.DOTALL)) != 0) {
throw new PlaywrightException("Unexpected RegEx flag, only CASE_INSENSITIVE, DOTALL and MULTILINE are supported.");
}
expected.regexFlags = toJsRegexFlags(pattern);
}
return expected;
}
@@ -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.impl;
import com.microsoft.playwright.PlaywrightException;
public class AssertionsTimeout {
static double defaultTimeout = 5_000;
public static void setDefaultTimeout(double ms) {
if (ms < 0) {
throw new PlaywrightException("Timeout cannot be negative");
}
defaultTimeout = ms;
}
}
@@ -20,24 +20,20 @@ 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 com.microsoft.playwright.options.*;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -47,6 +43,7 @@ import static java.util.Arrays.asList;
class BrowserContextImpl extends ChannelOwner implements BrowserContext {
private final BrowserImpl browser;
private final TracingImpl tracing;
private final APIRequestContextImpl request;
final List<PageImpl> pages = new ArrayList<>();
final Router routes = new Router();
private boolean isClosedOrClosing;
@@ -56,7 +53,17 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
final TimeoutSettings timeoutSettings = new TimeoutSettings();
Path videosDir;
URL baseUrl;
Path recordHarPath;
final Map<String, HarRecorder> harRecorders = new HashMap<>();
static class HarRecorder {
final Path path;
final HarContentPolicy contentPolicy;
HarRecorder(Path har, HarContentPolicy policy) {
path = har;
contentPolicy = policy;
}
}
enum EventType {
CLOSE,
@@ -74,7 +81,15 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} else {
browser = null;
}
this.tracing = new TracingImpl(this);
this.tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString());
tracing.isRemote = browser != null && browser.isRemote;
this.request = connection.getExistingObject(initializer.getAsJsonObject("requestContext").get("guid").getAsString());
}
void setRecordHar(Path path, HarContentPolicy policy) {
if (path != null) {
harRecorders.put("", new HarRecorder(path, policy));
}
}
void setBaseUrl(String spec) {
@@ -155,7 +170,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public Page waitForPage(WaitForPageOptions options, Runnable code) {
return withWaitLogging("BrowserContext.close", () -> waitForPageImpl(options, code));
return withWaitLogging("BrowserContext.close", logger -> waitForPageImpl(options, code));
}
private Page waitForPageImpl(WaitForPageOptions options, Runnable code) {
@@ -172,7 +187,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public List<Cookie> cookies(String url) {
return cookies(url == null ? new ArrayList<>() : asList(url));
return cookies(url == null ? new ArrayList<>() : Collections.singletonList(url));
}
private void closeImpl() {
@@ -181,15 +196,31 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
isClosedOrClosing = true;
try {
if (recordHarPath != null) {
JsonObject json = sendMessage("harExport").getAsJsonObject();
for (Map.Entry<String, HarRecorder> entry : harRecorders.entrySet()) {
JsonObject params = new JsonObject();
params.addProperty("harId", entry.getKey());
JsonObject json = sendMessage("harExport", params).getAsJsonObject();
ArtifactImpl artifact = connection.getExistingObject(json.getAsJsonObject("artifact").get("guid").getAsString());
// In case of CDP connection browser is null but since the connection is established by
// the driver it is safe to consider the artifact local.
if (browser() != null && browser().isRemote) {
artifact.isRemote = true;
}
artifact.saveAs(recordHarPath);
// Server side will compress artifact if content is attach or if file is .zip.
HarRecorder harParams = entry.getValue();
boolean isCompressed = harParams.contentPolicy == HarContentPolicy.ATTACH || harParams.path.toString().endsWith(".zip");
boolean needCompressed = harParams.path.toString().endsWith(".zip");
if (isCompressed && !needCompressed) {
String tmpPath = harParams.path + ".tmp";
artifact.saveAs(Paths.get(tmpPath));
JsonObject unzipParams = new JsonObject();
unzipParams.addProperty("zipFile", tmpPath);
unzipParams.addProperty("harFile", harParams.path.toString());
connection.localUtils.sendMessage("harUnzip", unzipParams);
} else {
artifact.saveAs(harParams.path);
}
artifact.delete();
}
@@ -329,9 +360,14 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
return new ArrayList<>(pages);
}
@Override
public APIRequestContextImpl request() {
return request;
}
@Override
public void route(String url, Consumer<Route> handler, RouteOptions options) {
route(new UrlMatcher(this.baseUrl, url), handler, options);
route(new UrlMatcher(baseUrl, url), handler, options);
}
@Override
@@ -344,6 +380,21 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
route(new UrlMatcher(url), handler, options);
}
@Override
public void routeFromHAR(Path har, RouteFromHAROptions options) {
if (options == null) {
options = new RouteFromHAROptions();
}
if (options.update != null && options.update) {
recordIntoHar(null, har, options);
return;
}
UrlMatcher matcher = UrlMatcher.forOneOf(baseUrl, options.url);
HARRouter harRouter = new HARRouter(connection.localUtils, har, options.notFound);
onClose(context -> harRouter.dispose());
route(matcher, route -> harRouter.handle(route), null);
}
private void route(UrlMatcher matcher, Consumer<Route> handler, RouteOptions options) {
withLogging("BrowserContext.route", () -> {
routes.add(matcher, handler, options == null ? null : options.times);
@@ -355,6 +406,22 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
});
}
void recordIntoHar(PageImpl page, Path har, RouteFromHAROptions options) {
JsonObject params = new JsonObject();
if (page != null) {
params.add("page", page.toProtocolRef());
}
JsonObject jsonOptions = new JsonObject();
jsonOptions.addProperty("path", har.toAbsolutePath().toString());
jsonOptions.addProperty("content", HarContentPolicy.ATTACH.name().toLowerCase());
jsonOptions.addProperty("mode", HarMode.MINIMAL.name().toLowerCase());
addHarUrlFilter(jsonOptions, options.url);
params.add("options", jsonOptions);
JsonObject json = sendMessage("harStart", params).getAsJsonObject();
String harId = json.get("harId").getAsString();
harRecorders.put(harId, new HarRecorder(har, HarContentPolicy.ATTACH));
}
@Override
public void setDefaultNavigationTimeout(double timeout) {
withLogging("BrowserContext.setDefaultNavigationTimeout", () -> {
@@ -424,7 +491,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public Tracing tracing() {
public TracingImpl tracing() {
return tracing;
}
@@ -469,11 +536,12 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
}
void handleRoute(Route route) {
boolean handled = routes.handle(route);
if (handled) {
void handleRoute(RouteImpl route) {
Router.HandleResult handled = routes.handle(route);
if (handled == Router.HandleResult.FoundMatchingHandler) {
maybeDisableNetworkInterception();
} else {
}
if (!route.isHandled()){
route.resume();
}
}
@@ -485,7 +553,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
protected void handleEvent(String event, JsonObject params) {
if ("route".equals(event)) {
Route route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
RouteImpl route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
handleRoute(route);
} else if ("page".equals(event)) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
@@ -555,4 +623,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
listeners.notify(EventType.CLOSE, this);
}
WritableStream createTempFile(String name) {
JsonObject params = new JsonObject();
params.addProperty("name", name);
JsonObject json = sendMessage("createTempFile", params).getAsJsonObject();
return connection.getExistingObject(json.getAsJsonObject("writableStream").get("guid").getAsString());
}
}
@@ -19,20 +19,21 @@ package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.HarContentPolicy;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter;
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 com.microsoft.playwright.impl.Utils.*;
import static com.microsoft.playwright.impl.Utils.convertType;
class BrowserImpl extends ChannelOwner implements Browser {
final Set<BrowserContextImpl> contexts = new HashSet<>();
@@ -40,6 +41,7 @@ class BrowserImpl extends ChannelOwner implements Browser {
boolean isRemote;
boolean isConnectedOverWebSocket;
private boolean isConnected = true;
BrowserTypeImpl browserType;
enum EventType {
DISCONNECTED,
@@ -59,6 +61,11 @@ class BrowserImpl extends ChannelOwner implements Browser {
listeners.remove(EventType.DISCONNECTED, handler);
}
@Override
public BrowserType browserType() {
return browserType;
}
@Override
public void close() {
withLogging("Browser.close", () -> closeImpl());
@@ -111,6 +118,9 @@ class BrowserImpl extends ChannelOwner implements Browser {
private BrowserContextImpl newContextImpl(NewContextOptions options) {
if (options == null) {
options = new NewContextOptions();
} else {
// Make a copy so that we can nullify some fields below.
options = convertType(options, NewContextOptions.class);
}
if (options.storageStatePath != null) {
try {
@@ -126,21 +136,50 @@ class BrowserImpl extends ChannelOwner implements Browser {
storageState = new Gson().fromJson(options.storageState, JsonObject.class);
options.storageState = null;
}
JsonObject recordHar = null;
Path recordHarPath = options.recordHarPath;
HarContentPolicy harContentPolicy = null;
if (options.recordHarPath != null) {
recordHar = new JsonObject();
recordHar.addProperty("path", options.recordHarPath.toString());
if (options.recordHarContent != null) {
harContentPolicy = options.recordHarContent;
} else if (options.recordHarOmitContent != null && options.recordHarOmitContent) {
harContentPolicy = HarContentPolicy.OMIT;
}
if (harContentPolicy != null) {
recordHar.addProperty("content", harContentPolicy.name().toLowerCase());
}
if (options.recordHarMode != null) {
recordHar.addProperty("mode", options.recordHarMode.name().toLowerCase());
}
addHarUrlFilter(recordHar, options.recordHarUrlFilter);
options.recordHarPath = null;
options.recordHarMode = null;
options.recordHarOmitContent = null;
options.recordHarContent = null;
options.recordHarUrlFilter = null;
} else {
if (options.recordHarOmitContent != null) {
throw new PlaywrightException("recordHarOmitContent is set but recordHarPath is null");
}
if (options.recordHarUrlFilter != null) {
throw new PlaywrightException("recordHarUrlFilter is set but recordHarPath is null");
}
if (options.recordHarMode != null) {
throw new PlaywrightException("recordHarMode is set but recordHarPath is null");
}
if (options.recordHarContent != null) {
throw new PlaywrightException("recordHarContent is set but recordHarPath is 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");
if (recordHar != null) {
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();
@@ -170,7 +209,7 @@ class BrowserImpl extends ChannelOwner implements Browser {
if (options.baseURL != null) {
context.setBaseUrl(options.baseURL);
}
context.recordHarPath = options.recordHarPath;
context.setRecordHar(recordHarPath, harContentPolicy);
contexts.add(context);
return context;
}
@@ -191,9 +230,7 @@ class BrowserImpl extends ChannelOwner implements Browser {
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
if (page != null) {
JsonObject jsonPage = new JsonObject();
jsonPage.addProperty("guid", ((PageImpl) page).guid);
params.add("page", jsonPage);
params.add("page", ((PageImpl) page).toProtocolRef());
}
sendMessage("startTracing", params);
}
@@ -209,7 +246,7 @@ class BrowserImpl extends ChannelOwner implements Browser {
}
private Page newPageImpl(NewPageOptions options) {
BrowserContextImpl context = newContext(convertViaJson(options, NewContextOptions.class));
BrowserContextImpl context = newContext(convertType(options, NewContextOptions.class));
PageImpl page = context.newPage();
page.ownedContext = context;
context.ownerPage = page;
@@ -22,14 +22,19 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.HarContentPolicy;
import java.io.IOException;
import java.nio.file.Path;
import java.util.function.Consumer;
import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.convertType;
class BrowserTypeImpl extends ChannelOwner implements BrowserType {
LocalUtils localUtils;
BrowserTypeImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@@ -45,7 +50,9 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
JsonElement result = sendMessage("launch", params);
return connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("browser").get("guid").getAsString());
BrowserImpl browser = connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("browser").get("guid").getAsString());
browser.browserType = this;
return browser;
}
@Override
@@ -60,9 +67,25 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
// We don't use gson() here as the headers map should be serialized to a json object.
JsonObject params = new Gson().toJsonTree(options).getAsJsonObject();
params.addProperty("wsEndpoint", wsEndpoint);
if (!params.has("headers")) {
params.add("headers", new JsonObject());
}
JsonObject headers = params.get("headers").getAsJsonObject();
boolean foundBrowserHeader = false;
for (String name : headers.keySet()) {
if ("x-playwright-browser".equalsIgnoreCase(name)) {
foundBrowserHeader = true;
break;
}
}
if (!foundBrowserHeader) {
headers.addProperty("x-playwright-browser", name());
}
JsonObject json = sendMessage("connect", params).getAsJsonObject();
JsonPipe pipe = connection.getExistingObject(json.getAsJsonObject("pipe").get("guid").getAsString());
Connection connection = new Connection(pipe);
Connection connection = new Connection(pipe, this.connection.env);
PlaywrightImpl playwright = connection.initializePlaywright();
if (!playwright.initializer.has("preLaunchedBrowser")) {
try {
@@ -76,6 +99,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
BrowserImpl browser = connection.getExistingObject(playwright.initializer.getAsJsonObject("preLaunchedBrowser").get("guid").getAsString());
browser.isRemote = true;
browser.isConnectedOverWebSocket = true;
browser.browserType = this;
Consumer<JsonPipe> connectionCloseListener = t -> browser.notifyRemoteClosed();
pipe.onClose(connectionCloseListener);
browser.onDisconnected(b -> {
@@ -109,6 +133,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
BrowserImpl browser = connection.getExistingObject(json.getAsJsonObject("browser").get("guid").getAsString());
browser.isRemote = true;
browser.browserType = this;
if (json.has("defaultContext")) {
String contextId = json.getAsJsonObject("defaultContext").get("guid").getAsString();
BrowserContextImpl defaultContext = connection.getExistingObject(contextId);
@@ -130,20 +155,52 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
private BrowserContextImpl launchPersistentContextImpl(Path userDataDir, LaunchPersistentContextOptions options) {
if (options == null) {
options = new LaunchPersistentContextOptions();
} else {
// Make a copy so that we can nullify some fields below.
options = convertType(options, LaunchPersistentContextOptions.class);
}
JsonObject recordHar = null;
Path recordHarPath = options.recordHarPath;
HarContentPolicy harContentPolicy = null;
if (options.recordHarPath != null) {
recordHar = new JsonObject();
recordHar.addProperty("path", options.recordHarPath.toString());
if (options.recordHarContent != null) {
harContentPolicy = options.recordHarContent;
} else if (options.recordHarOmitContent != null && options.recordHarOmitContent) {
harContentPolicy = HarContentPolicy.OMIT;
}
if (harContentPolicy != null) {
recordHar.addProperty("content", harContentPolicy.name().toLowerCase());
}
if (options.recordHarMode != null) {
recordHar.addProperty("mode", options.recordHarMode.toString().toLowerCase());
}
addHarUrlFilter(recordHar, options.recordHarUrlFilter);
options.recordHarPath = null;
options.recordHarMode = null;
options.recordHarOmitContent = null;
options.recordHarContent = null;
options.recordHarUrlFilter = null;
} else {
if (options.recordHarOmitContent != null) {
throw new PlaywrightException("recordHarOmitContent is set but recordHarPath is null");
}
if (options.recordHarUrlFilter != null) {
throw new PlaywrightException("recordHarUrlFilter is set but recordHarPath is null");
}
if (options.recordHarMode != null) {
throw new PlaywrightException("recordHarMode is set but recordHarPath is null");
}
if (options.recordHarContent != null) {
throw new PlaywrightException("recordHarContent is set but recordHarPath is null");
}
}
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");
if (recordHar != null) {
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();
@@ -173,7 +230,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
if (options.baseURL != null) {
context.setBaseUrl(options.baseURL);
}
context.recordHarPath = options.recordHarPath;
context.setRecordHar(recordHarPath, harContentPolicy);
return context;
}
@@ -22,11 +22,13 @@ import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
class ChannelOwner extends LoggingSupport {
final Connection connection;
private final ChannelOwner parent;
private ChannelOwner parent;
private final Map<String, ChannelOwner> objects = new HashMap<>();
final String type;
@@ -68,7 +70,13 @@ class ChannelOwner extends LoggingSupport {
objects.clear();
}
<T> T withWaitLogging(String apiName, Supplier<T> code) {
void adopt(ChannelOwner child) {
child.parent.objects.remove(child.guid);
objects.put(child.guid, child);
child.parent = this;
}
<T> T withWaitLogging(String apiName, Function<Logger, T> code) {
return new WaitForEventLogger<>(this, apiName, code).get();
}
@@ -108,4 +116,10 @@ class ChannelOwner extends LoggingSupport {
void handleEvent(String event, JsonObject parameters) {
}
JsonObject toProtocolRef() {
JsonObject json = new JsonObject();
json.addProperty("guid", guid);
return json;
}
}
@@ -16,23 +16,17 @@
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.Playwright;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.TimeoutError;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
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 {
@@ -62,7 +56,7 @@ 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 StackTraceCollector stackTraceCollector;
private final Map<Integer, WaitableResult<JsonElement>> callbacks = new HashMap<>();
private String apiName;
private static final boolean isLogging;
@@ -70,6 +64,8 @@ public class Connection {
String debug = System.getenv("DEBUG");
isLogging = (debug != null) && debug.contains("pw:channel");
}
LocalUtils localUtils;
final Map<String, String> env;
class Root extends ChannelOwner {
Root(Connection connection) {
@@ -84,21 +80,18 @@ public class Connection {
}
}
Connection(Transport transport) {
Connection(Transport transport, Map<String, String> env) {
this.env = env;
if (isLogging) {
transport = new TransportLogger(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 + "'");
}
}
stackTraceCollector = StackTraceCollector.createFromEnv(env);
}
boolean isCollectingStacks() {
return stackTraceCollector != null;
}
String setApiName(String name) {
@@ -119,45 +112,6 @@ 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<>();
@@ -168,11 +122,15 @@ public class Connection {
message.addProperty("method", method);
message.add("params", params);
JsonObject metadata = new JsonObject();
if (srcDir != null) {
metadata.add("stack", currentStackTrace());
}
if (apiName != null) {
if (apiName == null) {
metadata.addProperty("internal", true);
} else {
metadata.addProperty("apiName", apiName);
// All but first message in an API call are considered internal and will be hidden from the inspector.
apiName = null;
if (stackTraceCollector != null) {
metadata.add("stack", stackTraceCollector.currentStackTrace());
}
}
message.add("metadata", metadata);
transport.send(message);
@@ -183,6 +141,10 @@ public class Connection {
return (PlaywrightImpl) this.root.initialize();
}
LocalUtils localUtils() {
return localUtils;
}
public <T> T getExistingObject(String guid) {
@SuppressWarnings("unchecked") T result = (T) objects.get(guid);
if (result == null)
@@ -239,18 +201,24 @@ public class Connection {
createRemoteObject(message.guid, message.params);
return;
}
if (message.method.equals("__dispose__")) {
ChannelOwner object = objects.get(message.guid);
if (object == null) {
throw new PlaywrightException("Cannot find object to dispose: " + message.guid);
}
object.disconnect();
return;
}
ChannelOwner object = objects.get(message.guid);
if (object == null) {
throw new PlaywrightException("Cannot find object to call " + message.method + ": " + message.guid);
}
if (message.method.equals("__adopt__")) {
String childGuid = message.params.get("guid").getAsString();
ChannelOwner child = objects.get(childGuid);
if (child == null) {
throw new PlaywrightException("Unknown new child: " + childGuid);
}
object.adopt(child);
return;
}
if (message.method.equals("__dispose__")) {
object.disconnect();
return;
}
object.handleEvent(message.method, message.params);
}
@@ -303,7 +271,7 @@ public class Connection {
break;
case "APIRequestContext":
// Create fake object as this API is experimental an only exposed in Node.js.
result = new ChannelOwner(parent, type, guid, initializer);
result = new APIRequestContextImpl(parent, type, guid, initializer);
break;
case "Frame":
result = new FrameImpl(parent, type, guid, initializer);
@@ -314,6 +282,10 @@ public class Connection {
case "JsonPipe":
result = new JsonPipe(parent, type, guid, initializer);
break;
case "LocalUtils":
localUtils = new LocalUtils(parent, type, guid, initializer);
result = localUtils;
break;
case "Page":
result = new PageImpl(parent, type, guid, initializer);
break;
@@ -335,12 +307,18 @@ public class Connection {
case "Selectors":
result = new SelectorsImpl(parent, type, guid, initializer);
break;
case "Tracing":
result = new TracingImpl(parent, type, guid, initializer);
break;
case "WebSocket":
result = new WebSocketImpl(parent, type, guid, initializer);
break;
case "Worker":
result = new WorkerImpl(parent, type, guid, initializer);
break;
case "WritableStream":
result = new WritableStream(parent, type, guid, initializer);
break;
default:
throw new PlaywrightException("Unknown type " + type);
}
@@ -20,7 +20,6 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
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;
@@ -34,7 +33,8 @@ import java.util.Base64;
import java.util.List;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
import static com.microsoft.playwright.impl.Utils.*;
import static com.microsoft.playwright.impl.Utils.addLargeFileUploadParams;
import static com.microsoft.playwright.options.ScreenshotType.JPEG;
import static com.microsoft.playwright.options.ScreenshotType.PNG;
@@ -300,7 +300,7 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
}
@Override
public Frame ownerFrame() {
public FrameImpl ownerFrame() {
return withLogging("ElementHandle.ownerFrame", () -> {
JsonObject json = sendMessage("ownerFrame").getAsJsonObject();
if (!json.has("frame")) {
@@ -435,9 +435,9 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override
public void setChecked(boolean checked, SetCheckedOptions options) {
if (checked) {
check(convertViaJson(options, CheckOptions.class));
check(convertType(options, CheckOptions.class));
} else {
uncheck(convertViaJson(options, UncheckOptions.class));
uncheck(convertType(options, UncheckOptions.class));
}
}
@@ -456,7 +456,24 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override
public void setInputFiles(Path[] files, SetInputFilesOptions options) {
setInputFiles(Utils.toFilePayloads(files), options);
withLogging("ElementHandle.setInputFiles", () -> setInputFilesImpl(files, options));
}
void setInputFilesImpl(Path[] files, SetInputFilesOptions options) {
FrameImpl frame = ownerFrame();
if (frame == null) {
throw new Error("Cannot set input files to detached element");
}
if (hasLargeFile(files)) {
if (options == null) {
options = new SetInputFilesOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
addLargeFileUploadParams(files, params, frame.page().context());
sendMessage("setInputFilePaths", params);
} else {
setInputFilesImpl(Utils.toFilePayloads(files), options);
}
}
@Override
@@ -470,6 +487,7 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
}
void setInputFilesImpl(FilePayload[] files, SetInputFilesOptions options) {
checkFilePayloadSize(files);
if (options == null) {
options = new SetInputFilesOptions();
}
@@ -23,7 +23,7 @@ import com.microsoft.playwright.options.FilePayload;
import java.nio.file.Path;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
import static com.microsoft.playwright.impl.Utils.convertType;
class FileChooserImpl implements FileChooser {
private final PageImpl page;
@@ -58,7 +58,8 @@ class FileChooserImpl implements FileChooser {
@Override
public void setFiles(Path[] files, SetFilesOptions options) {
setFiles(Utils.toFilePayloads(files), options);
page.withLogging("FileChooser.setInputFiles",
() -> element.setInputFilesImpl(files, convertType(options, ElementHandle.SetInputFilesOptions.class)));
}
@Override
@@ -69,6 +70,6 @@ class FileChooserImpl implements FileChooser {
@Override
public void setFiles(FilePayload[] files, SetFilesOptions options) {
page.withLogging("FileChooser.setInputFiles",
() -> element.setInputFilesImpl(files, convertViaJson(options, ElementHandle.SetInputFilesOptions.class)));
() -> element.setInputFilesImpl(files, convertType(options, ElementHandle.SetInputFilesOptions.class)));
}
}
@@ -0,0 +1,58 @@
/*
* 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.options.FilePayload;
import com.microsoft.playwright.options.FormData;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
public class FormDataImpl implements FormData {
Map<String, Object> fields = new LinkedHashMap<>();
@Override
public FormData set(String name, String value) {
fields.put(name, value);
return this;
}
@Override
public FormData set(String name, boolean value) {
fields.put(name, value);
return this;
}
@Override
public FormData set(String name, int value) {
fields.put(name, value);
return this;
}
@Override
public FormData set(String name, Path value) {
fields.put(name, value);
return this;
}
@Override
public FormData set(String name, FilePayload value) {
fields.put(name, value);
return this;
}
}
@@ -31,9 +31,9 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Utils.*;
import static com.microsoft.playwright.options.WaitUntilState.*;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
public class FrameImpl extends ChannelOwner implements Frame {
private String name;
@@ -566,8 +566,8 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public LocatorImpl locator(String selector) {
return new LocatorImpl(this, selector);
public Locator locator(String selector, LocatorOptions options) {
return new LocatorImpl(this, selector, convertType(options, Locator.LocatorOptions.class));
}
boolean isVisibleImpl(String selector, IsVisibleOptions options) {
@@ -586,7 +586,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public Page page() {
public PageImpl page() {
return page;
}
@@ -656,9 +656,9 @@ public class FrameImpl extends ChannelOwner implements Frame {
void setCheckedImpl(String selector, boolean checked, SetCheckedOptions options) {
if (checked) {
checkImpl(selector, convertViaJson(options, CheckOptions.class));
checkImpl(selector, convertType(options, CheckOptions.class));
} else {
uncheckImpl(selector, convertViaJson(options, UncheckOptions.class));
uncheckImpl(selector, convertType(options, UncheckOptions.class));
}
}
@@ -686,21 +686,32 @@ public class FrameImpl extends ChannelOwner implements Frame {
withLogging("Frame.setInputFiles", () -> setInputFilesImpl(selector, files, options));
}
void setInputFilesImpl(String selector, Path[] files, SetInputFilesOptions options) {
if (hasLargeFile(files)) {
if (options == null) {
options = new SetInputFilesOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
addLargeFileUploadParams(files, params, page.context());
params.addProperty("selector", selector);
sendMessage("setInputFilePaths", params);
} else {
setInputFilesImpl(selector, Utils.toFilePayloads(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, FilePayload[] files, SetInputFilesOptions options) {
withLogging("Frame.setInputFiles", () -> setInputFilesImpl(selector, files, options));
}
void setInputFilesImpl(String selector, FilePayload[] files, SetInputFilesOptions options) {
checkFilePayloadSize(files);
if (options == null) {
options = new SetInputFilesOptions();
}
@@ -800,17 +811,17 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void waitForLoadState(LoadState state, WaitForLoadStateOptions options) {
withWaitLogging("Frame.waitForLoadState", () -> {
waitForLoadStateImpl(state, options);
withWaitLogging("Frame.waitForLoadState", logger -> {
waitForLoadStateImpl(state, options, logger);
return null;
});
}
void waitForLoadStateImpl(LoadState state, WaitForLoadStateOptions options) {
waitForLoadStateImpl(convertViaJson(state, WaitUntilState.class), options);
void waitForLoadStateImpl(LoadState state, WaitForLoadStateOptions options, Logger logger) {
waitForLoadStateImpl(convertType(state, WaitUntilState.class), options, logger);
}
private void waitForLoadStateImpl(WaitUntilState state, WaitForLoadStateOptions options) {
private void waitForLoadStateImpl(WaitUntilState state, WaitForLoadStateOptions options, Logger logger) {
if (options == null) {
options = new WaitForLoadStateOptions();
}
@@ -819,7 +830,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
List<Waitable<Void>> waitables = new ArrayList<>();
waitables.add(new WaitForLoadStateHelper(state));
waitables.add(new WaitForLoadStateHelper(state, logger));
waitables.add(page.createWaitForCloseHelper());
waitables.add(page.createWaitableTimeout(options.timeout));
runUntil(() -> {}, new WaitableRace<>(waitables));
@@ -827,10 +838,12 @@ public class FrameImpl extends ChannelOwner implements Frame {
private class WaitForLoadStateHelper implements Waitable<Void>, Consumer<WaitUntilState> {
private final WaitUntilState expectedState;
private final Logger logger;
private boolean isDone;
WaitForLoadStateHelper(WaitUntilState state) {
WaitForLoadStateHelper(WaitUntilState state, Logger logger) {
expectedState = state;
this.logger = logger;
isDone = loadStates.contains(state);
if (!isDone) {
internalListeners.add(InternalEventType.LOADSTATE, this);
@@ -839,6 +852,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void accept(WaitUntilState state) {
logger.log(" load state changed to " + state);
if (expectedState.equals(state)) {
isDone = true;
dispose();
@@ -862,20 +876,24 @@ public class FrameImpl extends ChannelOwner implements Frame {
private class WaitForNavigationHelper implements Waitable<Response>, Consumer<JsonObject> {
private final UrlMatcher matcher;
private final WaitUntilState expectedLoadState;
private final Logger logger;
private WaitForLoadStateHelper loadStateHelper;
private RequestImpl request;
private RuntimeException exception;
WaitForNavigationHelper(UrlMatcher matcher, WaitUntilState expectedLoadState) {
WaitForNavigationHelper(UrlMatcher matcher, WaitUntilState expectedLoadState, Logger logger) {
this.matcher = matcher;
this.expectedLoadState = expectedLoadState;
this.logger = logger;
internalListeners.add(InternalEventType.NAVIGATED, this);
}
@Override
public void accept(JsonObject params) {
if (!matcher.test(params.get("url").getAsString())) {
String url = params.get("url").getAsString();
logger.log(" navigated to " + url);
if (!matcher.test(url)) {
return;
}
if (params.has("error")) {
@@ -887,7 +905,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
request = connection.getExistingObject(jsonReq.get("guid").getAsString());
}
}
loadStateHelper = new WaitForLoadStateHelper(expectedLoadState);
loadStateHelper = new WaitForLoadStateHelper(expectedLoadState, logger);
}
internalListeners.remove(InternalEventType.NAVIGATED, this);
}
@@ -926,14 +944,14 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public Response waitForNavigation(WaitForNavigationOptions options, Runnable code) {
return withLogging("Frame.waitForNavigation", () -> waitForNavigationImpl(code, options, null));
return withWaitLogging("Frame.waitForNavigation", logger -> waitForNavigationImpl(logger, code, options, null));
}
Response waitForNavigationImpl(Runnable code, WaitForNavigationOptions options) {
return waitForNavigationImpl(code, options, null);
Response waitForNavigationImpl(Logger logger, Runnable code, WaitForNavigationOptions options) {
return waitForNavigationImpl(logger, code, options, null);
}
private Response waitForNavigationImpl(Runnable code, WaitForNavigationOptions options, UrlMatcher matcher) {
private Response waitForNavigationImpl(Logger logger, Runnable code, WaitForNavigationOptions options, UrlMatcher matcher) {
if (options == null) {
options = new WaitForNavigationOptions();
}
@@ -945,7 +963,8 @@ public class FrameImpl extends ChannelOwner implements Frame {
if (matcher == null) {
matcher = UrlMatcher.forOneOf(page.context().baseUrl, options.url);
}
waitables.add(new WaitForNavigationHelper(matcher, options.waitUntil));
logger.log("waiting for navigation " + matcher);
waitables.add(new WaitForNavigationHelper(matcher, options.waitUntil, logger));
waitables.add(page.createWaitForCloseHelper());
waitables.add(page.createWaitableFrameDetach(this));
waitables.add(page.createWaitableNavigationTimeout(options.timeout));
@@ -1003,18 +1022,35 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
private void waitForURL(UrlMatcher matcher, WaitForURLOptions options) {
withLogging("Frame.waitForURL", () -> waitForURLImpl(matcher, options));
withWaitLogging("Frame.waitForURL", logger -> {
waitForURLImpl(logger, matcher, options);
return null;
});
}
void waitForURLImpl(UrlMatcher matcher, WaitForURLOptions options) {
void waitForURLImpl(Logger logger, UrlMatcher matcher, WaitForURLOptions options) {
logger.log("waiting for url " + matcher);
if (options == null) {
options = new WaitForURLOptions();
}
if (matcher.test(url())) {
waitForLoadStateImpl(options.waitUntil, convertViaJson(options, WaitForLoadStateOptions.class));
waitForLoadStateImpl(options.waitUntil, convertType(options, WaitForLoadStateOptions.class), logger);
return;
}
waitForNavigationImpl(() -> {}, convertViaJson(options, WaitForNavigationOptions.class), matcher);
waitForNavigationImpl(logger, () -> {}, convertType(options, WaitForNavigationOptions.class), matcher);
}
int queryCount(String selector) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
JsonObject result = sendMessage("queryCount", params).getAsJsonObject();
return result.get("value").getAsInt();
}
void highlightImpl(String selector) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
sendMessage("highlight", params);
}
protected void handleEvent(String event, JsonObject params) {
@@ -1023,6 +1059,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
if (add != null) {
WaitUntilState state = loadStateFromProtocol(add.getAsString());
loadStates.add(state);
if (parentFrame == null && page != null) {
if (state == LOAD) {
page.listeners.notify(PageImpl.EventType.LOAD, page);
} else if (state == DOMCONTENTLOADED) {
page.listeners.notify(PageImpl.EventType.DOMCONTENTLOADED, page);
}
}
internalListeners.notify(InternalEventType.LOADSTATE, state);
}
JsonElement remove = params.get("remove");
@@ -17,6 +17,9 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.FrameLocator;
import com.microsoft.playwright.Locator;
import static com.microsoft.playwright.impl.Utils.convertType;
class FrameLocatorImpl implements FrameLocator {
private final FrameImpl frame;
@@ -43,8 +46,8 @@ class FrameLocatorImpl implements FrameLocator {
}
@Override
public LocatorImpl locator(String selector) {
return new LocatorImpl(frame, frameSelector + " >> control=enter-frame >> " + selector);
public Locator locator(String selector, LocatorOptions options) {
return new LocatorImpl(frame, frameSelector + " >> control=enter-frame >> " + selector, convertType(options, Locator.LocatorOptions.class));
}
@Override
@@ -0,0 +1,104 @@
/*
* 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 com.microsoft.playwright.Request;
import com.microsoft.playwright.Route;
import com.microsoft.playwright.options.HarNotFound;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Map;
import static com.microsoft.playwright.impl.LoggingSupport.*;
import static com.microsoft.playwright.impl.Serialization.fromNameValues;
import static com.microsoft.playwright.impl.Serialization.gson;
public class HARRouter {
private final LocalUtils localUtils;
private final HarNotFound defaultAction;
private final String harId;
HARRouter(LocalUtils localUtils, Path harFile, HarNotFound defaultAction) {
this.localUtils = localUtils;
this.defaultAction = defaultAction;
JsonObject params = new JsonObject();
params.addProperty("file", harFile.toString());
JsonObject json = localUtils.sendMessage("harOpen", params).getAsJsonObject();
if (json.has("error")) {
throw new PlaywrightException(json.get("error").getAsString());
}
harId = json.get("harId").getAsString();
}
void handle(Route route) {
Request request = route.request();
JsonObject params = new JsonObject();
params.addProperty("harId", harId);
params.addProperty("url", request.url());
params.addProperty("method", request.method());
params.add("headers", gson().toJsonTree(request.headersArray()));
if (request.postDataBuffer() != null) {
String base64 = Base64.getEncoder().encodeToString(request.postDataBuffer());
params.addProperty("postData", base64);
}
params.addProperty("isNavigationRequest", request.isNavigationRequest());
JsonObject response = localUtils.sendMessage("harLookup", params).getAsJsonObject();
String action = response.get("action").getAsString();
if ("redirect".equals(action)) {
String redirectURL = response.get("redirectURL").getAsString();
logApiIfEnabled("HAR: " + route.request().url() + " redirected to " + redirectURL);
((RouteImpl) route).redirectNavigationRequest(redirectURL);
return;
}
if ("fulfill".equals(action)) {
int status = response.get("status").getAsInt();
Map<String, String> headers = fromNameValues(response.getAsJsonArray("headers"));
byte[] buffer = Base64.getDecoder().decode(response.get("body").getAsString());
route.fulfill(new Route.FulfillOptions()
.setStatus(status)
.setHeaders(headers)
.setBodyBytes(buffer));
return;
}
if ("error".equals(action)) {
logApiIfEnabled("HAR: " + response.get("message").getAsString());
// Report the error, but fall through to the default handler.
}
if (defaultAction == HarNotFound.FALLBACK) {
route.fallback();
return;
}
// By default abort not matching requests.
route.abort();
}
void dispose() {
JsonObject params = new JsonObject();
params.addProperty("harId", harId);
localUtils.sendMessageAsync("harClose", params);
}
}
@@ -16,28 +16,20 @@
package com.microsoft.playwright.impl;
import java.util.function.Function;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
class WaitableAdapter<F, T> implements Waitable<T> {
private final Waitable<F> waitable;
private final Function<F, T> transformation;
import java.nio.file.Path;
WaitableAdapter(Waitable<F> waitable, Function<F, T> transformation) {
this.waitable = waitable;
this.transformation = transformation;
}
@Override
public boolean isDone() {
return waitable.isDone();
class LocalUtils extends ChannelOwner {
LocalUtils(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@Override
public T get() {
return transformation.apply(waitable.get());
}
@Override
public void dispose() {
waitable.dispose();
void zip(Path zipFile, JsonArray entries) {
JsonObject params = new JsonObject();
params.addProperty("zipFile", zipFile.toString());
params.add("entries", entries);
sendMessage("zip", params);
}
}
@@ -19,12 +19,13 @@ package com.microsoft.playwright.impl;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.assertions.LocatorAssertions;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.serializeArgument;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
import static com.microsoft.playwright.impl.Utils.convertType;
public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAssertions {
public LocatorAssertionsImpl(Locator locator) {
@@ -39,17 +40,19 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
public void containsText(String text, ContainsTextOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, text, "Locator expected to contain text", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.text", expected, text, "Locator expected to contain text", convertType(options, FrameExpectOptions.class));
}
@Override
public void containsText(Pattern pattern, ContainsTextOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, pattern, "Locator expected to contain regex", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.text", expected, pattern, "Locator expected to contain regex", convertType(options, FrameExpectOptions.class));
}
@Override
@@ -58,11 +61,12 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
for (String text : strings) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
list.add(expected);
}
expectImpl("to.contain.text.array", list, strings, "Locator expected to contain text", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.contain.text.array", list, strings, "Locator expected to contain text", convertType(options, FrameExpectOptions.class));
}
@Override
@@ -70,11 +74,12 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
List<ExpectedTextValue> list = new ArrayList<>();
for (Pattern pattern : patterns) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
list.add(expected);
}
expectImpl("to.contain.text.array", list, patterns, "Locator expected to contain text", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.contain.text.array", list, patterns, "Locator expected to contain text", convertType(options, FrameExpectOptions.class));
}
@Override
@@ -94,7 +99,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
if (options == null) {
options = new HasAttributeOptions();
}
FrameExpectOptions commonOptions = convertViaJson(options, FrameExpectOptions.class);
FrameExpectOptions commonOptions = convertType(options, FrameExpectOptions.class);
commonOptions.expressionArg = name;
String message = "Locator expected to have attribute '" + name + "'";
if (expectedValue instanceof Pattern) {
@@ -107,13 +112,13 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
public void hasClass(String text, HasClassOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expectImpl("to.have.class", expected, text, "Locator expected to have class", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.class", expected, text, "Locator expected to have class", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasClass(Pattern pattern, HasClassOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.class", expected, pattern, "Locator expected to have class matching regex", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.class", expected, pattern, "Locator expected to have class matching regex", convertType(options, FrameExpectOptions.class));
}
@Override
@@ -124,7 +129,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expected.string = text;
list.add(expected);
}
expectImpl("to.have.class.array", list, strings, "Locator expected to have class", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.class.array", list, strings, "Locator expected to have class", convertType(options, FrameExpectOptions.class));
}
@Override
@@ -134,7 +139,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
ExpectedTextValue expected = expectedRegex(pattern);
list.add(expected);
}
expectImpl("to.have.class.array", list, patterns, "Locator expected to have class matching regex", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.class.array", list, patterns, "Locator expected to have class matching regex", convertType(options, FrameExpectOptions.class));
}
@Override
@@ -142,7 +147,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
if (options == null) {
options = new HasCountOptions();
}
FrameExpectOptions commonOptions = convertViaJson(options, FrameExpectOptions.class);
FrameExpectOptions commonOptions = convertType(options, FrameExpectOptions.class);
commonOptions.expectedNumber = count;
List<ExpectedTextValue> expectedText = null;
expectImpl("to.have.count", expectedText, count, "Locator expected to have count", commonOptions);
@@ -165,7 +170,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
if (options == null) {
options = new HasCSSOptions();
}
FrameExpectOptions commonOptions = convertViaJson(options, FrameExpectOptions.class);
FrameExpectOptions commonOptions = convertType(options, FrameExpectOptions.class);
commonOptions.expressionArg = name;
String message = "Locator expected to have CSS property '" + name + "'";
if (expectedValue instanceof Pattern) {
@@ -178,7 +183,13 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
public void hasId(String id, HasIdOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = id;
expectImpl("to.have.id", expected, id, "Locator expected to have ID", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.id", expected, id, "Locator expected to have ID", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasId(Pattern pattern, HasIdOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.id", expected, pattern, "Locator expected to have ID matching regex", convertType(options, FrameExpectOptions.class));
}
@Override
@@ -186,7 +197,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
if (options == null) {
options = new HasJSPropertyOptions();
}
FrameExpectOptions commonOptions = convertViaJson(options, FrameExpectOptions.class);
FrameExpectOptions commonOptions = convertType(options, FrameExpectOptions.class);
commonOptions.expressionArg = name;
commonOptions.expectedValue = serializeArgument(value);
List<ExpectedTextValue> list = null;
@@ -197,18 +208,20 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
public void hasText(String text, HasTextOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = false;
expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, text, "Locator expected to have text", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.text", expected, text, "Locator expected to have text", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasText(Pattern pattern, HasTextOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
// Just match substring, same as containsText.
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, pattern, "Locator expected to have text matching regex", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.text", expected, pattern, "Locator expected to have text matching regex", convertType(options, FrameExpectOptions.class));
}
@Override
@@ -217,11 +230,12 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
for (String text : strings) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = false;
expected.normalizeWhiteSpace = true;
list.add(expected);
}
expectImpl("to.have.text.array", list, strings, "Locator expected to have text", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.text.array", list, strings, "Locator expected to have text", convertType(options, FrameExpectOptions.class));
}
@Override
@@ -229,64 +243,94 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
List<ExpectedTextValue> list = new ArrayList<>();
for (Pattern pattern : patterns) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
list.add(expected);
}
expectImpl("to.have.text.array", list, patterns, "Locator expected to have text matching regex", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.text.array", list, patterns, "Locator expected to have text matching regex", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasValue(String value, HasValueOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = value;
expectImpl("to.have.value", expected, value, "Locator expected to have value", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.value", expected, value, "Locator expected to have value", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasValue(Pattern pattern, HasValueOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.value", expected, pattern, "Locator expected to have value matching regex", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.value", expected, pattern, "Locator expected to have value matching regex", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasValues(String[] values, HasValuesOptions options) {
List<ExpectedTextValue> list = new ArrayList<>();
for (String text : values) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
list.add(expected);
}
expectImpl("to.have.values", list, values, "Locator expected to have values", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasValues(Pattern[] patterns, HasValuesOptions options) {
List<ExpectedTextValue> list = new ArrayList<>();
for (Pattern pattern : patterns) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.matchSubstring = true;
list.add(expected);
}
expectImpl("to.have.values", list, patterns, "Locator expected to have values matching regex", convertType(options, FrameExpectOptions.class));
}
@Override
public void isChecked(IsCheckedOptions options) {
expectTrue("to.be.checked", "Locator expected to be checked", convertViaJson(options, FrameExpectOptions.class));
String expression = (options != null && options.checked != null && !options.checked) ? "to.be.unchecked" : "to.be.checked";
expectTrue(expression, "Locator expected to be checked", convertType(options, FrameExpectOptions.class));
}
@Override
public void isDisabled(IsDisabledOptions options) {
expectTrue("to.be.disabled", "Locator expected to be disabled", convertViaJson(options, FrameExpectOptions.class));
expectTrue("to.be.disabled", "Locator expected to be disabled", convertType(options, FrameExpectOptions.class));
}
@Override
public void isEditable(IsEditableOptions options) {
expectTrue("to.be.editable", "Locator expected to be editable", convertViaJson(options, FrameExpectOptions.class));
FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class);
boolean editable = options == null || options.editable == null || options.editable == true;
expectTrue(editable ? "to.be.editable" : "to.be.readonly", "Locator expected to be editable", frameOptions);
}
@Override
public void isEmpty(IsEmptyOptions options) {
expectTrue("to.be.empty", "Locator expected to be empty", convertViaJson(options, FrameExpectOptions.class));
expectTrue("to.be.empty", "Locator expected to be empty", convertType(options, FrameExpectOptions.class));
}
@Override
public void isEnabled(IsEnabledOptions options) {
expectTrue("to.be.enabled", "Locator expected to be enabled", convertViaJson(options, FrameExpectOptions.class));
FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class);
boolean enabled = options == null || options.enabled == null || options.enabled == true;
expectTrue(enabled ? "to.be.enabled" : "to.be.disabled", "Locator expected to be enabled", frameOptions);
}
@Override
public void isFocused(IsFocusedOptions options) {
expectTrue("to.be.focused", "Locator expected to be focused", convertViaJson(options, FrameExpectOptions.class));
expectTrue("to.be.focused", "Locator expected to be focused", convertType(options, FrameExpectOptions.class));
}
@Override
public void isHidden(IsHiddenOptions options) {
expectTrue("to.be.hidden", "Locator expected to be hidden", convertViaJson(options, FrameExpectOptions.class));
expectTrue("to.be.hidden", "Locator expected to be hidden", convertType(options, FrameExpectOptions.class));
}
@Override
public void isVisible(IsVisibleOptions options) {
expectTrue("to.be.visible", "Locator expected to be visible", convertViaJson(options, FrameExpectOptions.class));
FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class);
boolean visible = options == null || options.visible == null || options.visible == true;
expectTrue(visible ? "to.be.visible" : "to.be.hidden", "Locator expected to be visible", frameOptions);
}
private void expectTrue(String expression, String message, FrameExpectOptions options) {
@@ -298,5 +342,17 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
public LocatorAssertions not() {
return new LocatorAssertionsImpl(actualLocator, !isNot);
}
}
private static Boolean shouldIgnoreCase(Object options) {
if (options == null) {
return null;
}
try {
Field fromField = options.getClass().getDeclaredField("ignoreCase");
Object value = fromField.get(options);
return (Boolean) value;
} catch (NoSuchFieldException | IllegalAccessException e) {
return null;
}
}
}
@@ -8,25 +8,83 @@ import com.microsoft.playwright.options.FilePayload;
import com.microsoft.playwright.options.SelectOption;
import com.microsoft.playwright.options.WaitForSelectorState;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Serialization.serializeArgument;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
import static com.microsoft.playwright.impl.Utils.convertType;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
class LocatorImpl implements Locator {
private final FrameImpl frame;
private final String selector;
public LocatorImpl(FrameImpl frame, String selector) {
private static class Filters {
private final Map<Field, String> filterFieldToEngine = new LinkedHashMap<>();
private void addFilter(String name, String engine) throws NoSuchFieldException {
filterFieldToEngine.put(LocatorOptions.class.getField(name), engine);
}
{
try {
addFilter("has", "has");
// addFilter("leftOf", "left-of");
// addFilter("rightOf", "right-of");
// addFilter("above", "above");
// addFilter("below", "below");
// addFilter("near", "near");
} catch (NoSuchFieldException e) {
throw new InternalError(e);
}
}
String addFiltersToSelector(String selector, LocatorOptions options, Frame frame) {
try {
for (Map.Entry<Field, String> p : filterFieldToEngine.entrySet()) {
LocatorImpl filter = (LocatorImpl) p.getKey().get(options);
if (filter == null) {
continue;
}
if (filter.frame != frame) {
throw new PlaywrightException("Inner '" + p.getKey().getName() + "' locator must belong to the same frame.");
}
selector += " >> " + p.getValue() + "=" + gson().toJson(filter.selector);
}
} catch (IllegalAccessException e) {
throw new PlaywrightException("Unexpected options", e);
}
return selector;
}
}
private static final Filters filters = new Filters();
public LocatorImpl(FrameImpl frame, String selector, LocatorOptions options) {
this.frame = frame;
if (options != null) {
if (options.hasText != null) {
if (options.hasText instanceof Pattern) {
Pattern pattern = (Pattern) options.hasText;
String jsRegex = "/" + pattern.pattern() + "/" + toJsRegexFlags(pattern);
selector += " >> has=" + gson().toJson("text=" + jsRegex);
} else if (options.hasText instanceof String) {
String text = (String) options.hasText;
selector += " >> :scope:has-text(" + escapeWithQuotes(text) + ")";
}
}
selector = filters.addFiltersToSelector(selector, options, frame);
}
this.selector = selector;
}
private static String escapeWithQuotes(String text) {
return gson().toJson(text);
}
private <R, O> R withElement(BiFunction<ElementHandle, O, R> callback, O options) {
ElementHandleOptions handleOptions = convertViaJson(options, ElementHandleOptions.class);
ElementHandleOptions handleOptions = convertType(options, ElementHandleOptions.class);
// TODO: support deadline based timeout
// Double timeout = null;
// if (handleOptions != null) {
@@ -64,7 +122,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new CheckOptions();
}
frame.check(selector, convertViaJson(options, Frame.CheckOptions.class).setStrict(true));
frame.check(selector, convertType(options, Frame.CheckOptions.class).setStrict(true));
}
@Override
@@ -72,12 +130,12 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new ClickOptions();
}
frame.click(selector, convertViaJson(options, Frame.ClickOptions.class).setStrict(true));
frame.click(selector, convertType(options, Frame.ClickOptions.class).setStrict(true));
}
@Override
public int count() {
return ((Number) evaluateAll("ee => ee.length")).intValue();
return frame.queryCount(selector);
}
@Override
@@ -85,7 +143,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new DblclickOptions();
}
frame.dblclick(selector, convertViaJson(options, Frame.DblclickOptions.class).setStrict(true));
frame.dblclick(selector, convertType(options, Frame.DblclickOptions.class).setStrict(true));
}
@Override
@@ -93,7 +151,17 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new DispatchEventOptions();
}
frame.dispatchEvent(selector, type, eventInit, convertViaJson(options, Frame.DispatchEventOptions.class).setStrict(true));
frame.dispatchEvent(selector, type, eventInit, convertType(options, Frame.DispatchEventOptions.class).setStrict(true));
}
@Override
public void dragTo(Locator target, DragToOptions options) {
if (options == null) {
options = new DragToOptions();
}
Frame.DragAndDropOptions frameOptions = convertType(options, Frame.DragAndDropOptions.class);
frameOptions.setStrict(true);
frame.dragAndDrop(selector, ((LocatorImpl) target).selector, frameOptions);
}
@Override
@@ -101,7 +169,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new ElementHandleOptions();
}
Frame.WaitForSelectorOptions frameOptions = convertViaJson(options, Frame.WaitForSelectorOptions.class);
Frame.WaitForSelectorOptions frameOptions = convertType(options, Frame.WaitForSelectorOptions.class);
frameOptions.setStrict(true);
frameOptions.setState(WaitForSelectorState.ATTACHED);
return frame.waitForSelector(selector, frameOptions);
@@ -132,12 +200,17 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new FillOptions();
}
frame.fill(selector, value, convertViaJson(options, Frame.FillOptions.class).setStrict(true));
frame.fill(selector, value, convertType(options, Frame.FillOptions.class).setStrict(true));
}
@Override
public Locator filter(FilterOptions options) {
return new LocatorImpl(frame, selector, convertType(options,LocatorOptions.class));
}
@Override
public Locator first() {
return new LocatorImpl(frame, selector + " >> nth=0");
return new LocatorImpl(frame, selector + " >> nth=0", null);
}
@Override
@@ -145,7 +218,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new FocusOptions();
}
frame.focus(selector, convertViaJson(options, Frame.FocusOptions.class).setStrict(true));
frame.focus(selector, convertType(options, Frame.FocusOptions.class).setStrict(true));
}
@Override
@@ -158,7 +231,12 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new GetAttributeOptions();
}
return frame.getAttribute(selector, name, convertViaJson(options, Frame.GetAttributeOptions.class).setStrict(true));
return frame.getAttribute(selector, name, convertType(options, Frame.GetAttributeOptions.class).setStrict(true));
}
@Override
public void highlight() {
frame.highlightImpl(selector);
}
@Override
@@ -166,7 +244,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new HoverOptions();
}
frame.hover(selector, convertViaJson(options, Frame.HoverOptions.class).setStrict(true));
frame.hover(selector, convertType(options, Frame.HoverOptions.class).setStrict(true));
}
@Override
@@ -174,7 +252,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new InnerHTMLOptions();
}
return frame.innerHTML(selector, convertViaJson(options, Frame.InnerHTMLOptions.class).setStrict(true));
return frame.innerHTML(selector, convertType(options, Frame.InnerHTMLOptions.class).setStrict(true));
}
@Override
@@ -182,7 +260,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new InnerTextOptions();
}
return frame.innerText(selector, convertViaJson(options, Frame.InnerTextOptions.class).setStrict(true));
return frame.innerText(selector, convertType(options, Frame.InnerTextOptions.class).setStrict(true));
}
@Override
@@ -190,7 +268,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new InputValueOptions();
}
return frame.inputValue(selector, convertViaJson(options, Frame.InputValueOptions.class).setStrict(true));
return frame.inputValue(selector, convertType(options, Frame.InputValueOptions.class).setStrict(true));
}
@Override
@@ -198,7 +276,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new IsCheckedOptions();
}
return frame.isChecked(selector, convertViaJson(options, Frame.IsCheckedOptions.class).setStrict(true));
return frame.isChecked(selector, convertType(options, Frame.IsCheckedOptions.class).setStrict(true));
}
@Override
@@ -206,7 +284,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new IsDisabledOptions();
}
return frame.isDisabled(selector, convertViaJson(options, Frame.IsDisabledOptions.class).setStrict(true));
return frame.isDisabled(selector, convertType(options, Frame.IsDisabledOptions.class).setStrict(true));
}
@Override
@@ -214,7 +292,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new IsEditableOptions();
}
return frame.isEditable(selector, convertViaJson(options, Frame.IsEditableOptions.class).setStrict(true));
return frame.isEditable(selector, convertType(options, Frame.IsEditableOptions.class).setStrict(true));
}
@Override
@@ -222,7 +300,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new IsEnabledOptions();
}
return frame.isEnabled(selector, convertViaJson(options, Frame.IsEnabledOptions.class).setStrict(true));
return frame.isEnabled(selector, convertType(options, Frame.IsEnabledOptions.class).setStrict(true));
}
@Override
@@ -230,7 +308,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new IsHiddenOptions();
}
return frame.isHidden(selector, convertViaJson(options, Frame.IsHiddenOptions.class).setStrict(true));
return frame.isHidden(selector, convertType(options, Frame.IsHiddenOptions.class).setStrict(true));
}
@Override
@@ -238,22 +316,27 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new IsVisibleOptions();
}
return frame.isVisible(selector, convertViaJson(options, Frame.IsVisibleOptions.class).setStrict(true));
return frame.isVisible(selector, convertType(options, Frame.IsVisibleOptions.class).setStrict(true));
}
@Override
public Locator last() {
return new LocatorImpl(frame, selector + " >> nth=-1");
return new LocatorImpl(frame, selector + " >> nth=-1", null);
}
@Override
public Locator locator(String selector) {
return new LocatorImpl(frame, this.selector + " >> " + selector);
public Locator locator(String selector, LocatorOptions options) {
return new LocatorImpl(frame, this.selector + " >> " + selector, options);
}
@Override
public Locator nth(int index) {
return new LocatorImpl(frame, selector + " >> nth=" + index);
return new LocatorImpl(frame, selector + " >> nth=" + index, null);
}
@Override
public Page page() {
return frame.page();
}
@Override
@@ -261,12 +344,12 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new PressOptions();
}
frame.press(selector, key, convertViaJson(options, Frame.PressOptions.class).setStrict(true));
frame.press(selector, key, convertType(options, Frame.PressOptions.class).setStrict(true));
}
@Override
public byte[] screenshot(ScreenshotOptions options) {
return withElement((h, o) -> h.screenshot(o), convertViaJson(options, ElementHandle.ScreenshotOptions.class));
return withElement((h, o) -> h.screenshot(o), convertType(options, ElementHandle.ScreenshotOptions.class));
}
@Override
@@ -274,7 +357,7 @@ class LocatorImpl implements Locator {
withElement((h, o) -> {
h.scrollIntoViewIfNeeded(o);
return null;
}, convertViaJson(options, ElementHandle.ScrollIntoViewIfNeededOptions.class));
}, convertType(options, ElementHandle.ScrollIntoViewIfNeededOptions.class));
}
@Override
@@ -282,7 +365,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new SelectOptionOptions();
}
return frame.selectOption(selector, values, convertViaJson(options, Frame.SelectOptionOptions.class).setStrict(true));
return frame.selectOption(selector, values, convertType(options, Frame.SelectOptionOptions.class).setStrict(true));
}
@Override
@@ -290,7 +373,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new SelectOptionOptions();
}
return frame.selectOption(selector, values, convertViaJson(options, Frame.SelectOptionOptions.class).setStrict(true));
return frame.selectOption(selector, values, convertType(options, Frame.SelectOptionOptions.class).setStrict(true));
}
@Override
@@ -298,7 +381,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new SelectOptionOptions();
}
return frame.selectOption(selector, values, convertViaJson(options, Frame.SelectOptionOptions.class).setStrict(true));
return frame.selectOption(selector, values, convertType(options, Frame.SelectOptionOptions.class).setStrict(true));
}
@Override
@@ -306,7 +389,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new SelectOptionOptions();
}
return frame.selectOption(selector, values, convertViaJson(options, Frame.SelectOptionOptions.class).setStrict(true));
return frame.selectOption(selector, values, convertType(options, Frame.SelectOptionOptions.class).setStrict(true));
}
@Override
@@ -314,7 +397,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new SelectOptionOptions();
}
return frame.selectOption(selector, values, convertViaJson(options, Frame.SelectOptionOptions.class).setStrict(true));
return frame.selectOption(selector, values, convertType(options, Frame.SelectOptionOptions.class).setStrict(true));
}
@Override
@@ -322,7 +405,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new SelectOptionOptions();
}
return frame.selectOption(selector, values, convertViaJson(options, Frame.SelectOptionOptions.class).setStrict(true));
return frame.selectOption(selector, values, convertType(options, Frame.SelectOptionOptions.class).setStrict(true));
}
@Override
@@ -330,7 +413,7 @@ class LocatorImpl implements Locator {
withElement((h, o) -> {
h.selectText(o);
return null;
}, convertViaJson(options, ElementHandle.SelectTextOptions.class));
}, convertType(options, ElementHandle.SelectTextOptions.class));
}
@Override
@@ -338,7 +421,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new SetCheckedOptions();
}
frame.setChecked(selector, checked, convertViaJson(options, Frame.SetCheckedOptions.class).setStrict(true));
frame.setChecked(selector, checked, convertType(options, Frame.SetCheckedOptions.class).setStrict(true));
}
@Override
@@ -346,7 +429,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new SetInputFilesOptions();
}
frame.setInputFiles(selector, files, convertViaJson(options, Frame.SetInputFilesOptions.class).setStrict(true));
frame.setInputFiles(selector, files, convertType(options, Frame.SetInputFilesOptions.class).setStrict(true));
}
@Override
@@ -354,7 +437,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new SetInputFilesOptions();
}
frame.setInputFiles(selector, files, convertViaJson(options, Frame.SetInputFilesOptions.class).setStrict(true));
frame.setInputFiles(selector, files, convertType(options, Frame.SetInputFilesOptions.class).setStrict(true));
}
@Override
@@ -362,7 +445,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new SetInputFilesOptions();
}
frame.setInputFiles(selector, files, convertViaJson(options, Frame.SetInputFilesOptions.class).setStrict(true));
frame.setInputFiles(selector, files, convertType(options, Frame.SetInputFilesOptions.class).setStrict(true));
}
@Override
@@ -370,7 +453,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new SetInputFilesOptions();
}
frame.setInputFiles(selector, files, convertViaJson(options, Frame.SetInputFilesOptions.class).setStrict(true));
frame.setInputFiles(selector, files, convertType(options, Frame.SetInputFilesOptions.class).setStrict(true));
}
@Override
@@ -378,7 +461,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new TapOptions();
}
frame.tap(selector, convertViaJson(options, Frame.TapOptions.class).setStrict(true));
frame.tap(selector, convertType(options, Frame.TapOptions.class).setStrict(true));
}
@Override
@@ -386,7 +469,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new TextContentOptions();
}
return frame.textContent(selector, convertViaJson(options, Frame.TextContentOptions.class).setStrict(true));
return frame.textContent(selector, convertType(options, Frame.TextContentOptions.class).setStrict(true));
}
@Override
@@ -394,7 +477,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new TypeOptions();
}
frame.type(selector, text, convertViaJson(options, Frame.TypeOptions.class).setStrict(true));
frame.type(selector, text, convertType(options, Frame.TypeOptions.class).setStrict(true));
}
@Override
@@ -402,7 +485,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new UncheckOptions();
}
frame.uncheck(selector, convertViaJson(options, Frame.UncheckOptions.class).setStrict(true));
frame.uncheck(selector, convertType(options, Frame.UncheckOptions.class).setStrict(true));
}
@Override
@@ -414,7 +497,7 @@ class LocatorImpl implements Locator {
}
private void waitForImpl(WaitForOptions options) {
frame.withLogging("Locator.waitFor", () -> frame.waitForSelectorImpl(selector, convertViaJson(options, Frame.WaitForSelectorOptions.class).setStrict(true), true));
frame.withLogging("Locator.waitFor", () -> frame.waitForSelectorImpl(selector, convertType(options, Frame.WaitForSelectorOptions.class).setStrict(true), true));
}
@Override
@@ -426,6 +509,13 @@ class LocatorImpl implements Locator {
return frame.withLogging("Locator.expect", () -> expectImpl(expression, options));
}
JsonObject toProtocol() {
JsonObject result = new JsonObject();
result.add("frame", frame.toProtocolRef());
result.addProperty("selector", selector);
return result;
}
private FrameExpectResult expectImpl(String expression, FrameExpectOptions options) {
if (options == null) {
options = new FrameExpectOptions();
@@ -0,0 +1,21 @@
/*
* 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;
interface Logger {
void log(String message);
}
@@ -60,7 +60,13 @@ class LoggingSupport {
System.err.println(timestamp + " " + message);
}
private void logApi(String message) {
static void logApiIfEnabled(String message) {
if (isEnabled) {
logApi(message);
}
}
static void logApi(String message) {
// This matches log format produced by the server.
logWithTimestamp("pw:api " + message);
}
@@ -20,7 +20,7 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.Mouse;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
import static com.microsoft.playwright.impl.Utils.convertType;
class MouseImpl implements Mouse {
private final ChannelOwner page;
@@ -54,7 +54,7 @@ class MouseImpl implements Mouse {
if (options == null) {
clickOptions = new ClickOptions();
} else {
clickOptions = convertViaJson(options, ClickOptions.class);
clickOptions = convertType(options, ClickOptions.class);
}
clickOptions.clickCount = 2;
click(x, y, clickOptions);
@@ -22,7 +22,7 @@ import com.microsoft.playwright.assertions.PageAssertions;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.UrlMatcher.resolveUrl;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
import static com.microsoft.playwright.impl.Utils.convertType;
public class PageAssertionsImpl extends AssertionsBase implements PageAssertions {
private final PageImpl actualPage;
@@ -40,13 +40,14 @@ public class PageAssertionsImpl extends AssertionsBase implements PageAssertions
public void hasTitle(String title, HasTitleOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = title;
expectImpl("to.have.title", expected, title, "Page title expected to be", convertViaJson(options, FrameExpectOptions.class));
expected.normalizeWhiteSpace = true;
expectImpl("to.have.title", expected, title, "Page title expected to be", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasTitle(Pattern pattern, HasTitleOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.title", expected, pattern, "Page title expected to match regex", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.title", expected, pattern, "Page title expected to match regex", convertType(options, FrameExpectOptions.class));
}
@Override
@@ -56,13 +57,13 @@ public class PageAssertionsImpl extends AssertionsBase implements PageAssertions
url = resolveUrl(actualPage.context().baseUrl, url);
}
expected.string = url;
expectImpl("to.have.url", expected, url, "Page URL expected to be", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.url", expected, url, "Page URL expected to be", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasURL(Pattern pattern, HasURLOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.url", expected, pattern, "Page URL expected to match regex", convertViaJson(options, FrameExpectOptions.class));
expectImpl("to.have.url", expected, pattern, "Page URL expected to match regex", convertType(options, FrameExpectOptions.class));
}
@Override
@@ -28,11 +28,11 @@ 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.convertType;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
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;
@@ -170,13 +170,11 @@ public class PageImpl extends ChannelOwner implements Page {
try {
bindingCall.call(binding);
} catch (RuntimeException e) {
e.printStackTrace();
if (!isSafeCloseError(e.getMessage())) {
logWithTimestamp(e.getMessage());
}
}
}
} else if ("load".equals(event)) {
listeners.notify(EventType.LOAD, this);
} else if ("domcontentloaded".equals(event)) {
listeners.notify(EventType.DOMCONTENTLOADED, this);
} else if ("frameAttached".equals(event)) {
String guid = params.getAsJsonObject("frame").get("guid").getAsString();
FrameImpl frame = connection.getExistingObject(guid);
@@ -196,11 +194,12 @@ public class PageImpl extends ChannelOwner implements Page {
}
listeners.notify(EventType.FRAMEDETACHED, frame);
} else if ("route".equals(event)) {
Route route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
boolean handled = routes.handle(route);
if (handled) {
RouteImpl route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
Router.HandleResult handled = routes.handle(route);
if (handled == Router.HandleResult.FoundMatchingHandler) {
maybeDisableNetworkInterception();
} else {
}
if (!route.isHandled()) {
browserContext.handleRoute(route);
}
} else if ("video".equals(event)) {
@@ -444,7 +443,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Page waitForClose(WaitForCloseOptions options, Runnable code) {
return withWaitLogging("Page.waitForClose", () -> waitForCloseImpl(options, code));
return withWaitLogging("Page.waitForClose", logger -> waitForCloseImpl(options, code));
}
private Page waitForCloseImpl(WaitForCloseOptions options, Runnable code) {
@@ -456,7 +455,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public ConsoleMessage waitForConsoleMessage(WaitForConsoleMessageOptions options, Runnable code) {
return withWaitLogging("Page.waitForConsoleMessage", () -> waitForConsoleMessageImpl(options, code));
return withWaitLogging("Page.waitForConsoleMessage", logger -> waitForConsoleMessageImpl(options, code));
}
private ConsoleMessage waitForConsoleMessageImpl(WaitForConsoleMessageOptions options, Runnable code) {
@@ -468,7 +467,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Download waitForDownload(WaitForDownloadOptions options, Runnable code) {
return withWaitLogging("Page.waitForDownload", () -> waitForDownloadImpl(options, code));
return withWaitLogging("Page.waitForDownload", logger -> waitForDownloadImpl(options, code));
}
private Download waitForDownloadImpl(WaitForDownloadOptions options, Runnable code) {
@@ -480,7 +479,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public FileChooser waitForFileChooser(WaitForFileChooserOptions options, Runnable code) {
return withWaitLogging("Page.waitForFileChooser", () -> waitForFileChooserImpl(options, code));
return withWaitLogging("Page.waitForFileChooser", logger -> waitForFileChooserImpl(options, code));
}
private FileChooser waitForFileChooserImpl(WaitForFileChooserOptions options, Runnable code) {
@@ -493,7 +492,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Page waitForPopup(WaitForPopupOptions options, Runnable code) {
return withWaitLogging("Page.waitForPopup", () -> waitForPopupImpl(options, code));
return withWaitLogging("Page.waitForPopup", logger -> waitForPopupImpl(options, code));
}
private Page waitForPopupImpl(WaitForPopupOptions options, Runnable code) {
@@ -505,7 +504,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public WebSocket waitForWebSocket(WaitForWebSocketOptions options, Runnable code) {
return withWaitLogging("Page.waitForWebSocket", () -> waitForWebSocketImpl(options, code));
return withWaitLogging("Page.waitForWebSocket", logger -> waitForWebSocketImpl(options, code));
}
private WebSocket waitForWebSocketImpl(WaitForWebSocketOptions options, Runnable code) {
@@ -517,7 +516,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Worker waitForWorker(WaitForWorkerOptions options, Runnable code) {
return withWaitLogging("Page.waitForWorker", () -> waitForWorkerImpl(options, code));
return withWaitLogging("Page.waitForWorker", logger -> waitForWorkerImpl(options, code));
}
private Worker waitForWorkerImpl(WaitForWorkerOptions options, Runnable code) {
@@ -556,7 +555,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public ElementHandle querySelector(String selector, QuerySelectorOptions options) {
return withLogging("Page.querySelector", () -> mainFrame.querySelectorImpl(
selector, convertViaJson(options, Frame.QuerySelectorOptions.class)));
selector, convertType(options, Frame.QuerySelectorOptions.class)));
}
@Override
@@ -567,7 +566,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Object evalOnSelector(String selector, String pageFunction, Object arg, EvalOnSelectorOptions options) {
return withLogging("Page.evalOnSelector", () -> mainFrame.evalOnSelectorImpl(
selector, pageFunction, arg, convertViaJson(options, Frame.EvalOnSelectorOptions.class)));
selector, pageFunction, arg, convertType(options, Frame.EvalOnSelectorOptions.class)));
}
@Override
@@ -601,13 +600,13 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public ElementHandle addScriptTag(AddScriptTagOptions options) {
return withLogging("Page.addScriptTag",
() -> mainFrame.addScriptTagImpl(convertViaJson(options, Frame.AddScriptTagOptions.class)));
() -> mainFrame.addScriptTagImpl(convertType(options, Frame.AddScriptTagOptions.class)));
}
@Override
public ElementHandle addStyleTag(AddStyleTagOptions options) {
return withLogging("Page.addStyleTag",
() -> mainFrame.addStyleTagImpl(convertViaJson(options, Frame.AddStyleTagOptions.class)));
() -> mainFrame.addStyleTagImpl(convertType(options, Frame.AddStyleTagOptions.class)));
}
@Override
@@ -618,13 +617,13 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void check(String selector, CheckOptions options) {
withLogging("Page.check",
() -> mainFrame.checkImpl(selector, convertViaJson(options, Frame.CheckOptions.class)));
() -> mainFrame.checkImpl(selector, convertType(options, Frame.CheckOptions.class)));
}
@Override
public void click(String selector, ClickOptions options) {
withLogging("Page.click",
() -> mainFrame.clickImpl(selector, convertViaJson(options, Frame.ClickOptions.class)));
() -> mainFrame.clickImpl(selector, convertType(options, Frame.ClickOptions.class)));
}
@Override
@@ -640,13 +639,13 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void dblclick(String selector, DblclickOptions options) {
withLogging("Page.dblclick",
() -> mainFrame.dblclickImpl(selector, convertViaJson(options, Frame.DblclickOptions.class)));
() -> mainFrame.dblclickImpl(selector, convertType(options, Frame.DblclickOptions.class)));
}
@Override
public void dispatchEvent(String selector, String type, Object eventInit, DispatchEventOptions options) {
withLogging("Page.dispatchEvent",
() -> mainFrame.dispatchEventImpl(selector, type, eventInit, convertViaJson(options, Frame.DispatchEventOptions.class)));
() -> mainFrame.dispatchEventImpl(selector, type, eventInit, convertType(options, Frame.DispatchEventOptions.class)));
}
@Override
@@ -703,13 +702,13 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void fill(String selector, String value, FillOptions options) {
withLogging("Page.fill",
() -> mainFrame.fillImpl(selector, value, convertViaJson(options, Frame.FillOptions.class)));
() -> mainFrame.fillImpl(selector, value, convertType(options, Frame.FillOptions.class)));
}
@Override
public void focus(String selector, FocusOptions options) {
withLogging("Page.focus",
() -> mainFrame.focusImpl(selector, convertViaJson(options, Frame.FocusOptions.class)));
() -> mainFrame.focusImpl(selector, convertType(options, Frame.FocusOptions.class)));
}
@Override
@@ -759,7 +758,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public String getAttribute(String selector, String name, GetAttributeOptions options) {
return withLogging("Page.getAttribute",
() -> mainFrame.getAttributeImpl(selector, name, convertViaJson(options, Frame.GetAttributeOptions.class)));
() -> mainFrame.getAttributeImpl(selector, name, convertType(options, Frame.GetAttributeOptions.class)));
}
@Override
@@ -798,44 +797,41 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public ResponseImpl navigate(String url, NavigateOptions options) {
return withLogging("Page.navigate", () ->
mainFrame.navigateImpl(url, convertViaJson(options, Frame.NavigateOptions.class)));
return withLogging("Page.navigate", () -> mainFrame.navigateImpl(url, convertType(options, Frame.NavigateOptions.class)));
}
@Override
public void hover(String selector, HoverOptions options) {
withLogging("Page.hover", () ->
mainFrame.hoverImpl(selector, convertViaJson(options, Frame.HoverOptions.class)));
withLogging("Page.hover", () -> mainFrame.hoverImpl(selector, convertType(options, Frame.HoverOptions.class)));
}
@Override
public void dragAndDrop(String source, String target, DragAndDropOptions options) {
withLogging("Page.dragAndDrop", () ->
mainFrame.dragAndDropImpl(source, target, convertViaJson(options, Frame.DragAndDropOptions.class)));
withLogging("Page.dragAndDrop", () -> mainFrame.dragAndDropImpl(source, target, convertType(options, Frame.DragAndDropOptions.class)));
}
@Override
public String innerHTML(String selector, InnerHTMLOptions options) {
return withLogging("Page.innerHTML",
() -> mainFrame.innerHTMLImpl(selector, convertViaJson(options, Frame.InnerHTMLOptions.class)));
() -> mainFrame.innerHTMLImpl(selector, convertType(options, Frame.InnerHTMLOptions.class)));
}
@Override
public String innerText(String selector, InnerTextOptions options) {
return withLogging("Page.innerText",
() -> mainFrame.innerTextImpl(selector, convertViaJson(options, Frame.InnerTextOptions.class)));
() -> mainFrame.innerTextImpl(selector, convertType(options, Frame.InnerTextOptions.class)));
}
@Override
public String inputValue(String selector, InputValueOptions options) {
return withLogging("Page.inputValue",
() -> mainFrame.inputValueImpl(selector, convertViaJson(options, Frame.InputValueOptions.class)));
() -> mainFrame.inputValueImpl(selector, convertType(options, Frame.InputValueOptions.class)));
}
@Override
public boolean isChecked(String selector, IsCheckedOptions options) {
return withLogging("Page.isChecked",
() -> mainFrame.isCheckedImpl(selector, convertViaJson(options, Frame.IsCheckedOptions.class)));
() -> mainFrame.isCheckedImpl(selector, convertType(options, Frame.IsCheckedOptions.class)));
}
@Override
@@ -846,31 +842,31 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public boolean isDisabled(String selector, IsDisabledOptions options) {
return withLogging("Page.isDisabled",
() -> mainFrame.isDisabledImpl(selector, convertViaJson(options, Frame.IsDisabledOptions.class)));
() -> mainFrame.isDisabledImpl(selector, convertType(options, Frame.IsDisabledOptions.class)));
}
@Override
public boolean isEditable(String selector, IsEditableOptions options) {
return withLogging("Page.isEditable",
() -> mainFrame.isEditableImpl(selector, convertViaJson(options, Frame.IsEditableOptions.class)));
() -> mainFrame.isEditableImpl(selector, convertType(options, Frame.IsEditableOptions.class)));
}
@Override
public boolean isEnabled(String selector, IsEnabledOptions options) {
return withLogging("Page.isEnabled",
() -> mainFrame.isEnabledImpl(selector, convertViaJson(options, Frame.IsEnabledOptions.class)));
() -> mainFrame.isEnabledImpl(selector, convertType(options, Frame.IsEnabledOptions.class)));
}
@Override
public boolean isHidden(String selector, IsHiddenOptions options) {
return withLogging("Page.isHidden",
() -> mainFrame.isHiddenImpl(selector, convertViaJson(options, Frame.IsHiddenOptions.class)));
() -> mainFrame.isHiddenImpl(selector, convertType(options, Frame.IsHiddenOptions.class)));
}
@Override
public boolean isVisible(String selector, IsVisibleOptions options) {
return withLogging("Page.isVisible",
() -> mainFrame.isVisibleImpl(selector, convertViaJson(options, Frame.IsVisibleOptions.class)));
() -> mainFrame.isVisibleImpl(selector, convertType(options, Frame.IsVisibleOptions.class)));
}
@Override
@@ -879,8 +875,8 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public LocatorImpl locator(String selector) {
return mainFrame.locator(selector);
public Locator locator(String selector, LocatorOptions options) {
return mainFrame.locator(selector, convertType(options, Frame.LocatorOptions.class));
}
@Override
@@ -933,7 +929,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void press(String selector, String key, PressOptions options) {
withLogging("Page.press",
() -> mainFrame.pressImpl(selector, key, convertViaJson(options, Frame.PressOptions.class)));
() -> mainFrame.pressImpl(selector, key, convertType(options, Frame.PressOptions.class)));
}
@Override
@@ -941,6 +937,11 @@ public class PageImpl extends ChannelOwner implements Page {
return withLogging("Page.reload", () -> reloadImpl(options));
}
@Override
public APIRequestContextImpl request() {
return browserContext.request();
}
private Response reloadImpl(ReloadOptions options) {
if (options == null) {
options = new ReloadOptions();
@@ -968,6 +969,21 @@ public class PageImpl extends ChannelOwner implements Page {
route(new UrlMatcher(url), handler, options);
}
@Override
public void routeFromHAR(Path har, RouteFromHAROptions options) {
if (options == null) {
options = new RouteFromHAROptions();
}
if (options.update != null && options.update) {
browserContext.recordIntoHar(this, har, convertType(options, BrowserContext.RouteFromHAROptions.class));
return;
}
UrlMatcher matcher = UrlMatcher.forOneOf(browserContext.baseUrl, options.url);
HARRouter harRouter = new HARRouter(connection.localUtils, har, options.notFound);
onClose(context -> harRouter.dispose());
route(matcher, route -> harRouter.handle(route), null);
}
private void route(UrlMatcher matcher, Consumer<Route> handler, RouteOptions options) {
withLogging("Page.route", () -> {
routes.add(matcher, handler, options == null ? null : options.times);
@@ -1030,8 +1046,18 @@ public class PageImpl extends ChannelOwner implements Page {
}
}
}
List<Locator> mask = options.mask;
options.mask = null;
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
options.mask = mask;
params.remove("path");
if (mask != null) {
JsonArray maskArray = new JsonArray();
for (Locator locator: mask) {
maskArray.add(((LocatorImpl) locator).toProtocol());
}
params.add("mask", maskArray);
}
JsonObject json = sendMessage("screenshot", params).getAsJsonObject();
byte[] buffer = Base64.getDecoder().decode(json.get("binary").getAsString());
@@ -1044,25 +1070,25 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public List<String> selectOption(String selector, SelectOption[] values, SelectOptionOptions options) {
return withLogging("Page.selectOption",
() -> mainFrame.selectOptionImpl(selector, values, convertViaJson(options, Frame.SelectOptionOptions.class)));
() -> mainFrame.selectOptionImpl(selector, values, convertType(options, Frame.SelectOptionOptions.class)));
}
@Override
public List<String> selectOption(String selector, ElementHandle[] values, SelectOptionOptions options) {
return withLogging("Page.selectOption",
() -> mainFrame.selectOptionImpl(selector, values, convertViaJson(options, Frame.SelectOptionOptions.class)));
() -> mainFrame.selectOptionImpl(selector, values, convertType(options, Frame.SelectOptionOptions.class)));
}
@Override
public void setChecked(String selector, boolean checked, SetCheckedOptions options) {
withLogging("Page.setChecked",
() -> mainFrame.setCheckedImpl(selector, checked, convertViaJson(options, Frame.SetCheckedOptions.class)));
() -> mainFrame.setCheckedImpl(selector, checked, convertType(options, Frame.SetCheckedOptions.class)));
}
@Override
public void setContent(String html, SetContentOptions options) {
withLogging("Page.setContent",
() -> mainFrame.setContentImpl(html, convertViaJson(options, Frame.SetContentOptions.class)));
() -> mainFrame.setContentImpl(html, convertType(options, Frame.SetContentOptions.class)));
}
@Override
@@ -1109,7 +1135,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void setInputFiles(String selector, Path[] files, SetInputFilesOptions options) {
withLogging("Page.setInputFiles",
() -> mainFrame.setInputFilesImpl(selector, files, convertViaJson(options, Frame.SetInputFilesOptions.class)));
() -> mainFrame.setInputFilesImpl(selector, files, convertType(options, Frame.SetInputFilesOptions.class)));
}
@Override
@@ -1120,7 +1146,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void setInputFiles(String selector, FilePayload[] files, SetInputFilesOptions options) {
withLogging("Page.setInputFiles",
() -> mainFrame.setInputFilesImpl(selector, files, convertViaJson(options, Frame.SetInputFilesOptions.class)));
() -> mainFrame.setInputFilesImpl(selector, files, convertType(options, Frame.SetInputFilesOptions.class)));
}
@Override
@@ -1136,13 +1162,13 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void tap(String selector, TapOptions options) {
withLogging("Page.tap",
() -> mainFrame.tapImpl(selector, convertViaJson(options, Frame.TapOptions.class)));
() -> mainFrame.tapImpl(selector, convertType(options, Frame.TapOptions.class)));
}
@Override
public String textContent(String selector, TextContentOptions options) {
return withLogging("Page.textContent",
() -> mainFrame.textContentImpl(selector, convertViaJson(options, Frame.TextContentOptions.class)));
() -> mainFrame.textContentImpl(selector, convertType(options, Frame.TextContentOptions.class)));
}
@Override
@@ -1158,13 +1184,13 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void type(String selector, String text, TypeOptions options) {
withLogging("Page.type",
() -> mainFrame.typeImpl(selector, text, convertViaJson(options, Frame.TypeOptions.class)));
() -> mainFrame.typeImpl(selector, text, convertType(options, Frame.TypeOptions.class)));
}
@Override
public void uncheck(String selector, UncheckOptions options) {
withLogging("Page.uncheck",
() -> mainFrame.uncheckImpl(selector, convertViaJson(options, Frame.UncheckOptions.class)));
() -> mainFrame.uncheckImpl(selector, convertType(options, Frame.UncheckOptions.class)));
}
@Override
@@ -1237,30 +1263,30 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public JSHandle waitForFunction(String pageFunction, Object arg, WaitForFunctionOptions options) {
return withLogging("Page.waitForFunction",
() -> mainFrame.waitForFunctionImpl(pageFunction, arg, convertViaJson(options, Frame.WaitForFunctionOptions.class)));
() -> mainFrame.waitForFunctionImpl(pageFunction, arg, convertType(options, Frame.WaitForFunctionOptions.class)));
}
@Override
public void waitForLoadState(LoadState state, WaitForLoadStateOptions options) {
withWaitLogging("Page.waitForLoadState", () -> {
mainFrame.waitForLoadStateImpl(state, convertViaJson(options, Frame.WaitForLoadStateOptions.class));
withWaitLogging("Page.waitForLoadState", logger -> {
mainFrame.waitForLoadStateImpl(state, convertType(options, Frame.WaitForLoadStateOptions.class), logger);
return null;
});
}
@Override
public Response waitForNavigation(WaitForNavigationOptions options, Runnable code) {
return withLogging("Page.waitForNavigation", () -> waitForNavigationImpl(code, options));
return withWaitLogging("Page.waitForNavigation", logger -> waitForNavigationImpl(logger, code, options));
}
Response waitForNavigationImpl(Runnable code, WaitForNavigationOptions options) {
private Response waitForNavigationImpl(Logger logger, Runnable code, WaitForNavigationOptions options) {
Frame.WaitForNavigationOptions frameOptions = new Frame.WaitForNavigationOptions();
if (options != null) {
frameOptions.timeout = options.timeout;
frameOptions.waitUntil = options.waitUntil;
frameOptions.url = options.url;
}
return mainFrame.waitForNavigationImpl(code, frameOptions);
return mainFrame.waitForNavigationImpl(logger, code, frameOptions);
}
void frameNavigated(FrameImpl frame) {
@@ -1312,21 +1338,28 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Request waitForRequest(String urlGlob, WaitForRequestOptions options, Runnable code) {
return waitForRequest(toRequestPredicate(new UrlMatcher(browserContext.baseUrl, urlGlob)), options, code);
return waitForRequest(new UrlMatcher(browserContext.baseUrl, urlGlob), null, options, code);
}
@Override
public Request waitForRequest(Pattern urlPattern, WaitForRequestOptions options, Runnable code) {
return waitForRequest(toRequestPredicate(new UrlMatcher(urlPattern)), options, code);
return waitForRequest(new UrlMatcher(urlPattern), null, options, code);
}
@Override
public Request waitForRequest(Predicate<Request> predicate, WaitForRequestOptions options, Runnable code) {
return withWaitLogging("Page.waitForRequest", () -> waitForRequestImpl(predicate, options, code));
return waitForRequest(null, predicate, options, code);
}
private static Predicate<Request> toRequestPredicate(UrlMatcher matcher) {
return request -> matcher.test(request.url());
private Request waitForRequest(UrlMatcher urlMatcher, Predicate<Request> predicate, WaitForRequestOptions options, Runnable code) {
return withWaitLogging("Page.waitForRequest", logger -> {
logger.log("waiting for request " + ((urlMatcher == null) ? "matching predicate" : urlMatcher.toString()));
Predicate<Request> requestPredicate = predicate;
if (requestPredicate == null) {
requestPredicate = request -> urlMatcher.test(request.url());;
}
return waitForRequestImpl(requestPredicate, options, code);
});
}
private Request waitForRequestImpl(Predicate<Request> predicate, WaitForRequestOptions options, Runnable code) {
@@ -1338,7 +1371,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Request waitForRequestFinished(WaitForRequestFinishedOptions options, Runnable code) {
return withWaitLogging("Page.waitForRequestFinished", () -> waitForRequestFinishedImpl(options, code));
return withWaitLogging("Page.waitForRequestFinished", logger -> waitForRequestFinishedImpl(options, code));
}
private Request waitForRequestFinishedImpl(WaitForRequestFinishedOptions options, Runnable code) {
@@ -1350,21 +1383,28 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Response waitForResponse(String urlGlob, WaitForResponseOptions options, Runnable code) {
return waitForResponse(toResponsePredicate(new UrlMatcher(browserContext.baseUrl, urlGlob)), options, code);
return waitForResponse(new UrlMatcher(browserContext.baseUrl, urlGlob), null, options, code);
}
@Override
public Response waitForResponse(Pattern urlPattern, WaitForResponseOptions options, Runnable code) {
return waitForResponse(toResponsePredicate(new UrlMatcher(urlPattern)), options, code);
return waitForResponse(new UrlMatcher(urlPattern), null, options, code);
}
@Override
public Response waitForResponse(Predicate<Response> predicate, WaitForResponseOptions options, Runnable code) {
return withLogging("Page.waitForResponse", () -> waitForResponseImpl(predicate, options, code));
return waitForResponse(null, predicate, options, code);
}
private static Predicate<Response> toResponsePredicate(UrlMatcher matcher) {
return response -> matcher.test(response.url());
private Response waitForResponse(UrlMatcher urlMatcher, Predicate<Response> predicate, WaitForResponseOptions options, Runnable code) {
return withWaitLogging("Page.waitForResponse", logger -> {
logger.log("waiting for response " + ((urlMatcher == null) ? "matching predicate" : urlMatcher.toString()));
Predicate<Response> responsePredicate = predicate;
if (responsePredicate == null) {
responsePredicate = response -> urlMatcher.test(response.url());;
}
return waitForResponseImpl(responsePredicate, options, code);
});
}
private Response waitForResponseImpl(Predicate<Response> predicate, WaitForResponseOptions options, Runnable code) {
@@ -1377,7 +1417,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public ElementHandle waitForSelector(String selector, WaitForSelectorOptions options) {
return withLogging("Page.waitForSelector",
() -> mainFrame.waitForSelectorImpl(selector, convertViaJson(options, Frame.WaitForSelectorOptions.class)));
() -> mainFrame.waitForSelectorImpl(selector, convertType(options, Frame.WaitForSelectorOptions.class)));
}
@Override
@@ -1401,7 +1441,10 @@ public class PageImpl extends ChannelOwner implements Page {
}
private void waitForURL(UrlMatcher matcher, WaitForURLOptions options) {
withLogging("Page.waitForURL", () -> mainFrame.waitForURLImpl(matcher, convertViaJson(options, Frame.WaitForURLOptions.class)));
withWaitLogging("Page.waitForURL", logger -> {
mainFrame.waitForURLImpl(logger, matcher, convertType(options, Frame.WaitForURLOptions.class));
return null;
});
}
@Override
@@ -17,12 +17,13 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.APIRequest;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Selectors;
import com.microsoft.playwright.impl.driver.Driver;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -31,17 +32,23 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
private Process driverProcess;
public static PlaywrightImpl create(CreateOptions options) {
return createImpl(options, false);
}
public static PlaywrightImpl createImpl(CreateOptions options, boolean forceNewDriverInstanceForTests) {
Map<String, String> env = Collections.emptyMap();
if (options != null && options.env != null) {
env = options.env;
}
Driver driver = forceNewDriverInstanceForTests ?
Driver.createAndInstall(env, true) :
Driver.ensureDriverInstalled(env, true);
try {
Map<String, String> env = Collections.emptyMap();
if (options != null && options.env != null) {
env = options.env;
}
Path driver = Driver.ensureDriverInstalled(env);
ProcessBuilder pb = new ProcessBuilder(driver.toString(), "run-driver");
ProcessBuilder pb = driver.createProcessBuilder();
pb.command().add("run-driver");
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
pb.environment().putAll(env);
Process p = pb.start();
Connection connection = new Connection(new PipeTransport(p.getInputStream(), p.getOutputStream()));
Connection connection = new Connection(new PipeTransport(p.getInputStream(), p.getOutputStream()), env);
PlaywrightImpl result = connection.initializePlaywright();
result.driverProcess = p;
result.initSharedSelectors(null);
@@ -55,7 +62,9 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
private final BrowserTypeImpl firefox;
private final BrowserTypeImpl webkit;
private final SelectorsImpl selectors;
private SharedSelectors sharedSelectors;;
private final APIRequestImpl apiRequest;
private final LocalUtils localUtils;
private SharedSelectors sharedSelectors;
PlaywrightImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
@@ -64,12 +73,17 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
webkit = parent.connection.getExistingObject(initializer.getAsJsonObject("webkit").get("guid").getAsString());
selectors = connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString());
apiRequest = new APIRequestImpl(this);
localUtils = connection.getExistingObject(initializer.getAsJsonObject("utils").get("guid").getAsString());
chromium.localUtils = localUtils;
firefox.localUtils = localUtils;
webkit.localUtils = localUtils;
}
void initSharedSelectors(PlaywrightImpl parent) {
assert sharedSelectors == null;
if (parent == null) {
sharedSelectors = new SharedSelectors();;
sharedSelectors = new SharedSelectors();
} else {
sharedSelectors = parent.sharedSelectors;
}
@@ -90,6 +104,11 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
return firefox;
}
@Override
public APIRequest request() {
return apiRequest;
}
@Override
public BrowserTypeImpl webkit() {
return webkit;
@@ -31,6 +31,7 @@ class SerializedValue{
// Possible values: { 'null, 'undefined, 'NaN, 'Infinity, '-Infinity, '-0 }
String v;
String d;
String u;
public static class R {
String p;
String f;
@@ -43,6 +44,8 @@ class SerializedValue{
}
O[] o;
Number h;
Integer id;
Integer ref;
}
class SerializedArgument{
@@ -83,6 +86,7 @@ class ExpectedTextValue {
String string;
String regexSource;
String regexFlags;
Boolean ignoreCase;
Boolean matchSubstring;
Boolean normalizeWhiteSpace;
}
@@ -42,6 +42,14 @@ public class RequestImpl extends ChannelOwner implements Request {
String failure;
Timing timing;
boolean didFailOrFinish;
private FallbackOverrides fallbackOverrides;
static class FallbackOverrides {
String url;
String method;
byte[] postData;
Map<String, String> headers;
}
RequestImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
@@ -75,6 +83,9 @@ public class RequestImpl extends ChannelOwner implements Request {
@Override
public Map<String, String> headers() {
if (fallbackOverrides != null && fallbackOverrides.headers != null) {
return new RawHeaders(Utils.toHeadersList(fallbackOverrides.headers)).headers();
}
return headers.headers();
}
@@ -95,19 +106,26 @@ public class RequestImpl extends ChannelOwner implements Request {
@Override
public String method() {
if (fallbackOverrides != null && fallbackOverrides.method != null) {
return fallbackOverrides.method;
}
return initializer.get("method").getAsString();
}
@Override
public String postData() {
if (postData == null) {
byte[] buffer = postDataBuffer();
if (buffer == null) {
return null;
}
return new String(postData, StandardCharsets.UTF_8);
return new String(buffer, StandardCharsets.UTF_8);
}
@Override
public byte[] postDataBuffer() {
if (fallbackOverrides != null && fallbackOverrides.postData != null) {
return fallbackOverrides.postData;
}
return postData;
}
@@ -156,6 +174,9 @@ public class RequestImpl extends ChannelOwner implements Request {
@Override
public String url() {
if (fallbackOverrides != null && fallbackOverrides.url != null) {
return fallbackOverrides.url;
}
return initializer.get("url").getAsString();
}
@@ -164,6 +185,9 @@ public class RequestImpl extends ChannelOwner implements Request {
}
private RawHeaders getRawHeaders() {
if (fallbackOverrides != null && fallbackOverrides.headers != null) {
return new RawHeaders(Utils.toHeadersList(fallbackOverrides.headers));
}
if (rawHeaders != null) {
return rawHeaders;
}
@@ -176,4 +200,26 @@ public class RequestImpl extends ChannelOwner implements Request {
rawHeaders = new RawHeaders(asList(gson().fromJson(rawHeadersJson, HttpHeader[].class)));
return rawHeaders;
}
void applyFallbackOverrides(FallbackOverrides overrides) {
if (fallbackOverrides == null) {
fallbackOverrides = new FallbackOverrides();
}
if (overrides.url != null) {
fallbackOverrides.url = overrides.url;
}
if (overrides.method != null) {
fallbackOverrides.method = overrides.method;
}
if (overrides.headers != null) {
fallbackOverrides.headers = overrides.headers;
}
if (overrides.postData != null) {
fallbackOverrides.postData = overrides.postData;
}
}
FallbackOverrides fallbackOverridesForResume() {
return fallbackOverrides;
}
}
@@ -0,0 +1,128 @@
/*
* 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.options.FormData;
import com.microsoft.playwright.options.RequestOptions;
import java.util.LinkedHashMap;
import java.util.Map;
public class RequestOptionsImpl implements RequestOptions {
Map<String, Object> params;
String method;
Map<String, String> headers;
Object data;
FormDataImpl form;
FormDataImpl multipart;
Boolean failOnStatusCode;
Boolean ignoreHTTPSErrors;
Double timeout;
Integer maxRedirects;
@Override
public RequestOptions setHeader(String name, String value) {
if (headers == null) {
headers = new LinkedHashMap<>();
}
headers.put(name, value);
return this;
}
@Override
public RequestOptions setData(String data) {
this.data = data;
return this;
}
@Override
public RequestOptions setData(byte[] data) {
this.data = data;
return this;
}
@Override
public RequestOptions setData(Object data) {
this.data = data;
return this;
}
@Override
public RequestOptions setForm(FormData form) {
this.form = (FormDataImpl) form;
return this;
}
@Override
public RequestOptions setMethod(String method) {
this.method = method;
return this;
}
@Override
public RequestOptions setMultipart(FormData form) {
this.multipart = (FormDataImpl) form;
return this;
}
@Override
public RequestOptions setQueryParam(String name, String value) {
return setQueryParamImpl(name, value);
}
@Override
public RequestOptions setQueryParam(String name, boolean value) {
return setQueryParamImpl(name, value);
}
@Override
public RequestOptions setQueryParam(String name, int value) {
return setQueryParamImpl(name, value);
}
private RequestOptions setQueryParamImpl(String name, Object value) {
if (params == null) {
params = new LinkedHashMap<>();
}
params.put(name, value);
return this;
}
@Override
public RequestOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
@Override
public RequestOptions setFailOnStatusCode(boolean failOnStatusCode) {
this.failOnStatusCode = failOnStatusCode;
return this;
}
@Override
public RequestOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) {
this.ignoreHTTPSErrors = ignoreHTTPSErrors;
return this;
}
@Override
public RequestOptions setMaxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
return this;
}
}
@@ -83,6 +83,11 @@ public class ResponseImpl extends ChannelOwner implements Response {
return request().frame();
}
@Override
public boolean fromServiceWorker() {
return initializer.get("fromServiceWorker").getAsBoolean();
}
@Override
public Map<String, String> headers() {
return headers.headers();
@@ -17,8 +17,8 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Request;
import com.microsoft.playwright.Route;
import java.io.IOException;
@@ -28,13 +28,20 @@ import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import static com.microsoft.playwright.impl.Utils.convertType;
public class RouteImpl extends ChannelOwner implements Route {
private final RequestImpl request;
private boolean handled;
public RouteImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
request = connection.getExistingObject(initializer.getAsJsonObject("request").get("guid").getAsString());
}
@Override
public void abort(String errorCode) {
startHandling();
withLogging("Route.abort", () -> {
JsonObject params = new JsonObject();
params.addProperty("errorCode", errorCode);
@@ -42,42 +49,72 @@ public class RouteImpl extends ChannelOwner implements Route {
});
}
@Override
public void resume(ResumeOptions options) {
withLogging("Route.resume", () -> resumeImpl(options));
boolean isHandled() {
return handled;
}
private void resumeImpl(ResumeOptions options) {
@Override
public void resume(ResumeOptions options) {
startHandling();
applyOverrides(convertType(options, FallbackOptions.class));
withLogging("Route.resume", () -> resumeImpl(request().fallbackOverridesForResume()));
}
@Override
public void fallback(FallbackOptions options) {
if (handled) {
throw new PlaywrightException("Route is already handled!");
}
applyOverrides(options);
}
private void applyOverrides(FallbackOptions options) {
if (options == null) {
options = new ResumeOptions();
}
JsonObject params = new JsonObject();
if (options.url != null) {
params.addProperty("url", options.url);
}
if (options.method != null) {
params.addProperty("method", options.method);
}
if (options.headers != null) {
params.add("headers", Serialization.toProtocol(options.headers));
return;
}
RequestImpl.FallbackOverrides overrides = new RequestImpl.FallbackOverrides();
overrides.url = options.url;
overrides.method = options.method;
overrides.headers = options.headers;
if (options.postData != null) {
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());
overrides.postData = getPostDataBytes(options.postData);
}
request().applyFallbackOverrides(overrides);
}
private void resumeImpl(RequestImpl.FallbackOverrides options) {
JsonObject params = new JsonObject();
if (options != null) {
if (options.url != null) {
params.addProperty("url", options.url);
}
if (options.method != null) {
params.addProperty("method", options.method);
}
if (options.headers != null) {
params.add("headers", Serialization.toProtocol(options.headers));
}
if (options.postData != null) {
String base64 = Base64.getEncoder().encodeToString(options.postData);
params.addProperty("postData", base64);
}
String base64 = Base64.getEncoder().encodeToString(bytes);
params.addProperty("postData", base64);
}
sendMessageAsync("continue", params);
}
private static byte[] getPostDataBytes(Object postData) {
if (postData instanceof byte[]) {
return (byte[]) postData;
}
if (postData instanceof String) {
return ((String) postData).getBytes(StandardCharsets.UTF_8);
}
throw new PlaywrightException("postData must be either String or byte[], found: " + postData.getClass().getName());
}
@Override
public void fulfill(FulfillOptions options) {
startHandling();
withLogging("Route.fulfill", () -> fulfillImpl(options));
}
@@ -86,16 +123,30 @@ public class RouteImpl extends ChannelOwner implements Route {
options = new FulfillOptions();
}
int status = options.status == null ? 200 : options.status;
String body = "";
Integer status = options.status;
Map<String, String> headersOption = options.headers;
String fetchResponseUid = null;
if (options.response != null) {
if (status == null) {
status = options.response.status();
}
if (headersOption == null) {
headersOption = options.response.headers();
}
}
if (status == null) {
status = 200;
}
String body = null;
boolean isBase64 = false;
int length = 0;
if (options.path != null) {
try {
byte[] buffer = Files.readAllBytes(options.path);
body = Base64.getEncoder().encodeToString(buffer);
isBase64 = true;
length = buffer.length;
byte[] buffer = Files.readAllBytes(options.path);
body = Base64.getEncoder().encodeToString(buffer);
isBase64 = true;
length = buffer.length;
} catch (IOException e) {
throw new PlaywrightException("Failed to read from file: " + options.path, e);
}
@@ -107,11 +158,22 @@ public class RouteImpl extends ChannelOwner implements Route {
body = Base64.getEncoder().encodeToString(options.bodyBytes);
isBase64 = true;
length = options.bodyBytes.length;
} else if (options.response != null) {
APIResponseImpl response = (APIResponseImpl) options.response;
if (response.context.connection == connection) {
fetchResponseUid = response.fetchUid();
} else {
byte[] bodyBytes = response.body();
body = Base64.getEncoder().encodeToString(bodyBytes);
isBase64 = true;
length = bodyBytes.length;
}
}
Map<String, String> headers = new LinkedHashMap<>();
if (options.headers != null) {
for (Map.Entry<String, String> h : options.headers.entrySet()) {
if (headersOption != null) {
for (Map.Entry<String, String> h : headersOption.entrySet()) {
headers.put(h.getKey().toLowerCase(), h.getValue());
}
}
@@ -128,11 +190,29 @@ public class RouteImpl extends ChannelOwner implements Route {
params.add("headers", Serialization.toProtocol(headers));
params.addProperty("isBase64", isBase64);
params.addProperty("body", body);
if (fetchResponseUid != null) {
params.addProperty("fetchResponseUid", fetchResponseUid);
}
sendMessageAsync("fulfill", params);
}
@Override
public Request request() {
return connection.getExistingObject(initializer.getAsJsonObject("request").get("guid").getAsString());
public RequestImpl request() {
return request;
}
void redirectNavigationRequest(String redirectURL) {
startHandling();
JsonObject params = new JsonObject();
params.addProperty("url", redirectURL);
// TODO: _raceWithPageClose ?
sendMessageAsync("redirectNavigationRequest", params);
}
private void startHandling() {
if (handled) {
throw new PlaywrightException("Route is already handled!");
}
handled = true;
}
}
@@ -19,6 +19,7 @@ package com.microsoft.playwright.impl;
import com.microsoft.playwright.Route;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -37,22 +38,16 @@ class Router {
this.times = times;
}
boolean handle(Route route) {
if (times != null && times <= 0) {
return false;
}
if (!matcher.test(route.request().url())) {
return false;
}
if (times != null) {
--times;
}
void handle(RouteImpl route) {
handler.accept(route);
return true;
}
boolean isDone() {
return times != null && times <= 0;
boolean decrementRemainingCallCount() {
if (times == null) {
return false;
}
--times;
return times <= 0;
}
}
@@ -70,15 +65,23 @@ class Router {
return routes.size();
}
boolean handle(Route route) {
for (RouteInfo info : routes) {
if (info.handle(route)) {
if (info.isDone()) {
routes.remove(info);
}
return true;
enum HandleResult { NoMatchingHandler, FoundMatchingHandler}
HandleResult handle(RouteImpl route) {
HandleResult result = HandleResult.NoMatchingHandler;
for (Iterator<RouteInfo> it = routes.iterator(); it.hasNext();) {
RouteInfo info = it.next();
if (!info.matcher.test(route.request().url())) {
continue;
}
if (info.decrementRemainingCallCount()) {
it.remove();
}
result = HandleResult.FoundMatchingHandler;
info.handle(route);
if (route.isHandled()) {
break;
}
}
return false;
return result;
}
}
@@ -28,19 +28,32 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
import static com.microsoft.playwright.impl.Utils.fromJsRegexFlags;
class Serialization {
private static Gson gson = new GsonBuilder()
private static final Gson gson = new GsonBuilder().disableHtmlEscaping()
.registerTypeAdapter(SameSiteAttribute.class, new SameSiteAdapter().nullSafe())
.registerTypeAdapter(BrowserChannel.class, new ToLowerCaseAndDashSerializer<BrowserChannel>())
.registerTypeAdapter(ColorScheme.class, new ToLowerCaseAndDashSerializer<ColorScheme>())
.registerTypeAdapter(Media.class, new ToLowerCaseSerializer<Media>())
.registerTypeAdapter(ForcedColors.class, new ToLowerCaseSerializer<ForcedColors>())
.registerTypeAdapter(ReducedMotion.class, new ToLowerCaseAndDashSerializer<ReducedMotion>())
.registerTypeAdapter(ScreenshotAnimations.class, new ToLowerCaseSerializer<ScreenshotAnimations>())
.registerTypeAdapter(ScreenshotType.class, new ToLowerCaseSerializer<ScreenshotType>())
.registerTypeAdapter(ScreenshotScale.class, new ToLowerCaseSerializer<ScreenshotScale>())
.registerTypeAdapter(ScreenshotCaret.class, new ToLowerCaseSerializer<ScreenshotCaret>())
.registerTypeAdapter(ServiceWorkerPolicy.class, new ToLowerCaseAndDashSerializer<ServiceWorkerPolicy>())
.registerTypeAdapter(MouseButton.class, new ToLowerCaseSerializer<MouseButton>())
.registerTypeAdapter(LoadState.class, new ToLowerCaseSerializer<LoadState>())
.registerTypeAdapter(WaitUntilState.class, new ToLowerCaseSerializer<WaitUntilState>())
@@ -50,7 +63,7 @@ class Serialization {
.registerTypeHierarchyAdapter(JSHandleImpl.class, new HandleSerializer())
.registerTypeAdapter((new TypeToken<Map<String, String>>(){}).getType(), new StringMapSerializer())
.registerTypeAdapter((new TypeToken<Map<String, Object>>(){}).getType(), new FirefoxUserPrefsSerializer())
.registerTypeHierarchyAdapter(Path.class, new PathSerializer()).create();;
.registerTypeHierarchyAdapter(Path.class, new PathSerializer()).create();
static Gson gson() {
return gson;
@@ -68,82 +81,138 @@ class Serialization {
return result;
}
private static SerializedValue serializeValue(Object value, List<JSHandleImpl> handles, int depth) {
if (depth > 100) {
throw new PlaywrightException("Maximum argument depth exceeded");
private static class ValueSerializer {
// hashCode() of a map containing itself as a key will throw stackoverflow exception,
// so we user wrappers.
private static class HashableValue {
final Object value;
HashableValue(Object value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
return value == ((HashableValue) o).value;
}
@Override
public int hashCode() {
return System.identityHashCode(value);
}
}
SerializedValue result = new SerializedValue();
if (value instanceof JSHandleImpl) {
result.h = handles.size();
handles.add((JSHandleImpl) value);
private final Map<HashableValue, Integer> valueToId = new HashMap<>();
private int lastId = 0;
private final List<JSHandleImpl> handles = new ArrayList<>();
private final SerializedValue serializedValue;
ValueSerializer(Object value) {
serializedValue = serializeValue(value);
}
SerializedArgument toSerializedArgument() {
SerializedArgument result = new SerializedArgument();
result.value = serializedValue;
result.handles = new Channel[handles.size()];
int i = 0;
for (JSHandleImpl handle : handles) {
result.handles[i] = new Channel();
result.handles[i].guid = handle.guid;
++i;
}
return result;
}
if (value == null) {
result.v = "undefined";
} else if (value instanceof Double) {
double d = ((Double) value);
if (d == Double.POSITIVE_INFINITY) {
result.v = "Infinity";
} else if (d == Double.NEGATIVE_INFINITY) {
result.v = "-Infinity";
} else if (d == -0) {
result.v = "-0";
} else if (Double.isNaN(d)) {
result.v = "NaN";
private SerializedValue serializeValue(Object value) {
SerializedValue result = new SerializedValue();
if (value instanceof JSHandleImpl) {
result.h = handles.size();
handles.add((JSHandleImpl) value);
return result;
}
if (value == null) {
result.v = "undefined";
} else if (value instanceof Double) {
double d = ((Double) value);
if (d == Double.POSITIVE_INFINITY) {
result.v = "Infinity";
} else if (d == Double.NEGATIVE_INFINITY) {
result.v = "-Infinity";
} else if (d == -0) {
result.v = "-0";
} else if (Double.isNaN(d)) {
result.v = "NaN";
} else {
result.n = d;
}
} else if (value instanceof Boolean) {
result.b = (Boolean) value;
} else if (value instanceof Integer) {
result.n = (Integer) value;
} else if (value instanceof String) {
result.s = (String) value;
} else if (value instanceof Date) {
result.d = ((Date)value).toInstant().toString();
} else if (value instanceof LocalDateTime) {
result.d = ((LocalDateTime)value).atZone(ZoneId.systemDefault()).toInstant().toString();
} else if (value instanceof URL) {
result.u = ((URL)value).toString();
} else if (value instanceof Pattern) {
result.r = new SerializedValue.R();
result.r.p = ((Pattern)value).pattern();
result.r.f = toJsRegexFlags(((Pattern)value));
} else {
result.n = d;
HashableValue mapKey = new HashableValue(value);
Integer id = valueToId.get(mapKey);
if (id != null) {
result.ref = id;
} else {
result.id = ++lastId;
valueToId.put(mapKey, lastId);
if (value instanceof List) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (List<?>) value) {
list.add(serializeValue(o));
}
result.a = list.toArray(new SerializedValue[0]);
} else if (value instanceof Map) {
List<SerializedValue.O> list = new ArrayList<>();
@SuppressWarnings("unchecked")
Map<String, ?> map = (Map<String, ?>) value;
for (Map.Entry<String, ?> e : map.entrySet()) {
SerializedValue.O o = new SerializedValue.O();
o.k = e.getKey();
o.v = serializeValue(e.getValue());
list.add(o);
}
result.o = list.toArray(new SerializedValue.O[0]);
} else if (value instanceof Object[]) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (Object[]) value) {
list.add(serializeValue(o));
}
result.a = list.toArray(new SerializedValue[0]);
} else {
throw new PlaywrightException("Unsupported type of argument: " + value);
}
}
}
} else if (value instanceof Boolean) {
result.b = (Boolean) value;
} else if (value instanceof Integer) {
result.n = (Integer) value;
} else if (value instanceof String) {
result.s = (String) value;
} else if (value instanceof List) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (List<?>) value) {
list.add(serializeValue(o, handles, depth + 1));
}
result.a = list.toArray(new SerializedValue[0]);
} else if (value instanceof Map) {
List<SerializedValue.O> list = new ArrayList<>();
@SuppressWarnings("unchecked")
Map<String, ?> map = (Map<String, ?>) value;
for (Map.Entry<String, ?> e : map.entrySet()) {
SerializedValue.O o = new SerializedValue.O();
o.k = e.getKey();
o.v = serializeValue(e.getValue(), handles, depth + 1);
list.add(o);
}
result.o = list.toArray(new SerializedValue.O[0]);
} else if (value instanceof Object[]) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (Object[]) value) {
list.add(serializeValue(o, handles, depth + 1));
}
result.a = list.toArray(new SerializedValue[0]);
} else {
throw new PlaywrightException("Unsupported type of argument: " + value);
return result;
}
return result;
}
static SerializedArgument serializeArgument(Object arg) {
SerializedArgument result = new SerializedArgument();
List<JSHandleImpl> handles = new ArrayList<>();
result.value = serializeValue(arg, handles, 0);
result.handles = new Channel[handles.size()];
int i = 0;
for (JSHandleImpl handle : handles) {
result.handles[i] = new Channel();
result.handles[i].guid = handle.guid;
++i;
}
return result;
return new ValueSerializer(arg).toSerializedArgument();
}
static <T> T deserialize(SerializedValue value) {
return deserialize(value, new HashMap<>());
}
@SuppressWarnings("unchecked")
static <T> T deserialize(SerializedValue value) {
private static <T> T deserialize(SerializedValue value, Map<Integer, Object> idToValue) {
if (value.ref != null) {
return (T) idToValue.get(value.ref);
}
if (value.n != null) {
if (value.n.doubleValue() == (double) value.n.intValue()) {
return (T) Integer.valueOf(value.n.intValue());
@@ -154,6 +223,17 @@ class Serialization {
return (T) value.b;
if (value.s != null)
return (T) value.s;
if (value.u != null) {
try {
return (T)(new URL(value.u));
} catch (MalformedURLException e) {
throw new PlaywrightException("Unexpected value: " + value.u, e);
}
}
if (value.d != null)
return (T)(Date.from(Instant.parse(value.d)));
if (value.r != null)
return (T)(Pattern.compile(value.r.p, fromJsRegexFlags(value.r.f)));
if (value.v != null) {
switch (value.v) {
case "undefined":
@@ -174,15 +254,17 @@ class Serialization {
}
if (value.a != null) {
List<Object> list = new ArrayList<>();
idToValue.put(value.id, list);
for (SerializedValue v : value.a) {
list.add(deserialize(v));
list.add(deserialize(v, idToValue));
}
return (T) list;
}
if (value.o != null) {
Map<String, Object> map = new LinkedHashMap<>();
idToValue.put(value.id, map);
for (SerializedValue.O o : value.o) {
map.put(o.k, deserialize(o.v));
map.put(o.k, deserialize(o.v, idToValue));
}
return (T) map;
}
@@ -209,39 +291,72 @@ class Serialization {
}
}
static JsonArray toJsonArray(Path[] files) {
JsonArray jsonFiles = new JsonArray();
for (Path p : files) {
jsonFiles.add(p.toAbsolutePath().toString());
}
return jsonFiles;
}
static JsonArray toJsonArray(FilePayload[] files) {
JsonArray jsonFiles = new JsonArray();
for (FilePayload p : files) {
JsonObject jsonFile = new JsonObject();
jsonFile.addProperty("name", p.name);
jsonFile.addProperty("mimeType", p.mimeType);
jsonFile.addProperty("buffer", Base64.getEncoder().encodeToString(p.buffer));
jsonFiles.add(jsonFile);
jsonFiles.add(toProtocol(p));
}
return jsonFiles;
}
static JsonObject toProtocol(FilePayload p) {
JsonObject jsonFile = new JsonObject();
jsonFile.addProperty("name", p.name);
jsonFile.addProperty("mimeType", p.mimeType);
jsonFile.addProperty("buffer", Base64.getEncoder().encodeToString(p.buffer));
return jsonFile;
}
static JsonArray toProtocol(ElementHandle[] handles) {
JsonArray jsonElements = new JsonArray();
for (ElementHandle handle : handles) {
JsonObject jsonHandle = new JsonObject();
jsonHandle.addProperty("guid", ((ElementHandleImpl) handle).guid);
jsonElements.add(jsonHandle);
jsonElements.add(((ElementHandleImpl) handle).toProtocolRef());
}
return jsonElements;
}
static JsonArray toProtocol(Map<String, String> map) {
return toNameValueArray(map);
}
static void addHarUrlFilter(JsonObject options, Object urlFilter) {
if (urlFilter instanceof String) {
options.addProperty("urlGlob", (String) urlFilter);
} else if (urlFilter instanceof Pattern) {
Pattern pattern = (Pattern) urlFilter;
options.addProperty("urlRegexSource", pattern.pattern());
options.addProperty("urlRegexFlags", toJsRegexFlags(pattern));
}
}
static JsonArray toNameValueArray(Map<String, ?> map) {
JsonArray array = new JsonArray();
for (Map.Entry<String, String> e : map.entrySet()) {
for (Map.Entry<String, ?> e : map.entrySet()) {
JsonObject item = new JsonObject();
item.addProperty("name", e.getKey());
item.addProperty("value", e.getValue());
item.add("value", gson().toJsonTree(e.getValue()));
array.add(item);
}
return array;
}
static Map<String, String> fromNameValues(JsonArray array) {
Map<String, String> map = new LinkedHashMap<>();
for (JsonElement element : array) {
JsonObject pair = element.getAsJsonObject();
map.put(pair.get("name").getAsString(), pair.get("value").getAsString());
}
return map;
}
static List<String> parseStringList(JsonArray array) {
List<String> result = new ArrayList<>();
for (JsonElement e : array) {
@@ -272,9 +387,7 @@ class Serialization {
private static class HandleSerializer implements JsonSerializer<JSHandleImpl> {
@Override
public JsonElement serialize(JSHandleImpl src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject json = new JsonObject();
json.addProperty("guid", src.guid);
return json;
return src.toProtocolRef();
}
}
@@ -0,0 +1,122 @@
/*
* 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.JsonArray;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
class StackTraceCollector {
static final String PLAYWRIGHT_JAVA_SRC = "PLAYWRIGHT_JAVA_SRC";
private final List<Path> srcDirs;
private final Map<Path, String> classToSourceCache = new HashMap<>();
static StackTraceCollector createFromEnv(Map<String, String> env) {
String srcRoots = null;
if (env != null) {
srcRoots = env.get(PLAYWRIGHT_JAVA_SRC);
}
if (srcRoots == null) {
srcRoots = System.getenv(PLAYWRIGHT_JAVA_SRC);
}
if (srcRoots == null) {
return null;
}
List<Path> srcDirs = Arrays.stream(srcRoots.split(File.pathSeparator)).map(p -> Paths.get(p)).collect(Collectors.toList());
for (Path srcDir: srcDirs) {
if (!Files.exists(srcDir.toAbsolutePath())) {
throw new PlaywrightException("Source location specified in " + PLAYWRIGHT_JAVA_SRC + " doesn't exist: '" + srcDir.toAbsolutePath() + "'");
}
}
return new StackTraceCollector(srcDirs);
}
private StackTraceCollector(List<Path> srcDirs) {
this.srcDirs = srcDirs;
}
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);
String file = frame.getFileName();
if (file == null) {
return "";
}
return resolveSourcePath(Paths.get(pkg).resolve(file));
}
private String resolveSourcePath(Path relativePath) {
String path = classToSourceCache.get(relativePath);
if (path == null) {
for (Path dir : srcDirs) {
Path absolutePath = dir.resolve(relativePath);
if (Files.exists(absolutePath)) {
path = absolutePath.toString();
classToSourceCache.put(relativePath, path);
break;
}
}
if (path == null) {
path = "";
classToSourceCache.put(relativePath, path);
}
}
return path;
}
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;
}
}
@@ -16,47 +16,61 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
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;
class TracingImpl extends ChannelOwner implements Tracing {
boolean isRemote;
TracingImpl(BrowserContextImpl context) {
this.context = context;
TracingImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
private void stopChunkImpl(Path path) {
JsonObject params = new JsonObject();
params.addProperty("save", path != null);
params.addProperty("skipCompress", false);
JsonObject json = context.sendMessage("tracingStopChunk", params).getAsJsonObject();
String mode = "doNotSave";
if (path != null) {
if (isRemote) {
mode = "compressTrace";
} else {
mode = "compressTraceAndSources";
}
}
params.addProperty("mode", mode);
JsonObject json = sendMessage("tracingStopChunk", params).getAsJsonObject();
if (!json.has("artifact")) {
return;
}
ArtifactImpl artifact = context.connection.getExistingObject(json.getAsJsonObject("artifact").get("guid").getAsString());
ArtifactImpl artifact = connection.getExistingObject(json.getAsJsonObject("artifact").get("guid").getAsString());
// In case of CDP connection browser is null but since the connection is established by
// the driver it is safe to consider the artifact local.
if (context.browser() != null && context.browser().isRemote) {
if (isRemote) {
artifact.isRemote = true;
}
artifact.saveAs(path);
artifact.delete();
// Add local sources to the remote trace if necessary.
if (isRemote && json.has("sourceEntries")) {
JsonArray entries = json.getAsJsonArray("sourceEntries");
connection.localUtils.zip(path, entries);
}
}
@Override
public void start(StartOptions options) {
context.withLogging("Tracing.start", () -> startImpl(options));
withLogging("Tracing.start", () -> startImpl(options));
}
@Override
public void startChunk(StartChunkOptions options) {
context.withLogging("Tracing.startChunk", () -> {
withLogging("Tracing.startChunk", () -> {
startChunkImpl(options);
});
}
@@ -66,7 +80,7 @@ class TracingImpl implements Tracing {
options = new StartChunkOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
context.sendMessage("tracingStartChunk", params);
sendMessage("tracingStartChunk", params);
}
private void startImpl(StartOptions options) {
@@ -74,21 +88,28 @@ class TracingImpl implements Tracing {
options = new StartOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
context.sendMessage("tracingStart", params);
context.sendMessage("tracingStartChunk");
boolean includeSources = options.sources != null && options.sources;
if (includeSources) {
if (!connection.isCollectingStacks()) {
throw new PlaywrightException("Source root directory must be specified via PLAYWRIGHT_JAVA_SRC environment variable when source collection is enabled");
}
params.addProperty("sources", true);
}
sendMessage("tracingStart", params);
sendMessage("tracingStartChunk");
}
@Override
public void stop(StopOptions options) {
context.withLogging("Tracing.stop", () -> {
withLogging("Tracing.stop", () -> {
stopChunkImpl(options == null ? null : options.path);
context.sendMessage("tracingStop");
sendMessage("tracingStop");
});
}
@Override
public void stopChunk(StopChunkOptions options) {
context.withLogging("Tracing.stopChunk", () -> {
withLogging("Tracing.stopChunk", () -> {
stopChunkImpl(options == null ? null : options.path);
});
}
@@ -97,4 +97,13 @@ class UrlMatcher {
public int hashCode() {
return Objects.hash(rawSource);
}
@Override
public String toString() {
if (rawSource == null)
return "<any>";
if (rawSource instanceof Predicate)
return "matching predicate";
return rawSource.toString();
}
}
@@ -16,7 +16,8 @@
package com.microsoft.playwright.impl;
import com.google.gson.*;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.FilePayload;
import com.microsoft.playwright.options.HttpHeader;
@@ -24,32 +25,57 @@ import com.microsoft.playwright.options.HttpHeader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.toJsonArray;
class Utils {
// TODO: generate converter.
static <F, T> T convertViaJson(F f, Class<T> t) {
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 <F, T> T convertType(F f, Class<T> t) {
if (f == null) {
return null;
}
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()));
// Make sure shallow copy is sufficient
if (!t.getSuperclass().equals(Object.class) && !t.getSuperclass().equals(Enum.class)) {
throw new PlaywrightException("Cannot convert to " + t.getCanonicalName() + " that has superclass " + t.getSuperclass().getCanonicalName());
}
if (!f.getClass().getSuperclass().equals(t.getSuperclass())) {
throw new PlaywrightException("Cannot convert from " + t.getCanonicalName() + " that has superclass " + t.getSuperclass().getCanonicalName());
}
if (f instanceof Enum) {
return (T) Enum.valueOf((Class) t, ((Enum) f).name());
}
try {
T result = t.getDeclaredConstructor().newInstance();
for (Field toField : t.getDeclaredFields()) {
// Skip fields added by test coverage tools, see https://github.com/microsoft/playwright-java/issues/802
if (toField.isSynthetic()) {
continue;
}
if (Modifier.isStatic(toField.getModifiers())) {
throw new RuntimeException("Unexpected field modifiers: " + t.getCanonicalName() + "." + toField.getName() + ", modifiers: " + toField.getModifiers());
}
try {
Field fromField = f.getClass().getDeclaredField(toField.getName());
Object value = fromField.get(f);
if (value != null) {
toField.set(result, value);
}
} catch (NoSuchFieldException e) {
continue;
}
}
return result;
} catch (Exception e) {
throw new PlaywrightException("Internal error", e);
}
}
@@ -62,7 +88,7 @@ class Utils {
for (int i = 0; i < glob.length(); ++i) {
char c = glob.charAt(i);
if (escapeGlobChars.contains(c)) {
tokens.append("\\" + c);
tokens.append("\\").append(c);
continue;
}
if (c == '*') {
@@ -100,7 +126,7 @@ class Utils {
tokens.append('|');
break;
}
tokens.append("\\" + c);
tokens.append("\\").append(c);
break;
default:
tokens.append(c);
@@ -123,20 +149,74 @@ class Utils {
return mimeType;
}
static final int maxUplodBufferSize = 50 * 1024 * 1024;
static boolean hasLargeFile(Path[] files) {
for (Path file: files) {
try {
if (Files.size(file)> maxUplodBufferSize) {
return true;
}
} catch (IOException e) {
throw new PlaywrightException("Cannot get file size.", e);
}
}
return false;
}
static void addLargeFileUploadParams(Path[] files, JsonObject params, BrowserContextImpl context) {
if (context.browser().isRemote) {
List<WritableStream> streams = new ArrayList<>();
JsonArray jsonStreams = new JsonArray();
for (Path path : files) {
WritableStream temp = context.createTempFile(path.getFileName().toString());
streams.add(temp);
try (OutputStream out = temp.stream()) {
Files.copy(path, out);
} catch (IOException e) {
throw new PlaywrightException("Failed to copy file to remote server.", e);
}
jsonStreams.add(temp.toProtocolRef());
}
params.add("streams", jsonStreams);
} else {
Path[] absolute = Arrays.stream(files).map(f -> {
try {
return f.toRealPath();
} catch (IOException e) {
throw new PlaywrightException("Cannot get absolute file path", e);
}
}).toArray(Path[]::new);
params.add("localPaths", toJsonArray(absolute));
}
}
static void checkFilePayloadSize(FilePayload[] files) {
for (FilePayload file: files) {
if (file.buffer.length > maxUplodBufferSize) {
throw new PlaywrightException("Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead.");
}
}
}
static FilePayload[] toFilePayloads(Path[] files) {
List<FilePayload> payloads = new ArrayList<>();
for (Path file : files) {
byte[] buffer;
try {
buffer = Files.readAllBytes(file);
} catch (IOException e) {
throw new PlaywrightException("Failed to read from file", e);
}
payloads.add(new FilePayload(file.getFileName().toString(), mimeType(file), buffer));
payloads.add(toFilePayload(file));
}
return payloads.toArray(new FilePayload[0]);
}
static FilePayload toFilePayload(Path file) {
byte[] buffer;
try {
buffer = Files.readAllBytes(file);
} catch (IOException e) {
throw new PlaywrightException("Failed to read from file", e);
}
return new FilePayload(file.getFileName().toString(), null, buffer);
}
static void mkParentDirs(Path file) {
Path dir = file.getParent();
if (dir != null) {
@@ -144,7 +224,7 @@ class Utils {
try {
Files.createDirectories(dir);
} catch (IOException e) {
throw new PlaywrightException("Failed to create parent directory: " + dir.toString(), e);
throw new PlaywrightException("Failed to create parent directory: " + dir, e);
}
}
}
@@ -177,7 +257,7 @@ class Utils {
}
static boolean isSafeCloseError(String error) {
return error.endsWith("Browser has been closed") || error.endsWith("Target page, context or browser has been closed");
return error.contains("Browser has been closed") || error.contains("Target page, context or browser has been closed");
}
static String createGuid() {
@@ -196,4 +276,49 @@ class Utils {
}
return map;
}
static List<HttpHeader> toHeadersList(Map<String, String> headers) {
List<HttpHeader> list = new ArrayList<>();
for (Map.Entry<String, String> entry: headers.entrySet()) {
HttpHeader header = new HttpHeader();
header.name = entry.getKey();
header.value = entry.getValue();
list.add(header);
}
return list;
}
static String toJsRegexFlags(Pattern pattern) {
String regexFlags = "";
if ((pattern.flags() & Pattern.CASE_INSENSITIVE) != 0) {
// Case-insensitive search.
regexFlags += "i";
}
if ((pattern.flags() & Pattern.DOTALL) != 0) {
// Allows . to match newline characters.
regexFlags += "s";
}
if ((pattern.flags() & Pattern.MULTILINE) != 0) {
// Multi-line search.
regexFlags += "m";
}
if ((pattern.flags() & ~(Pattern.MULTILINE | Pattern.CASE_INSENSITIVE | Pattern.DOTALL)) != 0) {
throw new PlaywrightException("Unexpected RegEx flag, only CASE_INSENSITIVE, DOTALL and MULTILINE are supported.");
}
return regexFlags;
}
static int fromJsRegexFlags(String regexFlags) {
int flags = 0;
if (regexFlags.contains("i")) {
flags |= Pattern.CASE_INSENSITIVE;
}
if (regexFlags.contains("s")) {
flags |= Pattern.DOTALL;
}
if (regexFlags.contains("m")) {
flags |= Pattern.MULTILINE;
}
return flags;
}
}
@@ -72,6 +72,9 @@ class VideoImpl implements Video {
@Override
public void saveAs(Path path) {
page.withLogging("Video.saveAs", () -> {
if (!page.isClosed()) {
throw new PlaywrightException("Page is not yet closed. Close the page prior to calling saveAs");
}
try {
waitForArtifact().saveAs(path);
} catch (PlaywrightException e) {
@@ -18,38 +18,52 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import java.util.function.Function;
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;
public class WaitForEventLogger<T> implements Supplier<T>, Logger {
private final Function<Logger, T> supplier;
private final ChannelOwner channel;
private final String waitId;
private final String apiName;
WaitForEventLogger(ChannelOwner channelOwner, String apiName, Supplier<T> supplier) {
WaitForEventLogger(ChannelOwner channelOwner, String apiName, Function<Logger, 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() {
return channel.withLogging(apiName, () -> {
{
JsonObject info = new JsonObject();
info.addProperty("phase", "before");
sendWaitForEventInfo(info);
}
JsonObject info = new JsonObject();
info.addProperty("phase", "after");
try {
return supplier.apply(this);
} catch (RuntimeException e) {
info.addProperty("error", e.getMessage());
throw e;
} finally {
sendWaitForEventInfo(info);
}
});
}
@Override
public void log(String message) {
LoggingSupport.logApiIfEnabled(message);
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);
}
info.addProperty("phase", "log");
info.addProperty("message", message);
sendWaitForEventInfo(info);
}
private void sendWaitForEventInfo(JsonObject info) {
@@ -57,6 +71,6 @@ public class WaitForEventLogger<T> implements Supplier<T> {
info.addProperty("waitId", waitId);
JsonObject params = new JsonObject();
params.add("info", info);
channel.withLogging(apiName, () -> channel.sendMessageAsync("waitForEventInfo", params));
channel.sendMessageAsync("waitForEventInfo", params);
}
}
@@ -22,7 +22,4 @@ interface Waitable<T> {
boolean isDone();
T get();
void dispose();
default <U> Waitable<U> apply(Function<T, U> transform) {
return new WaitableAdapter<T, U>(this, transform);
}
}
@@ -87,7 +87,7 @@ class WebSocketImpl extends ChannelOwner implements WebSocket {
@Override
public WebSocketFrame waitForFrameReceived(WaitForFrameReceivedOptions options, Runnable code) {
return withWaitLogging("WebSocket.waitForFrameReceived", () -> waitForFrameReceivedImpl(options, code));
return withWaitLogging("WebSocket.waitForFrameReceived", logger -> waitForFrameReceivedImpl(options, code));
}
private WebSocketFrame waitForFrameReceivedImpl(WaitForFrameReceivedOptions options, Runnable code) {
@@ -99,7 +99,7 @@ class WebSocketImpl extends ChannelOwner implements WebSocket {
@Override
public WebSocketFrame waitForFrameSent(WaitForFrameSentOptions options, Runnable code) {
return withWaitLogging("WebSocket.waitForFrameSent", () -> waitForFrameSentImpl(options, code));
return withWaitLogging("WebSocket.waitForFrameSent", logger -> waitForFrameSentImpl(options, code));
}
private WebSocketFrame waitForFrameSentImpl(WaitForFrameSentOptions options, Runnable code) {

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