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

Compare commits

...

181 Commits

Author SHA1 Message Date
Yury Semikhatsky ef8656036f chore: set version to 1.35.0 (#1306) 2023-06-12 16:27:49 -07:00
Yury Semikhatsky 2a047bff9a chore: roll 1.35.0 (#1304) 2023-06-12 15:54:40 -07:00
Yury Semikhatsky 4e5285950d chore: roll to 1.35.0-beta (#1298) 2023-06-08 18:23:19 -07:00
Yury Semikhatsky b8a9d888be fix: NPE in Page.pdf for persistent context (#1292)
Fixes #1291
2023-05-26 12:00:07 -07:00
Yury Semikhatsky 5a4640fe2a chore: set dev version to 1.35 (#1288) 2023-05-24 16:45:39 -07:00
Yury Semikhatsky 2ba018dd7e chore: roll driver to 1.34.2 (#1286) 2023-05-24 16:34:52 -07:00
Nour Z 7f3db2ff46 fix: updated dead links to use playwright homepage 2023-05-23 15:27:55 -07:00
Yury Semikhatsky 8547c706ea feat: roll 1.34 beta driver, implement context events (#1283) 2023-05-18 15:57:39 -07:00
Max Schmitt 30a696172a devops(docker): fix Docker publishing (#1277) 2023-05-09 08:55:36 +02:00
Yury Semikhatsky 23de4518f4 chore: bump dev version to 1.34 (#1273) 2023-05-02 13:40:57 -07:00
Yury Semikhatsky 343502d559 chore: roll driver to 1.33.0 (#1271) 2023-05-02 12:19:34 -07:00
Max Schmitt 83837af5e5 docker: use Jammy as Docker default (#1267) 2023-04-27 18:27:54 +02:00
Sébastien Richert 7163012709 test: Restrain sending http credentials on a specific origin (for roll 1.33 driver) (#1253)
* test: Restrain sending http credentials on a specific origin (for driver 1.33 roll)

Verify that the httpCredentials are not sent when origin mismatch (scheme or hostname or port). See https://github.com/microsoft/playwright/pull/20374

* test: Restrain sending http credentials on a specific origin

Verify that the httpCredentials are not sent when origin mismatch (scheme or hostname or port). See https://github.com/microsoft/playwright/pull/20374
2023-04-26 14:48:38 -07:00
Yury Semikhatsky be59662a62 feat: roll driver to 1.33.0 beta (#1266) 2023-04-26 14:28:46 -07:00
Yury Semikhatsky 45591df430 feat: roll driver to Apr 12, implement new APIs (#1264) 2023-04-12 13:38:04 -07:00
Max Schmitt 804c577070 devops: publish Jammy Docker images (#1262) 2023-04-12 19:41:41 +02:00
Yury Semikhatsky c237fae71e feat: bundle arm64 node binary for Apple Silicon (#1255) 2023-03-29 17:35:33 -07:00
Yury Semikhatsky 1783b117e6 chore: bump dev revision to 1.33 (#1252) 2023-03-27 15:58:29 -07:00
Yury Semikhatsky 5f3b5dbe27 test: align waitForNavigation test with upstream (#1250) 2023-03-27 14:19:39 -07:00
Yury Semikhatsky bc50392220 chore: throw on context.close() if it was closed externally (#1248)
This is a backport of https://github.com/microsoft/playwright/commit/09ff7eaaf2ab40fcaf0abb85f0308a91e1460a97
2023-03-27 14:00:42 -07:00
Max Schmitt 96d74ef678 devops: don't trigger release workflow for PRs (#1247) 2023-03-27 22:34:59 +02:00
Max Schmitt ab89f6a953 chore: fix Docker image build (#1245) 2023-03-27 22:27:48 +02:00
Yury Semikhatsky 05f641c5af chore: roll driver to 1.32.1 (#1244) 2023-03-27 12:53:58 -07:00
Yury Semikhatsky d0bbf54b3b chore: roll diver to 1.32.0-beta-1679357626000 (#1238) 2023-03-20 22:03:41 -07:00
Yury Semikhatsky 7db3e3f401 chore: update community link 2023-03-20 18:38:04 -07:00
Yury Semikhatsky 470df42b36 feat: roll 1.32 driver, implement waitForCondition (#1237)
* Rolled recent driver
* Implemented waitForCondition in Page and BrowserContext

https://github.com/microsoft/playwright-java/issues/1208
Fixes https://github.com/microsoft/playwright-java/issues/1228
2023-03-20 10:27:29 -07:00
Yury Semikhatsky b260125389 fix: do not modify fetch options.method (#1232) 2023-03-15 12:27:54 -07:00
Yury Semikhatsky f8fc1068bc chore: roll 1.32.0-alpha driver (#1223) 2023-03-10 10:05:19 -08:00
Yury Semikhatsky 2db2762a0d devops: delete question temolate 2023-03-02 15:33:00 -08:00
Yury Semikhatsky cd56352278 devops: update issue templates 2023-03-02 15:31:14 -08:00
Yury Semikhatsky 6c729a8c9d devops: print test timestamps in GHA logs (#1224) 2023-03-02 10:11:00 -08:00
Yury Semikhatsky fe3e3e55dc fix: better assertThat error message for isChecked=false (#1221)
Fixes #1209
2023-03-01 08:52:27 -08:00
Yury Semikhatsky 1dc938cc67 test: make waitForNavigation pass in ff (#1220) 2023-02-28 13:21:46 -08:00
Yury Semikhatsky 05239433d2 test: make firefox not hang on shouldRejectResponseFinishedIfPageCloses (#1218) 2023-02-27 18:43:46 -08:00
Yury Semikhatsky 9fdcbb63f9 chore: bump dev version to 1.32.0-SNAPSHOT (#1212) 2023-02-22 11:20:04 -08:00
Karl Heinz Marbaise 4762ad9768 devops: Improve plugin configuration inheritance usage (#1201) (#1202)
devops: Improve plugin configuration inheritance usage. (#1201)
 - removing duplication of plugin definition in modules
 - using single location to define all (parent)
 - defined all used plugins via pluginManagement with
   most recent versions.
 - removed life cycle binding from pluginManagement
 - moved life cycle binding to default location
 - using jar-no-fork of maven-source-plugin.
2023-02-17 15:38:52 -08:00
Karl Heinz Marbaise 0fb77bbc81 fix: Improve maven plugin configuration (#1200) 2023-02-17 09:48:45 -08:00
Yury Semikhatsky c4b27febd4 feat: roll driver 1.31.0-beta-1676591072000 (#1207) 2023-02-16 19:09:22 -08:00
Yury Semikhatsky a478acf6b2 test: response.finished() respects page.close (#1206) 2023-02-16 15:18:54 -08:00
Yury Semikhatsky da36841809 fix: properly escape slash inside attributes (#1205)
https://github.com/microsoft/playwright/issues/20471
2023-02-16 13:02:22 -08:00
Yury Semikhatsky 8dfb745da9 fix: page.pause should not throw (#1204) 2023-02-16 11:21:24 -08:00
Yury Semikhatsky e81b874bbb chore: set dev version 1.31.0-SNAPSHOT (#1186) 2023-01-23 11:55:55 -08:00
Yury Semikhatsky 96b432d528 chode: roll driver to 1.30.0-beta-1674276599000 (#1183) 2023-01-23 11:18:37 -08:00
Yury Semikhatsky 551c168884 chore: roll 1.30 driver (#1182) 2023-01-22 21:17:31 -08:00
Yury Semikhatsky 9010fa95a0 chore: bump dev version to 1.30 (#1180) 2023-01-18 12:21:03 -08:00
Yury Semikhatsky b2e17853d7 chore: roll driver to 1.29.2 (#1176) 2023-01-18 11:45:35 -08:00
Yury Semikhatsky d792d460b1 devops: delete unuzed pipeline (#1177) 2023-01-18 11:31:15 -08:00
Yury Semikhatsky 1b1343649e devops: check branch bash (#1174) 2023-01-18 11:22:21 -08:00
Yury Semikhatsky 65624d6658 devops: delete old publish.yml (#1173) 2023-01-18 09:36:02 -08:00
Yury Semikhatsky 93a62868a1 devops: remove sonatype plugins from pom (#1171) 2023-01-18 09:31:53 -08:00
Yury Semikhatsky 28e547fe2b devops: add azure pipeline for release publishing (#1170) 2023-01-18 09:23:16 -08:00
Vladislav 22b2932d2d Uprade readme.md 1.27 -> 1.28.1 (#1167) 2023-01-13 17:36:54 -08:00
Aria Moradi 8c0231b0f7 fix: handle when FileSystem already exists (#1140) 2023-01-06 13:20:42 -08:00
Yury Semikhatsky c1891cb9f7 fix(docker): adduser error (#1160) 2023-01-06 11:31:16 -08:00
Yury Semikhatsky 81d31d24f8 chore(tests): suppress surefire plugin "Corrupted STDOUT..." warning (#1151) 2022-12-28 15:14:10 -08:00
Yury Semikhatsky 17d8a9ac7a devops: use latest v1 for playwright gha (#1154) 2022-12-28 14:29:11 -08:00
Yury Semikhatsky 4246c375ac fix: Date and LocalDateTime serialization in post data (#1150) 2022-12-22 17:35:09 -08:00
Max Schmitt ec5f9604cd devops: set up CI with Azure Pipelines (#1146)
Co-authored-by: Yury Semikhatsky <yurys@chromium.org>
2022-12-22 13:46:49 +01:00
Yury Semikhatsky d9fea34baa feat: generate @since tags for methods (#1145) 2022-12-21 10:15:51 -08:00
Yury Semikhatsky 45f81c0659 chore: roll driver to 1.29.0 (#1144) 2022-12-20 00:11:27 -08:00
Yury Semikhatsky 74fafc4d7e docs: improve line wrapping (#1143) 2022-12-19 14:52:28 -08:00
Yury Semikhatsky 4efa174368 feat: roll driver, implement new APIs (#1142) 2022-12-19 12:31:32 -08:00
Yury Semikhatsky 048bca9d59 fix: asynchronous route handling (#1137) 2022-11-30 14:59:41 -08:00
Yury Semikhatsky afa20b91ae fix: implement LocatorImpl.getByRole (#1131) 2022-11-28 14:48:32 -08:00
Yury Semikhatsky 8904de9bb8 chore: set dev version to 1.29.0-SNAPSHOT (#1126) 2022-11-16 12:43:07 -08:00
Yury Semikhatsky a061ba0f30 chore: roll driver to 1.28.0 (#1124) 2022-11-16 11:47:17 -08:00
Yury Semikhatsky 5a3daf64b9 chore: roll driver to 1.28.0-beta-1668481322000 (#1123) 2022-11-14 22:08:11 -08:00
Yury Semikhatsky 170d07a005 feat: roll driver to 1.29.0-alpha-1668454236000 (#1121) 2022-11-14 14:16:32 -08:00
Yury Semikhatsky 1674f95bd1 fix: do not fail on a bad file name in stack trace (#1120) 2022-11-11 18:42:11 -08:00
Yury Semikhatsky 3cc198ea26 fix: NPE in setInputFiles (#1113) 2022-11-04 16:55:01 -07:00
Yury Semikhatsky 071ca8b90c test: cleanup user-data-dir after tests in TestDefaultBrowserContext2 (#1112) 2022-11-04 16:16:51 -07:00
Max Schmitt 805fa3a8cb devops: update repo for internal tests 2022-10-30 21:30:22 -07:00
Yury Semikhatsky 1825c13fde fix: use internal:text* selectors (#1108) 2022-10-26 11:18:13 -07:00
Yury Semikhatsky 48d9528675 chore: roll driver to 1.28.0-alpha-oct-26-2022 (#1106) 2022-10-26 10:34:05 -07:00
Yury Semikhatsky 4275ee3455 feat(docker): set JAVA_HOME to openjdk 17 (#1105) 2022-10-25 10:38:11 -07:00
Yury Semikhatsky 69f81ea8e9 docs: update version in readme 2022-10-10 12:20:58 -07:00
Yury Semikhatsky 261160e4cf chore: bump dev version to 1.28.0-SNAPSHOT (#1094) 2022-10-07 17:29:18 -07:00
Jonathan Leitschuh 9ac9347dc5 [SECURITY] Fix Zip Slip Vulnerability (#1078) 2022-10-07 17:21:37 -07:00
Yury Semikhatsky 02ac0380a8 chore: roll driver to 1.27.0 (#1092) 2022-10-07 17:21:15 -07:00
Yury Semikhatsky bb4f3297e8 feat: roll 1.27.0 alpha oct 5 2022 (#1091) 2022-10-07 16:20:04 -07:00
Yury Semikhatsky ae54a7da55 docs: update gradle snippet 2022-09-20 16:23:54 -07:00
Yury Semikhatsky c6192db180 docs: update version in README.md to 1.26.0 2022-09-20 16:21:18 -07:00
Yury Semikhatsky b5c09a3141 chore: set dev version to 1.27.0-SNAPSHOT (#1073) 2022-09-20 15:32:03 -07:00
Yury Semikhatsky 8ef960a5f7 chore: mark 1.26.0 (#1072) 2022-09-20 14:28:40 -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
244 changed files with 24168 additions and 8168 deletions
+60
View File
@@ -0,0 +1,60 @@
trigger:
none
# don't trigger for Pull Requests
pr: none
pool:
vmImage: ubuntu-22.04
steps:
- bash: |
if [[ ! "$CURRENT_BRANCH" =~ ^release-.* ]]; then
echo "Can only publish from a release branch."
echo "Unexpected branch name: $CURRENT_BRANCH"
exit 1
fi
env:
CURRENT_BRANCH: ${{ variables['Build.SourceBranchName'] }}
displayName: "Check the branch is a release branch"
- bash: |
echo "importing GPG key:"
# Pipeline variables do not preserve line ends so we use base64 instead of --armored as a workaround.
echo $GPG_PRIVATE_KEY_BASE64 | base64 -d | gpg --batch --import
echo "list keys after import:"
gpg --list-keys
env:
GPG_PRIVATE_KEY_BASE64: $(GPG_PRIVATE_KEY_BASE64) # secret variable has to be mapped to an env variable
displayName: "Import gpg key"
- bash: ./scripts/download_driver_for_all_platforms.sh
displayName: 'Download driver'
- bash: mvn -B deploy -D skipTests --no-transfer-progress --activate-profiles release -D gpg.passphrase=$GPG_PASSPHRASE -DaltDeploymentRepository=snapshot-repo::default::file:$(pwd)/local-build
displayName: 'Build and deploy to a local directory'
env:
GPG_PASSPHRASE: $(GPG_PASSPHRASE) # secret variable has to be mapped to an env variable
- bash: |
for file in $(find snapshots -type f); do
echo "processing: $file"
if [[ $file =~ \.(md5|sha1|sha256)$ ]]; then
continue
fi
sha256sum "$file" | cut -f1 -d \ > "$file.sha256"
done
displayName: 'Create .sha256 files'
- task: EsrpRelease@2
inputs:
ConnectedServiceName: 'Playwright-Java-ESRP'
Intent: 'PackageDistribution'
ContentType: 'Maven'
PackageLocation: './local-build'
Owners: 'yurys@microsoft.com'
Approvers: 'maxschmitt@microsoft.com'
ServiceEndpointUrl: 'https://api.esrp.microsoft.com'
MainPublisher: 'PlaywrightJava'
DomainTenantId: '72f988bf-86f1-41af-91ab-2d7cd011db47'
displayName: 'ESRP Release to Maven'
+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
+35 -12
View File
@@ -7,19 +7,34 @@ assignees: ''
---
**Context:**
- Playwright Version: [what Playwright version do you use?]
- Operating System: [e.g. Windows, Linux or Mac]
- Browser: [e.g. All, Chromium, Firefox, WebKit]
- Extra: [any specific details about your environment]
<!-- ⚠️⚠️ Do not delete this template ⚠️⚠️ -->
<!-- CLI to auto-capture this info -->
<!-- npx envinfo --preset playwright --markdown -->
<!-- 🔎 Search existing issues to avoid creating duplicates. -->
<!-- 🧪 Test using the latest Playwright release to see if your issue has already been fixed -->
<!-- 💡 Provide enough information for us to be able to reproduce your issue locally -->
**Code Snippet**
### System info
- Playwright Version: [v1.XX]
- Operating System: [All, Windows 11, Ubuntu 20, macOS 13.2, etc.]
- Browser: [All, Chromium, Firefox, WebKit]
- Other info:
Help us help you! Put down a short code snippet that illustrates your bug and
that we can run and debug locally. For example:
### Source code
- [ ] I provided exact source code that allows reproducing the issue locally.
<!-- For simple cases, please provide a self-contained test file along with the config file -->
<!-- For larger cases, you can provide a GitHub repo you created for this issue -->
<!-- If we can not reproduce the problem locally, we won't be able to act on it -->
<!-- You can still file without the exact code and we will try to help, but if we can't repro, it will be closed -->
**Link to the GitHub repository with the repro**
[https://github.com/your_profile/playwright_issue_title]
or
**Test file (self-contained)**
```java
import com.microsoft.playwright.*;
@@ -36,6 +51,14 @@ public class ExampleReproducible {
}
```
**Describe the bug**
**Steps**
- [Run the test]
- [...]
Add any other details about the problem here.
**Expected**
[Describe expected behavior]
**Actual**
[Describe actual behavior]
+3 -3
View File
@@ -1,4 +1,4 @@
contact_links:
- name: Join our Slack community
url: https://aka.ms/playwright-slack
about: Ask questions and discuss with other community members
- name: Join our Discord Server
url: https://aka.ms/playwright/discord
about: Ask questions and discuss with other community members
-10
View File
@@ -1,10 +0,0 @@
---
name: I have a question
about: Feel free to ask us your questions!
title: "[Question]"
labels: ''
assignees: ''
---
-32
View File
@@ -1,32 +0,0 @@
name: Publish
on:
release:
types: [published]
push:
branches:
- main
jobs:
build:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1.5.0
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml
server-username: MAVEN_USERNAME # env variable for username in deploy
server-password: MAVEN_PASSWORD # env variable for token in deploy
gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} # Value of the GPG private key to import
gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase
- name: Download drivers
shell: bash
run: scripts/download_driver_for_all_platforms.sh
- name: Publish to Maven Central
run: mvn deploy --batch-mode -D skipTests --activate-profiles release --no-transfer-progress
env:
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
+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')
+12 -32
View File
@@ -19,29 +19,23 @@ 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@v2
with:
distribution: zulu
java-version: 8
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Download drivers
shell: bash
run: scripts/download_driver_for_all_platforms.sh
- name: Build with Maven
run: mvn -B package -D skipTests --no-transfer-progress
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress --fail-at-end
run: mvn test --no-transfer-progress --fail-at-end -D org.slf4j.simpleLogger.showDateTime=true -D org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss
env:
BROWSER: ${{ matrix.browser }}
- name: Run tracing tests w/ sources
run: mvn test --no-transfer-progress --fail-at-end -D test=*TestTracing*
run: mvn test --no-transfer-progress --fail-at-end -D test=*TestTracing* -D org.slf4j.simpleLogger.showDateTime=true -D org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss
env:
BROWSER: ${{ matrix.browser }}
PLAYWRIGHT_JAVA_SRC: src/test/java
@@ -50,7 +44,6 @@ jobs:
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
@@ -70,7 +63,7 @@ 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
@@ -80,19 +73,13 @@ jobs:
with:
distribution: zulu
java-version: 8
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Download drivers
shell: bash
run: scripts/download_driver_for_all_platforms.sh
- name: Build with Maven
run: mvn -B package -D skipTests --no-transfer-progress
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress --fail-at-end
run: mvn test --no-transfer-progress --fail-at-end -D org.slf4j.simpleLogger.showDateTime=true -D org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss
env:
BROWSER: chromium
BROWSER_CHANNEL: ${{ matrix.browser-channel }}
@@ -106,23 +93,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1.5.0
- uses: microsoft/playwright-github-action@v1
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
distribution: adopt
java-version: 17
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: m2-${{ hashFiles('**/pom.xml') }}
restore-keys: m2
- name: Download drivers
shell: bash
run: scripts/download_driver_for_all_platforms.sh
- name: Build with Maven
run: mvn -B package -D skipTests --no-transfer-progress
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress --fail-at-end
env:
@@ -132,7 +113,6 @@ jobs:
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
+3
View File
@@ -29,3 +29,6 @@ jobs:
- name: Test CLI version
shell: bash
run: tools/test-cli-version/test.sh
- name: Test CLI Fatjar
shell: bash
run: tools/test-cli-fatjar/test.sh
+13 -8
View File
@@ -1,16 +1,16 @@
name: Test Docker
name: Docker
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:
@@ -18,13 +18,18 @@ on:
- release-*
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-20.04
name: Test
timeout-minutes: 120
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
flavor: [focal, jammy]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t playwright-java:localbuild-focal -f Dockerfile.focal .
run: bash utils/docker/build.sh --amd64 ${{ matrix.flavor }} playwright-java:localbuild-${{ matrix.flavor }}
- 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)"
CONTAINER_ID="$(docker run --rm --ipc=host -v $(pwd):/root/playwright --name playwright-docker-test -d -t playwright-java:localbuild-${{ matrix.flavor }} /bin/bash)"
docker exec "${CONTAINER_ID}" /root/playwright/tools/test-local-installation/create_project_and_run_tests.sh
@@ -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-browsers/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
+15 -7
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 -->100.0.4863.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->15.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->96.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->115.0.5790.24<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->16.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->113.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.17.0</version>
<version>1.28.1</version>
</dependency>
```
To run Playwright using Gradle add following dependency to your build.gradle file:
```gradle
dependencies {
implementation group: 'com.microsoft.playwright', name: 'playwright', version: '1.28.1'
}
```
#### 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.
@@ -57,7 +65,7 @@ You can find Maven project with the examples [here](./examples).
#### Page screenshot
This code snippet navigates to whatsmyuseragent.org in Chromium, Firefox and WebKit, and saves 3 screenshots.
This code snippet navigates to Playwright homepage in Chromium, Firefox and WebKit, and saves 3 screenshots.
```java
import com.microsoft.playwright.*;
@@ -78,7 +86,7 @@ public class PageScreenshot {
try (Browser browser = browserType.launch()) {
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("http://whatsmyuseragent.org/");
page.navigate("https://playwright.dev/");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("screenshot-" + browserType.name() + ".png")));
}
}
@@ -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).
+1 -20
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.19.0-SNAPSHOT</version>
<version>1.35.0</version>
</parent>
<artifactId>driver-bundle</artifactId>
@@ -16,25 +16,6 @@
It is intended to be used on the systems where Playwright driver is not preinstalled.
</description>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<configuration>
<excludeResources>true</excludeResources>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.microsoft.playwright</groupId>
@@ -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,7 +28,10 @@ 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;
public DriverJar() throws IOException {
// Allow specifying custom path for the driver installation
@@ -37,11 +42,29 @@ public class DriverJar extends Driver {
? 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, Boolean installBrowsers) 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();
logMessage("extracted driver from jar to " + driverPath());
if (installBrowsers)
installBrowsers(env);
}
@@ -55,13 +78,16 @@ public class DriverJar extends Driver {
System.out.println("Skipping browsers download because `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` env variable is set");
return;
}
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;
}
Path driver = driverPath();
if (!Files.exists(driver)) {
throw new RuntimeException("Failed to find driver: " + driver);
}
ProcessBuilder pb = new ProcessBuilder(driver.toString(), "install");
pb.environment().putAll(env);
setRequiredEnvironmentVariables(pb);
ProcessBuilder pb = createProcessBuilder();
pb.command().add("install");
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
Process p = pb.start();
@@ -80,13 +106,25 @@ public class DriverJar extends Driver {
return name.endsWith(".sh") || name.endsWith(".exe") || !name.contains(".");
}
private void extractDriverToTempDir() throws URISyntaxException, IOException {
private FileSystem initFileSystem(URI uri) throws IOException {
try {
return FileSystems.newFileSystem(uri, Collections.emptyMap());
} catch (FileSystemAlreadyExistsException e) {
return null;
}
}
public static URI getDriverResourceURI() throws URISyntaxException {
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
URI originalUri = classloader.getResource("driver/" + platformDir()).toURI();
return classloader.getResource("driver/" + platformDir()).toURI();
}
void extractDriverToTempDir() throws URISyntaxException, IOException {
URI originalUri = getDriverResourceURI();
URI uri = maybeExtractNestedJar(originalUri);
// Create zip filesystem if loading from jar.
try (FileSystem fileSystem = "jar".equals(uri.getScheme()) ? FileSystems.newFileSystem(uri, Collections.emptyMap()) : null) {
try (FileSystem fileSystem = "jar".equals(uri.getScheme()) ? initFileSystem(uri) : null) {
Path srcRoot = Paths.get(uri);
// jar file system's .relativize gives wrong results when used with
// spring-boot-maven-plugin, convert to the default filesystem to
@@ -94,6 +132,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 {
@@ -137,20 +181,30 @@ 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";
if (arch.equals("aarch64")) {
return "mac-arm64";
} else {
return "mac";
}
}
throw new RuntimeException("Unexpected os.name value: " + name);
}
@Override
Path driverDir() {
protected Path driverDir() {
return driverTempDir;
}
}
@@ -1,60 +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 com.microsoft.playwright.impl.DriverJar;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
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.assertTrue;
public class TestInstall {
@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");
}
@Test
void playwrightCliInstalled() throws Exception {
Path cli = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
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");
}
@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);
}
}
@@ -0,0 +1,164 @@
/*
* 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.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
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 -17
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.19.0-SNAPSHOT</version>
<version>1.35.0</version>
</parent>
<artifactId>driver</artifactId>
@@ -15,22 +15,6 @@
This module provides API for discovery and launching of Playwright driver.
</description>
<build>
<plugins>
<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>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
@@ -14,50 +14,56 @@
* limitations under the License.
*/
package com.microsoft.playwright.impl;
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(Map<String, String> env, Boolean installBrowsers) {
protected void initialize(Boolean installBrowsers) {
// no-op
}
@Override
Path driverDir() {
protected Path driverDir() {
return driverDir;
}
}
public static synchronized Path ensureDriverInstalled(Map<String, String> env, Boolean installBrowsers) {
public static synchronized Driver ensureDriverInstalled(Map<String, String> env, Boolean installBrowsers) {
if (instance == null) {
try {
instance = createDriver();
instance.initialize(env, installBrowsers);
} catch (Exception exception) {
throw new RuntimeException("Failed to create driver", exception);
}
instance = createAndInstall(env, installBrowsers);
}
return instance.driverPath();
return instance;
}
protected abstract void initialize(Map<String, String> env, Boolean installBrowsers) throws Exception;
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") ?
@@ -65,13 +71,16 @@ public abstract class Driver {
return driverDir().resolve(cliFileName);
}
public static void setRequiredEnvironmentVariables(ProcessBuilder pb) {
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() {
@@ -85,16 +94,34 @@ public abstract class Driver {
}
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 createDriver() throws Exception {
private static Driver newInstance() 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");
String driverImpl =
System.getProperty("playwright.driver.impl", "com.microsoft.playwright.impl.driver.jar.DriverJar");
Class<?> jarDriver = Class.forName(driverImpl);
return (Driver) jarDriver.getDeclaredConstructor().newInstance();
}
abstract Path driverDir();
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.19.0-SNAPSHOT</version>
<version>1.35.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.17.0</version>
<version>1.30.0</version>
</dependency>
</dependencies>
<build>
@@ -34,7 +34,7 @@ public class PageScreenshot {
try (Browser browser = browserType.launch()) {
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("http://whatsmyuseragent.org/");
page.navigate("https://playwright.dev/");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("screenshot-" + browserType.name() + ".png")));
}
}
@@ -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")));
}
}
}
@@ -24,7 +24,7 @@ public class WebKitScreenshot {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.webkit().launch();
Page page = browser.newPage();
page.navigate("http://whatsmyuseragent.org/");
page.navigate("https://playwright.dev/");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("example.png")));
}
}
+2 -14
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.19.0-SNAPSHOT</version>
<version>1.35.0</version>
</parent>
<artifactId>playwright</artifactId>
@@ -21,26 +21,14 @@
<build>
<plugins>
<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>
<configuration combine.self="append">
<subpackages>com.microsoft.playwright</subpackages>
<excludePackageNames>com.microsoft.playwright.impl</excludePackageNames>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
@@ -21,31 +21,33 @@ import java.nio.file.Path;
import java.util.*;
/**
* Exposes API that can be used for the Web API testing. Each Playwright browser context has a APIRequestContext instance
* attached which shares cookies with the page context. Its also possible to create a new APIRequestContext instance
* manually. For more information see <a href="https://playwright.dev/java/docs/class-apirequestcontext">here</a>.
* 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:
* 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} 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.
* An object containing additional HTTP headers to be sent with every request. Defaults to none.
*/
public Map<String, String> extraHTTPHeaders;
/**
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>.
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
*/
public HttpCredentials httpCredentials;
/**
@@ -71,7 +73,8 @@ public interface APIRequest {
*/
public Path storageStatePath;
/**
* Maximum time in milliseconds to wait for the response. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout.
* Maximum time in milliseconds to wait for the response. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable
* timeout.
*/
public Double timeout;
/**
@@ -81,12 +84,13 @@ public interface APIRequest {
/**
* 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:
* 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} 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>
@@ -96,20 +100,22 @@ public interface APIRequest {
return this;
}
/**
* An object containing additional HTTP headers to be sent with every request.
* An object containing additional HTTP headers to be sent with every request. Defaults to none.
*/
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>.
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
*/
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>.
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
*/
public NewContextOptions setHttpCredentials(HttpCredentials httpCredentials) {
this.httpCredentials = httpCredentials;
@@ -156,7 +162,8 @@ public interface APIRequest {
return this;
}
/**
* Maximum time in milliseconds to wait for the response. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout.
* 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;
@@ -172,12 +179,16 @@ public interface APIRequest {
}
/**
* Creates new instances of {@code APIRequestContext}.
*
* @since v1.16
*/
default APIRequestContext newContext() {
return newContext(null);
}
/**
* Creates new instances of {@code APIRequestContext}.
*
* @since v1.16
*/
APIRequestContext newContext(NewContextOptions options);
}
@@ -21,9 +21,24 @@ 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. When used on {@code Page} or a {@code BrowserContext}, this API will automatically use
* the cookies from the corresponding {@code BrowserContext}. This means that if you log in using this API, your e2e test will be
* logged in and vice versa.
* 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 {
@@ -48,6 +63,7 @@ public interface APIRequestContext {
* The method will automatically follow redirects.
*
* @param url Target URL.
* @since v1.16
*/
default APIResponse delete(String url) {
return delete(url, null);
@@ -59,46 +75,157 @@ public interface APIRequestContext {
*
* @param url Target URL.
* @param params Optional request parameters.
* @since v1.16
*/
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.
*
* @since v1.16
*/
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.
* context cookies from the response. The method will automatically follow redirects. JSON objects can be passed directly
* to the request.
*
* <p> **Usage**
* <pre>{@code
* Map<String, Object> data = new HashMap();
* data.put("title", "Book Title");
* data.put("body", "John Doe");
* request.fetch("https://example.com/api/createBook", RequestOptions.create().setMethod("post").setData(data));
* }</pre>
*
* <p> The common way to send file(s) in the body of a request is to encode it as form fields with {@code multipart/form-data}
* encoding. You can achieve that with Playwright API like this:
* <pre>{@code
* // Pass file path to the form data constructor:
* Path file = Paths.get("team.csv");
* APIResponse response = request.fetch("https://example.com/api/uploadTeamList",
* RequestOptions.create().setMethod("post").setMultipart(
* FormData.create().set("fileField", file)));
*
* // Or you can pass the file content directly as FilePayload object:
* FilePayload filePayload = new FilePayload("f.js", "text/javascript",
* "console.log(2022);".getBytes(StandardCharsets.UTF_8));
* APIResponse response = request.fetch("https://example.com/api/uploadTeamList",
* RequestOptions.create().setMethod("post").setMultipart(
* FormData.create().set("fileField", filePayload)));
* }</pre>
*
* @param urlOrRequest Target URL or Request to get all parameters from.
* @since v1.16
*/
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.
* context cookies from the response. The method will automatically follow redirects. JSON objects can be passed directly
* to the request.
*
* <p> **Usage**
* <pre>{@code
* Map<String, Object> data = new HashMap();
* data.put("title", "Book Title");
* data.put("body", "John Doe");
* request.fetch("https://example.com/api/createBook", RequestOptions.create().setMethod("post").setData(data));
* }</pre>
*
* <p> The common way to send file(s) in the body of a request is to encode it as form fields with {@code multipart/form-data}
* encoding. You can achieve that with Playwright API like this:
* <pre>{@code
* // Pass file path to the form data constructor:
* Path file = Paths.get("team.csv");
* APIResponse response = request.fetch("https://example.com/api/uploadTeamList",
* RequestOptions.create().setMethod("post").setMultipart(
* FormData.create().set("fileField", file)));
*
* // Or you can pass the file content directly as FilePayload object:
* FilePayload filePayload = new FilePayload("f.js", "text/javascript",
* "console.log(2022);".getBytes(StandardCharsets.UTF_8));
* APIResponse response = request.fetch("https://example.com/api/uploadTeamList",
* RequestOptions.create().setMethod("post").setMultipart(
* FormData.create().set("fileField", filePayload)));
* }</pre>
*
* @param urlOrRequest Target URL or Request to get all parameters from.
* @param params Optional request parameters.
* @since v1.16
*/
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.
* context cookies from the response. The method will automatically follow redirects. JSON objects can be passed directly
* to the request.
*
* <p> **Usage**
* <pre>{@code
* Map<String, Object> data = new HashMap();
* data.put("title", "Book Title");
* data.put("body", "John Doe");
* request.fetch("https://example.com/api/createBook", RequestOptions.create().setMethod("post").setData(data));
* }</pre>
*
* <p> The common way to send file(s) in the body of a request is to encode it as form fields with {@code multipart/form-data}
* encoding. You can achieve that with Playwright API like this:
* <pre>{@code
* // Pass file path to the form data constructor:
* Path file = Paths.get("team.csv");
* APIResponse response = request.fetch("https://example.com/api/uploadTeamList",
* RequestOptions.create().setMethod("post").setMultipart(
* FormData.create().set("fileField", file)));
*
* // Or you can pass the file content directly as FilePayload object:
* FilePayload filePayload = new FilePayload("f.js", "text/javascript",
* "console.log(2022);".getBytes(StandardCharsets.UTF_8));
* APIResponse response = request.fetch("https://example.com/api/uploadTeamList",
* RequestOptions.create().setMethod("post").setMultipart(
* FormData.create().set("fileField", filePayload)));
* }</pre>
*
* @param urlOrRequest Target URL or Request to get all parameters from.
* @since v1.16
*/
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.
* context cookies from the response. The method will automatically follow redirects. JSON objects can be passed directly
* to the request.
*
* <p> **Usage**
* <pre>{@code
* Map<String, Object> data = new HashMap();
* data.put("title", "Book Title");
* data.put("body", "John Doe");
* request.fetch("https://example.com/api/createBook", RequestOptions.create().setMethod("post").setData(data));
* }</pre>
*
* <p> The common way to send file(s) in the body of a request is to encode it as form fields with {@code multipart/form-data}
* encoding. You can achieve that with Playwright API like this:
* <pre>{@code
* // Pass file path to the form data constructor:
* Path file = Paths.get("team.csv");
* APIResponse response = request.fetch("https://example.com/api/uploadTeamList",
* RequestOptions.create().setMethod("post").setMultipart(
* FormData.create().set("fileField", file)));
*
* // Or you can pass the file content directly as FilePayload object:
* FilePayload filePayload = new FilePayload("f.js", "text/javascript",
* "console.log(2022);".getBytes(StandardCharsets.UTF_8));
* APIResponse response = request.fetch("https://example.com/api/uploadTeamList",
* RequestOptions.create().setMethod("post").setMultipart(
* FormData.create().set("fileField", filePayload)));
* }</pre>
*
* @param urlOrRequest Target URL or Request to get all parameters from.
* @param params Optional request parameters.
* @since v1.16
*/
APIResponse fetch(Request urlOrRequest, RequestOptions params);
/**
@@ -106,7 +233,17 @@ public interface APIRequestContext {
* response. The method will populate request cookies from the context and update context cookies from the response. The
* method will automatically follow redirects.
*
* <p> **Usage**
*
* <p> Request parameters can be configured with {@code params} option, they will be serialized into the URL search parameters:
* <pre>{@code
* request.get("https://example.com/api/getText", RequestOptions.create()
* .setQueryParam("isbn", "1234")
* .setQueryParam("page", 23));
* }</pre>
*
* @param url Target URL.
* @since v1.16
*/
default APIResponse get(String url) {
return get(url, null);
@@ -116,8 +253,18 @@ public interface APIRequestContext {
* response. The method will populate request cookies from the context and update context cookies from the response. The
* method will automatically follow redirects.
*
* <p> **Usage**
*
* <p> Request parameters can be configured with {@code params} option, they will be serialized into the URL search parameters:
* <pre>{@code
* request.get("https://example.com/api/getText", RequestOptions.create()
* .setQueryParam("isbn", "1234")
* .setQueryParam("page", 23));
* }</pre>
*
* @param url Target URL.
* @param params Optional request parameters.
* @since v1.16
*/
APIResponse get(String url, RequestOptions params);
/**
@@ -126,6 +273,7 @@ public interface APIRequestContext {
* method will automatically follow redirects.
*
* @param url Target URL.
* @since v1.16
*/
default APIResponse head(String url) {
return head(url, null);
@@ -137,6 +285,7 @@ public interface APIRequestContext {
*
* @param url Target URL.
* @param params Optional request parameters.
* @since v1.16
*/
APIResponse head(String url, RequestOptions params);
/**
@@ -145,6 +294,7 @@ public interface APIRequestContext {
* The method will automatically follow redirects.
*
* @param url Target URL.
* @since v1.16
*/
default APIResponse patch(String url) {
return patch(url, null);
@@ -156,6 +306,7 @@ public interface APIRequestContext {
*
* @param url Target URL.
* @param params Optional request parameters.
* @since v1.16
*/
APIResponse patch(String url, RequestOptions params);
/**
@@ -163,7 +314,44 @@ public interface APIRequestContext {
* response. The method will populate request cookies from the context and update context cookies from the response. The
* method will automatically follow redirects.
*
* <p> **Usage**
*
* <p> JSON objects can be passed directly to the request:
* <pre>{@code
* Map<String, Object> data = new HashMap();
* data.put("title", "Book Title");
* data.put("body", "John Doe");
* request.post("https://example.com/api/createBook", RequestOptions.create().setData(data));
* }</pre>
*
* <p> To send form data to the server use {@code form} option. Its value will be encoded into the request body with {@code
* application/x-www-form-urlencoded} encoding (see below how to use {@code multipart/form-data} form encoding to send
* files):
* <pre>{@code
* request.post("https://example.com/api/findBook", RequestOptions.create().setForm(
* FormData.create().set("title", "Book Title").set("body", "John Doe")
* ));
* }</pre>
*
* <p> The common way to send file(s) in the body of a request is to upload them as form fields with {@code
* multipart/form-data} encoding. You can achieve that with Playwright API like this:
* <pre>{@code
* // Pass file path to the form data constructor:
* Path file = Paths.get("team.csv");
* APIResponse response = request.post("https://example.com/api/uploadTeamList",
* RequestOptions.create().setMultipart(
* FormData.create().set("fileField", file)));
*
* // Or you can pass the file content directly as FilePayload object:
* FilePayload filePayload = new FilePayload("f.js", "text/javascript",
* "console.log(2022);".getBytes(StandardCharsets.UTF_8));
* APIResponse response = request.post("https://example.com/api/uploadTeamList",
* RequestOptions.create().setMultipart(
* FormData.create().set("fileField", filePayload)));
* }</pre>
*
* @param url Target URL.
* @since v1.16
*/
default APIResponse post(String url) {
return post(url, null);
@@ -173,8 +361,45 @@ public interface APIRequestContext {
* response. The method will populate request cookies from the context and update context cookies from the response. The
* method will automatically follow redirects.
*
* <p> **Usage**
*
* <p> JSON objects can be passed directly to the request:
* <pre>{@code
* Map<String, Object> data = new HashMap();
* data.put("title", "Book Title");
* data.put("body", "John Doe");
* request.post("https://example.com/api/createBook", RequestOptions.create().setData(data));
* }</pre>
*
* <p> To send form data to the server use {@code form} option. Its value will be encoded into the request body with {@code
* application/x-www-form-urlencoded} encoding (see below how to use {@code multipart/form-data} form encoding to send
* files):
* <pre>{@code
* request.post("https://example.com/api/findBook", RequestOptions.create().setForm(
* FormData.create().set("title", "Book Title").set("body", "John Doe")
* ));
* }</pre>
*
* <p> The common way to send file(s) in the body of a request is to upload them as form fields with {@code
* multipart/form-data} encoding. You can achieve that with Playwright API like this:
* <pre>{@code
* // Pass file path to the form data constructor:
* Path file = Paths.get("team.csv");
* APIResponse response = request.post("https://example.com/api/uploadTeamList",
* RequestOptions.create().setMultipart(
* FormData.create().set("fileField", file)));
*
* // Or you can pass the file content directly as FilePayload object:
* FilePayload filePayload = new FilePayload("f.js", "text/javascript",
* "console.log(2022);".getBytes(StandardCharsets.UTF_8));
* APIResponse response = request.post("https://example.com/api/uploadTeamList",
* RequestOptions.create().setMultipart(
* FormData.create().set("fileField", filePayload)));
* }</pre>
*
* @param url Target URL.
* @param params Optional request parameters.
* @since v1.16
*/
APIResponse post(String url, RequestOptions params);
/**
@@ -183,6 +408,7 @@ public interface APIRequestContext {
* method will automatically follow redirects.
*
* @param url Target URL.
* @since v1.16
*/
default APIResponse put(String url) {
return put(url, null);
@@ -194,11 +420,14 @@ public interface APIRequestContext {
*
* @param url Target URL.
* @param params Optional request parameters.
* @since v1.16
*/
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.
*
* @since v1.16
*/
default String storageState() {
return storageState(null);
@@ -206,6 +435,8 @@ public interface APIRequestContext {
/**
* Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to
* the constructor.
*
* @since v1.16
*/
String storageState(StorageStateOptions options);
}
@@ -20,45 +20,63 @@ import com.microsoft.playwright.options.*;
import java.util.*;
/**
* {@code APIResponse} class represents responses returned by {@link APIRequestContext#get APIRequestContext.get()} and similar
* methods.
* {@code APIResponse} class represents responses returned by {@link APIRequestContext#get APIRequestContext.get()} and
* similar methods.
*/
public interface APIResponse {
/**
* Returns the buffer with response body.
*
* @since v1.16
*/
byte[] body();
/**
* Disposes the body of this response. If not called then the body will stay in memory until the context closes.
*
* @since v1.16
*/
void dispose();
/**
* An object with all the response HTTP headers associated with this response.
*
* @since v1.16
*/
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.
*
* @since v1.16
*/
List<HttpHeader> headersArray();
/**
* Contains a boolean stating whether the response was successful (status in the range 200-299) or not.
*
* @since v1.16
*/
boolean ok();
/**
* Contains the status code of the response (e.g., 200 for a success).
*
* @since v1.16
*/
int status();
/**
* Contains the status text of the response (e.g. usually an "OK" for a success).
*
* @since v1.16
*/
String statusText();
/**
* Returns the text representation of response body.
*
* @since v1.16
*/
String text();
/**
* Contains the URL of the response.
*
* @since v1.16
*/
String url();
}
File diff suppressed because it is too large Load Diff
@@ -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.function.BooleanSupplier;
import java.util.function.Predicate;
import java.util.regex.Pattern;
@@ -57,17 +58,62 @@ public interface BrowserContext extends AutoCloseable {
*/
void offClose(Consumer<BrowserContext> handler);
/**
* Emitted when JavaScript within the page calls one of console API methods, e.g. {@code console.log} or {@code
* console.dir}. Also emitted if the page throws an error or a warning.
*
* <p> The arguments passed into {@code console.log} and the page are available on the {@code ConsoleMessage} event handler
* argument.
*
* <p> **Usage**
* <pre>{@code
* context.onConsoleMessage(msg -> {
* for (int i = 0; i < msg.args().size(); ++i)
* System.out.println(i + ": " + msg.args().get(i).jsonValue());
* });
* page.evaluate("() => console.log('hello', 5, { foo: 'bar' })");
* }</pre>
*/
void onConsoleMessage(Consumer<ConsoleMessage> handler);
/**
* Removes handler that was previously added with {@link #onConsoleMessage onConsoleMessage(handler)}.
*/
void offConsoleMessage(Consumer<ConsoleMessage> handler);
/**
* Emitted when a JavaScript dialog appears, such as {@code alert}, {@code prompt}, {@code confirm} or {@code
* beforeunload}. Listener **must** either {@link Dialog#accept Dialog.accept()} or {@link Dialog#dismiss Dialog.dismiss()}
* the dialog - otherwise the page will <a
* href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking">freeze</a> waiting for the
* dialog, and actions like click will never finish.
*
* <p> **Usage**
* <pre>{@code
* context.onDialog(dialog -> {
* dialog.accept();
* });
* }</pre>
*
* <p> <strong>NOTE:</strong> When no {@link Page#onDialog Page.onDialog()} or {@link BrowserContext#onDialog BrowserContext.onDialog()} listeners are
* present, all dialogs are automatically dismissed.
*/
void onDialog(Consumer<Dialog> handler);
/**
* Removes handler that was previously added with {@link #onDialog onDialog(handler)}.
*/
void offDialog(Consumer<Dialog> handler);
/**
* The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event will
* also fire for popup pages. See also {@link Page#onPopup Page.onPopup()} to receive events about popups relevant to a
* specific page.
*
* <p> The earliest moment that page is available is when it has navigated to the initial url. For example, when opening a
* popup with {@code window.open('http://example.com')}, this event will fire when the network request to "http://example.com" is
* done and its response has started loading in the popup.
* popup with {@code window.open('http://example.com')}, this event will fire when the network request to
* "http://example.com" is done and its response has started loading in the popup.
* <pre>{@code
* Page newPage = context.waitForPage(() -> {
* page.click("a[target=_blank]");
* page.getByText("open new page").click();
* });
* System.out.println(newPage.evaluate("location.href"));
* }</pre>
@@ -110,8 +156,8 @@ public interface BrowserContext extends AutoCloseable {
/**
* Emitted when a request finishes successfully after downloading the response body. For a successful response, the
* sequence of events is {@code request}, {@code response} and {@code requestfinished}. To listen for successful requests from a particular
* page, use {@link Page#onRequestFinished Page.onRequestFinished()}.
* sequence of events is {@code request}, {@code response} and {@code requestfinished}. To listen for successful requests
* from a particular page, use {@link Page#onRequestFinished Page.onRequestFinished()}.
*/
void onRequestFinished(Consumer<Request> handler);
/**
@@ -121,8 +167,8 @@ public interface BrowserContext extends AutoCloseable {
/**
* Emitted when [response] status and headers are received for a request. For a successful response, the sequence of events
* is {@code request}, {@code response} and {@code requestfinished}. To listen for response events from a particular page, use {@link
* Page#onResponse Page.onResponse()}.
* is {@code request}, {@code response} and {@code requestfinished}. To listen for response events from a particular page,
* use {@link Page#onResponse Page.onResponse()}.
*/
void onResponse(Consumer<Response> handler);
/**
@@ -174,6 +220,92 @@ 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. The file is
* written to disk when {@link BrowserContext#close BrowserContext.close()} is called.
*/
public Boolean update;
/**
* Optional setting to control resource content management. If {@code attach} is specified, resources are persisted as
* separate files or entries in the ZIP archive. If {@code embed} is specified, content is stored inline the HAR file.
*/
public RouteFromHarUpdateContentPolicy updateContent;
/**
* 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
* minimal}.
*/
public HarMode updateMode;
/**
* 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. The file is
* written to disk when {@link BrowserContext#close BrowserContext.close()} is called.
*/
public RouteFromHAROptions setUpdate(boolean update) {
this.update = update;
return this;
}
/**
* Optional setting to control resource content management. If {@code attach} is specified, resources are persisted as
* separate files or entries in the ZIP archive. If {@code embed} is specified, content is stored inline the HAR file.
*/
public RouteFromHAROptions setUpdateContent(RouteFromHarUpdateContentPolicy updateContent) {
this.updateContent = updateContent;
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
* minimal}.
*/
public RouteFromHAROptions setUpdateMode(HarMode updateMode) {
this.updateMode = updateMode;
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
@@ -190,14 +322,59 @@ public interface BrowserContext extends AutoCloseable {
return this;
}
}
class WaitForConditionOptions {
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()} or
* {@link Page#setDefaultTimeout Page.setDefaultTimeout()} methods.
*/
public Double timeout;
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()} or
* {@link Page#setDefaultTimeout Page.setDefaultTimeout()} methods.
*/
public WaitForConditionOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class WaitForConsoleMessageOptions {
/**
* Receives the {@code ConsoleMessage} object and resolves to truthy value when the waiting should resolve.
*/
public Predicate<ConsoleMessage> predicate;
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
/**
* Receives the {@code ConsoleMessage} object and resolves to truthy value when the waiting should resolve.
*/
public WaitForConsoleMessageOptions setPredicate(Predicate<ConsoleMessage> predicate) {
this.predicate = predicate;
return this;
}
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public WaitForConsoleMessageOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class WaitForPageOptions {
/**
* Receives the {@code Page} object and resolves to truthy value when the waiting should resolve.
*/
public Predicate<Page> predicate;
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
@@ -209,8 +386,8 @@ public interface BrowserContext extends AutoCloseable {
return this;
}
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public WaitForPageOptions setTimeout(double timeout) {
this.timeout = timeout;
@@ -220,9 +397,16 @@ public interface BrowserContext extends AutoCloseable {
/**
* Adds cookies into this browser context. All pages within this context will have these cookies installed. Cookies can be
* obtained via {@link BrowserContext#cookies BrowserContext.cookies()}.
*
* <p> **Usage**
* <pre>{@code
* browserContext.addCookies(Arrays.asList(cookieObject1, cookieObject2));
* }</pre>
*
* @param cookies Adds cookies to the browser context.
*
* <p> For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com".
* @since v1.8
*/
void addCookies(List<Cookie> cookies);
/**
@@ -236,6 +420,8 @@ public interface BrowserContext extends AutoCloseable {
* <p> The script is evaluated after the document was created but before any of its scripts were run. This is useful to amend
* the JavaScript environment, e.g. to seed {@code Math.random}.
*
* <p> **Usage**
*
* <p> An example of overriding {@code Math.random} before the page loads:
* <pre>{@code
* // In your playwright script, assuming the preload.js file is in same directory.
@@ -246,6 +432,7 @@ public interface BrowserContext extends AutoCloseable {
* BrowserContext.addInitScript()} and {@link Page#addInitScript Page.addInitScript()} is not defined.
*
* @param script Script to be evaluated in all pages in the browser context.
* @since v1.8
*/
void addInitScript(String script);
/**
@@ -259,6 +446,8 @@ public interface BrowserContext extends AutoCloseable {
* <p> The script is evaluated after the document was created but before any of its scripts were run. This is useful to amend
* the JavaScript environment, e.g. to seed {@code Math.random}.
*
* <p> **Usage**
*
* <p> An example of overriding {@code Math.random} before the page loads:
* <pre>{@code
* // In your playwright script, assuming the preload.js file is in same directory.
@@ -269,35 +458,48 @@ public interface BrowserContext extends AutoCloseable {
* BrowserContext.addInitScript()} and {@link Page#addInitScript Page.addInitScript()} is not defined.
*
* @param script Script to be evaluated in all pages in the browser context.
* @since v1.8
*/
void addInitScript(Path script);
/**
* Returns the browser instance of the context. If it was launched as a persistent context null gets returned.
*
* @since v1.8
*/
Browser browser();
/**
* Clears context cookies.
*
* @since v1.8
*/
void clearCookies();
/**
* Clears all permission overrides for the browser context.
*
* <p> **Usage**
* <pre>{@code
* BrowserContext context = browser.newContext();
* context.grantPermissions(Arrays.asList("clipboard-read"));
* // do stuff ..
* context.clearPermissions();
* }</pre>
*
* @since v1.8
*/
void clearPermissions();
/**
* Closes the browser context. All the pages that belong to the browser context will be closed.
*
* <p> <strong>NOTE:</strong> The default browser context cannot be closed.
*
* @since v1.8
*/
void close();
/**
* If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those URLs
* are returned.
*
* @since v1.8
*/
default List<Cookie> cookies() {
return cookies((String) null);
@@ -307,6 +509,7 @@ public interface BrowserContext extends AutoCloseable {
* are returned.
*
* @param urls Optional list of URLs.
* @since v1.8
*/
List<Cookie> cookies(String urls);
/**
@@ -314,21 +517,24 @@ public interface BrowserContext extends AutoCloseable {
* are returned.
*
* @param urls Optional list of URLs.
* @since v1.8
*/
List<Cookie> cookies(List<String> urls);
/**
* The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context. When
* called, the function executes {@code callback} and returns a <a
* The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context.
* When called, the function executes {@code callback} and returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a> which
* resolves to the return value of {@code callback}. If the {@code callback} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, it will be
* awaited.
*
* <p> The first argument of the {@code callback} function contains information about the caller: {@code { browserContext: BrowserContext,
* page: Page, frame: Frame }}.
* <p> The first argument of the {@code callback} function contains information about the caller: {@code { browserContext:
* BrowserContext, page: Page, frame: Frame }}.
*
* <p> See {@link Page#exposeBinding Page.exposeBinding()} for page-only version.
*
* <p> **Usage**
*
* <p> An example of exposing page URL to all frames in all pages in the context:
* <pre>{@code
* import com.microsoft.playwright.*;
@@ -348,7 +554,7 @@ public interface BrowserContext extends AutoCloseable {
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>");
* page.click("button");
* page.getByRole(AriaRole.BUTTON).click();
* }
* }
* }
@@ -371,23 +577,26 @@ public interface BrowserContext extends AutoCloseable {
*
* @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context.
* @since v1.8
*/
default void exposeBinding(String name, BindingCallback callback) {
exposeBinding(name, callback, null);
}
/**
* The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context. When
* called, the function executes {@code callback} and returns a <a
* The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context.
* When called, the function executes {@code callback} and returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a> which
* resolves to the return value of {@code callback}. If the {@code callback} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, it will be
* awaited.
*
* <p> The first argument of the {@code callback} function contains information about the caller: {@code { browserContext: BrowserContext,
* page: Page, frame: Frame }}.
* <p> The first argument of the {@code callback} function contains information about the caller: {@code { browserContext:
* BrowserContext, page: Page, frame: Frame }}.
*
* <p> See {@link Page#exposeBinding Page.exposeBinding()} for page-only version.
*
* <p> **Usage**
*
* <p> An example of exposing page URL to all frames in all pages in the context:
* <pre>{@code
* import com.microsoft.playwright.*;
@@ -407,7 +616,7 @@ public interface BrowserContext extends AutoCloseable {
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>");
* page.click("button");
* page.getByRole(AriaRole.BUTTON).click();
* }
* }
* }
@@ -430,11 +639,12 @@ public interface BrowserContext extends AutoCloseable {
*
* @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context.
* @since v1.8
*/
void exposeBinding(String name, BindingCallback callback, ExposeBindingOptions options);
/**
* The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context. When
* called, the function executes {@code callback} and returns a <a
* The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context.
* When called, the function executes {@code callback} and returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a> which
* resolves to the return value of {@code callback}.
*
@@ -444,6 +654,8 @@ public interface BrowserContext extends AutoCloseable {
*
* <p> See {@link Page#exposeFunction Page.exposeFunction()} for page-only version.
*
* <p> **Usage**
*
* <p> An example of adding a {@code sha256} function to all pages in the context:
* <pre>{@code
* import com.microsoft.playwright.*;
@@ -477,7 +689,7 @@ public interface BrowserContext extends AutoCloseable {
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>\n");
* page.click("button");
* page.getByRole(AriaRole.BUTTON).click();
* }
* }
* }
@@ -485,6 +697,7 @@ public interface BrowserContext extends AutoCloseable {
*
* @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context.
* @since v1.8
*/
void exposeFunction(String name, FunctionCallback callback);
/**
@@ -497,7 +710,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>
@@ -510,6 +722,7 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "clipboard-write"}</li>
* <li> {@code "payment-handler"}</li>
* </ul>
* @since v1.8
*/
default void grantPermissions(List<String> permissions) {
grantPermissions(permissions, null);
@@ -524,7 +737,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>
@@ -537,27 +749,36 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "clipboard-write"}</li>
* <li> {@code "payment-handler"}</li>
* </ul>
* @since v1.8
*/
void grantPermissions(List<String> permissions, GrantPermissionsOptions options);
/**
* Creates a new page in the browser context.
*
* @since v1.8
*/
Page newPage();
/**
* Returns all open pages in the context.
*
* @since v1.8
*/
List<Page> pages();
/**
* API testing helper associated with this context. Requests made with this API will use context cookies.
*
* @since v1.16
*/
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> **Usage**
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -595,10 +816,11 @@ public interface BrowserContext extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the context
* options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
*/
default void route(String url, Consumer<Route> handler) {
route(url, handler, null);
@@ -607,9 +829,11 @@ 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> **Usage**
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -647,19 +871,22 @@ public interface BrowserContext extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the context
* options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
*/
void route(String url, Consumer<Route> handler, RouteOptions options);
/**
* 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> **Usage**
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -697,10 +924,11 @@ public interface BrowserContext extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the context
* options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
*/
default void route(Pattern url, Consumer<Route> handler) {
route(url, handler, null);
@@ -709,9 +937,11 @@ 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> **Usage**
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -749,19 +979,22 @@ public interface BrowserContext extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the context
* options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
*/
void route(Pattern url, Consumer<Route> handler, RouteOptions options);
/**
* 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> **Usage**
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -799,10 +1032,11 @@ public interface BrowserContext extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the context
* options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
*/
default void route(Predicate<String> url, Consumer<Route> handler) {
route(url, handler, null);
@@ -811,9 +1045,11 @@ 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> **Usage**
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -851,12 +1087,41 @@ public interface BrowserContext extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the context
* options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
*/
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.
* @since v1.23
*/
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.
* @since v1.23
*/
void routeFromHAR(Path har, RouteFromHAROptions options);
/**
* This setting will change the default maximum navigation time for the following methods and related shortcuts:
* <ul>
@@ -873,6 +1138,7 @@ public interface BrowserContext extends AutoCloseable {
* BrowserContext.setDefaultNavigationTimeout()}.
*
* @param timeout Maximum navigation time in milliseconds
* @since v1.8
*/
void setDefaultNavigationTimeout(double timeout);
/**
@@ -884,6 +1150,7 @@ public interface BrowserContext extends AutoCloseable {
* BrowserContext.setDefaultTimeout()}.
*
* @param timeout Maximum time in milliseconds
* @since v1.8
*/
void setDefaultTimeout(double timeout);
/**
@@ -895,34 +1162,49 @@ public interface BrowserContext extends AutoCloseable {
* in the outgoing requests.
*
* @param headers An object containing additional HTTP headers to be sent with every request. All header values must be strings.
* @since v1.8
*/
void setExtraHTTPHeaders(Map<String, String> headers);
/**
* Sets the context's geolocation. Passing {@code null} or {@code undefined} emulates position unavailable.
*
* <p> **Usage**
* <pre>{@code
* browserContext.setGeolocation(new Geolocation(59.95, 30.31667));
* }</pre>
*
* <p> <strong>NOTE:</strong> Consider using {@link BrowserContext#grantPermissions BrowserContext.grantPermissions()} to grant permissions for the
* browser context pages to read its geolocation.
*
* @since v1.8
*/
void setGeolocation(Geolocation geolocation);
/**
*
*
* @param offline Whether to emulate network being offline for the browser context.
* @since v1.8
*/
void setOffline(boolean offline);
/**
* Returns storage state for this browser context, contains current cookies and local storage snapshot.
*
* @since v1.8
*/
default String storageState() {
return storageState(null);
}
/**
* Returns storage state for this browser context, contains current cookies and local storage snapshot.
*
* @since v1.8
*/
String storageState(StorageStateOptions options);
/**
*
*
* @since v1.12
*/
Tracing tracing();
/**
* Removes a route created with {@link BrowserContext#route BrowserContext.route()}. When {@code handler} is not specified,
@@ -930,6 +1212,7 @@ public interface BrowserContext extends AutoCloseable {
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link BrowserContext#route
* BrowserContext.route()}.
* @since v1.8
*/
default void unroute(String url) {
unroute(url, null);
@@ -941,6 +1224,7 @@ public interface BrowserContext extends AutoCloseable {
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link BrowserContext#route
* BrowserContext.route()}.
* @param handler Optional handler function used to register a routing with {@link BrowserContext#route BrowserContext.route()}.
* @since v1.8
*/
void unroute(String url, Consumer<Route> handler);
/**
@@ -949,6 +1233,7 @@ public interface BrowserContext extends AutoCloseable {
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link BrowserContext#route
* BrowserContext.route()}.
* @since v1.8
*/
default void unroute(Pattern url) {
unroute(url, null);
@@ -960,6 +1245,7 @@ public interface BrowserContext extends AutoCloseable {
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link BrowserContext#route
* BrowserContext.route()}.
* @param handler Optional handler function used to register a routing with {@link BrowserContext#route BrowserContext.route()}.
* @since v1.8
*/
void unroute(Pattern url, Consumer<Route> handler);
/**
@@ -968,6 +1254,7 @@ public interface BrowserContext extends AutoCloseable {
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link BrowserContext#route
* BrowserContext.route()}.
* @since v1.8
*/
default void unroute(Predicate<String> url) {
unroute(url, null);
@@ -979,24 +1266,97 @@ public interface BrowserContext extends AutoCloseable {
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link BrowserContext#route
* BrowserContext.route()}.
* @param handler Optional handler function used to register a routing with {@link BrowserContext#route BrowserContext.route()}.
* @since v1.8
*/
void unroute(Predicate<String> url, Consumer<Route> handler);
/**
* Performs action and waits for a new {@code Page} to be created in the context. If predicate is provided, it passes {@code Page}
* value into the {@code predicate} function and waits for {@code predicate(event)} to return a truthy value. Will throw an error if
* the context closes before new {@code Page} is created.
* The method will block until the condition returns true. All Playwright events will be dispatched while the method is
* waiting for the condition.
*
* <p> **Usage**
*
* <p> Use the method to wait for a condition that depends on page events:
* <pre>{@code
* List<String> failedUrls = new ArrayList<>();
* context.onResponse(response -> {
* if (!response.ok()) {
* failedUrls.add(response.url());
* }
* });
* page1.getByText("Create user").click();
* page2.getByText("Submit button").click();
* context.waitForCondition(() -> failedUrls.size() > 3);
* }</pre>
*
* @param condition Condition to wait for.
* @since v1.32
*/
default void waitForCondition(BooleanSupplier condition) {
waitForCondition(condition, null);
}
/**
* The method will block until the condition returns true. All Playwright events will be dispatched while the method is
* waiting for the condition.
*
* <p> **Usage**
*
* <p> Use the method to wait for a condition that depends on page events:
* <pre>{@code
* List<String> failedUrls = new ArrayList<>();
* context.onResponse(response -> {
* if (!response.ok()) {
* failedUrls.add(response.url());
* }
* });
* page1.getByText("Create user").click();
* page2.getByText("Submit button").click();
* context.waitForCondition(() -> failedUrls.size() > 3);
* }</pre>
*
* @param condition Condition to wait for.
* @since v1.32
*/
void waitForCondition(BooleanSupplier condition, WaitForConditionOptions options);
/**
* Performs action and waits for a {@code ConsoleMessage} to be logged by in the pages in the context. If predicate is
* provided, it passes {@code ConsoleMessage} value into the {@code predicate} function and waits for {@code
* predicate(message)} to return a truthy value. Will throw an error if the page is closed before the {@link
* BrowserContext#onConsoleMessage BrowserContext.onConsoleMessage()} event is fired.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.34
*/
default ConsoleMessage waitForConsoleMessage(Runnable callback) {
return waitForConsoleMessage(null, callback);
}
/**
* Performs action and waits for a {@code ConsoleMessage} to be logged by in the pages in the context. If predicate is
* provided, it passes {@code ConsoleMessage} value into the {@code predicate} function and waits for {@code
* predicate(message)} to return a truthy value. Will throw an error if the page is closed before the {@link
* BrowserContext#onConsoleMessage BrowserContext.onConsoleMessage()} event is fired.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.34
*/
ConsoleMessage waitForConsoleMessage(WaitForConsoleMessageOptions options, Runnable callback);
/**
* Performs action and waits for a new {@code Page} to be created in the context. If predicate is provided, it passes
* {@code Page} value into the {@code predicate} function and waits for {@code predicate(event)} to return a truthy value.
* Will throw an error if the context closes before new {@code Page} is created.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.9
*/
default Page waitForPage(Runnable callback) {
return waitForPage(null, callback);
}
/**
* Performs action and waits for a new {@code Page} to be created in the context. If predicate is provided, it passes {@code Page}
* value into the {@code predicate} function and waits for {@code predicate(event)} to return a truthy value. Will throw an error if
* the context closes before new {@code Page} is created.
* Performs action and waits for a new {@code Page} to be created in the context. If predicate is provided, it passes
* {@code Page} value into the {@code predicate} function and waits for {@code predicate(event)} to return a truthy value.
* Will throw an error if the context closes before new {@code Page} is created.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.9
*/
Page waitForPage(WaitForPageOptions options, Runnable callback);
}
@@ -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;
@@ -92,8 +91,8 @@ 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 30000} (30 seconds). Pass
* {@code 0} to disable timeout.
*/
public Double timeout;
@@ -113,8 +112,8 @@ 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 30000} (30 seconds). Pass
* {@code 0} to disable timeout.
*/
public ConnectOverCDPOptions 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;
/**
@@ -138,8 +137,8 @@ public interface BrowserType {
*/
public Boolean chromiumSandbox;
/**
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is {@code true}, the {@code headless}
* option will be set {@code false}.
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is {@code true}, the {@code
* headless} option will be set {@code false}.
*/
public Boolean devtools;
/**
@@ -178,18 +177,18 @@ public interface BrowserType {
/**
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true} unless the
* {@code devtools} option is {@code true}.
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true}
* unless the {@code devtools} option is {@code true}.
*/
public Boolean headless;
/**
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}. Dangerous option;
* use with care. Defaults to {@code false}.
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}.
* Dangerous option; use with care. Defaults to {@code false}.
*/
public Boolean ignoreAllDefaultArgs;
/**
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}. Dangerous option;
* use with care.
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}.
* Dangerous option; use with care.
*/
public List<String> ignoreDefaultArgs;
/**
@@ -201,8 +200,8 @@ public interface BrowserType {
*/
public Double slowMo;
/**
* Maximum time in milliseconds to wait for the browser instance to start. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to
* disable timeout.
* Maximum time in milliseconds to wait for the browser instance to start. Defaults to {@code 30000} (30 seconds). Pass
* {@code 0} to disable timeout.
*/
public Double timeout;
/**
@@ -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;
@@ -245,8 +244,8 @@ public interface BrowserType {
return this;
}
/**
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is {@code true}, the {@code headless}
* option will be set {@code false}.
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is {@code true}, the {@code
* headless} option will be set {@code false}.
*/
public LaunchOptions setDevtools(boolean devtools) {
this.devtools = devtools;
@@ -309,24 +308,24 @@ public interface BrowserType {
/**
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true} unless the
* {@code devtools} option is {@code true}.
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true}
* unless the {@code devtools} option is {@code true}.
*/
public LaunchOptions setHeadless(boolean headless) {
this.headless = headless;
return this;
}
/**
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}. Dangerous option;
* use with care. Defaults to {@code false}.
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}.
* Dangerous option; use with care. Defaults to {@code false}.
*/
public LaunchOptions setIgnoreAllDefaultArgs(boolean ignoreAllDefaultArgs) {
this.ignoreAllDefaultArgs = ignoreAllDefaultArgs;
return this;
}
/**
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}. Dangerous option;
* use with care.
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}.
* Dangerous option; use with care.
*/
public LaunchOptions setIgnoreDefaultArgs(List<String> ignoreDefaultArgs) {
this.ignoreDefaultArgs = ignoreDefaultArgs;
@@ -353,8 +352,8 @@ public interface BrowserType {
return this;
}
/**
* Maximum time in milliseconds to wait for the browser instance to start. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to
* disable timeout.
* Maximum time in milliseconds to wait for the browser instance to start. Defaults to {@code 30000} (30 seconds). Pass
* {@code 0} to disable timeout.
*/
public LaunchOptions setTimeout(double timeout) {
this.timeout = timeout;
@@ -382,24 +381,26 @@ public interface BrowserType {
* When using {@link Page#navigate Page.navigate()}, {@link Page#route Page.route()}, {@link Page#waitForURL
* Page.waitForURL()}, {@link Page#waitForRequest Page.waitForRequest()}, or {@link Page#waitForResponse
* Page.waitForResponse()} it takes the base URL in 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:
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code URL()}</a> constructor for building the
* corresponding URL. Unset by default. Examples:
* <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} 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;
/**
* Toggles bypassing page's Content-Security-Policy.
* Toggles bypassing page's Content-Security-Policy. Defaults to {@code false}.
*/
public Boolean bypassCSP;
/**
* 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;
/**
@@ -407,17 +408,19 @@ public interface BrowserType {
*/
public Boolean chromiumSandbox;
/**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code "no-preference"}. See
* {@link Page#emulateMedia Page.emulateMedia()} for more details. Defaults to {@code "light"}.
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code
* "no-preference"}. See {@link Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "light"}.
*/
public ColorScheme colorScheme;
public Optional<ColorScheme> colorScheme;
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}.
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
*/
public Double deviceScaleFactor;
/**
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is {@code true}, the {@code headless}
* option will be set {@code false}.
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is {@code true}, the {@code
* headless} option will be set {@code false}.
*/
public Boolean devtools;
/**
@@ -437,17 +440,15 @@ public interface BrowserType {
*/
public Path executablePath;
/**
* An object containing additional HTTP headers to be sent with every request.
* An object containing additional HTTP headers to be sent with every request. Defaults to none.
*/
public Map<String, String> extraHTTPHeaders;
/**
* 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.
* Emulates {@code "forced-colors"} media feature, supported values are {@code "active"}, {@code "none"}. See {@link
* Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults.
* Defaults to {@code "none"}.
*/
public ForcedColors forcedColors;
public Optional<ForcedColors> forcedColors;
public Geolocation geolocation;
/**
* Close the browser process on SIGHUP. Defaults to {@code true}.
@@ -462,28 +463,30 @@ public interface BrowserType {
*/
public Boolean handleSIGTERM;
/**
* Specifies if viewport supports touch events. Defaults to false.
* Specifies if viewport supports touch events. Defaults to false. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#devices">mobile emulation</a>.
*/
public Boolean hasTouch;
/**
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true} unless the
* {@code devtools} option is {@code true}.
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true}
* unless the {@code devtools} option is {@code true}.
*/
public Boolean headless;
/**
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>.
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
*/
public HttpCredentials httpCredentials;
/**
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}. Dangerous option;
* use with care. Defaults to {@code false}.
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}.
* Dangerous option; use with care. Defaults to {@code false}.
*/
public Boolean ignoreAllDefaultArgs;
/**
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}. Dangerous option;
* use with care.
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}.
* Dangerous option; use with care.
*/
public List<String> ignoreDefaultArgs;
/**
@@ -491,32 +494,49 @@ public interface BrowserType {
*/
public Boolean ignoreHTTPSErrors;
/**
* Whether the {@code meta viewport} tag is taken into account and touch events are enabled. Defaults to {@code false}. Not supported
* in Firefox.
* Whether the {@code meta viewport} tag is taken into account and touch events are enabled. isMobile is a part of device,
* so you don't actually need to set it manually. Defaults to {@code false} and is not supported in Firefox. Learn more
* about <a href="https://playwright.dev/java/docs/emulation#isMobile">mobile emulation</a>.
*/
public Boolean isMobile;
/**
* Whether or not to enable JavaScript in the context. Defaults to {@code true}.
* Whether or not to enable JavaScript in the context. Defaults to {@code true}. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#javascript-enabled">disabling JavaScript</a>.
*/
public Boolean javaScriptEnabled;
/**
* Specify user locale, for example {@code en-GB}, {@code de-DE}, etc. Locale will affect {@code navigator.language} value, {@code Accept-Language}
* request header value as well as number and date formatting rules.
* Specify user locale, for example {@code en-GB}, {@code de-DE}, etc. Locale will affect {@code navigator.language} value,
* {@code Accept-Language} request header value as well as number and date formatting rules. Defaults to the system default
* locale. Learn more about emulation in our <a
* href="https://playwright.dev/java/docs/emulation#locale--timezone">emulation guide</a>.
*/
public String locale;
/**
* Whether to emulate network being offline. Defaults to {@code false}.
* Whether to emulate network being offline. Defaults to {@code false}. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#offline">network emulation</a>.
*/
public Boolean offline;
/**
* A list of permissions to grant to all pages in this context. See {@link BrowserContext#grantPermissions
* BrowserContext.grantPermissions()} for more details.
* BrowserContext.grantPermissions()} for more details. Defaults to none.
*/
public List<String> permissions;
/**
* 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 persisted 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}.
*/
@@ -527,6 +547,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.
@@ -534,39 +555,50 @@ public interface BrowserType {
public Path recordVideoDir;
/**
* Dimensions of the recorded videos. If not specified the size will be equal to {@code viewport} scaled down to fit into
* 800x800. If {@code viewport} is not configured explicitly the video size defaults to 800x450. Actual picture of each page will
* be scaled down if necessary to fit the specified size.
* 800x800. If {@code viewport} is not configured explicitly the video size defaults to 800x450. Actual picture of each
* page will be scaled down if necessary to fit the specified size.
*/
public RecordVideoSize recordVideoSize;
/**
* Emulates {@code "prefers-reduced-motion"} media feature, supported values are {@code "reduce"}, {@code "no-preference"}. See {@link
* Page#emulateMedia Page.emulateMedia()} for more details. Defaults to {@code "no-preference"}.
* Emulates {@code "prefers-reduced-motion"} media feature, supported values are {@code "reduce"}, {@code "no-preference"}.
* See {@link Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system
* defaults. Defaults to {@code "no-preference"}.
*/
public ReducedMotion reducedMotion;
public Optional<ReducedMotion> reducedMotion;
/**
* Emulates consistent window screen size available inside web page via {@code window.screen}. Is only used when the {@code viewport}
* is set.
* Emulates consistent window screen size available inside web page via {@code window.screen}. Is only used when the {@code
* viewport} is set.
*/
public ScreenSize screenSize;
/**
* 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
* 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.
* If set to true, 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. This option does not
* affect any Locator APIs (Locators are always strict). Defaults to {@code false}. See {@code Locator} to learn more about
* the strict mode.
*/
public Boolean strictSelectors;
/**
* Maximum time in milliseconds to wait for the browser instance to start. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to
* disable timeout.
* Maximum time in milliseconds to wait for the browser instance to start. Defaults to {@code 30000} (30 seconds). Pass
* {@code 0} to disable timeout.
*/
public Double timeout;
/**
* Changes the timezone of the context. See <a
* href="https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1">ICU's
* metaZones.txt</a> for a list of supported timezone IDs.
* metaZones.txt</a> for a list of supported timezone IDs. Defaults to the system timezone.
*/
public String timezoneId;
/**
@@ -578,7 +610,12 @@ public interface BrowserType {
*/
public String userAgent;
/**
* Emulates consistent viewport for each page. Defaults to an 1280x720 viewport. {@code null} disables the default viewport.
* Emulates consistent viewport for each page. Defaults to an 1280x720 viewport. Use {@code null} to disable the
* consistent viewport emulation. Learn more about <a href="https://playwright.dev/java/docs/emulation#viewport">viewport
* emulation</a>.
*
* <p> <strong>NOTE:</strong> The {@code null} value opts out from the default presets, makes viewport depend on the host window size defined by the
* operating system. It makes the execution of the tests non-deterministic.
*/
public Optional<ViewportSize> viewportSize;
@@ -601,11 +638,13 @@ public interface BrowserType {
* When using {@link Page#navigate Page.navigate()}, {@link Page#route Page.route()}, {@link Page#waitForURL
* Page.waitForURL()}, {@link Page#waitForRequest Page.waitForRequest()}, or {@link Page#waitForResponse
* Page.waitForResponse()} it takes the base URL in 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:
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code URL()}</a> constructor for building the
* corresponding URL. Unset by default. Examples:
* <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} 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>
@@ -615,7 +654,7 @@ public interface BrowserType {
return this;
}
/**
* Toggles bypassing page's Content-Security-Policy.
* Toggles bypassing page's Content-Security-Policy. Defaults to {@code false}.
*/
public LaunchPersistentContextOptions setBypassCSP(boolean bypassCSP) {
this.bypassCSP = bypassCSP;
@@ -625,7 +664,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;
@@ -634,7 +673,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;
@@ -648,23 +687,25 @@ public interface BrowserType {
return this;
}
/**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code "no-preference"}. See
* {@link Page#emulateMedia Page.emulateMedia()} for more details. Defaults to {@code "light"}.
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code
* "no-preference"}. See {@link Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "light"}.
*/
public LaunchPersistentContextOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = colorScheme;
this.colorScheme = Optional.ofNullable(colorScheme);
return this;
}
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}.
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
*/
public LaunchPersistentContextOptions setDeviceScaleFactor(double deviceScaleFactor) {
this.deviceScaleFactor = deviceScaleFactor;
return this;
}
/**
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is {@code true}, the {@code headless}
* option will be set {@code false}.
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is {@code true}, the {@code
* headless} option will be set {@code false}.
*/
public LaunchPersistentContextOptions setDevtools(boolean devtools) {
this.devtools = devtools;
@@ -696,21 +737,19 @@ public interface BrowserType {
return this;
}
/**
* An object containing additional HTTP headers to be sent with every request.
* An object containing additional HTTP headers to be sent with every request. Defaults to none.
*/
public LaunchPersistentContextOptions setExtraHTTPHeaders(Map<String, String> extraHTTPHeaders) {
this.extraHTTPHeaders = extraHTTPHeaders;
return this;
}
/**
* 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.
* Emulates {@code "forced-colors"} media feature, supported values are {@code "active"}, {@code "none"}. See {@link
* Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults.
* Defaults to {@code "none"}.
*/
public LaunchPersistentContextOptions setForcedColors(ForcedColors forcedColors) {
this.forcedColors = forcedColors;
this.forcedColors = Optional.ofNullable(forcedColors);
return this;
}
public LaunchPersistentContextOptions setGeolocation(double latitude, double longitude) {
@@ -742,7 +781,8 @@ public interface BrowserType {
return this;
}
/**
* Specifies if viewport supports touch events. Defaults to false.
* Specifies if viewport supports touch events. Defaults to false. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#devices">mobile emulation</a>.
*/
public LaunchPersistentContextOptions setHasTouch(boolean hasTouch) {
this.hasTouch = hasTouch;
@@ -751,37 +791,39 @@ public interface BrowserType {
/**
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true} unless the
* {@code devtools} option is {@code true}.
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true}
* unless the {@code devtools} option is {@code true}.
*/
public LaunchPersistentContextOptions setHeadless(boolean headless) {
this.headless = headless;
return this;
}
/**
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>.
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
*/
public LaunchPersistentContextOptions 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>.
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
*/
public LaunchPersistentContextOptions setHttpCredentials(HttpCredentials httpCredentials) {
this.httpCredentials = httpCredentials;
return this;
}
/**
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}. Dangerous option;
* use with care. Defaults to {@code false}.
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}.
* Dangerous option; use with care. Defaults to {@code false}.
*/
public LaunchPersistentContextOptions setIgnoreAllDefaultArgs(boolean ignoreAllDefaultArgs) {
this.ignoreAllDefaultArgs = ignoreAllDefaultArgs;
return this;
}
/**
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}. Dangerous option;
* use with care.
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}.
* Dangerous option; use with care.
*/
public LaunchPersistentContextOptions setIgnoreDefaultArgs(List<String> ignoreDefaultArgs) {
this.ignoreDefaultArgs = ignoreDefaultArgs;
@@ -795,30 +837,35 @@ public interface BrowserType {
return this;
}
/**
* Whether the {@code meta viewport} tag is taken into account and touch events are enabled. Defaults to {@code false}. Not supported
* in Firefox.
* Whether the {@code meta viewport} tag is taken into account and touch events are enabled. isMobile is a part of device,
* so you don't actually need to set it manually. Defaults to {@code false} and is not supported in Firefox. Learn more
* about <a href="https://playwright.dev/java/docs/emulation#isMobile">mobile emulation</a>.
*/
public LaunchPersistentContextOptions setIsMobile(boolean isMobile) {
this.isMobile = isMobile;
return this;
}
/**
* Whether or not to enable JavaScript in the context. Defaults to {@code true}.
* Whether or not to enable JavaScript in the context. Defaults to {@code true}. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#javascript-enabled">disabling JavaScript</a>.
*/
public LaunchPersistentContextOptions setJavaScriptEnabled(boolean javaScriptEnabled) {
this.javaScriptEnabled = javaScriptEnabled;
return this;
}
/**
* Specify user locale, for example {@code en-GB}, {@code de-DE}, etc. Locale will affect {@code navigator.language} value, {@code Accept-Language}
* request header value as well as number and date formatting rules.
* Specify user locale, for example {@code en-GB}, {@code de-DE}, etc. Locale will affect {@code navigator.language} value,
* {@code Accept-Language} request header value as well as number and date formatting rules. Defaults to the system default
* locale. Learn more about emulation in our <a
* href="https://playwright.dev/java/docs/emulation#locale--timezone">emulation guide</a>.
*/
public LaunchPersistentContextOptions setLocale(String locale) {
this.locale = locale;
return this;
}
/**
* Whether to emulate network being offline. Defaults to {@code false}.
* Whether to emulate network being offline. Defaults to {@code false}. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#offline">network emulation</a>.
*/
public LaunchPersistentContextOptions setOffline(boolean offline) {
this.offline = offline;
@@ -826,7 +873,7 @@ public interface BrowserType {
}
/**
* A list of permissions to grant to all pages in this context. See {@link BrowserContext#grantPermissions
* BrowserContext.grantPermissions()} for more details.
* BrowserContext.grantPermissions()} for more details. Defaults to none.
*/
public LaunchPersistentContextOptions setPermissions(List<String> permissions) {
this.permissions = permissions;
@@ -845,6 +892,24 @@ 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 persisted 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}.
*/
@@ -861,6 +926,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.
@@ -871,44 +944,57 @@ public interface BrowserType {
}
/**
* Dimensions of the recorded videos. If not specified the size will be equal to {@code viewport} scaled down to fit into
* 800x800. If {@code viewport} is not configured explicitly the video size defaults to 800x450. Actual picture of each page will
* be scaled down if necessary to fit the specified size.
* 800x800. If {@code viewport} is not configured explicitly the video size defaults to 800x450. Actual picture of each
* page will be scaled down if necessary to fit the specified size.
*/
public LaunchPersistentContextOptions setRecordVideoSize(int width, int height) {
return setRecordVideoSize(new RecordVideoSize(width, height));
}
/**
* Dimensions of the recorded videos. If not specified the size will be equal to {@code viewport} scaled down to fit into
* 800x800. If {@code viewport} is not configured explicitly the video size defaults to 800x450. Actual picture of each page will
* be scaled down if necessary to fit the specified size.
* 800x800. If {@code viewport} is not configured explicitly the video size defaults to 800x450. Actual picture of each
* page will be scaled down if necessary to fit the specified size.
*/
public LaunchPersistentContextOptions setRecordVideoSize(RecordVideoSize recordVideoSize) {
this.recordVideoSize = recordVideoSize;
return this;
}
/**
* Emulates {@code "prefers-reduced-motion"} media feature, supported values are {@code "reduce"}, {@code "no-preference"}. See {@link
* Page#emulateMedia Page.emulateMedia()} for more details. Defaults to {@code "no-preference"}.
* Emulates {@code "prefers-reduced-motion"} media feature, supported values are {@code "reduce"}, {@code "no-preference"}.
* See {@link Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system
* defaults. Defaults to {@code "no-preference"}.
*/
public LaunchPersistentContextOptions setReducedMotion(ReducedMotion reducedMotion) {
this.reducedMotion = reducedMotion;
this.reducedMotion = Optional.ofNullable(reducedMotion);
return this;
}
/**
* Emulates consistent window screen size available inside web page via {@code window.screen}. Is only used when the {@code viewport}
* is set.
* Emulates consistent window screen size available inside web page via {@code window.screen}. Is only used when the {@code
* viewport} is set.
*/
public LaunchPersistentContextOptions setScreenSize(int width, int height) {
return setScreenSize(new ScreenSize(width, height));
}
/**
* Emulates consistent window screen size available inside web page via {@code window.screen}. Is only used when the {@code viewport}
* is set.
* Emulates consistent window screen size available inside web page via {@code window.screen}. Is only used when the {@code
* viewport} is set.
*/
public LaunchPersistentContextOptions setScreenSize(ScreenSize screenSize) {
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.
*/
@@ -917,17 +1003,18 @@ public interface BrowserType {
return this;
}
/**
* It 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.
* If set to true, 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. This option does not
* affect any Locator APIs (Locators are always strict). Defaults to {@code false}. See {@code Locator} to learn more about
* the strict mode.
*/
public LaunchPersistentContextOptions setStrictSelectors(boolean strictSelectors) {
this.strictSelectors = strictSelectors;
return this;
}
/**
* Maximum time in milliseconds to wait for the browser instance to start. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to
* disable timeout.
* Maximum time in milliseconds to wait for the browser instance to start. Defaults to {@code 30000} (30 seconds). Pass
* {@code 0} to disable timeout.
*/
public LaunchPersistentContextOptions setTimeout(double timeout) {
this.timeout = timeout;
@@ -936,7 +1023,7 @@ public interface BrowserType {
/**
* Changes the timezone of the context. See <a
* href="https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1">ICU's
* metaZones.txt</a> for a list of supported timezone IDs.
* metaZones.txt</a> for a list of supported timezone IDs. Defaults to the system timezone.
*/
public LaunchPersistentContextOptions setTimezoneId(String timezoneId) {
this.timezoneId = timezoneId;
@@ -957,13 +1044,23 @@ public interface BrowserType {
return this;
}
/**
* Emulates consistent viewport for each page. Defaults to an 1280x720 viewport. {@code null} disables the default viewport.
* Emulates consistent viewport for each page. Defaults to an 1280x720 viewport. Use {@code null} to disable the
* consistent viewport emulation. Learn more about <a href="https://playwright.dev/java/docs/emulation#viewport">viewport
* emulation</a>.
*
* <p> <strong>NOTE:</strong> The {@code null} value opts out from the default presets, makes viewport depend on the host window size defined by the
* operating system. It makes the execution of the tests non-deterministic.
*/
public LaunchPersistentContextOptions setViewportSize(int width, int height) {
return setViewportSize(new ViewportSize(width, height));
}
/**
* Emulates consistent viewport for each page. Defaults to an 1280x720 viewport. {@code null} disables the default viewport.
* Emulates consistent viewport for each page. Defaults to an 1280x720 viewport. Use {@code null} to disable the
* consistent viewport emulation. Learn more about <a href="https://playwright.dev/java/docs/emulation#viewport">viewport
* emulation</a>.
*
* <p> <strong>NOTE:</strong> The {@code null} value opts out from the default presets, makes viewport depend on the host window size defined by the
* operating system. It makes the execution of the tests non-deterministic.
*/
public LaunchPersistentContextOptions setViewportSize(ViewportSize viewportSize) {
this.viewportSize = Optional.ofNullable(viewportSize);
@@ -971,50 +1068,76 @@ 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.
* @since v1.8
*/
default Browser connect(String wsEndpoint) {
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.
* @since v1.8
*/
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.
*
* @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}.
* <p> **Usage**
* <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}.
* @since v1.9
*/
default Browser connectOverCDP(String endpointURL) {
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.
*
* @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}.
* <p> **Usage**
* <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}.
* @since v1.9
*/
Browser connectOverCDP(String endpointURL, ConnectOverCDPOptions options);
/**
* A path where Playwright expects to find a bundled browser executable.
*
* @since v1.8
*/
String executablePath();
/**
* Returns the browser instance.
*
* <p> **Usage**
*
* <p> You can use {@code ignoreDefaultArgs} to filter out {@code --mute-audio} from default arguments:
* <pre>{@code
* // Or "firefox" or "webkit".
@@ -1040,6 +1163,8 @@ public interface BrowserType {
* other differences between Chromium and Chrome. <a
* href="https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md">This article</a>
* describes some differences for Linux users.
*
* @since v1.8
*/
default Browser launch() {
return launch(null);
@@ -1047,6 +1172,8 @@ public interface BrowserType {
/**
* Returns the browser instance.
*
* <p> **Usage**
*
* <p> You can use {@code ignoreDefaultArgs} to filter out {@code --mute-audio} from default arguments:
* <pre>{@code
* // Or "firefox" or "webkit".
@@ -1072,6 +1199,8 @@ public interface BrowserType {
* other differences between Chromium and Chrome. <a
* href="https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md">This article</a>
* describes some differences for Linux users.
*
* @since v1.8
*/
Browser launch(LaunchOptions options);
/**
@@ -1083,8 +1212,9 @@ public interface BrowserType {
* @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for <a
* href="https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile">Firefox</a>. Note that
* Chromium's user data directory is the **parent** directory of the "Profile Path" seen at {@code chrome://version}. Pass an
* empty string to use a temporary directory instead.
* Chromium's user data directory is the **parent** directory of the "Profile Path" seen at {@code chrome://version}. Pass
* an empty string to use a temporary directory instead.
* @since v1.8
*/
default BrowserContext launchPersistentContext(Path userDataDir) {
return launchPersistentContext(userDataDir, null);
@@ -1098,12 +1228,15 @@ public interface BrowserType {
* @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for <a
* href="https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile">Firefox</a>. Note that
* Chromium's user data directory is the **parent** directory of the "Profile Path" seen at {@code chrome://version}. Pass an
* empty string to use a temporary directory instead.
* Chromium's user data directory is the **parent** directory of the "Profile Path" seen at {@code chrome://version}. Pass
* an empty string to use a temporary directory instead.
* @since v1.8
*/
BrowserContext launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options);
/**
* Returns browser name. For example: {@code "chromium"}, {@code "webkit"} or {@code "firefox"}.
*
* @since v1.8
*/
String name();
}
@@ -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,10 +29,9 @@ import static java.util.Arrays.asList;
*/
public class CLI {
public static void main(String[] args) throws IOException, InterruptedException {
Path driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
ProcessBuilder pb = new ProcessBuilder(driver.toString());
Driver driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
ProcessBuilder pb = driver.createProcessBuilder();
pb.command().addAll(asList(args));
Driver.setRequiredEnvironmentVariables(pb);
String version = Playwright.class.getPackage().getImplementationVersion();
if (version != null) {
pb.environment().put("PW_CLI_DISPLAY_VERSION", version);
@@ -19,25 +19,62 @@ 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 console messages and print them to the standard output.
* page.onConsoleMessage(msg -> System.out.println(msg.text()));
*
* // Listen for all console messages and print errors to the standard output.
* page.onConsoleMessage(msg -> {
* if ("error".equals(msg.type()))
* System.out.println("Error text: " + msg.text());
* });
*
* // Get the next console message
* 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 {
/**
* List of arguments passed to a {@code console} function call. See also {@link Page#onConsoleMessage Page.onConsoleMessage()}.
* List of arguments passed to a {@code console} function call. See also {@link Page#onConsoleMessage
* Page.onConsoleMessage()}.
*
* @since v1.8
*/
List<JSHandle> args();
/**
* URL of the resource followed by 0-based line and column numbers in the resource formatted as {@code URL:line:column}.
*
* @since v1.8
*/
String location();
/**
* The page that produced this console message, if any.
*
* @since v1.34
*/
Page page();
/**
* The text of the console message.
*
* @since v1.8
*/
String text();
/**
* One of the following values: {@code "log"}, {@code "debug"}, {@code "info"}, {@code "error"}, {@code "warning"}, {@code "dir"}, {@code "dirxml"}, {@code "table"},
* {@code "trace"}, {@code "clear"}, {@code "startGroup"}, {@code "startGroupCollapsed"}, {@code "endGroup"}, {@code "assert"}, {@code "profile"}, {@code "profileEnd"},
* {@code "count"}, {@code "timeEnd"}.
* One of the following values: {@code "log"}, {@code "debug"}, {@code "info"}, {@code "error"}, {@code "warning"}, {@code
* "dir"}, {@code "dirxml"}, {@code "table"}, {@code "trace"}, {@code "clear"}, {@code "startGroup"}, {@code
* "startGroupCollapsed"}, {@code "endGroup"}, {@code "assert"}, {@code "profile"}, {@code "profileEnd"}, {@code "count"},
* {@code "timeEnd"}.
*
* @since v1.8
*/
String type();
}
@@ -50,6 +50,8 @@ package com.microsoft.playwright;
public interface Dialog {
/**
* Returns when the dialog has been accepted.
*
* @since v1.8
*/
default void accept() {
accept(null);
@@ -58,22 +60,37 @@ public interface Dialog {
* Returns when the dialog has been accepted.
*
* @param promptText A text to enter in prompt. Does not cause any effects if the dialog's {@code type} is not prompt. Optional.
* @since v1.8
*/
void accept(String promptText);
/**
* If dialog is prompt, returns default prompt value. Otherwise, returns empty string.
*
* @since v1.8
*/
String defaultValue();
/**
* Returns when the dialog has been dismissed.
*
* @since v1.8
*/
void dismiss();
/**
* A message displayed in the dialog.
*
* @since v1.8
*/
String message();
/**
* The page that initiated this dialog, if available.
*
* @since v1.34
*/
Page page();
/**
* Returns dialog's type, can be one of {@code alert}, {@code beforeunload}, {@code confirm} or {@code prompt}.
*
* @since v1.8
*/
String type();
}
@@ -27,14 +27,8 @@ import java.nio.file.Path;
* <p> Download event is emitted once the download starts. Download path becomes available once download completes:
* <pre>{@code
* // wait for download to start
* Download download = page.waitForDownload(() -> page.click("a"));
* // wait for download to complete
* Path path = download.path();
* }</pre>
* <pre>{@code
* // wait for download to start
* Download download = page.waitForDownload(() -> {
* page.click("a");
* page.getByText("Download file").click();
* });
* // wait for download to complete
* Path path = download.path();
@@ -42,24 +36,34 @@ import java.nio.file.Path;
*/
public interface Download {
/**
* Cancels a download. Will not fail if the download is already finished or canceled. Upon successful cancellations,
* {@code download.failure()} would resolve to {@code "canceled"}.
* Cancels a download. Will not fail if the download is already finished or canceled. Upon successful cancellations, {@code
* download.failure()} would resolve to {@code "canceled"}.
*
* @since v1.13
*/
void cancel();
/**
* Returns readable stream for current download or {@code null} if download failed.
*
* @since v1.8
*/
InputStream createReadStream();
/**
* Deletes the downloaded file. Will wait for the download to finish if necessary.
*
* @since v1.8
*/
void delete();
/**
* Returns download error if any. Will wait for the download to finish if necessary.
*
* @since v1.8
*/
String failure();
/**
* Get the page that the download belongs to.
*
* @since v1.12
*/
Page page();
/**
@@ -68,6 +72,8 @@ public interface Download {
*
* <p> Note that the download's file name is a random GUID, use {@link Download#suggestedFilename Download.suggestedFilename()}
* to get suggested file name.
*
* @since v1.8
*/
Path path();
/**
@@ -75,18 +81,23 @@ public interface Download {
* wait for the download to finish if necessary.
*
* @param path Path where the download should be copied.
* @since v1.8
*/
void saveAs(Path path);
/**
* Returns suggested filename for this download. It is typically computed by the browser from the <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition">{@code Content-Disposition}</a> response
* header or the {@code download} attribute. See the spec on <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition">{@code Content-Disposition}</a>
* response header or the {@code download} attribute. See the spec on <a
* href="https://html.spec.whatwg.org/#downloading-resources">whatwg</a>. Different browsers can use different logic for
* computing it.
*
* @since v1.8
*/
String suggestedFilename();
/**
* Returns downloaded url.
*
* @since v1.8
*/
String url();
}
File diff suppressed because it is too large Load Diff
@@ -22,7 +22,7 @@ import java.nio.file.Path;
/**
* {@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.getByText("Upload file").click());
* fileChooser.setFiles(Paths.get("myfile.pdf"));
* }</pre>
*/
@@ -35,9 +35,9 @@ public interface FileChooser {
*/
public Boolean noWaitAfter;
/**
* Maximum time in milliseconds, defaults to 30 seconds, pass {@code 0} to disable timeout. The default value can be changed by
* using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()} or {@link Page#setDefaultTimeout
* Page.setDefaultTimeout()} methods.
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()} or {@link
* Page#setDefaultTimeout Page.setDefaultTimeout()} methods.
*/
public Double timeout;
@@ -51,9 +51,9 @@ public interface FileChooser {
return this;
}
/**
* Maximum time in milliseconds, defaults to 30 seconds, pass {@code 0} to disable timeout. The default value can be changed by
* using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()} or {@link Page#setDefaultTimeout
* Page.setDefaultTimeout()} methods.
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()} or {@link
* Page#setDefaultTimeout Page.setDefaultTimeout()} methods.
*/
public SetFilesOptions setTimeout(double timeout) {
this.timeout = timeout;
@@ -62,62 +62,84 @@ public interface FileChooser {
}
/**
* Returns input element associated with this file chooser.
*
* @since v1.8
*/
ElementHandle element();
/**
* Returns whether this file chooser accepts multiple files.
*
* @since v1.8
*/
boolean isMultiple();
/**
* Returns page this file chooser belongs to.
*
* @since v1.8
*/
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.
* 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 current working directory. For empty array, clears the selected files.
*
* @since v1.8
*/
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.
* 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 current working directory. For empty array, clears the selected files.
*
* @since v1.8
*/
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.
* 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 current working directory. For empty array, clears the selected files.
*
* @since v1.8
*/
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.
* 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 current working directory. For empty array, clears the selected files.
*
* @since v1.8
*/
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.
* 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 current working directory. For empty array, clears the selected files.
*
* @since v1.8
*/
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.
* 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 current working directory. For empty array, clears the selected files.
*
* @since v1.8
*/
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.
* 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 current working directory. For empty array, clears the selected files.
*
* @since v1.8
*/
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.
* 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 current working directory. For empty array, clears the selected files.
*
* @since v1.8
*/
void setFiles(FilePayload[] files, SetFilesOptions options);
}
File diff suppressed because it is too large Load Diff
@@ -16,27 +16,28 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
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}
* and locate elements in that iframe. FrameLocator can be created with either {@link Page#frameLocator
* FrameLocator represents a view to the {@code iframe} on the page. It captures the logic sufficient to retrieve the
* {@code iframe} and locate elements in that iframe. FrameLocator can be created with either {@link Page#frameLocator
* Page.frameLocator()} or {@link Locator#frameLocator Locator.frameLocator()} method.
* <pre>{@code
* Locator locator = page.frameLocator("#my-frame").locator("text=Submit");
* Locator locator = page.frameLocator("#my-frame").getByText("Submit");
* locator.click();
* }</pre>
*
* <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();
* page.frame_locator(".result-frame").getByRole(AriaRole.BUTTON).click();
*
* // Works because we explicitly tell locator to pick the first frame:
* page.frame_locator(".result-frame").first().locator("button").click();
* page.frame_locator(".result-frame").first().getByRole(AriaRole.BUTTON).click();
* }</pre>
*
* <p> **Converting Locator to FrameLocator**
@@ -48,6 +49,240 @@ import java.util.regex.Pattern;
* }</pre>
*/
public interface FrameLocator {
class GetByAltTextOptions {
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/
public Boolean exact;
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/
public GetByAltTextOptions setExact(boolean exact) {
this.exact = exact;
return this;
}
}
class GetByLabelOptions {
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/
public Boolean exact;
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/
public GetByLabelOptions setExact(boolean exact) {
this.exact = exact;
return this;
}
}
class GetByPlaceholderOptions {
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/
public Boolean exact;
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/
public GetByPlaceholderOptions setExact(boolean exact) {
this.exact = exact;
return this;
}
}
class GetByRoleOptions {
/**
* An attribute that is usually set by {@code aria-checked} or native {@code <input type=checkbox>} controls.
*
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>.
*/
public Boolean checked;
/**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
*
* <p> <strong>NOTE:</strong> Unlike most other attributes, {@code disabled} is inherited through the DOM hierarchy. Learn more about <a
* href="https://www.w3.org/TR/wai-aria-1.2/#aria-disabled">{@code aria-disabled}</a>.
*/
public Boolean disabled;
/**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name}
* is a regular expression. Note that exact match still trims whitespace.
*/
public Boolean exact;
/**
* An attribute that is usually set by {@code aria-expanded}.
*
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-expanded">{@code aria-expanded}</a>.
*/
public Boolean expanded;
/**
* Option that controls whether hidden elements are matched. By default, only non-hidden elements, as <a
* href="https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion">defined by ARIA</a>, are matched by role selector.
*
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-hidden">{@code aria-hidden}</a>.
*/
public Boolean includeHidden;
/**
* A number attribute that is usually present for roles {@code heading}, {@code listitem}, {@code row}, {@code treeitem},
* with default values for {@code <h1>-<h6>} elements.
*
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-level">{@code aria-level}</a>.
*/
public Integer level;
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-name">accessible name</a>. By default,
* matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-name">accessible name</a>.
*/
public Object name;
/**
* An attribute that is usually set by {@code aria-pressed}.
*
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-pressed">{@code aria-pressed}</a>.
*/
public Boolean pressed;
/**
* An attribute that is usually set by {@code aria-selected}.
*
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-selected">{@code aria-selected}</a>.
*/
public Boolean selected;
/**
* An attribute that is usually set by {@code aria-checked} or native {@code <input type=checkbox>} controls.
*
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>.
*/
public GetByRoleOptions setChecked(boolean checked) {
this.checked = checked;
return this;
}
/**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
*
* <p> <strong>NOTE:</strong> Unlike most other attributes, {@code disabled} is inherited through the DOM hierarchy. Learn more about <a
* href="https://www.w3.org/TR/wai-aria-1.2/#aria-disabled">{@code aria-disabled}</a>.
*/
public GetByRoleOptions setDisabled(boolean disabled) {
this.disabled = disabled;
return this;
}
/**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name}
* is a regular expression. Note that exact match still trims whitespace.
*/
public GetByRoleOptions setExact(boolean exact) {
this.exact = exact;
return this;
}
/**
* An attribute that is usually set by {@code aria-expanded}.
*
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-expanded">{@code aria-expanded}</a>.
*/
public GetByRoleOptions setExpanded(boolean expanded) {
this.expanded = expanded;
return this;
}
/**
* Option that controls whether hidden elements are matched. By default, only non-hidden elements, as <a
* href="https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion">defined by ARIA</a>, are matched by role selector.
*
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-hidden">{@code aria-hidden}</a>.
*/
public GetByRoleOptions setIncludeHidden(boolean includeHidden) {
this.includeHidden = includeHidden;
return this;
}
/**
* A number attribute that is usually present for roles {@code heading}, {@code listitem}, {@code row}, {@code treeitem},
* with default values for {@code <h1>-<h6>} elements.
*
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-level">{@code aria-level}</a>.
*/
public GetByRoleOptions setLevel(int level) {
this.level = level;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-name">accessible name</a>. By default,
* matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-name">accessible name</a>.
*/
public GetByRoleOptions setName(String name) {
this.name = name;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-name">accessible name</a>. By default,
* matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-name">accessible name</a>.
*/
public GetByRoleOptions setName(Pattern name) {
this.name = name;
return this;
}
/**
* An attribute that is usually set by {@code aria-pressed}.
*
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-pressed">{@code aria-pressed}</a>.
*/
public GetByRoleOptions setPressed(boolean pressed) {
this.pressed = pressed;
return this;
}
/**
* An attribute that is usually set by {@code aria-selected}.
*
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-selected">{@code aria-selected}</a>.
*/
public GetByRoleOptions setSelected(boolean selected) {
this.selected = selected;
return this;
}
}
class GetByTextOptions {
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/
public Boolean exact;
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/
public GetByTextOptions setExact(boolean exact) {
this.exact = exact;
return this;
}
}
class GetByTitleOptions {
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/
public Boolean exact;
/**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/
public GetByTitleOptions setExact(boolean exact) {
this.exact = exact;
return this;
}
}
class LocatorOptions {
/**
* Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer one.
@@ -57,8 +292,22 @@ public interface FrameLocator {
*/
public Locator has;
/**
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example,
* {@code "Playwright"} matches {@code <article><div>Playwright</div></article>}.
* Matches elements that do not contain an element that matches an inner locator. Inner locator is queried against the
* outer one. For example, {@code article} that does not have {@code div} matches {@code
* <article><span>Playwright</span></article>}.
*
* <p> Note that outer and inner locators must belong to the same frame. Inner locator must not contain {@code FrameLocator}s.
*/
public Locator hasNot;
/**
* Matches elements that do not contain 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.
*/
public Object hasNotText;
/**
* 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;
@@ -73,16 +322,45 @@ public interface FrameLocator {
return this;
}
/**
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example,
* {@code "Playwright"} matches {@code <article><div>Playwright</div></article>}.
* Matches elements that do not contain an element that matches an inner locator. Inner locator is queried against the
* outer one. For example, {@code article} that does not have {@code div} matches {@code
* <article><span>Playwright</span></article>}.
*
* <p> Note that outer and inner locators must belong to the same frame. Inner locator must not contain {@code FrameLocator}s.
*/
public LocatorOptions setHasNot(Locator hasNot) {
this.hasNot = hasNot;
return this;
}
/**
* Matches elements that do not contain 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.
*/
public LocatorOptions setHasNotText(String hasNotText) {
this.hasNotText = hasNotText;
return this;
}
/**
* Matches elements that do not contain 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.
*/
public LocatorOptions setHasNotText(Pattern hasNotText) {
this.hasNotText = hasNotText;
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. For example,
* {@code "Playwright"} matches {@code <article><div>Playwright</div></article>}.
* 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;
@@ -91,38 +369,626 @@ public interface FrameLocator {
}
/**
* Returns locator to the first matching frame.
*
* @since v1.17
*/
FrameLocator first();
/**
* 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
* selectors</a> for more details.
* @param selector A selector to use when resolving DOM element.
* @since v1.17
*/
FrameLocator frameLocator(String selector);
/**
* Allows locating elements by their alt text.
*
* <p> **Usage**
*
* <p> For example, this method will find the image by alt text "Playwright logo":
* <pre>{@code
* page.getByAltText("Playwright logo").click();
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
default Locator getByAltText(String text) {
return getByAltText(text, null);
}
/**
* Allows locating elements by their alt text.
*
* <p> **Usage**
*
* <p> For example, this method will find the image by alt text "Playwright logo":
* <pre>{@code
* page.getByAltText("Playwright logo").click();
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
Locator getByAltText(String text, GetByAltTextOptions options);
/**
* Allows locating elements by their alt text.
*
* <p> **Usage**
*
* <p> For example, this method will find the image by alt text "Playwright logo":
* <pre>{@code
* page.getByAltText("Playwright logo").click();
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
default Locator getByAltText(Pattern text) {
return getByAltText(text, null);
}
/**
* Allows locating elements by their alt text.
*
* <p> **Usage**
*
* <p> For example, this method will find the image by alt text "Playwright logo":
* <pre>{@code
* page.getByAltText("Playwright logo").click();
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
Locator getByAltText(Pattern text, GetByAltTextOptions options);
/**
* Allows locating input elements by the text of the associated {@code <label>} or {@code aria-labelledby} element, or by
* the {@code aria-label} attribute.
*
* <p> **Usage**
*
* <p> For example, this method will find inputs by label "Username" and "Password" in the following DOM:
* <pre>{@code
* page.getByLabel("Username").fill("john");
* page.getByLabel("Password").fill("secret");
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
default Locator getByLabel(String text) {
return getByLabel(text, null);
}
/**
* Allows locating input elements by the text of the associated {@code <label>} or {@code aria-labelledby} element, or by
* the {@code aria-label} attribute.
*
* <p> **Usage**
*
* <p> For example, this method will find inputs by label "Username" and "Password" in the following DOM:
* <pre>{@code
* page.getByLabel("Username").fill("john");
* page.getByLabel("Password").fill("secret");
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
Locator getByLabel(String text, GetByLabelOptions options);
/**
* Allows locating input elements by the text of the associated {@code <label>} or {@code aria-labelledby} element, or by
* the {@code aria-label} attribute.
*
* <p> **Usage**
*
* <p> For example, this method will find inputs by label "Username" and "Password" in the following DOM:
* <pre>{@code
* page.getByLabel("Username").fill("john");
* page.getByLabel("Password").fill("secret");
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
default Locator getByLabel(Pattern text) {
return getByLabel(text, null);
}
/**
* Allows locating input elements by the text of the associated {@code <label>} or {@code aria-labelledby} element, or by
* the {@code aria-label} attribute.
*
* <p> **Usage**
*
* <p> For example, this method will find inputs by label "Username" and "Password" in the following DOM:
* <pre>{@code
* page.getByLabel("Username").fill("john");
* page.getByLabel("Password").fill("secret");
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
Locator getByLabel(Pattern text, GetByLabelOptions options);
/**
* Allows locating input elements by the placeholder text.
*
* <p> **Usage**
*
* <p> For example, consider the following DOM structure.
*
* <p> You can fill the input after locating it by the placeholder text:
* <pre>{@code
* page.getByPlaceholder("name@example.com").fill("playwright@microsoft.com");
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
default Locator getByPlaceholder(String text) {
return getByPlaceholder(text, null);
}
/**
* Allows locating input elements by the placeholder text.
*
* <p> **Usage**
*
* <p> For example, consider the following DOM structure.
*
* <p> You can fill the input after locating it by the placeholder text:
* <pre>{@code
* page.getByPlaceholder("name@example.com").fill("playwright@microsoft.com");
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
Locator getByPlaceholder(String text, GetByPlaceholderOptions options);
/**
* Allows locating input elements by the placeholder text.
*
* <p> **Usage**
*
* <p> For example, consider the following DOM structure.
*
* <p> You can fill the input after locating it by the placeholder text:
* <pre>{@code
* page.getByPlaceholder("name@example.com").fill("playwright@microsoft.com");
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
default Locator getByPlaceholder(Pattern text) {
return getByPlaceholder(text, null);
}
/**
* Allows locating input elements by the placeholder text.
*
* <p> **Usage**
*
* <p> For example, consider the following DOM structure.
*
* <p> You can fill the input after locating it by the placeholder text:
* <pre>{@code
* page.getByPlaceholder("name@example.com").fill("playwright@microsoft.com");
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
Locator getByPlaceholder(Pattern text, GetByPlaceholderOptions options);
/**
* Allows locating elements by their <a href="https://www.w3.org/TR/wai-aria-1.2/#roles">ARIA role</a>, <a
* href="https://www.w3.org/TR/wai-aria-1.2/#aria-attributes">ARIA attributes</a> and <a
* href="https://w3c.github.io/accname/#dfn-accessible-name">accessible name</a>.
*
* <p> **Usage**
*
* <p> Consider the following DOM structure.
*
* <p> You can locate each element by it's implicit role:
* <pre>{@code
* assertThat(page
* .getByRole(AriaRole.HEADING,
* new Page.GetByRoleOptions().setName("Sign up")))
* .isVisible();
*
* page.getByRole(AriaRole.CHECKBOX,
* new Page.GetByRoleOptions().setName("Subscribe"))
* .check();
*
* page.getByRole(AriaRole.BUTTON,
* new Page.GetByRoleOptions().setName(
* Pattern.compile("submit", Pattern.CASE_INSENSITIVE)))
* .click();
* }</pre>
*
* <p> **Details**
*
* <p> Role selector **does not replace** accessibility audits and conformance tests, but rather gives early feedback about the
* ARIA guidelines.
*
* <p> Many html elements have an implicitly <a href="https://w3c.github.io/html-aam/#html-element-role-mappings">defined
* role</a> that is recognized by the role selector. You can find all the <a
* href="https://www.w3.org/TR/wai-aria-1.2/#role_definitions">supported roles here</a>. ARIA guidelines **do not
* recommend** duplicating implicit roles and attributes by setting {@code role} and/or {@code aria-*} attributes to
* default values.
*
* @param role Required aria role.
* @since v1.27
*/
default Locator getByRole(AriaRole role) {
return getByRole(role, null);
}
/**
* Allows locating elements by their <a href="https://www.w3.org/TR/wai-aria-1.2/#roles">ARIA role</a>, <a
* href="https://www.w3.org/TR/wai-aria-1.2/#aria-attributes">ARIA attributes</a> and <a
* href="https://w3c.github.io/accname/#dfn-accessible-name">accessible name</a>.
*
* <p> **Usage**
*
* <p> Consider the following DOM structure.
*
* <p> You can locate each element by it's implicit role:
* <pre>{@code
* assertThat(page
* .getByRole(AriaRole.HEADING,
* new Page.GetByRoleOptions().setName("Sign up")))
* .isVisible();
*
* page.getByRole(AriaRole.CHECKBOX,
* new Page.GetByRoleOptions().setName("Subscribe"))
* .check();
*
* page.getByRole(AriaRole.BUTTON,
* new Page.GetByRoleOptions().setName(
* Pattern.compile("submit", Pattern.CASE_INSENSITIVE)))
* .click();
* }</pre>
*
* <p> **Details**
*
* <p> Role selector **does not replace** accessibility audits and conformance tests, but rather gives early feedback about the
* ARIA guidelines.
*
* <p> Many html elements have an implicitly <a href="https://w3c.github.io/html-aam/#html-element-role-mappings">defined
* role</a> that is recognized by the role selector. You can find all the <a
* href="https://www.w3.org/TR/wai-aria-1.2/#role_definitions">supported roles here</a>. ARIA guidelines **do not
* recommend** duplicating implicit roles and attributes by setting {@code role} and/or {@code aria-*} attributes to
* default values.
*
* @param role Required aria role.
* @since v1.27
*/
Locator getByRole(AriaRole role, GetByRoleOptions options);
/**
* Locate element by the test id.
*
* <p> **Usage**
*
* <p> Consider the following DOM structure.
*
* <p> You can locate the element by it's test id:
* <pre>{@code
* page.getByTestId("directions").click();
* }</pre>
*
* <p> **Details**
*
* <p> By default, the {@code data-testid} attribute is used as a test id. Use {@link Selectors#setTestIdAttribute
* Selectors.setTestIdAttribute()} to configure a different test id attribute if necessary.
*
* @param testId Id to locate the element by.
* @since v1.27
*/
Locator getByTestId(String testId);
/**
* Locate element by the test id.
*
* <p> **Usage**
*
* <p> Consider the following DOM structure.
*
* <p> You can locate the element by it's test id:
* <pre>{@code
* page.getByTestId("directions").click();
* }</pre>
*
* <p> **Details**
*
* <p> By default, the {@code data-testid} attribute is used as a test id. Use {@link Selectors#setTestIdAttribute
* Selectors.setTestIdAttribute()} to configure a different test id attribute if necessary.
*
* @param testId Id to locate the element by.
* @since v1.27
*/
Locator getByTestId(Pattern testId);
/**
* Allows locating elements that contain given text.
*
* <p> See also {@link Locator#filter Locator.filter()} that allows to match by another criteria, like an accessible role, and
* then filter by the text content.
*
* <p> **Usage**
*
* <p> Consider the following DOM structure:
*
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
*
* // Matches first <div>
* page.getByText("Hello world")
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* }</pre>
*
* <p> **Details**
*
* <p> Matching by text always normalizes whitespace, even with exact match. For example, it turns multiple spaces into one,
* turns line breaks into spaces and ignores leading and trailing whitespace.
*
* <p> Input elements of the type {@code button} and {@code submit} are matched by their {@code value} instead of the text
* content. For example, locating by text {@code "Log in"} matches {@code <input type=button value="Log in">}.
*
* @param text Text to locate the element for.
* @since v1.27
*/
default Locator getByText(String text) {
return getByText(text, null);
}
/**
* Allows locating elements that contain given text.
*
* <p> See also {@link Locator#filter Locator.filter()} that allows to match by another criteria, like an accessible role, and
* then filter by the text content.
*
* <p> **Usage**
*
* <p> Consider the following DOM structure:
*
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
*
* // Matches first <div>
* page.getByText("Hello world")
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* }</pre>
*
* <p> **Details**
*
* <p> Matching by text always normalizes whitespace, even with exact match. For example, it turns multiple spaces into one,
* turns line breaks into spaces and ignores leading and trailing whitespace.
*
* <p> Input elements of the type {@code button} and {@code submit} are matched by their {@code value} instead of the text
* content. For example, locating by text {@code "Log in"} matches {@code <input type=button value="Log in">}.
*
* @param text Text to locate the element for.
* @since v1.27
*/
Locator getByText(String text, GetByTextOptions options);
/**
* Allows locating elements that contain given text.
*
* <p> See also {@link Locator#filter Locator.filter()} that allows to match by another criteria, like an accessible role, and
* then filter by the text content.
*
* <p> **Usage**
*
* <p> Consider the following DOM structure:
*
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
*
* // Matches first <div>
* page.getByText("Hello world")
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* }</pre>
*
* <p> **Details**
*
* <p> Matching by text always normalizes whitespace, even with exact match. For example, it turns multiple spaces into one,
* turns line breaks into spaces and ignores leading and trailing whitespace.
*
* <p> Input elements of the type {@code button} and {@code submit} are matched by their {@code value} instead of the text
* content. For example, locating by text {@code "Log in"} matches {@code <input type=button value="Log in">}.
*
* @param text Text to locate the element for.
* @since v1.27
*/
default Locator getByText(Pattern text) {
return getByText(text, null);
}
/**
* Allows locating elements that contain given text.
*
* <p> See also {@link Locator#filter Locator.filter()} that allows to match by another criteria, like an accessible role, and
* then filter by the text content.
*
* <p> **Usage**
*
* <p> Consider the following DOM structure:
*
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
*
* // Matches first <div>
* page.getByText("Hello world")
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* }</pre>
*
* <p> **Details**
*
* <p> Matching by text always normalizes whitespace, even with exact match. For example, it turns multiple spaces into one,
* turns line breaks into spaces and ignores leading and trailing whitespace.
*
* <p> Input elements of the type {@code button} and {@code submit} are matched by their {@code value} instead of the text
* content. For example, locating by text {@code "Log in"} matches {@code <input type=button value="Log in">}.
*
* @param text Text to locate the element for.
* @since v1.27
*/
Locator getByText(Pattern text, GetByTextOptions options);
/**
* Allows locating elements by their title attribute.
*
* <p> **Usage**
*
* <p> Consider the following DOM structure.
*
* <p> You can check the issues count after locating it by the title text:
* <pre>{@code
* assertThat(page.getByTitle("Issues count")).hasText("25 issues");
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
default Locator getByTitle(String text) {
return getByTitle(text, null);
}
/**
* Allows locating elements by their title attribute.
*
* <p> **Usage**
*
* <p> Consider the following DOM structure.
*
* <p> You can check the issues count after locating it by the title text:
* <pre>{@code
* assertThat(page.getByTitle("Issues count")).hasText("25 issues");
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
Locator getByTitle(String text, GetByTitleOptions options);
/**
* Allows locating elements by their title attribute.
*
* <p> **Usage**
*
* <p> Consider the following DOM structure.
*
* <p> You can check the issues count after locating it by the title text:
* <pre>{@code
* assertThat(page.getByTitle("Issues count")).hasText("25 issues");
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
default Locator getByTitle(Pattern text) {
return getByTitle(text, null);
}
/**
* Allows locating elements by their title attribute.
*
* <p> **Usage**
*
* <p> Consider the following DOM structure.
*
* <p> You can check the issues count after locating it by the title text:
* <pre>{@code
* assertThat(page.getByTitle("Issues count")).hasText("25 issues");
* }</pre>
*
* @param text Text to locate the element for.
* @since v1.27
*/
Locator getByTitle(Pattern text, GetByTitleOptions options);
/**
* Returns locator to the last matching frame.
*
* @since v1.17
*/
FrameLocator last();
/**
* The method finds an element matching the specified selector in the FrameLocator's subtree.
* The method finds an element matching the specified selector in the locator's subtree. It also accepts filter options,
* similar to {@link Locator#filter Locator.filter()} method.
*
* @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.
* <p> <a href="https://playwright.dev/java/docs/locators">Learn more about locators</a>.
*
* @param selectorOrLocator A selector or locator to use when resolving DOM element.
* @since v1.17
*/
default Locator locator(String selector) {
return locator(selector, null);
default Locator locator(String selectorOrLocator) {
return locator(selectorOrLocator, null);
}
/**
* The method finds an element matching the specified selector in the FrameLocator's subtree.
* The method finds an element matching the specified selector in the locator's subtree. It also accepts filter options,
* similar to {@link Locator#filter Locator.filter()} method.
*
* @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.
* <p> <a href="https://playwright.dev/java/docs/locators">Learn more about locators</a>.
*
* @param selectorOrLocator A selector or locator to use when resolving DOM element.
* @since v1.17
*/
Locator locator(String selector, LocatorOptions options);
Locator locator(String selectorOrLocator, LocatorOptions options);
/**
* Returns locator to the n-th matching frame.
* The method finds an element matching the specified selector in the locator's subtree. It also accepts filter options,
* similar to {@link Locator#filter Locator.filter()} method.
*
* <p> <a href="https://playwright.dev/java/docs/locators">Learn more about locators</a>.
*
* @param selectorOrLocator A selector or locator to use when resolving DOM element.
* @since v1.17
*/
default Locator locator(Locator selectorOrLocator) {
return locator(selectorOrLocator, null);
}
/**
* The method finds an element matching the specified selector in the locator's subtree. It also accepts filter options,
* similar to {@link Locator#filter Locator.filter()} method.
*
* <p> <a href="https://playwright.dev/java/docs/locators">Learn more about locators</a>.
*
* @param selectorOrLocator A selector or locator to use when resolving DOM element.
* @since v1.17
*/
Locator locator(Locator selectorOrLocator, LocatorOptions options);
/**
* Returns locator to the n-th matching frame. It's zero based, {@code nth(0)} selects the first frame.
*
* @since v1.17
*/
FrameLocator nth(int index);
}
@@ -36,10 +36,14 @@ import java.util.*;
public interface JSHandle {
/**
* Returns either {@code null} or the object handle itself, if the object handle is an instance of {@code ElementHandle}.
*
* @since v1.8
*/
ElementHandle asElement();
/**
* The {@code jsHandle.dispose} method stops referencing the element handle.
*
* @since v1.8
*/
void dispose();
/**
@@ -48,17 +52,18 @@ public interface JSHandle {
* <p> This method passes this handle as the first argument to {@code expression}.
*
* <p> If {@code expression} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then
* {@code handle.evaluate} would wait for the promise to resolve and return its value.
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@code
* handle.evaluate} would wait for the promise to resolve and return its value.
*
* <p> Examples:
* <p> **Usage**
* <pre>{@code
* ElementHandle tweetHandle = page.querySelector(".tweet .retweets");
* assertEquals("10 retweets", tweetHandle.evaluate("node => node.innerText"));
* }</pre>
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
* @since v1.8
*/
default Object evaluate(String expression) {
return evaluate(expression, null);
@@ -69,18 +74,19 @@ public interface JSHandle {
* <p> This method passes this handle as the first argument to {@code expression}.
*
* <p> If {@code expression} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then
* {@code handle.evaluate} would wait for the promise to resolve and return its value.
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@code
* handle.evaluate} would wait for the promise to resolve and return its value.
*
* <p> Examples:
* <p> **Usage**
* <pre>{@code
* ElementHandle tweetHandle = page.querySelector(".tweet .retweets");
* assertEquals("10 retweets", tweetHandle.evaluate("node => node.innerText"));
* }</pre>
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
* @param arg Optional argument to pass to {@code expression}.
* @since v1.8
*/
Object evaluate(String expression, Object arg);
/**
@@ -88,17 +94,18 @@ public interface JSHandle {
*
* <p> This method passes this handle as the first argument to {@code expression}.
*
* <p> The only difference between {@code jsHandle.evaluate} and {@code jsHandle.evaluateHandle} is that {@code jsHandle.evaluateHandle} returns
* {@code JSHandle}.
* <p> The only difference between {@code jsHandle.evaluate} and {@code jsHandle.evaluateHandle} is that {@code
* jsHandle.evaluateHandle} returns {@code JSHandle}.
*
* <p> If the function passed to the {@code jsHandle.evaluateHandle} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then
* {@code jsHandle.evaluateHandle} would wait for the promise to resolve and return its value.
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@code
* jsHandle.evaluateHandle} would wait for the promise to resolve and return its value.
*
* <p> See {@link Page#evaluateHandle Page.evaluateHandle()} for more details.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
* @since v1.8
*/
default JSHandle evaluateHandle(String expression) {
return evaluateHandle(expression, null);
@@ -108,35 +115,41 @@ public interface JSHandle {
*
* <p> This method passes this handle as the first argument to {@code expression}.
*
* <p> The only difference between {@code jsHandle.evaluate} and {@code jsHandle.evaluateHandle} is that {@code jsHandle.evaluateHandle} returns
* {@code JSHandle}.
* <p> The only difference between {@code jsHandle.evaluate} and {@code jsHandle.evaluateHandle} is that {@code
* jsHandle.evaluateHandle} returns {@code JSHandle}.
*
* <p> If the function passed to the {@code jsHandle.evaluateHandle} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then
* {@code jsHandle.evaluateHandle} would wait for the promise to resolve and return its value.
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@code
* jsHandle.evaluateHandle} would wait for the promise to resolve and return its value.
*
* <p> See {@link Page#evaluateHandle Page.evaluateHandle()} for more details.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
* @param arg Optional argument to pass to {@code expression}.
* @since v1.8
*/
JSHandle evaluateHandle(String expression, Object arg);
/**
* The method returns a map with **own property names** as keys and JSHandle instances for the property values.
*
* <p> **Usage**
* <pre>{@code
* JSHandle handle = page.evaluateHandle("() => ({window, document}"););
* JSHandle handle = page.evaluateHandle("() => ({window, document})");
* Map<String, JSHandle> properties = handle.getProperties();
* JSHandle windowHandle = properties.get("window");
* JSHandle documentHandle = properties.get("document");
* handle.dispose();
* }</pre>
*
* @since v1.8
*/
Map<String, JSHandle> getProperties();
/**
* Fetches a single property from the referenced object.
*
* @param propertyName property to get
* @since v1.8
*/
JSHandle getProperty(String propertyName);
/**
@@ -144,6 +157,8 @@ public interface JSHandle {
*
* <p> <strong>NOTE:</strong> The method will return an empty JSON object if the referenced object is not stringifiable. It will throw an error if the
* object has circular references.
*
* @since v1.8
*/
Object jsonValue();
}
@@ -20,7 +20,8 @@ import com.microsoft.playwright.options.*;
/**
* Keyboard provides an api for managing a virtual keyboard. The high level api is {@link Keyboard#type Keyboard.type()},
* which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page.
* which takes raw characters and generates proper {@code keydown}, {@code keypress}/{@code input}, and {@code keyup}
* events on your page.
*
* <p> For finer control, you can use {@link Keyboard#down Keyboard.down()}, {@link Keyboard#up Keyboard.up()}, and {@link
* Keyboard#insertText Keyboard.insertText()} to manually fire events as if they were generated from a real keyboard.
@@ -89,18 +90,21 @@ public interface Keyboard {
* character to generate the text for. A superset of the {@code key} values can be found <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a>. Examples of the keys are:
*
* <p> {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus}, {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab},
* {@code Delete}, {@code Escape}, {@code ArrowDown}, {@code End}, {@code Enter}, {@code Home}, {@code Insert}, {@code PageDown}, {@code PageUp}, {@code ArrowRight}, {@code ArrowUp}, etc.
* <p> {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus},
* {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab}, {@code Delete}, {@code Escape}, {@code ArrowDown},
* {@code End}, {@code Enter}, {@code Home}, {@code Insert}, {@code PageDown}, {@code PageUp}, {@code ArrowRight}, {@code
* ArrowUp}, etc.
*
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code ShiftLeft}.
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code
* ShiftLeft}.
*
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
*
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate different respective
* texts.
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate
* different respective texts.
*
* <p> If {@code key} is a modifier key, {@code Shift}, {@code Meta}, {@code Control}, or {@code Alt}, subsequent key presses will be sent with that modifier
* active. To release the modifier key, use {@link Keyboard#up Keyboard.up()}.
* <p> If {@code key} is a modifier key, {@code Shift}, {@code Meta}, {@code Control}, or {@code Alt}, subsequent key presses
* will be sent with that modifier active. To release the modifier key, use {@link Keyboard#up Keyboard.up()}.
*
* <p> After the key is pressed once, subsequent calls to {@link Keyboard#down Keyboard.down()} will have <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat">repeat</a> set to true. To release the key,
@@ -109,17 +113,22 @@ public interface Keyboard {
* <p> <strong>NOTE:</strong> Modifier keys DO influence {@code keyboard.down}. Holding down {@code Shift} will type the text in upper case.
*
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
* @since v1.8
*/
void down(String key);
/**
* Dispatches only {@code input} event, does not emit the {@code keydown}, {@code keyup} or {@code keypress} events.
*
* <p> **Usage**
* <pre>{@code
* page.keyboard().insertText("嗨");
* }</pre>
*
* <p> <strong>NOTE:</strong> Modifier keys DO NOT effect {@code keyboard.insertText}. Holding down {@code Shift} will not type the text in upper case.
* <p> <strong>NOTE:</strong> Modifier keys DO NOT effect {@code keyboard.insertText}. Holding down {@code Shift} will not type the text in upper
* case.
*
* @param text Sets input to the specified text value.
* @since v1.8
*/
void insertText(String text);
/**
@@ -128,18 +137,23 @@ public interface Keyboard {
* character to generate the text for. A superset of the {@code key} values can be found <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a>. Examples of the keys are:
*
* <p> {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus}, {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab},
* {@code Delete}, {@code Escape}, {@code ArrowDown}, {@code End}, {@code Enter}, {@code Home}, {@code Insert}, {@code PageDown}, {@code PageUp}, {@code ArrowRight}, {@code ArrowUp}, etc.
* <p> {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus},
* {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab}, {@code Delete}, {@code Escape}, {@code ArrowDown},
* {@code End}, {@code Enter}, {@code Home}, {@code Insert}, {@code PageDown}, {@code PageUp}, {@code ArrowRight}, {@code
* ArrowUp}, etc.
*
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code ShiftLeft}.
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code
* ShiftLeft}.
*
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
*
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate different respective
* texts.
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate
* different respective texts.
*
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with the
* modifier, modifier is pressed and being held while the subsequent key is being pressed.
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with
* the modifier, modifier is pressed and being held while the subsequent key is being pressed.
*
* <p> **Usage**
* <pre>{@code
* Page page = browser.newPage();
* page.navigate("https://keycode.info");
@@ -155,6 +169,7 @@ public interface Keyboard {
* <p> Shortcut for {@link Keyboard#down Keyboard.down()} and {@link Keyboard#up Keyboard.up()}.
*
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
* @since v1.8
*/
default void press(String key) {
press(key, null);
@@ -165,18 +180,23 @@ public interface Keyboard {
* character to generate the text for. A superset of the {@code key} values can be found <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a>. Examples of the keys are:
*
* <p> {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus}, {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab},
* {@code Delete}, {@code Escape}, {@code ArrowDown}, {@code End}, {@code Enter}, {@code Home}, {@code Insert}, {@code PageDown}, {@code PageUp}, {@code ArrowRight}, {@code ArrowUp}, etc.
* <p> {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus},
* {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab}, {@code Delete}, {@code Escape}, {@code ArrowDown},
* {@code End}, {@code Enter}, {@code Home}, {@code Insert}, {@code PageDown}, {@code PageUp}, {@code ArrowRight}, {@code
* ArrowUp}, etc.
*
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code ShiftLeft}.
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code
* ShiftLeft}.
*
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
*
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate different respective
* texts.
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate
* different respective texts.
*
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with the
* modifier, modifier is pressed and being held while the subsequent key is being pressed.
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with
* the modifier, modifier is pressed and being held while the subsequent key is being pressed.
*
* <p> **Usage**
* <pre>{@code
* Page page = browser.newPage();
* page.navigate("https://keycode.info");
@@ -192,12 +212,15 @@ public interface Keyboard {
* <p> Shortcut for {@link Keyboard#down Keyboard.down()} and {@link Keyboard#up Keyboard.up()}.
*
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
* @since v1.8
*/
void press(String key, PressOptions options);
/**
* Sends a {@code keydown}, {@code keypress}/{@code input}, and {@code keyup} event for each character in the text.
*
* <p> To press a special key, like {@code Control} or {@code ArrowDown}, use {@link Keyboard#press Keyboard.press()}.
*
* <p> **Usage**
* <pre>{@code
* // Types instantly
* page.keyboard().type("Hello");
@@ -210,6 +233,7 @@ public interface Keyboard {
* <p> <strong>NOTE:</strong> For characters that are not on a US keyboard, only an {@code input} event will be sent.
*
* @param text A text to type into a focused element.
* @since v1.8
*/
default void type(String text) {
type(text, null);
@@ -218,6 +242,8 @@ public interface Keyboard {
* Sends a {@code keydown}, {@code keypress}/{@code input}, and {@code keyup} event for each character in the text.
*
* <p> To press a special key, like {@code Control} or {@code ArrowDown}, use {@link Keyboard#press Keyboard.press()}.
*
* <p> **Usage**
* <pre>{@code
* // Types instantly
* page.keyboard().type("Hello");
@@ -230,12 +256,14 @@ public interface Keyboard {
* <p> <strong>NOTE:</strong> For characters that are not on a US keyboard, only an {@code input} event will be sent.
*
* @param text A text to type into a focused element.
* @since v1.8
*/
void type(String text, TypeOptions options);
/**
* Dispatches a {@code keyup} event.
*
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
* @since v1.8
*/
void up(String key);
}
File diff suppressed because it is too large Load Diff
@@ -122,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;
@@ -161,17 +161,23 @@ public interface Mouse {
}
/**
* Shortcut for {@link Mouse#move Mouse.move()}, {@link Mouse#down Mouse.down()}, {@link Mouse#up Mouse.up()}.
*
* @since v1.8
*/
default void click(double x, double y) {
click(x, y, null);
}
/**
* Shortcut for {@link Mouse#move Mouse.move()}, {@link Mouse#down Mouse.down()}, {@link Mouse#up Mouse.up()}.
*
* @since v1.8
*/
void click(double x, double y, ClickOptions options);
/**
* Shortcut for {@link Mouse#move Mouse.move()}, {@link Mouse#down Mouse.down()}, {@link Mouse#up Mouse.up()}, {@link
* Mouse#down Mouse.down()} and {@link Mouse#up Mouse.up()}.
*
* @since v1.8
*/
default void dblclick(double x, double y) {
dblclick(x, y, null);
@@ -179,36 +185,50 @@ public interface Mouse {
/**
* Shortcut for {@link Mouse#move Mouse.move()}, {@link Mouse#down Mouse.down()}, {@link Mouse#up Mouse.up()}, {@link
* Mouse#down Mouse.down()} and {@link Mouse#up Mouse.up()}.
*
* @since v1.8
*/
void dblclick(double x, double y, DblclickOptions options);
/**
* Dispatches a {@code mousedown} event.
*
* @since v1.8
*/
default void down() {
down(null);
}
/**
* Dispatches a {@code mousedown} event.
*
* @since v1.8
*/
void down(DownOptions options);
/**
* Dispatches a {@code mousemove} event.
*
* @since v1.8
*/
default void move(double x, double y) {
move(x, y, null);
}
/**
* Dispatches a {@code mousemove} event.
*
* @since v1.8
*/
void move(double x, double y, MoveOptions options);
/**
* Dispatches a {@code mouseup} event.
*
* @since v1.8
*/
default void up() {
up(null);
}
/**
* Dispatches a {@code mouseup} event.
*
* @since v1.8
*/
void up(UpOptions options);
/**
@@ -219,6 +239,7 @@ public interface Mouse {
*
* @param deltaX Pixels to scroll horizontally.
* @param deltaY Pixels to scroll vertically.
* @since v1.15
*/
void wheel(double deltaX, double deltaY);
}
File diff suppressed because it is too large Load Diff
@@ -58,39 +58,53 @@ public interface Playwright extends AutoCloseable {
}
/**
* This object can be used to launch or connect to Chromium, returning instances of {@code Browser}.
*
* @since v1.8
*/
BrowserType chromium();
/**
* This object can be used to launch or connect to Firefox, returning instances of {@code Browser}.
*
* @since v1.8
*/
BrowserType firefox();
/**
* Exposes API that can be used for the Web API testing.
*
* @since v1.16
*/
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/extensibility">extensibility</a> for more information.
*
* @since v1.8
*/
Selectors selectors();
/**
* This object can be used to launch or connect to WebKit, returning instances of {@code Browser}.
*
* @since v1.8
*/
BrowserType webkit();
/**
* Terminates this instance of Playwright, will also close all created browsers if they are still running.
*
* @since v1.9
*/
void close();
/**
* Launches new Playwright driver process and connects to it. {@link Playwright#close Playwright.close()} should be called
* when the instance is no longer needed.
* <pre>{@code
* Playwright playwright = Playwright.create()) {
* Playwright playwright = Playwright.create();
* Browser browser = playwright.webkit().launch();
* Page page = browser.newPage();
* page.navigate("https://www.w3.org/");
* playwright.close();
* }</pre>
*
* @since v1.10
*/
static Playwright create(CreateOptions options) {
return PlaywrightImpl.create(options);
@@ -28,74 +28,98 @@ import java.util.*;
* complete.</li>
* </ul>
*
* <p> If request fails at some point, then instead of {@code "requestfinished"} event (and possibly instead of 'response' event),
* the {@link Page#onRequestFailed Page.onRequestFailed()} event is emitted.
* <p> If request fails at some point, then instead of {@code "requestfinished"} event (and possibly instead of 'response'
* event), the {@link Page#onRequestFailed Page.onRequestFailed()} event is emitted.
*
* <p> <strong>NOTE:</strong> HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request will complete
* with {@code "requestfinished"} event.
*
* <p> If request gets a 'redirect' response, the request is successfully finished with the 'requestfinished' event, and a new
* request is issued to a redirected url.
* <p> If request gets a 'redirect' response, the request is successfully finished with the {@code requestfinished} event, and
* a new request is issued to a redirected url.
*/
public interface Request {
/**
* An object with all the request HTTP headers associated with this request. The header names are lower-cased.
*
* @since v1.15
*/
Map<String, String> allHeaders();
/**
* The method returns {@code null} unless this request has failed, as reported by {@code requestfailed} event.
*
* <p> **Usage**
*
* <p> Example of logging of all the failed requests:
* <pre>{@code
* page.onRequestFailed(request -> {
* System.out.println(request.url() + " " + request.failure());
* });
* }</pre>
*
* @since v1.8
*/
String failure();
/**
* Returns the {@code Frame} that initiated this request.
*
* @since v1.8
*/
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.
*
* @since v1.8
*/
Map<String, String> headers();
/**
* An array with all the request HTTP headers associated with this request. Unlike {@link Request#allHeaders
* Request.allHeaders()}, header names are NOT lower-cased. Headers with multiple entries, such as {@code Set-Cookie}, appear in
* the array multiple times.
* Request.allHeaders()}, header names are NOT lower-cased. Headers with multiple entries, such as {@code Set-Cookie},
* appear in the array multiple times.
*
* @since v1.15
*/
List<HttpHeader> headersArray();
/**
* Returns the value of the header matching the name. The name is case insensitive.
*
* @param name Name of the header.
* @since v1.15
*/
String headerValue(String name);
/**
* Whether this request is driving frame's navigation.
*
* @since v1.8
*/
boolean isNavigationRequest();
/**
* Request's method (GET, POST, etc.)
*
* @since v1.8
*/
String method();
/**
* Request's post body, if any.
*
* @since v1.8
*/
String postData();
/**
* Request's post body in a binary form, if any.
*
* @since v1.8
*/
byte[] postDataBuffer();
/**
* Request that was redirected by the server to this one, if any.
*
* <p> When the server responds with a redirect, Playwright creates a new {@code Request} object. The two requests are connected by
* {@code redirectedFrom()} and {@code redirectedTo()} methods. When multiple server redirects has happened, it is possible to
* construct the whole redirect chain by repeatedly calling {@code redirectedFrom()}.
* <p> When the server responds with a redirect, Playwright creates a new {@code Request} object. The two requests are
* connected by {@code redirectedFrom()} and {@code redirectedTo()} methods. When multiple server redirects has happened,
* it is possible to construct the whole redirect chain by repeatedly calling {@code redirectedFrom()}.
*
* <p> **Usage**
*
* <p> For example, if the website {@code http://example.com} redirects to {@code https://example.com}:
* <pre>{@code
@@ -108,35 +132,49 @@ public interface Request {
* Response response = page.navigate("https://google.com");
* System.out.println(response.request().redirectedFrom()); // null
* }</pre>
*
* @since v1.8
*/
Request redirectedFrom();
/**
* New request issued by the browser if the server responded with redirect.
*
* <p> **Usage**
*
* <p> This method is the opposite of {@link Request#redirectedFrom Request.redirectedFrom()}:
* <pre>{@code
* System.out.println(request.redirectedFrom().redirectedTo() == request); // true
* }</pre>
*
* @since v1.8
*/
Request redirectedTo();
/**
* Contains the request's resource type as it was perceived by the rendering engine. ResourceType will be one of the
* following: {@code document}, {@code stylesheet}, {@code image}, {@code media}, {@code font}, {@code script}, {@code texttrack}, {@code xhr}, {@code fetch}, {@code eventsource},
* {@code websocket}, {@code manifest}, {@code other}.
* following: {@code document}, {@code stylesheet}, {@code image}, {@code media}, {@code font}, {@code script}, {@code
* texttrack}, {@code xhr}, {@code fetch}, {@code eventsource}, {@code websocket}, {@code manifest}, {@code other}.
*
* @since v1.8
*/
String resourceType();
/**
* Returns the matching {@code Response} object, or {@code null} if the response was not received due to error.
*
* @since v1.8
*/
Response response();
/**
* Returns resource size information for given request.
*
* @since v1.15
*/
Sizes sizes();
/**
* Returns resource timing information for given request. Most of the timing values become available upon the response,
* {@code responseEnd} becomes available when request finishes. Find more information at <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming">Resource Timing API</a>.
*
* <p> **Usage**
* <pre>{@code
* page.onRequestFinished(request -> {
* Timing timing = request.timing();
@@ -144,10 +182,14 @@ public interface Request {
* });
* page.navigate("http://example.com");
* }</pre>
*
* @since v1.8
*/
Timing timing();
/**
* URL of the request.
*
* @since v1.8
*/
String url();
}
@@ -25,75 +25,113 @@ import java.util.*;
public interface Response {
/**
* An object with all the response HTTP headers associated with this response.
*
* @since v1.15
*/
Map<String, String> allHeaders();
/**
* Returns the buffer with response body.
*
* @since v1.8
*/
byte[] body();
/**
* Waits for this response to finish, returns always {@code null}.
*
* @since v1.8
*/
String finished();
/**
* Returns the {@code Frame} that initiated this response.
*
* @since v1.8
*/
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>).
*
* @since v1.23
*/
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.
*
* @since v1.8
*/
Map<String, String> headers();
/**
* An array with all the request HTTP headers associated with this response. Unlike {@link Response#allHeaders
* Response.allHeaders()}, header names are NOT lower-cased. Headers with multiple entries, such as {@code Set-Cookie}, appear in
* the array multiple times.
* Response.allHeaders()}, header names are NOT lower-cased. Headers with multiple entries, such as {@code Set-Cookie},
* appear in the array multiple times.
*
* @since v1.15
*/
List<HttpHeader> headersArray();
/**
* Returns the value of the header matching the name. The name is case insensitive. If multiple headers have the same name
* (except {@code set-cookie}), they are returned as a list separated by {@code , }. For {@code set-cookie}, the {@code \n} separator is used. If
* no headers are found, {@code null} is returned.
* (except {@code set-cookie}), they are returned as a list separated by {@code , }. For {@code set-cookie}, the {@code \n}
* separator is used. If no headers are found, {@code null} is returned.
*
* @param name Name of the header.
* @since v1.15
*/
String headerValue(String name);
/**
* Returns all values of the headers matching the name, for example {@code set-cookie}. The name is case insensitive.
*
* @param name Name of the header.
* @since v1.15
*/
List<String> headerValues(String name);
/**
* Contains a boolean stating whether the response was successful (status in the range 200-299) or not.
*
* @since v1.8
*/
boolean ok();
/**
* Returns the matching {@code Request} object.
*
* @since v1.8
*/
Request request();
/**
* Returns SSL and other security information.
*
* @since v1.13
*/
SecurityDetails securityDetails();
/**
* Returns the IP address and port of the server.
*
* @since v1.13
*/
ServerAddr serverAddr();
/**
* Contains the status code of the response (e.g., 200 for a success).
*
* @since v1.8
*/
int status();
/**
* Contains the status text of the response (e.g. usually an "OK" for a success).
*
* @since v1.8
*/
String statusText();
/**
* Returns the text representation of response body.
*
* @since v1.8
*/
String text();
/**
* Contains the URL of the response.
*
* @since v1.8
*/
String url();
}
@@ -22,6 +22,8 @@ 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 {
@@ -30,11 +32,11 @@ public interface Route {
*/
public Map<String, String> headers;
/**
* If set changes the request method (e.g. GET or POST)
* If set changes the request method (e.g. GET or POST).
*/
public String method;
/**
* If set changes the post data of request
* If set changes the post data of request.
*/
public Object postData;
/**
@@ -50,21 +52,21 @@ public interface Route {
return this;
}
/**
* If set changes the request method (e.g. GET or POST)
* If set changes the request method (e.g. GET or POST).
*/
public ResumeOptions setMethod(String method) {
this.method = method;
return this;
}
/**
* If set changes the post data of request
* If set changes the post data of request.
*/
public ResumeOptions setPostData(String postData) {
this.postData = postData;
return this;
}
/**
* If set changes the post data of request
* If set changes the post data of request.
*/
public ResumeOptions setPostData(byte[] postData) {
this.postData = postData;
@@ -78,6 +80,140 @@ 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 FetchOptions {
/**
* If set changes the request HTTP headers. Header values will be converted to a string.
*/
public Map<String, String> headers;
/**
* Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is
* exceeded. Defaults to {@code 20}. Pass {@code 0} to not follow redirects.
*/
public Integer maxRedirects;
/**
* 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;
/**
* Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout.
*/
public Double timeout;
/**
* If set changes the request URL. New URL must have same protocol as original one.
*/
public String url;
/**
* If set changes the request HTTP headers. Header values will be converted to a string.
*/
public FetchOptions setHeaders(Map<String, String> headers) {
this.headers = headers;
return this;
}
/**
* Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is
* exceeded. Defaults to {@code 20}. Pass {@code 0} to not follow redirects.
*/
public FetchOptions setMaxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
return this;
}
/**
* If set changes the request method (e.g. GET or POST).
*/
public FetchOptions setMethod(String method) {
this.method = method;
return this;
}
/**
* If set changes the post data of request.
*/
public FetchOptions setPostData(String postData) {
this.postData = postData;
return this;
}
/**
* If set changes the post data of request.
*/
public FetchOptions setPostData(byte[] postData) {
this.postData = postData;
return this;
}
/**
* Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout.
*/
public FetchOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
/**
* If set changes the request URL. New URL must have same protocol as original one.
*/
public FetchOptions setUrl(String url) {
this.url = url;
return this;
}
}
class FulfillOptions {
/**
* Optional response body as text.
@@ -96,13 +232,13 @@ public interface Route {
*/
public Map<String, String> headers;
/**
* File path to respond with. The content type will be inferred from file extension. If {@code path} is a relative path, then it
* is resolved relative to the current working directory.
* File path to respond with. The content type will be inferred from file extension. If {@code path} is a relative path,
* then it 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.
* {@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;
/**
@@ -139,16 +275,16 @@ public interface Route {
return this;
}
/**
* File path to respond with. The content type will be inferred from file extension. If {@code path} is a relative path, then it
* is resolved relative to the current working directory.
* File path to respond with. The content type will be inferred from file extension. If {@code path} is a relative path,
* then it is resolved relative to the current working directory.
*/
public FulfillOptions setPath(Path path) {
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.
* {@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;
@@ -164,6 +300,8 @@ public interface Route {
}
/**
* Aborts the route's request.
*
* @since v1.8
*/
default void abort() {
abort(null);
@@ -175,11 +313,11 @@ public interface Route {
* <ul>
* <li> {@code "aborted"} - An operation was aborted (due to user action)</li>
* <li> {@code "accessdenied"} - Permission to access a resource, other than the network, was denied</li>
* <li> {@code "addressunreachable"} - The IP address is unreachable. This usually means that there is no route to the specified host
* or network.</li>
* <li> {@code "addressunreachable"} - The IP address is unreachable. This usually means that there is no route to the specified
* host or network.</li>
* <li> {@code "blockedbyclient"} - The client chose to block the request.</li>
* <li> {@code "blockedbyresponse"} - The request failed because the response was delivered along with requirements which are not met
* ('X-Frame-Options' and 'Content-Security-Policy' ancestor checks, for instance).</li>
* <li> {@code "blockedbyresponse"} - The request failed because the response was delivered along with requirements which are
* not met ('X-Frame-Options' and 'Content-Security-Policy' ancestor checks, for instance).</li>
* <li> {@code "connectionaborted"} - A connection timed out as a result of not receiving an ACK for data sent.</li>
* <li> {@code "connectionclosed"} - A connection was closed (corresponding to a TCP FIN).</li>
* <li> {@code "connectionfailed"} - A connection attempt failed.</li>
@@ -190,39 +328,246 @@ public interface Route {
* <li> {@code "timedout"} - An operation timed out.</li>
* <li> {@code "failed"} - A generic failure occurred.</li>
* </ul>
* @since v1.8
*/
void abort(String errorCode);
/**
* Continues route's request with optional overrides.
*
* <p> **Usage**
* <pre>{@code
* page.route("**\/*", route -> {
* // Override headers
* Map<String, String> headers = new HashMap<>(route.request().headers());
* headers.put("foo", "bar"); // set "foo" header
* headers.remove("origin"); // remove "origin" header
* headers.put("foo", "foo-value"); // set "foo" header
* headers.remove("bar"); // remove "bar" header
* route.resume(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
*
* <p> **Details**
*
* <p> Note that any overrides such as {@code url} or {@code headers} only apply to the request being routed. If this request
* results in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header
* through redirects, use the combination of {@link Route#fetch Route.fetch()} and {@link Route#fulfill Route.fulfill()}
* instead.
*
* @since v1.8
*/
default void resume() {
resume(null);
}
/**
* Continues route's request with optional overrides.
*
* <p> **Usage**
* <pre>{@code
* page.route("**\/*", route -> {
* // Override headers
* Map<String, String> headers = new HashMap<>(route.request().headers());
* headers.put("foo", "bar"); // set "foo" header
* headers.remove("origin"); // remove "origin" header
* headers.put("foo", "foo-value"); // set "foo" header
* headers.remove("bar"); // remove "bar" header
* route.resume(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
*
* <p> **Details**
*
* <p> Note that any overrides such as {@code url} or {@code headers} only apply to the request being routed. If this request
* results in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header
* through redirects, use the combination of {@link Route#fetch Route.fetch()} and {@link Route#fulfill Route.fulfill()}
* instead.
*
* @since v1.8
*/
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.
*
* <p> **Usage**
* <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>
*
* @since v1.23
*/
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.
*
* <p> **Usage**
* <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>
*
* @since v1.23
*/
void fallback(FallbackOptions options);
/**
* Performs the request and fetches result without fulfilling it, so that the response could be modified and then
* fulfilled.
*
* <p> **Usage**
* <pre>{@code
* page.route("https://dog.ceo/api/breeds/list/all", route -> {
* APIResponse response = route.fetch();
* JsonObject json = new Gson().fromJson(response.text(), JsonObject.class);
* JsonObject message = itemObj.get("json").getAsJsonObject();
* message.set("big_red_dog", new JsonArray());
* route.fulfill(new Route.FulfillOptions()
* .setResponse(response)
* .setBody(json.toString()));
* });
* }</pre>
*
* <p> **Details**
*
* <p> Note that {@code headers} option will apply to the fetched request as well as any redirects initiated by it. If you want
* to only apply {@code headers} to the original request, but not to redirects, look into {@link Route#resume
* Route.resume()} instead.
*
* @since v1.29
*/
default APIResponse fetch() {
return fetch(null);
}
/**
* Performs the request and fetches result without fulfilling it, so that the response could be modified and then
* fulfilled.
*
* <p> **Usage**
* <pre>{@code
* page.route("https://dog.ceo/api/breeds/list/all", route -> {
* APIResponse response = route.fetch();
* JsonObject json = new Gson().fromJson(response.text(), JsonObject.class);
* JsonObject message = itemObj.get("json").getAsJsonObject();
* message.set("big_red_dog", new JsonArray());
* route.fulfill(new Route.FulfillOptions()
* .setResponse(response)
* .setBody(json.toString()));
* });
* }</pre>
*
* <p> **Details**
*
* <p> Note that {@code headers} option will apply to the fetched request as well as any redirects initiated by it. If you want
* to only apply {@code headers} to the original request, but not to redirects, look into {@link Route#resume
* Route.resume()} instead.
*
* @since v1.29
*/
APIResponse fetch(FetchOptions options);
/**
* Fulfills route's request with given response.
*
* <p> **Usage**
*
* <p> An example of fulfilling all requests with 404 responses:
* <pre>{@code
* page.route("**\/*", route -> {
@@ -238,6 +583,8 @@ public interface Route {
* page.route("**\/xhr_endpoint", route -> route.fulfill(
* new Route.FulfillOptions().setPath(Paths.get("mock_data.json"))));
* }</pre>
*
* @since v1.8
*/
default void fulfill() {
fulfill(null);
@@ -245,6 +592,8 @@ public interface Route {
/**
* Fulfills route's request with given response.
*
* <p> **Usage**
*
* <p> An example of fulfilling all requests with 404 responses:
* <pre>{@code
* page.route("**\/*", route -> {
@@ -260,10 +609,14 @@ public interface Route {
* page.route("**\/xhr_endpoint", route -> route.fulfill(
* new Route.FulfillOptions().setPath(Paths.get("mock_data.json"))));
* }</pre>
*
* @since v1.8
*/
void fulfill(FulfillOptions options);
/**
* A request to be routed.
*
* @since v1.8
*/
Request request();
}
@@ -20,21 +20,21 @@ import java.nio.file.Path;
/**
* 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/extensibility">extensibility</a> for more information.
*/
public interface Selectors {
class RegisterOptions {
/**
* Whether to run this selector engine in isolated JavaScript environment. This environment has access to the same DOM, but
* not any JavaScript objects from the frame's scripts. Defaults to {@code false}. Note that running as a content script is not
* guaranteed when this engine is used together with other registered engines.
* not any JavaScript objects from the frame's scripts. Defaults to {@code false}. Note that running as a content script is
* not guaranteed when this engine is used together with other registered engines.
*/
public Boolean contentScript;
/**
* Whether to run this selector engine in isolated JavaScript environment. This environment has access to the same DOM, but
* not any JavaScript objects from the frame's scripts. Defaults to {@code false}. Note that running as a content script is not
* guaranteed when this engine is used together with other registered engines.
* not any JavaScript objects from the frame's scripts. Defaults to {@code false}. Note that running as a content script is
* not guaranteed when this engine is used together with other registered engines.
*/
public RegisterOptions setContentScript(boolean contentScript) {
this.contentScript = contentScript;
@@ -42,9 +42,13 @@ public interface Selectors {
}
}
/**
* An example of registering selector engine that queries elements based on a tag name:
* Selectors must be registered before creating the page.
*
* <p> **Usage**
*
* <p> 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,24 +66,29 @@ public interface Selectors {
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Combine it with built-in locators.
* page.locator("tag=div").getByText("Click me").click();
* // Can use it in any methods supporting selectors.
* 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 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. The script is evaluated in the page context.
* @since v1.8
*/
default void register(String name, String script) {
register(name, script, null);
}
/**
* An example of registering selector engine that queries elements based on a tag name:
* Selectors must be registered before creating the page.
*
* <p> **Usage**
*
* <p> 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,22 +106,27 @@ public interface Selectors {
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Combine it with built-in locators.
* page.locator("tag=div").getByText("Click me").click();
* // Can use it in any methods supporting selectors.
* 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 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. The script is evaluated in the page context.
* @since v1.8
*/
void register(String name, String script, RegisterOptions options);
/**
* An example of registering selector engine that queries elements based on a tag name:
* Selectors must be registered before creating the page.
*
* <p> **Usage**
*
* <p> 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,24 +144,29 @@ public interface Selectors {
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Combine it with built-in locators.
* page.locator("tag=div").getByText("Click me").click();
* // Can use it in any methods supporting selectors.
* 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 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. The script is evaluated in the page context.
* @since v1.8
*/
default void register(String name, Path script) {
register(name, script, null);
}
/**
* An example of registering selector engine that queries elements based on a tag name:
* Selectors must be registered before creating the page.
*
* <p> **Usage**
*
* <p> 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 +184,26 @@ public interface Selectors {
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Combine it with built-in locators.
* page.locator("tag=div").getByText("Click me").click();
* // Can use it in any methods supporting selectors.
* 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 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. The script is evaluated in the page context.
* @since v1.8
*/
void register(String name, Path script, RegisterOptions options);
/**
* Defines custom attribute name to be used in {@link Page#getByTestId Page.getByTestId()}. {@code data-testid} is used by
* default.
*
* @param attributeName Test id attribute name.
* @since v1.27
*/
void setTestIdAttribute(String attributeName);
}
@@ -24,6 +24,10 @@ package com.microsoft.playwright;
public interface Touchscreen {
/**
* Dispatches a {@code touchstart} and {@code touchend} event with a single touch at the position ({@code x},{@code y}).
*
* <p> <strong>NOTE:</strong> {@link Page#tap Page.tap()} the method will throw if {@code hasTouch} option of the browser context is false.
*
* @since v1.8
*/
void tap(double x, double y);
}
@@ -20,7 +20,7 @@ import java.nio.file.Path;
/**
* 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
@@ -38,8 +38,8 @@ import java.nio.file.Path;
public interface Tracing {
class StartOptions {
/**
* If specified, the trace is going to be saved into the file with the given name inside the {@code tracesDir} folder specified
* in {@link BrowserType#launch BrowserType.launch()}.
* If specified, the trace is going to be saved into the file with the given name inside the {@code tracesDir} folder
* specified in {@link BrowserType#launch BrowserType.launch()}.
*/
public String name;
/**
@@ -56,7 +56,8 @@ public interface Tracing {
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.
* provided via {@code PLAYWRIGHT_JAVA_SRC} environment variable (the paths should be separated by ';' on Windows and by
* ':' on other platforms).
*/
public Boolean sources;
/**
@@ -65,8 +66,8 @@ public interface Tracing {
public String title;
/**
* If specified, the trace is going to be saved into the file with the given name inside the {@code tracesDir} folder specified
* in {@link BrowserType#launch BrowserType.launch()}.
* If specified, the trace is going to be saved into the file with the given name inside the {@code tracesDir} folder
* specified in {@link BrowserType#launch BrowserType.launch()}.
*/
public StartOptions setName(String name) {
this.name = name;
@@ -92,7 +93,8 @@ public interface Tracing {
}
/**
* 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.
* 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;
@@ -107,11 +109,24 @@ public interface Tracing {
}
}
class StartChunkOptions {
/**
* If specified, the trace is going to be saved into the file with the given name inside the {@code tracesDir} folder
* specified in {@link BrowserType#launch BrowserType.launch()}.
*/
public String name;
/**
* Trace name to be shown in the Trace Viewer.
*/
public String title;
/**
* If specified, the trace is going to be saved into the file with the given name inside the {@code tracesDir} folder
* specified in {@link BrowserType#launch BrowserType.launch()}.
*/
public StartChunkOptions setName(String name) {
this.name = name;
return this;
}
/**
* Trace name to be shown in the Trace Viewer.
*/
@@ -152,6 +167,8 @@ public interface Tracing {
}
/**
* Start tracing.
*
* <p> **Usage**
* <pre>{@code
* context.tracing().start(new Tracing.StartOptions()
* .setScreenshots(true)
@@ -161,12 +178,16 @@ public interface Tracing {
* context.tracing().stop(new Tracing.StopOptions()
* .setPath(Paths.get("trace.zip")));
* }</pre>
*
* @since v1.12
*/
default void start() {
start(null);
}
/**
* Start tracing.
*
* <p> **Usage**
* <pre>{@code
* context.tracing().start(new Tracing.StartOptions()
* .setScreenshots(true)
@@ -176,12 +197,16 @@ public interface Tracing {
* context.tracing().stop(new Tracing.StopOptions()
* .setPath(Paths.get("trace.zip")));
* }</pre>
*
* @since v1.12
*/
void start(StartOptions options);
/**
* Start a new trace chunk. If you'd like to record multiple traces on the same {@code BrowserContext}, use {@link Tracing#start
* Tracing.start()} once, and then create multiple trace chunks with {@link Tracing#startChunk Tracing.startChunk()} and
* {@link Tracing#stopChunk Tracing.stopChunk()}.
* Start a new trace chunk. If you'd like to record multiple traces on the same {@code BrowserContext}, use {@link
* Tracing#start Tracing.start()} once, and then create multiple trace chunks with {@link Tracing#startChunk
* Tracing.startChunk()} and {@link Tracing#stopChunk Tracing.stopChunk()}.
*
* <p> **Usage**
* <pre>{@code
* context.tracing().start(new Tracing.StartOptions()
* .setScreenshots(true)
@@ -190,7 +215,7 @@ public interface Tracing {
* page.navigate("https://playwright.dev");
*
* context.tracing().startChunk();
* page.click("text=Get Started");
* page.getByText("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")));
@@ -201,14 +226,18 @@ public interface Tracing {
* context.tracing().stopChunk(new Tracing.StopChunkOptions()
* .setPath(Paths.get("trace2.zip")));
* }</pre>
*
* @since v1.15
*/
default void startChunk() {
startChunk(null);
}
/**
* Start a new trace chunk. If you'd like to record multiple traces on the same {@code BrowserContext}, use {@link Tracing#start
* Tracing.start()} once, and then create multiple trace chunks with {@link Tracing#startChunk Tracing.startChunk()} and
* {@link Tracing#stopChunk Tracing.stopChunk()}.
* Start a new trace chunk. If you'd like to record multiple traces on the same {@code BrowserContext}, use {@link
* Tracing#start Tracing.start()} once, and then create multiple trace chunks with {@link Tracing#startChunk
* Tracing.startChunk()} and {@link Tracing#stopChunk Tracing.stopChunk()}.
*
* <p> **Usage**
* <pre>{@code
* context.tracing().start(new Tracing.StartOptions()
* .setScreenshots(true)
@@ -217,7 +246,7 @@ public interface Tracing {
* page.navigate("https://playwright.dev");
*
* context.tracing().startChunk();
* page.click("text=Get Started");
* page.getByText("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")));
@@ -228,26 +257,36 @@ public interface Tracing {
* context.tracing().stopChunk(new Tracing.StopChunkOptions()
* .setPath(Paths.get("trace2.zip")));
* }</pre>
*
* @since v1.15
*/
void startChunk(StartChunkOptions options);
/**
* Stop tracing.
*
* @since v1.12
*/
default void stop() {
stop(null);
}
/**
* Stop tracing.
*
* @since v1.12
*/
void stop(StopOptions options);
/**
* Stop the trace chunk. See {@link Tracing#startChunk Tracing.startChunk()} for more details about multiple trace chunks.
*
* @since v1.15
*/
default void stopChunk() {
stopChunk(null);
}
/**
* Stop the trace chunk. See {@link Tracing#startChunk Tracing.startChunk()} for more details about multiple trace chunks.
*
* @since v1.15
*/
void stopChunk(StopChunkOptions options);
}
@@ -27,11 +27,15 @@ import java.nio.file.Path;
public interface Video {
/**
* Deletes the video file. Will wait for the video to finish if necessary.
*
* @since v1.11
*/
void delete();
/**
* Returns the file system path this video will be recorded to. The video is guaranteed to be written to the filesystem
* upon closing the browser context. This method throws when connected remotely.
*
* @since v1.8
*/
Path path();
/**
@@ -39,6 +43,7 @@ public interface Video {
* the page has closed. This method waits until the page is closed and the video is fully saved.
*
* @param path Path where the video should be saved.
* @since v1.11
*/
void saveAs(Path path);
}
@@ -66,8 +66,8 @@ public interface WebSocket {
*/
public Predicate<WebSocketFrame> predicate;
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
@@ -79,8 +79,8 @@ public interface WebSocket {
return this;
}
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public WaitForFrameReceivedOptions setTimeout(double timeout) {
this.timeout = timeout;
@@ -93,8 +93,8 @@ public interface WebSocket {
*/
public Predicate<WebSocketFrame> predicate;
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
@@ -106,8 +106,8 @@ public interface WebSocket {
return this;
}
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public WaitForFrameSentOptions setTimeout(double timeout) {
this.timeout = timeout;
@@ -116,46 +116,54 @@ public interface WebSocket {
}
/**
* Indicates that the web socket has been closed.
*
* @since v1.8
*/
boolean isClosed();
/**
* Contains the URL of the WebSocket.
*
* @since v1.8
*/
String url();
/**
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into the
* {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an error if the
* WebSocket or Page is closed before the frame is received.
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into
* the {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an
* error if the WebSocket or Page is closed before the frame is received.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.10
*/
default WebSocketFrame waitForFrameReceived(Runnable callback) {
return waitForFrameReceived(null, callback);
}
/**
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into the
* {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an error if the
* WebSocket or Page is closed before the frame is received.
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into
* the {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an
* error if the WebSocket or Page is closed before the frame is received.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.10
*/
WebSocketFrame waitForFrameReceived(WaitForFrameReceivedOptions options, Runnable callback);
/**
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into the
* {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an error if the
* WebSocket or Page is closed before the frame is sent.
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into
* the {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an
* error if the WebSocket or Page is closed before the frame is sent.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.10
*/
default WebSocketFrame waitForFrameSent(Runnable callback) {
return waitForFrameSent(null, callback);
}
/**
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into the
* {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an error if the
* WebSocket or Page is closed before the frame is sent.
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into
* the {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an
* error if the WebSocket or Page is closed before the frame is sent.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.10
*/
WebSocketFrame waitForFrameSent(WaitForFrameSentOptions options, Runnable callback);
}
@@ -18,17 +18,21 @@ package com.microsoft.playwright;
/**
* The {@code WebSocketFrame} class represents frames sent over {@code WebSocket} connections in the page. Frame payload is returned by
* either {@link WebSocketFrame#text WebSocketFrame.text()} or {@link WebSocketFrame#binary WebSocketFrame.binary()} method
* depending on the its type.
* The {@code WebSocketFrame} class represents frames sent over {@code WebSocket} connections in the page. Frame payload is
* returned by either {@link WebSocketFrame#text WebSocketFrame.text()} or {@link WebSocketFrame#binary
* WebSocketFrame.binary()} method depending on the its type.
*/
public interface WebSocketFrame {
/**
* Returns binary payload.
*
* @since v1.9
*/
byte[] binary();
/**
* Returns text payload.
*
* @since v1.9
*/
String text();
}
@@ -20,8 +20,8 @@ import java.util.function.Consumer;
/**
* The Worker class represents a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">WebWorker</a>.
* {@code worker} event is emitted on the page object to signal a worker creation. {@code close} event is emitted on the worker object
* when the worker is gone.
* {@code worker} event is emitted on the page object to signal a worker creation. {@code close} event is emitted on the
* worker object when the worker is gone.
* <pre>{@code
* page.onWorker(worker -> {
* System.out.println("Worker created: " + worker.url());
@@ -46,14 +46,14 @@ public interface Worker {
class WaitForCloseOptions {
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public WaitForCloseOptions setTimeout(double timeout) {
this.timeout = timeout;
@@ -68,11 +68,12 @@ public interface Worker {
* Worker#evaluate Worker.evaluate()} would wait for the promise to resolve and return its value.
*
* <p> If the function passed to the {@link Worker#evaluate Worker.evaluate()} returns a non-[Serializable] value, then {@link
* Worker#evaluate Worker.evaluate()} returns {@code undefined}. Playwright also supports transferring some additional values
* that are not serializable by {@code JSON}: {@code -0}, {@code NaN}, {@code Infinity}, {@code -Infinity}.
* Worker#evaluate Worker.evaluate()} returns {@code undefined}. Playwright also supports transferring some additional
* values that are not serializable by {@code JSON}: {@code -0}, {@code NaN}, {@code Infinity}, {@code -Infinity}.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
* @since v1.8
*/
default Object evaluate(String expression) {
return evaluate(expression, null);
@@ -85,12 +86,13 @@ public interface Worker {
* Worker#evaluate Worker.evaluate()} would wait for the promise to resolve and return its value.
*
* <p> If the function passed to the {@link Worker#evaluate Worker.evaluate()} returns a non-[Serializable] value, then {@link
* Worker#evaluate Worker.evaluate()} returns {@code undefined}. Playwright also supports transferring some additional values
* that are not serializable by {@code JSON}: {@code -0}, {@code NaN}, {@code Infinity}, {@code -Infinity}.
* Worker#evaluate Worker.evaluate()} returns {@code undefined}. Playwright also supports transferring some additional
* values that are not serializable by {@code JSON}: {@code -0}, {@code NaN}, {@code Infinity}, {@code -Infinity}.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
* @param arg Optional argument to pass to {@code expression}.
* @since v1.8
*/
Object evaluate(String expression, Object arg);
/**
@@ -103,8 +105,9 @@ public interface Worker {
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@link
* Worker#evaluateHandle Worker.evaluateHandle()} would wait for the promise to resolve and return its value.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
* @since v1.8
*/
default JSHandle evaluateHandle(String expression) {
return evaluateHandle(expression, null);
@@ -119,16 +122,23 @@ public interface Worker {
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@link
* Worker#evaluateHandle Worker.evaluateHandle()} would wait for the promise to resolve and return its value.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
* @param arg Optional argument to pass to {@code expression}.
* @since v1.8
*/
JSHandle evaluateHandle(String expression, Object arg);
/**
*
*
* @since v1.8
*/
String url();
/**
* Performs action and waits for the Worker to close.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.10
*/
default Worker waitForClose(Runnable callback) {
return waitForClose(null, callback);
@@ -137,6 +147,7 @@ public interface Worker {
* Performs action and waits for the Worker to close.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.10
*/
Worker waitForClose(WaitForCloseOptions options, Runnable callback);
}
@@ -18,9 +18,8 @@ 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()}:
* The {@code APIResponseAssertions} class provides assertion methods that can be used to make assertions about the {@code
* APIResponse} in the tests.
* <pre>{@code
* ...
* import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
@@ -43,13 +42,19 @@ public interface APIResponseAssertions {
* <pre>{@code
* assertThat(response).not().isOK();
* }</pre>
*
* @since v1.20
*/
APIResponseAssertions not();
/**
* Ensures the response status code is within [200..299] range.
* Ensures the response status code is within {@code 200..299} range.
*
* <p> **Usage**
* <pre>{@code
* assertThat(response).isOK();
* }</pre>
*
* @since v1.18
*/
void isOK();
}
File diff suppressed because it is too large Load Diff
@@ -19,9 +19,8 @@ package com.microsoft.playwright.assertions;
import java.util.regex.Pattern;
/**
* 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 PageAssertions} is created by calling {@link PlaywrightAssertions#assertThat
* PlaywrightAssertions.assertThat()}:
* The {@code PageAssertions} class provides assertion methods that can be used to make assertions about the {@code Page}
* state in the tests.
* <pre>{@code
* ...
* import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
@@ -31,7 +30,7 @@ import java.util.regex.Pattern;
* @Test
* void navigatesToLoginPage() {
* ...
* page.click("#login");
* page.getByText("Sign in").click();
* assertThat(page).hasURL(Pattern.compile(".*\/login"));
* }
* }
@@ -40,12 +39,12 @@ import java.util.regex.Pattern;
public interface PageAssertions {
class HasTitleOptions {
/**
* Time to retry the assertion for.
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public Double timeout;
/**
* Time to retry the assertion for.
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public HasTitleOptions setTimeout(double timeout) {
this.timeout = timeout;
@@ -54,12 +53,12 @@ public interface PageAssertions {
}
class HasURLOptions {
/**
* Time to retry the assertion for.
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public Double timeout;
/**
* Time to retry the assertion for.
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public HasURLOptions setTimeout(double timeout) {
this.timeout = timeout;
@@ -72,86 +71,112 @@ public interface PageAssertions {
* <pre>{@code
* assertThat(page).not().hasURL("error");
* }</pre>
*
* @since v1.20
*/
PageAssertions not();
/**
* Ensures the page has the given title.
*
* <p> **Usage**
* <pre>{@code
* assertThat(page).hasTitle("Playwright");
* }</pre>
*
* @param titleOrRegExp Expected title or RegExp.
* @since v1.20
*/
default void hasTitle(String titleOrRegExp) {
hasTitle(titleOrRegExp, null);
}
/**
* Ensures the page has the given title.
*
* <p> **Usage**
* <pre>{@code
* assertThat(page).hasTitle("Playwright");
* }</pre>
*
* @param titleOrRegExp Expected title or RegExp.
* @since v1.20
*/
void hasTitle(String titleOrRegExp, HasTitleOptions options);
/**
* Ensures the page has the given title.
*
* <p> **Usage**
* <pre>{@code
* assertThat(page).hasTitle("Playwright");
* }</pre>
*
* @param titleOrRegExp Expected title or RegExp.
* @since v1.20
*/
default void hasTitle(Pattern titleOrRegExp) {
hasTitle(titleOrRegExp, null);
}
/**
* Ensures the page has the given title.
*
* <p> **Usage**
* <pre>{@code
* assertThat(page).hasTitle("Playwright");
* }</pre>
*
* @param titleOrRegExp Expected title or RegExp.
* @since v1.20
*/
void hasTitle(Pattern titleOrRegExp, HasTitleOptions options);
/**
* Ensures the page is navigated to the given URL.
*
* <p> **Usage**
* <pre>{@code
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
* @param urlOrRegExp Expected URL string or RegExp.
* @since v1.20
*/
default void hasURL(String urlOrRegExp) {
hasURL(urlOrRegExp, null);
}
/**
* Ensures the page is navigated to the given URL.
*
* <p> **Usage**
* <pre>{@code
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
* @param urlOrRegExp Expected URL string or RegExp.
* @since v1.20
*/
void hasURL(String urlOrRegExp, HasURLOptions options);
/**
* Ensures the page is navigated to the given URL.
*
* <p> **Usage**
* <pre>{@code
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
* @param urlOrRegExp Expected URL string or RegExp.
* @since v1.20
*/
default void hasURL(Pattern urlOrRegExp) {
hasURL(urlOrRegExp, null);
}
/**
* Ensures the page is navigated to the given URL.
*
* <p> **Usage**
* <pre>{@code
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
* @param urlOrRegExp Expected URL string or RegExp.
* @since v1.20
*/
void hasURL(Pattern urlOrRegExp, HasURLOptions options);
}
@@ -20,12 +20,13 @@ 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
@@ -37,26 +38,29 @@ import com.microsoft.playwright.impl.PageAssertionsImpl;
* @Test
* void statusBecomesSubmitted() {
* ...
* page.click("#submit-button");
* page.locator("#submit-button").click();
* assertThat(page.locator(".status")).hasText("Submitted");
* }
* }
* }</pre>
*
* <p> Playwright will be re-testing the node with the selector {@code .status} until fetched Node has the {@code "Submitted"} text. It
* will be re-fetching the node and checking it over and over, until the condition is met or until the timeout is reached.
* You can pass this timeout as an option.
* <p> Playwright will be re-testing the node with the selector {@code .status} until fetched Node has the {@code "Submitted"}
* text. It will be re-fetching the node and checking it over and over, until the condition is met or until the timeout is
* reached. You can pass this timeout as an option.
*
* <p> By default, the timeout for assertions is set to 5 seconds.
*/
public interface PlaywrightAssertions {
/**
* Creates a {@code APIResponseAssertions} object for the given {@code APIResponse}.
*
* <p> **Usage**
* <pre>{@code
* PlaywrightAssertions.assertThat(response).isOK();
* }</pre>
*
* @param response {@code APIResponse} object to use for assertions.
* @since v1.18
*/
static APIResponseAssertions assertThat(APIResponse response) {
return new APIResponseAssertionsImpl(response);
@@ -64,11 +68,14 @@ public interface PlaywrightAssertions {
/**
* Creates a {@code LocatorAssertions} object for the given {@code Locator}.
*
* <p> **Usage**
* <pre>{@code
* PlaywrightAssertions.assertThat(locator).isVisible();
* }</pre>
*
* @param locator {@code Locator} object to use for assertions.
* @since v1.18
*/
static LocatorAssertions assertThat(Locator locator) {
return new LocatorAssertionsImpl(locator);
@@ -76,15 +83,33 @@ public interface PlaywrightAssertions {
/**
* Creates a {@code PageAssertions} object for the given {@code Page}.
*
* <p> **Usage**
* <pre>{@code
* PlaywrightAssertions.assertThat(page).hasTitle("News");
* }</pre>
*
* @param page {@code Page} object to use for assertions.
* @since v1.18
*/
static PageAssertions assertThat(Page page) {
return new PageAssertionsImpl(page);
}
/**
* Changes default timeout for Playwright assertions from 5 seconds to the specified value.
*
* <p> **Usage**
* <pre>{@code
* PlaywrightAssertions.setDefaultAssertionTimeout(30_000);
* }</pre>
*
* @param timeout Timeout in milliseconds.
* @since v1.25
*/
static void setDefaultAssertionTimeout(double milliseconds) {
AssertionsTimeout.setDefaultTimeout(milliseconds);
}
}
@@ -110,6 +110,12 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
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"));
}
@@ -187,7 +193,7 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
}
private static RequestOptionsImpl ensureOptions(RequestOptions options, String method) {
RequestOptionsImpl impl = (RequestOptionsImpl) options;
RequestOptionsImpl impl = Utils.clone((RequestOptionsImpl) options);
if (impl == null) {
impl = new RequestOptionsImpl();
}
@@ -21,6 +21,7 @@ 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;
@@ -54,6 +55,20 @@ public class APIResponseAssertionsImpl implements APIResponseAssertions {
if (!log.isEmpty()) {
log = "\nCall log:\n" + log;
}
throw new AssertionFailedError(message + 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);
}
}
@@ -26,7 +26,6 @@ import java.nio.file.Path;
import static com.microsoft.playwright.impl.Utils.writeToFile;
class ArtifactImpl extends ChannelOwner {
boolean isRemote;
public ArtifactImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@@ -57,7 +56,7 @@ class ArtifactImpl extends ChannelOwner {
}
public Path pathAfterFinished() {
if (isRemote) {
if (connection.isRemote) {
throw new PlaywrightException("Path is not available when using browserType.connect(). Use download.saveAs() to save a local copy.");
}
JsonObject json = sendMessage("pathAfterFinished").getAsJsonObject();
@@ -65,7 +64,7 @@ class ArtifactImpl extends ChannelOwner {
}
public void saveAs(Path path) {
if (isRemote) {
if (connection.isRemote) {
JsonObject jsonObject = sendMessage("saveAsStream").getAsJsonObject();
Stream stream = connection.getExistingObject(jsonObject.getAsJsonObject("stream").get("guid").getAsString());
writeToFile(stream.stream(), path);
@@ -52,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");
@@ -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,21 +20,21 @@ 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.nio.file.Paths;
import java.util.*;
import java.util.function.BooleanSupplier;
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,17 +47,40 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
private final APIRequestContextImpl request;
final List<PageImpl> pages = new ArrayList<>();
final Router routes = new Router();
private boolean isClosedOrClosing;
private boolean closeWasCalled;
private final WaitableEvent<EventType, ?> closePromise;
final Map<String, BindingCallback> bindings = new HashMap<>();
PageImpl ownerPage;
private final ListenerCollection<EventType> listeners = new ListenerCollection<>();
private static final Map<EventType, String> eventSubscriptions() {
Map<EventType, String> result = new HashMap<>();
result.put(EventType.CONSOLE, "console");
result.put(EventType.DIALOG, "dialog");
result.put(EventType.REQUEST, "request");
result.put(EventType.RESPONSE, "response");
result.put(EventType.REQUESTFINISHED, "requestFinished");
result.put(EventType.REQUESTFAILED, "requestFailed");
return result;
}
private final ListenerCollection<EventType> listeners = new ListenerCollection<>(eventSubscriptions(), this);
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,
CONSOLE,
DIALOG,
PAGE,
REQUEST,
REQUESTFAILED,
@@ -72,9 +95,15 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} else {
browser = null;
}
this.tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString());
tracing.isRemote = browser != null && browser.isRemote;
this.request = connection.getExistingObject(initializer.getAsJsonObject("APIRequestContext").get("guid").getAsString());
tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString());
request = connection.getExistingObject(initializer.getAsJsonObject("requestContext").get("guid").getAsString());
closePromise = new WaitableEvent<>(listeners, EventType.CLOSE);
}
void setRecordHar(Path path, HarContentPolicy policy) {
if (path != null) {
harRecorders.put("", new HarRecorder(path, policy));
}
}
void setBaseUrl(String spec) {
@@ -95,6 +124,26 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
listeners.remove(EventType.CLOSE, handler);
}
@Override
public void onConsoleMessage(Consumer<ConsoleMessage> handler) {
listeners.add(EventType.CONSOLE, handler);
}
@Override
public void offConsoleMessage(Consumer<ConsoleMessage> handler) {
listeners.remove(EventType.CONSOLE, handler);
}
@Override
public void onDialog(Consumer<Dialog> handler) {
listeners.add(EventType.DIALOG, handler);
}
@Override
public void offDialog(Consumer<Dialog> handler) {
listeners.remove(EventType.DIALOG, handler);
}
@Override
public void onPage(Consumer<Page> handler) {
listeners.add(EventType.PAGE, handler);
@@ -155,7 +204,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) {
@@ -176,29 +225,32 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
private void closeImpl() {
if (isClosedOrClosing) {
return;
}
isClosedOrClosing = true;
try {
if (recordHarPath != null) {
JsonObject json = sendMessage("harExport").getAsJsonObject();
if (!closeWasCalled) {
closeWasCalled = true;
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;
// 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.saveAs(recordHarPath);
artifact.delete();
}
sendMessage("close");
} catch (PlaywrightException e) {
if (!isSafeCloseError(e)) {
throw e;
}
}
runUntil(() -> {}, closePromise);
}
@Override
@@ -336,7 +388,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@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
@@ -349,17 +401,48 @@ 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);
if (routes.size() == 1) {
JsonObject params = new JsonObject();
params.addProperty("enabled", true);
sendMessage("setNetworkInterceptionEnabled", params);
}
updateInterceptionPatterns();
});
}
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", options.updateContent == null ?
HarContentPolicy.ATTACH.name().toLowerCase() :
options.updateContent.name().toLowerCase());
jsonOptions.addProperty("mode", options.updateMode == null ?
HarMode.MINIMAL.name().toLowerCase() :
options.updateMode.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", () -> {
@@ -448,6 +531,27 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
unroute(new UrlMatcher(url), handler);
}
@Override
public void waitForCondition(BooleanSupplier predicate, WaitForConditionOptions options) {
List<Waitable<Void>> waitables = new ArrayList<>();
waitables.add(new WaitableContextClose<>());
waitables.add(timeoutSettings.createWaitable(options == null ? null : options.timeout));
waitables.add(new WaitablePredicate<>(predicate));
runUntil(() -> {}, new WaitableRace<>(waitables));
}
@Override
public ConsoleMessage waitForConsoleMessage(WaitForConsoleMessageOptions options, Runnable code) {
return withWaitLogging("BrowserContext.waitForConsoleMessage", logger -> waitForConsoleMessageImpl(options, code));
}
private ConsoleMessage waitForConsoleMessageImpl(WaitForConsoleMessageOptions options, Runnable code) {
if (options == null) {
options = new WaitForConsoleMessageOptions();
}
return waitForEventWithTimeout(EventType.CONSOLE, code, options.predicate, options.timeout);
}
private class WaitableContextClose<R> extends WaitableEvent<EventType, R> {
WaitableContextClose() {
super(BrowserContextImpl.this.listeners, EventType.CLOSE);
@@ -462,35 +566,61 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
private void unroute(UrlMatcher matcher, Consumer<Route> handler) {
withLogging("BrowserContext.unroute", () -> {
routes.remove(matcher, handler);
maybeDisableNetworkInterception();
updateInterceptionPatterns();
});
}
private void maybeDisableNetworkInterception() {
if (routes.size() == 0) {
JsonObject params = new JsonObject();
params.addProperty("enabled", false);
sendMessage("setNetworkInterceptionEnabled", params);
private void updateInterceptionPatterns() {
sendMessage("setNetworkInterceptionPatterns", routes.interceptionPatterns());
}
void handleRoute(RouteImpl route) {
Router.HandleResult handled = routes.handle(route);
if (handled != Router.HandleResult.NoMatchingHandler) {
updateInterceptionPatterns();
}
if (handled == Router.HandleResult.NoMatchingHandler || handled == Router.HandleResult.Fallback) {
route.resume(null, true);
}
}
void handleRoute(Route route) {
boolean handled = routes.handle(route);
if (handled) {
maybeDisableNetworkInterception();
} else {
route.resume();
}
}
void pause() {
sendMessage("pause");
WaitableResult<JsonElement> pause() {
return sendMessageAsync("pause", new JsonObject());
}
@Override
protected void handleEvent(String event, JsonObject params) {
if ("route".equals(event)) {
Route route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
if ("dialog".equals(event)) {
String guid = params.getAsJsonObject("dialog").get("guid").getAsString();
DialogImpl dialog = connection.getExistingObject(guid);
boolean hasListeners = false;
if (listeners.hasListeners(EventType.DIALOG)) {
hasListeners = true;
listeners.notify(EventType.DIALOG, dialog);
}
PageImpl page = dialog.page();
if (page != null) {
if (page.listeners.hasListeners(PageImpl.EventType.DIALOG)) {
hasListeners = true;
page.listeners.notify(PageImpl.EventType.DIALOG, dialog);
}
}
// Although we do similar handling on the server side, we still need this logic
// on the client side due to a possible race condition between two async calls:
// a) removing "dialog" listener subscription (client->server)
// b) actual "dialog" event (server->client)
if (!hasListeners) {
if ("beforeunload".equals(dialog.type())) {
try {
dialog.accept();
} catch (PlaywrightException e) {
}
} else {
dialog.dismiss();
}
}
} else if ("route".equals(event)) {
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());
@@ -505,6 +635,14 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
if (binding != null) {
bindingCall.call(binding);
}
} else if ("console".equals(event)) {
String guid = params.getAsJsonObject("message").get("guid").getAsString();
ConsoleMessageImpl message = connection.getExistingObject(guid);
listeners.notify(BrowserContextImpl.EventType.CONSOLE, message);
PageImpl page = message.page();
if (page != null) {
page.listeners.notify(PageImpl.EventType.CONSOLE, message);
}
} else if ("request".equals(event)) {
String guid = params.getAsJsonObject("request").get("guid").getAsString();
RequestImpl request = connection.getExistingObject(guid);
@@ -554,10 +692,16 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
void didClose() {
isClosedOrClosing = true;
if (browser != null) {
browser.contexts.remove(this);
}
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,28 +19,29 @@ 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.*;
import static com.microsoft.playwright.impl.Utils.convertType;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
class BrowserImpl extends ChannelOwner implements Browser {
final Set<BrowserContextImpl> contexts = new HashSet<>();
private final ListenerCollection<EventType> listeners = new ListenerCollection<>();
boolean isRemote;
boolean isConnectedOverWebSocket;
private boolean isConnected = true;
LocalUtils localUtils;
BrowserTypeImpl browserType;
BrowserType.LaunchOptions launchOptions;
enum EventType {
DISCONNECTED,
@@ -60,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());
@@ -112,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 {
@@ -127,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();
@@ -171,8 +209,10 @@ class BrowserImpl extends ChannelOwner implements Browser {
if (options.baseURL != null) {
context.setBaseUrl(options.baseURL);
}
context.recordHarPath = options.recordHarPath;
context.tracing().localUtils = localUtils;
context.setRecordHar(recordHarPath, harContentPolicy);
if (launchOptions != null) {
context.tracing().setTracesDir(launchOptions.tracesDir);
}
contexts.add(context);
return context;
}
@@ -193,9 +233,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);
}
@@ -22,12 +22,15 @@ 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;
@@ -48,7 +51,8 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
JsonElement result = sendMessage("launch", params);
BrowserImpl browser = connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("browser").get("guid").getAsString());
browser.localUtils = localUtils;
browser.browserType = this;
browser.launchOptions = options;
return browser;
}
@@ -64,9 +68,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);
JsonObject json = sendMessage("connect", params).getAsJsonObject();
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 = connection.localUtils().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, this.connection.localUtils);
PlaywrightImpl playwright = connection.initializePlaywright();
if (!playwright.initializer.has("preLaunchedBrowser")) {
try {
@@ -78,9 +98,8 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
}
playwright.initSharedSelectors(this.connection.getExistingObject("Playwright"));
BrowserImpl browser = connection.getExistingObject(playwright.initializer.getAsJsonObject("preLaunchedBrowser").get("guid").getAsString());
browser.isRemote = true;
browser.isConnectedOverWebSocket = true;
browser.localUtils = localUtils;
browser.browserType = this;
Consumer<JsonPipe> connectionCloseListener = t -> browser.notifyRemoteClosed();
pipe.onClose(connectionCloseListener);
browser.onDisconnected(b -> {
@@ -113,8 +132,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
JsonObject json = sendMessage("connectOverCDP", params).getAsJsonObject();
BrowserImpl browser = connection.getExistingObject(json.getAsJsonObject("browser").get("guid").getAsString());
browser.isRemote = true;
browser.localUtils = localUtils;
browser.browserType = this;
if (json.has("defaultContext")) {
String contextId = json.getAsJsonObject("defaultContext").get("guid").getAsString();
BrowserContextImpl defaultContext = connection.getExistingObject(contextId);
@@ -136,20 +154,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();
@@ -179,8 +229,8 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
if (options.baseURL != null) {
context.setBaseUrl(options.baseURL);
}
context.recordHarPath = options.recordHarPath;
context.tracing().localUtils = localUtils;
context.setRecordHar(recordHarPath, harContentPolicy);
context.tracing().setTracesDir(options.tracesDir);
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,6 +16,7 @@
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;
@@ -28,6 +29,7 @@ import java.util.HashMap;
import java.util.Map;
import static com.microsoft.playwright.impl.Serialization.gson;
import static java.lang.System.currentTimeMillis;
class Message {
int id;
@@ -55,6 +57,7 @@ public class Connection {
private final Transport transport;
private final Map<String, ChannelOwner> objects = new HashMap<>();
private final Root root;
final boolean isRemote;
private int lastId = 0;
private final StackTraceCollector stackTraceCollector;
private final Map<Integer, WaitableResult<JsonElement>> callbacks = new HashMap<>();
@@ -64,6 +67,9 @@ public class Connection {
String debug = System.getenv("DEBUG");
isLogging = (debug != null) && debug.contains("pw:channel");
}
LocalUtils localUtils;
final Map<String, String> env;
private int tracingCount;
class Root extends ChannelOwner {
Root(Connection connection) {
@@ -78,17 +84,32 @@ public class Connection {
}
}
Connection(Transport transport) {
Connection(Transport pipe, Map<String, String> env, LocalUtils localUtils) {
this(pipe, env, true);
this.localUtils = localUtils;
}
Connection(Transport transport, Map<String, String> env) {
this(transport, env, false);
}
private Connection(Transport transport, Map<String, String> env, boolean isRemote) {
this.env = env;
this.isRemote = isRemote;
if (isLogging) {
transport = new TransportLogger(transport);
}
this.transport = transport;
root = new Root(this);
stackTraceCollector = StackTraceCollector.createFromEnv();
stackTraceCollector = StackTraceCollector.createFromEnv(env);
}
boolean isCollectingStacks() {
return stackTraceCollector != null;
void setIsTracing(boolean tracing) {
if (tracing) {
++tracingCount;
} else {
--tracingCount;
}
}
String setApiName(String name) {
@@ -106,10 +127,10 @@ public class Connection {
}
public WaitableResult<JsonElement> sendMessageAsync(String guid, String method, JsonObject params) {
return internalSendMessage(guid, method, params);
return internalSendMessage(guid, method, params, true);
}
private WaitableResult<JsonElement> internalSendMessage(String guid, String method, JsonObject params) {
private WaitableResult<JsonElement> internalSendMessage(String guid, String method, JsonObject params, boolean sendStack) {
int id = ++lastId;
WaitableResult<JsonElement> result = new WaitableResult<>();
callbacks.put(id, result);
@@ -119,6 +140,8 @@ public class Connection {
message.addProperty("method", method);
message.add("params", params);
JsonObject metadata = new JsonObject();
metadata.addProperty("wallTime", currentTimeMillis());
JsonArray stack = null;
if (apiName == null) {
metadata.addProperty("internal", true);
} else {
@@ -126,11 +149,27 @@ public class Connection {
// 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());
stack = stackTraceCollector.currentStackTrace();
if (!stack.isEmpty()) {
JsonObject location = new JsonObject();
JsonObject frame = stack.get(0).getAsJsonObject();
location.addProperty("file", frame.get("file").getAsString());
location.addProperty("line", frame.get("line").getAsInt());
location.addProperty("column", frame.get("column").getAsInt());
metadata.add("location", location);
}
}
}
message.add("metadata", metadata);
transport.send(message);
if (sendStack && tracingCount > 0 && stack != null && !method.startsWith("LocalUtils")) {
JsonObject callData = new JsonObject();
callData.addProperty("id", id);
callData.add("stack", stack);
JsonObject stackParams = new JsonObject();
stackParams.add("callData", callData);
internalSendMessage(localUtils.guid,"addStackToTracingNoReply", stackParams, false);
}
return result;
}
@@ -138,6 +177,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)
@@ -194,18 +237,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);
}
@@ -271,6 +320,9 @@ public class Connection {
break;
case "LocalUtils":
result = new LocalUtils(parent, type, guid, initializer);
if (localUtils == null) {
localUtils = (LocalUtils) result;
}
break;
case "Page":
result = new PageImpl(parent, type, guid, initializer);
@@ -293,6 +345,8 @@ public class Connection {
case "Selectors":
result = new SelectorsImpl(parent, type, guid, initializer);
break;
case "SocksSupport":
break;
case "Tracing":
result = new TracingImpl(parent, type, guid, initializer);
break;
@@ -302,6 +356,9 @@ public class Connection {
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,6 +20,7 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.ConsoleMessage;
import com.microsoft.playwright.JSHandle;
import com.microsoft.playwright.Page;
import java.util.ArrayList;
import java.util.List;
@@ -27,8 +28,16 @@ import java.util.List;
import static com.microsoft.playwright.impl.Serialization.gson;
public class ConsoleMessageImpl extends ChannelOwner implements ConsoleMessage {
private PageImpl page;
public ConsoleMessageImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
// Note: currently, we only report console messages for pages and they always have a page.
// However, in the future we might report console messages for service workers or something else,
// where page() would be null.
if (initializer.has("page")) {
page = connection.getExistingObject(initializer.getAsJsonObject("page").get("guid").getAsString());
}
}
public String type() {
@@ -55,4 +64,9 @@ public class ConsoleMessageImpl extends ChannelOwner implements ConsoleMessage {
location.get("lineNumber").getAsNumber() + ":" +
location.get("columnNumber").getAsNumber();
}
@Override
public PageImpl page() {
return page;
}
}
@@ -18,10 +18,18 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Dialog;
import com.microsoft.playwright.Page;
class DialogImpl extends ChannelOwner implements Dialog {
private PageImpl page;
DialogImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
// Note: dialogs that open early during page initialization block it.
// Therefore, we must report the dialog without a page to be able to handle it.
if (initializer.has("page")) {
page = connection.getExistingObject(initializer.getAsJsonObject("page").get("guid").getAsString());
}
}
@Override
@@ -50,6 +58,11 @@ class DialogImpl extends ChannelOwner implements Dialog {
return initializer.get("message").getAsString();
}
@Override
public PageImpl page() {
return page;
}
@Override
public String type() {
return initializer.get("type").getAsString();
@@ -33,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.convertType;
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;
@@ -299,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")) {
@@ -374,11 +375,14 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override
public List<String> selectOption(String[] values, SelectOptionOptions options) {
if (values == null) {
return selectOption(new SelectOption[0], options);
if (options == null) {
options = new SelectOptionOptions();
}
return selectOption(Arrays.asList(values).stream().map(
v -> new SelectOption().setValue(v)).toArray(SelectOption[]::new), options);
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
if (values != null) {
params.add("options", toSelectValueOrLabel(values));
}
return selectOption(params);
}
@Override
@@ -455,7 +459,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
@@ -469,6 +490,7 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
}
void setInputFilesImpl(FilePayload[] files, SetInputFilesOptions options) {
checkFilePayloadSize(files);
if (options == null) {
options = new SetInputFilesOptions();
}
@@ -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, convertType(options, ElementHandle.SetInputFilesOptions.class)));
() -> element.setInputFilesImpl(files, convertType(options, ElementHandle.SetInputFilesOptions.class)));
}
}
@@ -31,7 +31,8 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Utils.convertType;
import static com.microsoft.playwright.impl.LocatorUtils.*;
import static com.microsoft.playwright.impl.Utils.*;
import static com.microsoft.playwright.options.WaitUntilState.*;
import static com.microsoft.playwright.impl.Serialization.*;
@@ -109,11 +110,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public List<String> selectOption(String selector, String[] values, SelectOptionOptions options) {
if (values == null) {
return selectOption(selector, new SelectOption[0], options);
}
return selectOption(selector, Arrays.asList(values).stream().map(
v -> new SelectOption().setValue(v)).toArray(SelectOption[]::new), options);
return withLogging("Frame.selectOption", () -> selectOptionImpl(selector, values, options));
}
@Override
@@ -373,6 +370,71 @@ public class FrameImpl extends ChannelOwner implements Frame {
return withLogging("Frame.getAttribute", () -> getAttributeImpl(selector, name, options));
}
@Override
public Locator getByAltText(String text, GetByAltTextOptions options) {
return locator(getByAltTextSelector(text, convertType(options, Locator.GetByAltTextOptions.class)));
}
@Override
public Locator getByAltText(Pattern text, GetByAltTextOptions options) {
return locator(getByAltTextSelector(text, convertType(options, Locator.GetByAltTextOptions.class)));
}
@Override
public Locator getByLabel(String text, GetByLabelOptions options) {
return locator(getByLabelSelector(text, convertType(options, Locator.GetByLabelOptions.class)));
}
@Override
public Locator getByLabel(Pattern text, GetByLabelOptions options) {
return locator(getByLabelSelector(text, convertType(options, Locator.GetByLabelOptions.class)));
}
@Override
public Locator getByPlaceholder(String text, GetByPlaceholderOptions options) {
return locator(getByPlaceholderSelector(text, convertType(options, Locator.GetByPlaceholderOptions.class)));
}
@Override
public Locator getByPlaceholder(Pattern text, GetByPlaceholderOptions options) {
return locator(getByPlaceholderSelector(text, convertType(options, Locator.GetByPlaceholderOptions.class)));
}
@Override
public Locator getByRole(AriaRole role, GetByRoleOptions options) {
return locator(getByRoleSelector(role, convertType(options, Locator.GetByRoleOptions.class)));
}
@Override
public Locator getByTestId(String testId) {
return locator(getByTestIdSelector(testId));
}
@Override
public Locator getByTestId(Pattern testId) {
return locator(getByTestIdSelector(testId));
}
@Override
public Locator getByText(String text, GetByTextOptions options) {
return locator(getByTextSelector(text, convertType(options, Locator.GetByTextOptions.class)));
}
@Override
public Locator getByText(Pattern text, GetByTextOptions options) {
return locator(getByTextSelector(text, convertType(options, Locator.GetByTextOptions.class)));
}
@Override
public Locator getByTitle(String text, GetByTitleOptions options) {
return locator(getByTitleSelector(text, convertType(options, Locator.GetByTitleOptions.class)));
}
@Override
public Locator getByTitle(Pattern text, GetByTitleOptions options) {
return locator(getByTitleSelector(text, convertType(options, Locator.GetByTitleOptions.class)));
}
String getAttributeImpl(String selector, String name, GetAttributeOptions options) {
if (options == null) {
options = new GetAttributeOptions();
@@ -627,6 +689,18 @@ public class FrameImpl extends ChannelOwner implements Frame {
return selectOption(params);
}
List<String> selectOptionImpl(String selector, String[] values, SelectOptionOptions options) {
if (options == null) {
options = new SelectOptionOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
if (values != null) {
params.add("options", toSelectValueOrLabel(values));
}
return selectOption(params);
}
@Override
public List<String> selectOption(String selector, ElementHandle[] values, SelectOptionOptions options) {
return withLogging("Frame.selectOption", () -> selectOptionImpl(selector, values, options));
@@ -686,21 +760,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 +885,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(convertType(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 +904,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 +912,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 +926,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 +950,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 +979,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 +1018,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 +1037,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 +1096,22 @@ 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, convertType(options, WaitForLoadStateOptions.class));
waitForLoadStateImpl(options.waitUntil, convertType(options, WaitForLoadStateOptions.class), logger);
return;
}
waitForNavigationImpl(() -> {}, convertType(options, WaitForNavigationOptions.class), matcher);
waitForNavigationImpl(logger, () -> {}, convertType(options, WaitForNavigationOptions.class), matcher);
}
int queryCount(String selector) {
@@ -1024,12 +1121,25 @@ public class FrameImpl extends ChannelOwner implements Frame {
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) {
if ("loadstate".equals(event)) {
JsonElement add = params.get("add");
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");
@@ -18,7 +18,12 @@ package com.microsoft.playwright.impl;
import com.microsoft.playwright.FrameLocator;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.AriaRole;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.LocatorUtils.*;
import static com.microsoft.playwright.impl.Utils.convertType;
class FrameLocatorImpl implements FrameLocator {
@@ -37,7 +42,72 @@ class FrameLocatorImpl implements FrameLocator {
@Override
public FrameLocatorImpl frameLocator(String selector) {
return new FrameLocatorImpl(frame, frameSelector + " >> control=enter-frame >> " + selector);
return new FrameLocatorImpl(frame, frameSelector + " >> internal:control=enter-frame >> " + selector);
}
@Override
public Locator getByAltText(String text, GetByAltTextOptions options) {
return locator(getByAltTextSelector(text, convertType(options, Locator.GetByAltTextOptions.class)));
}
@Override
public Locator getByAltText(Pattern text, GetByAltTextOptions options) {
return locator(getByAltTextSelector(text, convertType(options, Locator.GetByAltTextOptions.class)));
}
@Override
public Locator getByLabel(String text, GetByLabelOptions options) {
return locator(getByLabelSelector(text, convertType(options, Locator.GetByLabelOptions.class)));
}
@Override
public Locator getByLabel(Pattern text, GetByLabelOptions options) {
return locator(getByLabelSelector(text, convertType(options, Locator.GetByLabelOptions.class)));
}
@Override
public Locator getByPlaceholder(String text, GetByPlaceholderOptions options) {
return locator(getByPlaceholderSelector(text, convertType(options, Locator.GetByPlaceholderOptions.class)));
}
@Override
public Locator getByPlaceholder(Pattern text, GetByPlaceholderOptions options) {
return locator(getByPlaceholderSelector(text, convertType(options, Locator.GetByPlaceholderOptions.class)));
}
@Override
public Locator getByRole(AriaRole role, GetByRoleOptions options) {
return locator(getByRoleSelector(role, convertType(options, Locator.GetByRoleOptions.class)));
}
@Override
public Locator getByTestId(String testId) {
return locator(getByTestIdSelector(testId));
}
@Override
public Locator getByTestId(Pattern testId) {
return locator(getByTestIdSelector(testId));
}
@Override
public Locator getByText(String text, GetByTextOptions options) {
return locator(getByTextSelector(text, convertType(options, Locator.GetByTextOptions.class)));
}
@Override
public Locator getByText(Pattern text, GetByTextOptions options) {
return locator(getByTextSelector(text, convertType(options, Locator.GetByTextOptions.class)));
}
@Override
public Locator getByTitle(String text, GetByTitleOptions options) {
return locator(getByTitleSelector(text, convertType(options, Locator.GetByTitleOptions.class)));
}
@Override
public Locator getByTitle(Pattern text, GetByTitleOptions options) {
return locator(getByTitleSelector(text, convertType(options, Locator.GetByTitleOptions.class)));
}
@Override
@@ -47,7 +117,16 @@ class FrameLocatorImpl implements FrameLocator {
@Override
public Locator locator(String selector, LocatorOptions options) {
return new LocatorImpl(frame, frameSelector + " >> control=enter-frame >> " + selector, convertType(options, Locator.LocatorOptions.class));
return new LocatorImpl(frame, frameSelector + " >> internal:control=enter-frame >> " + selector, convertType(options, Locator.LocatorOptions.class));
}
@Override
public Locator locator(Locator selectorOrLocator, LocatorOptions options) {
LocatorImpl other = (LocatorImpl) selectorOrLocator;
if (other.frame != frame) {
throw new PlaywrightException("Locators must belong to the same frame.");
}
return locator(other.selector, options);
}
@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,14 +16,23 @@
package com.microsoft.playwright.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import com.google.gson.JsonObject;
import java.util.*;
import java.util.function.Consumer;
class ListenerCollection <EventType> {
private final HashMap<EventType, List<Consumer<?>>> listeners = new HashMap<>();
private final Map<EventType, String> eventSubscriptions;
private final ChannelOwner channelOwner;
ListenerCollection() {
this(null, null);
}
ListenerCollection(Map<EventType, String> eventSubscriptions, ChannelOwner channelOwner) {
this.eventSubscriptions = eventSubscriptions;
this.channelOwner = channelOwner;
}
<T> void notify(EventType eventType, T param) {
List<Consumer<?>> list = listeners.get(eventType);
@@ -41,6 +50,7 @@ class ListenerCollection <EventType> {
if (list == null) {
list = new ArrayList<>();
listeners.put(type, list);
updateSubscription(type, true);
}
list.add(listener);
}
@@ -52,6 +62,7 @@ class ListenerCollection <EventType> {
}
list.removeAll(Collections.singleton(listener));
if (list.isEmpty()) {
updateSubscription(type, false);
listeners.remove(type);
}
}
@@ -59,4 +70,18 @@ class ListenerCollection <EventType> {
boolean hasListeners(EventType type) {
return listeners.containsKey(type);
}
private void updateSubscription(EventType eventType, boolean enabled) {
if (eventSubscriptions == null) {
return;
}
String protocolEvent = eventSubscriptions.get(eventType);
if (protocolEvent == null) {
return;
}
JsonObject params = new JsonObject();
params.addProperty("event", protocolEvent);
params.addProperty("enabled", enabled);
channelOwner.sendMessageAsync("updateSubscription", params);
}
}
@@ -20,16 +20,38 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.nio.file.Path;
import java.util.List;
import static com.microsoft.playwright.impl.Serialization.gson;
class LocalUtils extends ChannelOwner {
LocalUtils(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
void zip(Path zipFile, JsonArray entries) {
void zip(Path zipFile, JsonArray entries, String stacksId, boolean appendMode, boolean includeSources) {
JsonObject params = new JsonObject();
params.addProperty("zipFile", zipFile.toString());
params.add("entries", entries);
params.addProperty("mode", appendMode ? "append" : "write");
params.addProperty("stacksId", stacksId);
params.addProperty("includeSources", includeSources);
sendMessage("zip", params);
}
void traceDiscarded(String stacksId) {
JsonObject params = new JsonObject();
params.addProperty("stacksId", stacksId);
sendMessage("traceDiscarded", params);
}
String tracingStarted(String tracesDir, String traceName) {
JsonObject params = new JsonObject();
if (tracesDir != null) {
params.addProperty("tracesDir", "");
}
params.addProperty("traceName", traceName);
JsonObject json = connection.localUtils().sendMessage("tracingStarted", params).getAsJsonObject();
return json.get("stacksId").getAsString();
}
}
@@ -19,6 +19,7 @@ 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;
@@ -39,6 +40,7 @@ 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", convertType(options, FrameExpectOptions.class));
@@ -47,6 +49,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
@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", convertType(options, FrameExpectOptions.class));
@@ -58,6 +61,7 @@ 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);
@@ -70,6 +74,7 @@ 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);
@@ -143,7 +148,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
options = new HasCountOptions();
}
FrameExpectOptions commonOptions = convertType(options, FrameExpectOptions.class);
commonOptions.expectedNumber = count;
commonOptions.expectedNumber = (double) count;
List<ExpectedTextValue> expectedText = null;
expectImpl("to.have.count", expectedText, count, "Locator expected to have count", commonOptions);
}
@@ -203,6 +208,7 @@ 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", convertType(options, FrameExpectOptions.class));
@@ -211,6 +217,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
@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;
@@ -223,6 +230,7 @@ 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);
@@ -235,6 +243,7 @@ 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);
@@ -255,9 +264,34 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
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", convertType(options, FrameExpectOptions.class));
boolean unchecked = options != null && options.checked != null && !options.checked;
String expression = unchecked ? "to.be.unchecked" : "to.be.checked";
String message = "Locator expected to be " + (unchecked ? "un" : "") + "checked";
expectTrue(expression, message, convertType(options, FrameExpectOptions.class));
}
@Override
@@ -267,7 +301,10 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
@Override
public void isEditable(IsEditableOptions options) {
expectTrue("to.be.editable", "Locator expected to be editable", convertType(options, FrameExpectOptions.class));
FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class);
boolean editable = options == null || options.editable == null || options.editable == true;
String message = "Locator expected to be " + (editable ? "editable" : "readonly");
expectTrue(editable ? "to.be.editable" : "to.be.readonly", message, frameOptions);
}
@Override
@@ -277,7 +314,10 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
@Override
public void isEnabled(IsEnabledOptions options) {
expectTrue("to.be.enabled", "Locator expected to be enabled", convertType(options, FrameExpectOptions.class));
FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class);
boolean enabled = options == null || options.enabled == null || options.enabled == true;
String message = "Locator expected to be " + (enabled ? "enabled" : "disabled");
expectTrue(enabled ? "to.be.enabled" : "to.be.disabled", message, frameOptions);
}
@Override
@@ -290,9 +330,21 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expectTrue("to.be.hidden", "Locator expected to be hidden", convertType(options, FrameExpectOptions.class));
}
@Override
public void isInViewport(IsInViewportOptions options) {
FrameExpectOptions expectOptions = convertType(options, FrameExpectOptions.class);
if (options != null && options.ratio != null) {
expectOptions.expectedNumber = options.ratio;
}
expectTrue("to.be.in.viewport", "Locator expected to be in viewport", expectOptions);
}
@Override
public void isVisible(IsVisibleOptions options) {
expectTrue("to.be.visible", "Locator expected to be visible", convertType(options, FrameExpectOptions.class));
FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class);
boolean visible = options == null || options.visible == null || options.visible == true;
String message = "Locator expected to be " + (visible ? "visible" : "hidden");
expectTrue(visible ? "to.be.visible" : "to.be.hidden", message, frameOptions);
}
private void expectTrue(String expression, String message, FrameExpectOptions options) {
@@ -304,5 +356,25 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
public LocatorAssertions not() {
return new LocatorAssertionsImpl(actualLocator, !isNot);
}
}
@Override
public void isAttached(IsAttachedOptions options) {
FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class);
boolean attached = options == null || options.attached == null || options.attached == true;
String message = "Locator expected to be " + (attached ? "attached" : "detached");
expectTrue(attached ? "to.be.attached" : "to.be.detached", message, frameOptions);
}
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;
}
}
}
@@ -1,47 +1,49 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.BoundingBox;
import com.microsoft.playwright.options.FilePayload;
import com.microsoft.playwright.options.SelectOption;
import com.microsoft.playwright.options.WaitForSelectorState;
import com.microsoft.playwright.options.*;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.ArrayList;
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.LocatorUtils.*;
import static com.microsoft.playwright.impl.Serialization.gson;
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;
final FrameImpl frame;
final String selector;
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;
selector += " >> :scope:text-matches(" + escapeWithQuotes(pattern.pattern()) + ", \"" + toJsRegexFlags(pattern) + "\")";
} else if (options.hasText instanceof String) {
String text = (String) options.hasText;
selector += " >> :scope:has-text(" + escapeWithQuotes(text) + ")";
}
selector += " >> internal:has-text=" + escapeForTextSelector(options.hasText, false);
}
if (options.hasNotText != null) {
selector += " >> internal:has-not-text=" + escapeForTextSelector(options.hasNotText, false);
}
if (options.has != null) {
LocatorImpl has = (LocatorImpl) options.has;
if (has.frame != frame) {
throw new PlaywrightException("Inner 'has' locator must belong to the same frame.");
}
selector += " >> has=" + gson().toJson(has.selector);
LocatorImpl locator = (LocatorImpl) options.has;
if (locator.frame != frame)
throw new Error("Inner 'has' locator must belong to the same frame.");
selector += " >> internal:has=" + gson().toJson(locator.selector);
}
if (options.hasNot != null) {
LocatorImpl locator = (LocatorImpl) options.hasNot;
if (locator.frame != frame)
throw new Error("Inner 'hasNot' locator must belong to the same frame.");
selector += " >> internal:has-not=" + gson().toJson(locator.selector);
}
}
this.selector = selector;
}
@@ -69,6 +71,16 @@ class LocatorImpl implements Locator {
}
}
@Override
public List<Locator> all() {
List<Locator> result = new ArrayList<>();
int count = this.count();
for (int i = 0; i < count; i++) {
result.add(nth(i));
}
return result;
}
@Override
public List<String> allInnerTexts() {
return (List<String>) frame.evalOnSelectorAll(selector, "ee => ee.map(e => e.innerText)");
@@ -79,6 +91,29 @@ class LocatorImpl implements Locator {
return (List<String>) frame.evalOnSelectorAll(selector, "ee => ee.map(e => e.textContent || '')");
}
@Override
public Locator and(Locator locator) {
LocatorImpl other = (LocatorImpl) locator;
if (other.frame != frame)
throw new Error("Locators must belong to the same frame.");
return new LocatorImpl(frame, selector + " >> internal:and=" + gson().toJson(other.selector), null);
}
@Override
public void blur(BlurOptions options) {
frame.withLogging("Locator.blur", () -> blurImpl(options));
}
private void blurImpl(BlurOptions options) {
if (options == null) {
options = new BlurOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
params.addProperty("strict", true);
frame.sendMessage("blur", params);
}
@Override
public BoundingBox boundingBox(BoundingBoxOptions options) {
return withElement((h, o) -> h.boundingBox(), options);
@@ -92,6 +127,11 @@ class LocatorImpl implements Locator {
frame.check(selector, convertType(options, Frame.CheckOptions.class).setStrict(true));
}
@Override
public void clear(ClearOptions options) {
fill("", convertType(options, FillOptions.class));
}
@Override
public void click(ClickOptions options) {
if (options == null) {
@@ -170,6 +210,11 @@ class LocatorImpl implements Locator {
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", null);
@@ -196,6 +241,76 @@ class LocatorImpl implements Locator {
return frame.getAttribute(selector, name, convertType(options, Frame.GetAttributeOptions.class).setStrict(true));
}
@Override
public Locator getByAltText(String text, GetByAltTextOptions options) {
return locator(getByAltTextSelector(text, options));
}
@Override
public Locator getByAltText(Pattern text, GetByAltTextOptions options) {
return locator(getByAltTextSelector(text, options));
}
@Override
public Locator getByLabel(String text, GetByLabelOptions options) {
return locator(getByLabelSelector(text, options));
}
@Override
public Locator getByLabel(Pattern text, GetByLabelOptions options) {
return locator(getByLabelSelector(text, options));
}
@Override
public Locator getByPlaceholder(String text, GetByPlaceholderOptions options) {
return locator(getByPlaceholderSelector(text, options));
}
@Override
public Locator getByPlaceholder(Pattern text, GetByPlaceholderOptions options) {
return locator(getByPlaceholderSelector(text, options));
}
@Override
public Locator getByRole(AriaRole role, GetByRoleOptions options) {
return locator(getByRoleSelector(role, options));
}
@Override
public Locator getByTestId(String testId) {
return locator(getByTestIdSelector(testId));
}
@Override
public Locator getByTestId(Pattern testId) {
return locator(getByTestIdSelector(testId));
}
@Override
public Locator getByText(String text, GetByTextOptions options) {
return locator(getByTextSelector(text, options));
}
@Override
public Locator getByText(Pattern text, GetByTextOptions options) {
return locator(getByTextSelector(text, options));
}
@Override
public Locator getByTitle(String text, GetByTitleOptions options) {
return locator(getByTitleSelector(text, options));
}
@Override
public Locator getByTitle(Pattern text, GetByTitleOptions options) {
return locator(getByTitleSelector(text, options));
}
@Override
public void highlight() {
frame.highlightImpl(selector);
}
@Override
public void hover(HoverOptions options) {
if (options == null) {
@@ -286,11 +401,28 @@ class LocatorImpl implements Locator {
return new LocatorImpl(frame, this.selector + " >> " + selector, options);
}
@Override
public Locator locator(Locator selectorOrLocator, LocatorOptions options) {
LocatorImpl other = (LocatorImpl) selectorOrLocator;
if (other.frame != frame) {
throw new PlaywrightException("Locators must belong to the same frame.");
}
return locator(other.selector, options);
}
@Override
public Locator nth(int index) {
return new LocatorImpl(frame, selector + " >> nth=" + index, null);
}
@Override
public Locator or(Locator locator) {
LocatorImpl other = (LocatorImpl) locator;
if (other.frame != frame)
throw new Error("Locators must belong to the same frame.");
return new LocatorImpl(frame, selector + " >> internal:or=" + gson().toJson(other.selector), null);
}
@Override
public Page page() {
return frame.page();
@@ -466,6 +598,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,127 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.options.AriaRole;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
public class LocatorUtils {
private static volatile String testIdAttributeName = "data-testid";;
static void setTestIdAttributeName(String name) {
testIdAttributeName = name;
}
static String getByTextSelector(Object text, Locator.GetByTextOptions options) {
boolean exact = options != null && options.exact != null && options.exact;
return "internal:text=" + escapeForTextSelector(text, exact);
}
static String getByLabelSelector(Object text, Locator.GetByLabelOptions options) {
boolean exact = options != null && options.exact != null && options.exact;
return "internal:label=" + escapeForTextSelector(text, exact);
}
private static String getByAttributeTextSelector(String attrName, Object value, boolean exact) {
if (value instanceof Pattern) {
return "internal:attr=[" + attrName + "=" + toJsRegExp((Pattern) value) + "]";
}
return "internal:attr=[" + attrName + "=" + escapeForAttributeSelector((String) value, exact) + "]";
}
static String getByTestIdSelector(Object testId) {
return getByAttributeTextSelector(testIdAttributeName, testId, true);
}
static String getByAltTextSelector(Object text, Locator.GetByAltTextOptions options) {
boolean exact = options != null && options.exact != null && options.exact;
return getByAttributeTextSelector("alt", text, exact);
}
static String getByTitleSelector(Object text, Locator.GetByTitleOptions options) {
boolean exact = options != null && options.exact != null && options.exact;
return getByAttributeTextSelector("title", text, exact);
}
static String getByPlaceholderSelector(Object text, Locator.GetByPlaceholderOptions options) {
boolean exact = options != null && options.exact != null && options.exact;
return getByAttributeTextSelector("placeholder", text, exact);
}
private static void addAttr(StringBuilder result, String name, String value) {
result.append("[").append(name).append("=").append(value).append("]");
}
static String getByRoleSelector(AriaRole role, Locator.GetByRoleOptions options) {
StringBuilder result = new StringBuilder();
result.append("internal:role=").append(role.name().toLowerCase());
if (options != null) {
if (options.checked != null)
addAttr(result, "checked", options.checked.toString());
if (options.disabled != null)
addAttr(result, "disabled", options.disabled.toString());
if (options.selected != null)
addAttr(result, "selected", options.selected.toString());
if (options.expanded != null)
addAttr(result, "expanded", options.expanded.toString());
if (options.includeHidden != null)
addAttr(result, "include-hidden", options.includeHidden.toString());
if (options.level != null)
addAttr(result, "level", options.level.toString());
if (options.name != null) {
String name;
if (options.name instanceof String) {
name = escapeForAttributeSelector((String) options.name, options.exact != null && options.exact);
} else if (options.name instanceof Pattern) {
name = toJsRegExp((Pattern) options.name);
} else {
throw new IllegalArgumentException("options.name can be String or Pattern, found: " + options.name);
}
addAttr(result, "name", name);
}
if (options.pressed != null)
addAttr(result, "pressed", options.pressed.toString());
}
return result.toString();
}
static String escapeForTextSelector(Object text, boolean exact) {
return escapeForTextSelector(text, exact, false);
}
private static String escapeForTextSelector(Object param, boolean exact, boolean caseSensitive) {
if (param instanceof Pattern) {
return toJsRegExp((Pattern) param);
}
if (!(param instanceof String)) {
throw new IllegalArgumentException("text parameter must be Pattern or String: " + param);
}
String text = (String) param;
if (exact) {
return '"' + text.replace("\"", "\\\"") + '"';
}
if (text.contains("\"") || text.contains(">>") || text.startsWith("/")) {
return "/" + escapeForRegex(text).replaceAll("\\s+", "\\\\s+") + "/" + (caseSensitive ? "" : "i");
}
return text;
}
private static String escapeForRegex(String text) {
return text.replaceAll("[.*+?^>${}()|\\[\\]\\\\]", "\\\\\\\\$0");
}
private static String escapeForAttributeSelector(String value, boolean exact) {
// TODO: this should actually be
// cssEscape(value).replace(/\\ /g, ' ')
// However, our attribute selectors do not conform to CSS parsing spec,
// so we escape them differently.
return '"' + value.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\"") + '"' + (exact ? "" : "i");
}
private static String toJsRegExp(Pattern pattern) {
return "/" + pattern.pattern() + "/" + toJsRegexFlags(pattern);
}
}
@@ -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);
}
@@ -17,6 +17,7 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
@@ -24,15 +25,15 @@ import com.microsoft.playwright.options.*;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Utils.convertType;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.*;
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.isSafeCloseError;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes;
import static java.util.Arrays.asList;
@@ -48,23 +49,18 @@ public class PageImpl extends ChannelOwner implements Page {
private ViewportSize viewport;
private final Router routes = new Router();
private final Set<FrameImpl> frames = new LinkedHashSet<>();
final ListenerCollection<EventType> listeners = new ListenerCollection<EventType>() {
@Override
void add(EventType eventType, Consumer<?> listener) {
if (eventType == EventType.FILECHOOSER) {
willAddFileChooserListener();
}
super.add(eventType, listener);
}
@Override
void remove(EventType eventType, Consumer<?> listener) {
super.remove(eventType, listener);
if (eventType == EventType.FILECHOOSER) {
didRemoveFileChooserListener();
}
}
};
private static final Map<EventType, String> eventSubscriptions() {
Map<EventType, String> result = new HashMap<>();
result.put(EventType.CONSOLE, "console");
result.put(EventType.DIALOG, "dialog");
result.put(EventType.REQUEST, "request");
result.put(EventType.RESPONSE, "response");
result.put(EventType.REQUESTFINISHED, "requestFinished");
result.put(EventType.REQUESTFAILED, "requestFailed");
result.put(EventType.FILECHOOSER, "fileChooser");
return result;
}
final ListenerCollection<EventType> listeners = new ListenerCollection<EventType>(eventSubscriptions(), this);
final Map<String, BindingCallback> bindings = new HashMap<>();
BrowserContextImpl ownedContext;
private boolean isClosed;
@@ -119,22 +115,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
protected void handleEvent(String event, JsonObject params) {
if ("dialog".equals(event)) {
String guid = params.getAsJsonObject("dialog").get("guid").getAsString();
DialogImpl dialog = connection.getExistingObject(guid);
if (listeners.hasListeners(EventType.DIALOG)) {
listeners.notify(EventType.DIALOG, dialog);
} else {
if ("beforeunload".equals(dialog.type())) {
try {
dialog.accept();
} catch (PlaywrightException e) {
}
} else {
dialog.dismiss();
}
}
} else if ("worker".equals(event)) {
if ("worker".equals(event)) {
String guid = params.getAsJsonObject("worker").get("guid").getAsString();
WorkerImpl worker = connection.getExistingObject(guid);
worker.page = this;
@@ -144,14 +125,9 @@ public class PageImpl extends ChannelOwner implements Page {
String guid = params.getAsJsonObject("webSocket").get("guid").getAsString();
WebSocketImpl webSocket = connection.getExistingObject(guid);
listeners.notify(EventType.WEBSOCKET, webSocket);
} else if ("console".equals(event)) {
String guid = params.getAsJsonObject("message").get("guid").getAsString();
ConsoleMessageImpl message = connection.getExistingObject(guid);
listeners.notify(EventType.CONSOLE, message);
} else if ("download".equals(event)) {
String artifactGuid = params.getAsJsonObject("artifact").get("guid").getAsString();
ArtifactImpl artifact = connection.getExistingObject(artifactGuid);
artifact.isRemote = browserContext.browser() != null && browserContext.browser().isRemote;
DownloadImpl download = new DownloadImpl(this, artifact, params);
listeners.notify(EventType.DOWNLOAD, download);
} else if ("fileChooser".equals(event)) {
@@ -175,10 +151,6 @@ public class PageImpl extends ChannelOwner implements Page {
}
}
}
} 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);
@@ -198,11 +170,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) {
maybeDisableNetworkInterception();
} else {
RouteImpl route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
Router.HandleResult handled = routes.handle(route);
if (handled != Router.HandleResult.NoMatchingHandler) {
updateInterceptionPatterns();
}
if (handled == Router.HandleResult.NoMatchingHandler || handled == Router.HandleResult.Fallback) {
browserContext.handleRoute(route);
}
} else if ("video".equals(event)) {
@@ -236,24 +209,6 @@ public class PageImpl extends ChannelOwner implements Page {
listeners.notify(EventType.CLOSE, this);
}
private void willAddFileChooserListener() {
if (!listeners.hasListeners(EventType.FILECHOOSER)) {
updateFileChooserInterception(true);
}
}
private void didRemoveFileChooserListener() {
if (!listeners.hasListeners(EventType.FILECHOOSER)) {
updateFileChooserInterception(false);
}
}
private void updateFileChooserInterception(boolean enabled) {
JsonObject params = new JsonObject();
params.addProperty("intercepted", enabled);
sendMessage("setFileChooserInterceptedNoReply", params);
}
@Override
public void onClose(Consumer<Page> handler) {
listeners.add(EventType.CLOSE, handler);
@@ -446,7 +401,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) {
@@ -458,7 +413,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) {
@@ -470,7 +425,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) {
@@ -482,7 +437,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) {
@@ -495,7 +450,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) {
@@ -507,7 +462,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) {
@@ -519,7 +474,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) {
@@ -539,19 +494,21 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void close(CloseOptions options) {
if (isClosed) {
return;
if (options == null) {
options = new CloseOptions();
}
JsonObject params = options == null ? new JsonObject() : gson().toJsonTree(options).getAsJsonObject();
try {
sendMessage("close", params);
} catch (PlaywrightException exception) {
if (!isSafeCloseError(exception)) {
throw exception;
if (ownedContext != null) {
ownedContext.close();
} else {
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("close", params);
}
}
if (ownedContext != null) {
ownedContext.close();
} catch (PlaywrightException exception) {
if (isSafeCloseError(exception) && (options.runBeforeUnload == null || !options.runBeforeUnload)) {
return;
}
throw exception;
}
}
@@ -764,6 +721,82 @@ public class PageImpl extends ChannelOwner implements Page {
() -> mainFrame.getAttributeImpl(selector, name, convertType(options, Frame.GetAttributeOptions.class)));
}
@Override
public Locator getByAltText(String text, GetByAltTextOptions options) {
return withLogging("Page.getAttribute",
() -> mainFrame.getByAltText(text, convertType(options, Frame.GetByAltTextOptions.class)));
}
@Override
public Locator getByAltText(Pattern text, GetByAltTextOptions options) {
return withLogging("Page.getByAltText",
() -> mainFrame.getByAltText(text, convertType(options, Frame.GetByAltTextOptions.class)));
}
@Override
public Locator getByLabel(String text, GetByLabelOptions options) {
return withLogging("Page.getByLabel",
() -> mainFrame.getByLabel(text, convertType(options, Frame.GetByLabelOptions.class)));
}
@Override
public Locator getByLabel(Pattern text, GetByLabelOptions options) {
return withLogging("Page.getByLabel",
() -> mainFrame.getByLabel(text, convertType(options, Frame.GetByLabelOptions.class)));
}
@Override
public Locator getByPlaceholder(String text, GetByPlaceholderOptions options) {
return withLogging("Page.getByPlaceholder",
() -> mainFrame.getByPlaceholder(text, convertType(options, Frame.GetByPlaceholderOptions.class)));
}
@Override
public Locator getByPlaceholder(Pattern text, GetByPlaceholderOptions options) {
return withLogging("Page.getByPlaceholder",
() -> mainFrame.getByPlaceholder(text, convertType(options, Frame.GetByPlaceholderOptions.class)));
}
@Override
public Locator getByRole(AriaRole role, GetByRoleOptions options) {
return withLogging("Page.getByRole",
() -> mainFrame.getByRole(role, convertType(options, Frame.GetByRoleOptions.class)));
}
@Override
public Locator getByTestId(String testId) {
return withLogging("Page.getByTestId", () -> mainFrame.getByTestId(testId));
}
@Override
public Locator getByTestId(Pattern testId) {
return withLogging("Page.getByTestId", () -> mainFrame.getByTestId(testId));
}
@Override
public Locator getByText(String text, GetByTextOptions options) {
return withLogging("Page.getByText",
() -> mainFrame.getByText(text, convertType(options, Frame.GetByTextOptions.class)));
}
@Override
public Locator getByText(Pattern text, GetByTextOptions options) {
return withLogging("Page.getByText",
() -> mainFrame.getByText(text, convertType(options, Frame.GetByTextOptions.class)));
}
@Override
public Locator getByTitle(String text, GetByTitleOptions options) {
return withLogging("Page.getByTitle",
() -> mainFrame.getByTitle(text, convertType(options, Frame.GetByTitleOptions.class)));
}
@Override
public Locator getByTitle(Pattern text, GetByTitleOptions options) {
return withLogging("Page.getByTitle",
() -> mainFrame.getByTitle(text, convertType(options, Frame.GetByTitleOptions.class)));
}
@Override
public Response goBack(GoBackOptions options) {
return withLogging("Page.goBack", () -> goBackImpl(options));
@@ -902,8 +935,17 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void pause() {
withLogging("BrowserContext.pause", () -> {
context().pause();
withLogging("Page.pause", () -> {
Double defaultNavigationTimeout = browserContext.timeoutSettings.defaultNavigationTimeout();
Double defaultTimeout = browserContext.timeoutSettings.defaultTimeout();
browserContext.setDefaultNavigationTimeout(0);
browserContext.setDefaultTimeout(0);
try {
runUntil(() -> {}, new WaitableRace<>(asList(context().pause(), (Waitable<JsonElement>) waitableClosedOrCrashed)));
} finally {
browserContext.setDefaultNavigationTimeout(defaultNavigationTimeout);
browserContext.setDefaultTimeout(defaultTimeout);
}
});
}
@@ -913,9 +955,6 @@ public class PageImpl extends ChannelOwner implements Page {
}
private byte[] pdfImpl(PdfOptions options) {
if (!browserContext.browser().isChromium()) {
throw new PlaywrightException("Page.pdf only supported in headless Chromium");
}
if (options == null) {
options = new PdfOptions();
}
@@ -972,14 +1011,25 @@ 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);
if (routes.size() == 1) {
JsonObject params = new JsonObject();
params.addProperty("enabled", true);
sendMessage("setNetworkInterceptionEnabled", params);
}
updateInterceptionPatterns();
});
}
@@ -988,8 +1038,6 @@ public class PageImpl extends ChannelOwner implements Page {
return withLogging("Page.screenshot", () -> screenshotImpl(options));
}
@Override
public List<String> selectOption(String selector, String value, SelectOptionOptions options) {
String[] values = value == null ? null : new String[]{ value };
@@ -1004,11 +1052,8 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public List<String> selectOption(String selector, String[] values, SelectOptionOptions options) {
if (values == null) {
return selectOption(selector, new SelectOption[0], options);
}
return selectOption(selector, Arrays.asList(values).stream().map(
v -> new SelectOption().setValue(v)).toArray(SelectOption[]::new), options);
return withLogging("Page.selectOption",
() -> mainFrame.selectOptionImpl(selector, values, convertType(options, Frame.SelectOptionOptions.class)));
}
@Override
@@ -1034,8 +1079,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());
@@ -1189,16 +1244,12 @@ public class PageImpl extends ChannelOwner implements Page {
private void unroute(UrlMatcher matcher, Consumer<Route> handler) {
withLogging("Page.unroute", () -> {
routes.remove(matcher, handler);
maybeDisableNetworkInterception();
updateInterceptionPatterns();
});
}
private void maybeDisableNetworkInterception() {
if (routes.size() == 0) {
JsonObject params = new JsonObject();
params.addProperty("enabled", false);
sendMessage("setNetworkInterceptionEnabled", params);
}
private void updateInterceptionPatterns() {
sendMessage("setNetworkInterceptionPatterns", routes.interceptionPatterns());
}
@Override
@@ -1246,25 +1297,25 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void waitForLoadState(LoadState state, WaitForLoadStateOptions options) {
withWaitLogging("Page.waitForLoadState", () -> {
mainFrame.waitForLoadStateImpl(state, convertType(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) {
@@ -1316,21 +1367,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) {
@@ -1342,7 +1400,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) {
@@ -1354,21 +1412,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) {
@@ -1384,6 +1449,15 @@ public class PageImpl extends ChannelOwner implements Page {
() -> mainFrame.waitForSelectorImpl(selector, convertType(options, Frame.WaitForSelectorOptions.class)));
}
@Override
public void waitForCondition(BooleanSupplier predicate, WaitForConditionOptions options) {
List<Waitable<Void>> waitables = new ArrayList<>();
waitables.add(createWaitForCloseHelper());
waitables.add(createWaitableTimeout(options == null ? null : options.timeout));
waitables.add(new WaitablePredicate<>(predicate));
runUntil(() -> {}, new WaitableRace<>(waitables));
}
@Override
public void waitForTimeout(double timeout) {
withLogging("Page.waitForTimeout", () -> mainFrame.waitForTimeoutImpl(timeout));
@@ -1405,7 +1479,10 @@ public class PageImpl extends ChannelOwner implements Page {
}
private void waitForURL(UrlMatcher matcher, WaitForURLOptions options) {
withLogging("Page.waitForURL", () -> mainFrame.waitForURLImpl(matcher, convertType(options, Frame.WaitForURLOptions.class)));
withWaitLogging("Page.waitForURL", logger -> {
mainFrame.waitForURLImpl(logger, matcher, convertType(options, Frame.WaitForURLOptions.class));
return null;
});
}
@Override
@@ -21,9 +21,9 @@ 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;
@@ -32,18 +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, true);
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);
Driver.setRequiredEnvironmentVariables(pb);
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);
@@ -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;
}
@@ -90,7 +94,7 @@ class ExpectedTextValue {
class FrameExpectOptions {
Object expressionArg;
List<ExpectedTextValue> expectedText;
Integer expectedNumber;
Double expectedNumber;
SerializedArgument expectedValue;
Boolean useInnerText;
boolean isNot;
@@ -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;
}
}
@@ -32,6 +32,7 @@ public class RequestOptionsImpl implements RequestOptions {
Boolean failOnStatusCode;
Boolean ignoreHTTPSErrors;
Double timeout;
Integer maxRedirects;
@Override
public RequestOptions setHeader(String name, String value) {
@@ -118,4 +119,10 @@ public class RequestOptionsImpl implements RequestOptions {
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.PlaywrightException;
import com.microsoft.playwright.Route;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.RequestOptions;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -27,11 +27,18 @@ 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;
boolean fallbackCalled;
boolean shouldResumeIfFallbackIsCalled;
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
@@ -40,45 +47,109 @@ public class RouteImpl extends ChannelOwner implements Route {
withLogging("Route.abort", () -> {
JsonObject params = new JsonObject();
params.addProperty("errorCode", errorCode);
params.addProperty("requestUrl", request.initializer.get("url").getAsString());
sendMessageAsync("abort", params);
});
}
@Override
public void resume(ResumeOptions options) {
startHandling();
withLogging("Route.resume", () -> resumeImpl(options));
boolean isHandled() {
return handled;
}
private void resumeImpl(ResumeOptions options) {
@Override
public void resume(ResumeOptions options) {
resume(options, false);
}
void resume(ResumeOptions options, boolean isFallback) {
startHandling();
applyOverrides(convertType(options, FallbackOptions.class));
withLogging("Route.resume", () -> resumeImpl(request().fallbackOverridesForResume(), isFallback));
}
@Override
public void fallback(FallbackOptions options) {
fallbackCalled = true;
if (handled) {
throw new PlaywrightException("Route is already handled!");
}
applyOverrides(options);
if (shouldResumeIfFallbackIsCalled) {
resume(null, true);
}
}
@Override
public APIResponse fetch(FetchOptions fetchOptions) {
RequestOptionsImpl options = convertType(fetchOptions, RequestOptionsImpl.class);
if (options == null) {
options = new ResumeOptions();
options = new RequestOptionsImpl();
}
JsonObject params = new JsonObject();
if (options.url != null) {
params.addProperty("url", options.url);
if (options.method == null) {
options.method = request.method();
}
if (options.method != null) {
params.addProperty("method", options.method);
if (options.headers == null) {
options.headers = request.headers();
}
if (options.headers != null) {
params.add("headers", Serialization.toProtocol(options.headers));
if (fetchOptions != null && fetchOptions.postData != null) {
options.data = fetchOptions.postData;
} else {
options.data = request.postDataBuffer();
}
if (fetchOptions != null && fetchOptions.timeout != null) {
options.timeout = fetchOptions.timeout;
}
APIRequestContextImpl apiRequest = request.frame().page().context().request();
String url = (fetchOptions == null || fetchOptions.url == null) ? request().url() : fetchOptions.url;
return apiRequest.fetch(url, options);
}
private void applyOverrides(FallbackOptions options) {
if (options == null) {
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());
}
String base64 = Base64.getEncoder().encodeToString(bytes);
params.addProperty("postData", base64);
overrides.postData = getPostDataBytes(options.postData);
}
request().applyFallbackOverrides(overrides);
}
private void resumeImpl(RequestImpl.FallbackOverrides options, boolean isFallback) {
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);
}
}
params.addProperty("requestUrl", request.initializer.get("url").getAsString());
params.addProperty("isFallback", isFallback);
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();
@@ -160,12 +231,21 @@ public class RouteImpl extends ChannelOwner implements Route {
if (fetchResponseUid != null) {
params.addProperty("fetchResponseUid", fetchResponseUid);
}
params.addProperty("requestUrl", request.initializer.get("url").getAsString());
sendMessageAsync("fulfill", params);
}
@Override
public RequestImpl request() {
return connection.getExistingObject(initializer.getAsJsonObject("request").get("guid").getAsString());
return request;
}
void redirectNavigationRequest(String redirectURL) {
startHandling();
JsonObject params = new JsonObject();
params.addProperty("url", redirectURL);
// TODO: _raceWithPageClose ?
sendMessageAsync("redirectNavigationRequest", params);
}
private void startHandling() {
@@ -16,13 +16,19 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
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.regex.Pattern;
import java.util.stream.Collectors;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
class Router {
private List<RouteInfo> routes = new ArrayList<>();
@@ -37,22 +43,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 +70,56 @@ 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, Handled, Fallback, PendingHandler }
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();
}
route.fallbackCalled = false;
info.handle(route);
if (route.isHandled()) {
return HandleResult.Handled;
}
// Not immediately handled and fallback() was not called => the route
// must be handled asynchronously.
if (!route.fallbackCalled) {
route.shouldResumeIfFallbackIsCalled = true;
return HandleResult.PendingHandler;
}
// Fallback was called, continue to the remaining handlers.
result = HandleResult.Fallback;
}
return false;
return result;
}
JsonObject interceptionPatterns() {
JsonArray jsonPatterns = new JsonArray();
for (RouteInfo route : routes) {
JsonObject jsonPattern = new JsonObject();
Object urlFilter = route.matcher.rawSource;
if (urlFilter instanceof String) {
jsonPattern.addProperty("glob", (String) urlFilter);
} else if (urlFilter instanceof Pattern) {
Pattern pattern = (Pattern) urlFilter;
jsonPattern.addProperty("regexSource", pattern.pattern());
jsonPattern.addProperty("regexFlags", toJsRegexFlags(pattern));
} else {
// Match all requests.
jsonPattern.addProperty("glob", "**/*");
jsonPatterns = new JsonArray();
jsonPatterns.add(jsonPattern);
break;
}
jsonPatterns.add(jsonPattern);
}
JsonObject result = new JsonObject();
result.add("patterns", jsonPatterns);
return result;
}
}
@@ -28,19 +28,38 @@ 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.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
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 final Gson gson = new GsonBuilder()
private static final Gson gson = new GsonBuilder().disableHtmlEscaping()
.registerTypeAdapter(Date.class, new DateSerializer())
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeSerializer())
.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>())
@@ -68,82 +87,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 +229,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 +260,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,6 +297,14 @@ 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) {
@@ -228,9 +324,7 @@ class Serialization {
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;
}
@@ -239,6 +333,16 @@ class Serialization {
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, ?> e : map.entrySet()) {
@@ -250,6 +354,25 @@ class Serialization {
return array;
}
static JsonArray toSelectValueOrLabel(String[] values) {
JsonArray jsonOptions = new JsonArray();
for (String value : values) {
JsonObject option = new JsonObject();
option.addProperty("valueOrLabel", value);
jsonOptions.add(option);
}
return jsonOptions;
}
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) {
@@ -271,7 +394,7 @@ class Serialization {
public JsonElement serialize(Optional<?> src, Type typeOfSrc, JsonSerializationContext context) {
assert isSupported(typeOfSrc) : "Unexpected optional type: " + typeOfSrc.getTypeName();
if (!src.isPresent()) {
return new JsonPrimitive("null");
return new JsonPrimitive("no-override");
}
return context.serialize(src.get());
}
@@ -280,9 +403,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();
}
}
@@ -306,12 +427,6 @@ class Serialization {
}
}
private static class BrowserChannelSerializer implements JsonSerializer<BrowserChannel> {
@Override
public JsonElement serialize(BrowserChannel src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString().toLowerCase().replace('_', '-'));
}
}
private static class ToLowerCaseAndDashSerializer<E extends Enum<E>> implements JsonSerializer<E> {
@Override
public JsonElement serialize(E src, Type typeOfSrc, JsonSerializationContext context) {
@@ -359,5 +474,29 @@ class Serialization {
return SameSiteAttribute.valueOf(value.toUpperCase());
}
}
private static DateFormat iso8601Format() {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat;
}
private static final DateFormat dateFormat = iso8601Format();
private static class DateSerializer implements JsonSerializer<Date> {
@Override
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(dateFormat.format(src));
}
}
private static class LocalDateTimeSerializer implements JsonSerializer<LocalDateTime> {
@Override
public JsonElement serialize(LocalDateTime src, Type typeOfSrc, JsonSerializationContext context) {
ZoneOffset offset = ZoneId.systemDefault().getRules().getOffset(src);
Instant instant = src.toInstant(offset);
return new JsonPrimitive(dateFormat.format(Date.from(instant)));
}
}
}
@@ -25,6 +25,7 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static com.microsoft.playwright.impl.LocatorUtils.setTestIdAttributeName;
import static java.nio.charset.StandardCharsets.UTF_8;
public class SharedSelectors extends LoggingSupport implements Selectors {
@@ -61,6 +62,12 @@ public class SharedSelectors extends LoggingSupport implements Selectors {
});
}
@Override
public void setTestIdAttribute(String attributeName) {
// TODO: set it per playwright insttance
setTestIdAttributeName(attributeName);
}
void addChannel(SelectorsImpl channel) {
registrations.forEach(r -> channel.registerImpl(r.name, r.script, r.options));
channels.add(channel);
@@ -28,18 +28,25 @@ 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() {
String srcRoots = System.getenv("PLAYWRIGHT_JAVA_SRC");
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() + "'");
throw new PlaywrightException("Source location specified in " + PLAYWRIGHT_JAVA_SRC + " doesn't exist: '" + srcDir.toAbsolutePath() + "'");
}
}
return new StackTraceCollector(srcDirs);
@@ -62,7 +69,13 @@ class StackTraceCollector {
if (file == null) {
return "";
}
return resolveSourcePath(Paths.get(pkg).resolve(file));
try {
// The file name can contain an arbitrary string which may cause Path implementation
// to throw. See https://github.com/microsoft/playwright-java/issues/1115
return resolveSourcePath(Paths.get(pkg).resolve(file));
} catch (RuntimeException e) {
return "";
}
}
private String resolveSourcePath(Path relativePath) {
@@ -105,6 +118,7 @@ class StackTraceCollector {
JsonObject jsonFrame = new JsonObject();
jsonFrame.addProperty("file", sourceFile(frame));
jsonFrame.addProperty("line", frame.getLineNumber());
jsonFrame.addProperty("column", 0);
jsonFrame.addProperty("function", frame.getClassName() + "." + frame.getMethodName());
jsonStack.add(jsonFrame);
}
@@ -30,6 +30,13 @@ class TimeoutSettings {
this.parent = parent;
}
Double defaultTimeout() {
return defaultTimeout;
}
Double defaultNavigationTimeout() {
return defaultNavigationTimeout;
}
void setDefaultTimeout(double timeout) {
defaultTimeout = timeout;
}
@@ -73,5 +80,4 @@ class TimeoutSettings {
}
return new WaitableTimeout<>(timeout(timeout));
}
}
@@ -18,7 +18,6 @@ package com.microsoft.playwright.impl;
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;
@@ -26,43 +25,56 @@ import java.nio.file.Path;
import static com.microsoft.playwright.impl.Serialization.gson;
class TracingImpl extends ChannelOwner implements Tracing {
LocalUtils localUtils;
boolean isRemote;
private boolean includeSources;
private Path tracesDir;
private boolean isTracing;
private String stacksId;
TracingImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
private void stopChunkImpl(Path path) {
JsonObject params = new JsonObject();
String mode = "doNotSave";
if (path != null) {
if (isRemote) {
mode = "compressTrace";
} else {
mode = "compressTraceAndSources";
}
if (isTracing) {
isTracing = false;
connection.setIsTracing(false);
}
params.addProperty("mode", mode);
JsonObject params = new JsonObject();
// Not interested in artifacts.
if (path == null) {
params.addProperty("mode", "discard");
sendMessage("tracingStopChunk", params);
if (stacksId != null) {
connection.localUtils().traceDiscarded(stacksId);
}
return;
}
boolean isLocal = !connection.isRemote;
if (isLocal) {
params.addProperty("mode", "entries");
JsonObject json = sendMessage("tracingStopChunk", params).getAsJsonObject();
JsonArray entries = json.getAsJsonArray("entries");
connection.localUtils.zip(path, entries, stacksId, false, includeSources);
return;
}
params.addProperty("mode", "archive");
JsonObject json = sendMessage("tracingStopChunk", params).getAsJsonObject();
// The artifact may be missing if the browser closed while stopping tracing.
if (!json.has("artifact")) {
if (stacksId != null) {
connection.localUtils().traceDiscarded(stacksId);
}
return;
}
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 (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");
localUtils.zip(path, entries);
}
connection.localUtils.zip(path, new JsonArray(), stacksId, true, includeSources);
}
@Override
@@ -81,8 +93,27 @@ class TracingImpl extends ChannelOwner implements Tracing {
if (options == null) {
options = new StartChunkOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("tracingStartChunk", params);
tracingStartChunk(options.name, options.title);
}
private void tracingStartChunk(String name, String title) {
JsonObject params = new JsonObject();
if (name != null) {
params.addProperty("name", name);
}
if (title != null) {
params.addProperty("title", title);
}
JsonObject result = sendMessage("tracingStartChunk", params).getAsJsonObject();
startCollectingStacks(result.get("traceName").getAsString());
}
private void startCollectingStacks(String traceName) {
if (!isTracing) {
isTracing = true;
connection.setIsTracing(true);
}
stacksId = connection.localUtils().tracingStarted(tracesDir == null ? null : tracesDir.toString(), traceName);
}
private void startImpl(StartOptions options) {
@@ -90,15 +121,12 @@ class TracingImpl extends ChannelOwner implements Tracing {
options = new StartOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
includeSources = options.sources != null;
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");
tracingStartChunk(options.name, options.title);
}
@Override
@@ -115,4 +143,8 @@ class TracingImpl extends ChannelOwner implements Tracing {
stopChunkImpl(options == null ? null : options.path);
});
}
void setTracesDir(Path tracesDir) {
this.tracesDir = tracesDir;
}
}
@@ -27,7 +27,7 @@ import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Utils.globToRegex;
class UrlMatcher {
private final Object rawSource;
final Object rawSource;
private final Predicate<String> predicate;
private static Predicate<String> toPredicate(Pattern pattern) {
@@ -90,6 +90,11 @@ class UrlMatcher {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UrlMatcher that = (UrlMatcher) o;
if (rawSource instanceof Pattern && that.rawSource instanceof Pattern) {
Pattern a = (Pattern) rawSource;
Pattern b = (Pattern) that.rawSource;
return a.pattern().equals(b.pattern()) && a.flags() == b.flags();
}
return Objects.equals(rawSource, that.rawSource);
}
@@ -97,4 +102,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();
}
}

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