1
0
mirror of synced 2026-08-31 19:55:37 +00:00

Compare commits

...

168 Commits

Author SHA1 Message Date
Yury Semikhatsky 9d712e6f8c test: fix remaining windows bot failures
- accept all close-time error message variants in
  waitForNavigation-after-disconnect, the surfaced message depends on
  which call hits the closed connection first
- shouldDetachWhenPageCloses: only check that detach throws, like the
  upstream test
- skip @TempDir cleanup for user data dirs: on Windows Chromium 149
  keeps chrome_debug.log locked briefly after close
2026-06-12 13:20:30 -07:00
Yury Semikhatsky 4fb8a95178 chore: trim comments 2026-06-12 12:56:46 -07:00
Yury Semikhatsky 1d06a09651 test: fix failures on firefox, webkit and windows bots
- connectOverCDP is now supported in WebKit: skip the not-chromium test
  there and expect the new error message
- accept all close-time messages in waitForNavigation-after-disconnect
- wait 2s for page errors to accumulate, mirrors upstream #38378
- regenerate firefox screenshot expectations for Firefox 151
2026-06-12 12:56:46 -07:00
Yury Semikhatsky 0e3b273fbf chore: roll driver to 1.61.0-beta-1781285686000
Ported upstream changes:
- #40843 page.localStorage()/sessionStorage() (WebStorage API)
- #40849 context.credentials() WebAuthn virtual authenticator
- #40932 APIResponse.securityDetails()/serverAddr()
- #41162 ScreencastFrame.timestamp()
- #40916 screencast cursor and size options
- #40844 comma-separated testIdAttribute (getByTestId now uses internal:testid)
- #40718 waitForEventInfo replaced with fire-and-forget __waitInfo__
- #40780 protocol Page.close split into close and runBeforeUnload
- #40801 Frame.expect failures are now protocol errors with errorDetails
- #41014 connectOverCDP allowed for WebKit, artifactsDir option
2026-06-12 12:25:16 -07:00
Yury Semikhatsky 1b7d164873 chore: extract common skill conventions into CLAUDE.md (#1928) 2026-06-12 11:14:19 -07:00
Yury Semikhatsky 7a59a68828 feat: assemble driver from npm instead of CDN (#1927)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:09:55 -07:00
dependabot[bot] 07b50c85e3 chore(deps-dev): bump the all group with 2 updates (#1925) 2026-06-12 11:04:05 -07:00
Yury Semikhatsky c949d8398d chore: add playwright-java-release skill (#1924) 2026-05-21 08:38:33 +01:00
Yury Semikhatsky 793b1d3d61 chore: roll driver to 1.60.0 (#1921) 2026-05-18 16:42:22 -07:00
Yury Semikhatsky 8f66ef1f13 test_docker: align container launch with playwright-browsers (#1919) 2026-05-08 12:06:54 -07:00
dependabot[bot] dc66e4d1a3 chore(deps): bump com.google.code.gson:gson from 2.13.2 to 2.14.0 in the all group (#1915) 2026-05-08 11:11:24 -07:00
Yury Semikhatsky d84a2db0b3 chore: roll driver to 1.60.0-beta-1778180503000 (#1918) 2026-05-07 15:45:23 -07:00
Yury Semikhatsky f081667e44 chore: roll to 1.60.0-alpha-2026-05-05 (#1916) 2026-05-06 08:30:19 -07:00
Yury Semikhatsky 5b33729849 chore: reduce CI flakiness across webkit/macOS, ubuntu, windows (#1914) 2026-04-30 17:40:14 -07:00
Yury Semikhatsky b01bf64e64 chore: resolve Object langAliases in function argument types (#1908) 2026-04-09 13:50:18 -07:00
Yury Semikhatsky 7dbd6cac3b chore: use langAliases from api.json in api generator (#1907) 2026-04-09 12:00:26 -07:00
Yury Semikhatsky c3e4b92982 chore: roll to 1.59.1-beta-1775752988000 (#1906) 2026-04-09 11:31:19 -07:00
dependabot[bot] 9e73db411c chore(deps): bump the actions group with 2 updates (#1904)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 11:18:56 -07:00
dependabot[bot] 10ed7e036d chore(deps-dev): bump org.apache.maven.plugins:maven-resources-plugin from 3.4.0 to 3.5.0 in the all group (#1903) 2026-04-09 11:17:56 -07:00
Yury Semikhatsky 5ccdd3e4b9 chore: roll to 1.59.0-alpha-1774622285000 (#1901) 2026-03-27 12:31:06 -07:00
Yury Semikhatsky afd80add27 chore: roll to 1.59.0-alpha (#1900) 2026-03-23 13:58:14 -07:00
dependabot[bot] 605e428fd7 chore(deps-dev): bump the all group with 2 updates (#1894) 2026-03-20 19:28:22 -07:00
nanne-rl 932669036b fix: handle null close code and reason in WebSocketRoute (#1886) 2026-01-29 12:55:15 -08:00
Copilot 480400793e Update Docker images to Java 25 and Maven 3.9.12 (#1888) 2026-01-28 17:35:11 -08:00
dependabot[bot] 647d8fc034 chore(deps): bump actions/cache from 4 to 5 in the actions group (#1878) 2026-01-28 14:59:28 -08:00
Simon Knott b5c2160d32 chore: roll to 1.58.0 (#1883) 2026-01-28 10:39:26 -08:00
Yury Semikhatsky 63bb008857 chore: roll driver to 1.57.0-beta-1764692940000 (#1870) 2025-12-02 13:59:36 -08:00
dependabot[bot] 9b3a788806 chore(deps): bump actions/checkout from 5 to 6 in the actions group (#1868) 2025-12-01 15:00:50 -08:00
dependabot[bot] 2f387edf0d chore(deps): bump the all group with 3 updates (#1867) 2025-12-01 14:59:52 -08:00
arukiidou 6eb30e275c chore(deps): bump junit.version from 5.13.4 to 5.14.1 (#1859) 2025-11-12 10:00:13 -08:00
arukiidou 0f14588df1 Migrate ExtensionContext.Store.CloseableResource to AutoCloseable (#1860) 2025-11-12 09:59:06 -08:00
arukiidou e417cad372 Fix document typo - setContextOptions (#1862) 2025-11-12 09:45:13 -08:00
Yury Semikhatsky 059667e311 chore: remove background pages implementation (#1861) 2025-10-31 10:25:35 -07:00
Yury Semikhatsky 98296d9cdf devops: update ado approver (#1857) 2025-10-24 13:38:05 -07:00
Yury Semikhatsky 1599e1c7bc chore: roll 1.56.1 (#1855) 2025-10-17 11:11:43 -07:00
dependabot[bot] a2555ddf9e chore(deps): bump the all group with 4 updates (#1846) 2025-10-03 15:46:53 -07:00
Yury Semikhatsky 0deadc2b90 chore: roll driver to 1.56.0-beta (#1849) 2025-10-03 15:45:53 -07:00
Simon Knott eb1fea9907 fix(trace): waitForLoadState title (#1840) 2025-09-08 09:49:00 +02:00
dependabot[bot] dd99ce8b34 chore(deps): bump the actions group with 2 updates (#1838) 2025-09-04 16:18:58 -07:00
dependabot[bot] ed8e9c434f chore(deps): bump the all group across 1 directory with 7 updates (#1839) 2025-09-04 16:18:15 -07:00
Yury Semikhatsky aee298b293 chore: rename headful -> headed (#1835) 2025-08-27 09:51:30 -07:00
Max Schmitt fd2ab4708a devops: enable retries in Docker tests (#1834) 2025-08-26 21:50:28 +02:00
Max Schmitt 2a6cdff664 chore: migrate Trace Viewer tests to use real Trace viewer (#1830) 2025-08-26 10:43:18 +02:00
Max Schmitt 44161e0558 chore: fix Maven test commands (#1832) 2025-08-26 00:35:06 +02:00
Max Schmitt 954b1c43ef refactor: remove unused ImplUtils class (#1833) 2025-08-25 15:29:53 -07:00
Simon Knott f4c7b9734f chore: roll 1.55.0 (#1827) 2025-08-21 17:09:15 +02:00
Yury Semikhatsky dd87b300fb fix: npe in page.pause() (#1828) 2025-08-18 15:51:17 -07:00
Janne Hyötylä f83c03af68 fix: Fix masking in single element screenshots. (#1825) 2025-08-11 11:40:00 -07:00
JONGSHIN d26dd0b112 fix: Replaced classLoader in DriverJar (#1811) 2025-07-31 11:03:19 -07:00
Yury Semikhatsky 0cf8c4e17f chore: unflake tracing test (#1822) 2025-07-21 11:32:08 -07:00
Yury Semikhatsky 1fb593e1e2 chore: roll 1.54.1 (#1821) 2025-07-21 11:06:39 -07:00
Simon Knott 915ee8d64c chore: roll 1.54.0-alpha-2025-07-09 (#1817) 2025-07-09 10:29:18 +02:00
Yury Semikhatsky 9df2165e93 devops: fix release branch name check (#1813) 2025-06-24 13:20:20 -07:00
Yury Semikhatsky 859eb9b8b8 chore: require explicit timeout parameter in sendMessage() (#1807) 2025-06-23 10:37:40 -07:00
Yury Semikhatsky f28cb55795 chore: roll 1.53.1 (#1808) 2025-06-23 09:45:36 -07:00
Max Schmitt 07867c2db5 devops: ignore tests in CodeQL checks (#1805) 2025-06-20 16:25:15 -07:00
Simon Knott b4151b1231 chore: remove withLogging (#1804) 2025-06-13 15:33:22 -07:00
Simon Knott 4698b91d8e chore: roll 1.53.0 (#1801) 2025-06-12 12:35:29 +02:00
Simon Knott 8a89e36ce3 chore: roll to 1.53.0-alpha-2025-05-21 (#1798) 2025-06-10 18:08:32 +02:00
campersau f1e6100b33 fix(docker): set default shell encoding (#1793) 2025-05-30 16:13:42 +01:00
Yury Semikhatsky fddb146d73 devops: trigger publish on new tag (#1787) 2025-05-02 10:55:07 -07:00
Max Schmitt 9ad596ac75 devops: add linux-arm64 Docker tests (#1784) 2025-05-01 19:21:22 +02:00
Simon Knott 8cca01851a chore: roll 1.52.0 driver, implement new features (#1780) 2025-04-29 09:13:50 -07:00
dependabot[bot] 478417bb56 chore(deps): bump org.apache.maven.plugins:maven-surefire-plugin from 3.5.2 to 3.5.3 in the all group (#1774) 2025-04-01 11:38:02 -07:00
Max Schmitt 739202fddf test: do not send Content-Length header for HEAD requests (#1771) 2025-03-20 18:41:01 +01:00
Max Schmitt 8593941005 test: fix 'SLF4J(W): No SLF4J providers were found.' warning (#1772) 2025-03-20 18:38:34 +01:00
Max Schmitt b2852f5d57 devops: migrate to GitHub App for automation (#1770) 2025-03-19 14:24:34 +01:00
Yury Semikhatsky 6c059e351f chore: roll driver to 1.51.1 (#1768) 2025-03-17 12:03:43 -07:00
Yury Semikhatsky e86911ed2a docs: remove obsolete toc from readme (#1767) 2025-03-17 19:37:04 +01:00
Yury Semikhatsky 17cc3b8297 docs: abridge readme, redirect to playwright.dev (#1766) 2025-03-17 11:17:28 -07:00
dependabot[bot] 43016df241 chore(deps): bump the all group with 3 updates (#1764) 2025-03-17 11:16:44 -07:00
dependabot[bot] ba4eb3ce7d chore(deps): bump the actions group with 2 updates (#1765) 2025-03-17 11:16:22 -07:00
Max Schmitt ba7dd3cd89 devops: update GitHub Action workflows via dependabot (#1763) 2025-03-17 19:05:26 +01:00
Max Schmitt 5e37b7f5ca devops: add DevContainer config (#1762) 2025-03-17 19:02:33 +01:00
Yury Semikhatsky 84eaf8f3cb devops: retry failures up to 3 times on GHA (#1758) 2025-03-10 11:50:10 -07:00
Yury Semikhatsky c090824c93 chore: roll 1.51.0 driver, implement new features (#1757) 2025-03-07 19:02:19 -08:00
dependabot[bot] add7f56117 chore(deps): bump the all group with 7 updates (#1754) 2025-03-04 11:25:31 -08:00
Max Schmitt 676f38d22c devops: use PME environment for ESRP publishing (#1755) 2025-03-03 16:52:51 +01:00
Max Schmitt bc82f2fa68 chore: auto update example pw version in pom (#1748) 2025-02-13 10:08:42 +01:00
stevenfuhr 995cf902fb fix: add pfxBase64 to jsonCert instead of params (#1746) 2025-02-07 12:33:20 -08:00
Yury Semikhatsky 87152ecc71 devops: make publish step type: releaseJob (#1744) 2025-02-06 11:28:50 -08:00
Yury Semikhatsky dbc0478e40 chore: roll 1.50.1-beta (#1739) 2025-02-03 09:22:37 -08:00
dependabot[bot] 9eb1db9034 chore(deps): bump com.google.code.gson:gson from 2.11.0 to 2.12.1 in the all group (#1738) 2025-02-03 08:27:25 -08:00
Max Schmitt 4820088457 fix(urlMatcher): normalize URLs to align with Node.js parser behavior (#1734) 2025-01-27 10:26:39 -08:00
Max Schmitt fcd0444c57 fix(webSocketRoute): resolve URL against baseURL (#1722) 2025-01-27 15:43:47 +01:00
Yury Semikhatsky 067e69f339 chore: roll 1.50.0 (#1733) 2025-01-23 12:14:30 -08:00
Yury Semikhatsky 015939b150 feat: roll driver to 1.50 alpha (#1729) 2025-01-17 09:21:19 -08:00
Adam Gastineau eb8cf62d74 fix(tracing): Properly log Clock calls (#1727) 2025-01-16 07:04:53 -08:00
dependabot[bot] 308b9913e7 chore(deps): bump the all group with 5 updates (#1723) 2025-01-02 10:36:48 -08:00
Max Schmitt 6b621ce6f7 chore: refactor UrlMatcher (#1720) 2024-12-28 10:47:01 +01:00
Yury Semikhatsky 42d0203b49 fix: waitForCondition should not call predicate after it returned true (#1721) 2024-12-19 12:25:42 -08:00
Max Schmitt c591a1470a test: do not create stray files when running tests (#1718) 2024-12-18 11:49:18 +01:00
dependabot[bot] eb08046e94 chore(deps): bump the all group with 2 updates (#1710) 2024-12-02 10:49:48 -08:00
Yury Semikhatsky 2ff37da5f5 chore: mark 1.50 snapshot (#1702) 2024-11-18 17:01:09 -08:00
Yury Semikhatsky ee99afc3a3 chore: print actual message for TestBrowserContextCDPSession.shouldDe… (#1701) 2024-11-18 16:25:10 -08:00
Yury Semikhatsky 34017a26a3 chore: roll 1.49.0 (#1700) 2024-11-18 15:25:34 -08:00
Yury Semikhatsky 29f58a5840 test: unflake TestPageClock (#1699) 2024-11-15 14:38:02 -08:00
Yury Semikhatsky d2d78a7299 chore: stop using microsoft/playwright-github-action@v1 (#1698) 2024-11-15 11:20:37 -08:00
Yury Semikhatsky 6e66ee7c35 chore: roll 1.49-beta (#1697) 2024-11-15 09:24:21 -08:00
dependabot[bot] 4bda800e11 chore(deps): bump the all group with 4 updates (#1695) 2024-11-14 08:54:00 -08:00
Max Schmitt 2cce9776be devops: stop publishing Ubuntu 20.04 (#1690) 2024-10-21 17:00:06 +02:00
Yury Semikhatsky 20b13ad0c0 chore: roll driver 1.48.1 (#1687) 2024-10-17 12:06:13 -07:00
dependabot[bot] 08ac52ca53 chore(deps): bump the all group with 4 updates (#1680) 2024-10-17 11:56:19 -07:00
Yury Semikhatsky 9c220cd359 test: update web socket tests to properly dispatch ws messages (#1683) 2024-10-03 11:19:15 -07:00
Yury Semikhatsky ab443d1638 chore: roll 1.48 beta driver (#1681) 2024-10-02 14:21:13 -07:00
Max Schmitt 186aede95c devops: use wget for driver downloads (#1679) 2024-10-01 09:39:38 +02:00
Yury Semikhatsky db52fa94e7 docs: remove snapshot badge from README 2024-09-12 17:04:25 -07:00
Yury Semikhatsky ee18e1a499 chore: roll 1.47.0-beta-1726138322000 (#1672) 2024-09-12 15:02:49 -07:00
Yury Semikhatsky 9cf4bf2263 docs: Update ROLLING.md with new roll script 2024-09-12 12:03:29 -07:00
Yury Semikhatsky 36350f3c5c chore: roll 1.47.0 (#1670) 2024-09-09 11:06:17 -07:00
Max Schmitt f3476c68ff test: fix client-certificate tests (#1669) 2024-09-09 09:05:37 -07:00
Max Schmitt 8fd8f1c831 test: remove per-context proxy hacks for Windows/Chromium (#1668) 2024-09-09 10:09:00 +02:00
dependabot[bot] 7d2066693b chore(deps): bump the all group with 2 updates (#1664) 2024-09-03 08:55:04 -07:00
Yury Semikhatsky 6b01b878cb chore: roll driver to 1.47.0 alpha 2024 08 28 (#1663) 2024-08-28 16:02:34 -07:00
uchagani 2e32eb704f feat(junit): Implement automatic saving of traces and screenshots via fixtures (#1560) 2024-08-28 13:52:07 -07:00
Chris Kocel b81b144680 fix: null check in ListenerCollection notify method (#1661) 2024-08-28 13:50:55 -07:00
Yury Semikhatsky 256e41a505 docs: add SUPPORT.md (#1662) 2024-08-28 11:09:38 -07:00
Yury Semikhatsky 3054364101 devops(dependabot): update all deps in single PR monthly (#1656) 2024-08-19 13:21:31 -07:00
dependabot[bot] 40b8802874 chore(deps): bump org.apache.maven.plugins:maven-deploy-plugin from 3.1.2 to 3.1.3 (#1654) 2024-08-19 12:11:29 -07:00
dependabot[bot] 49f53fceaf chore(deps): bump org.apache.maven.plugins:maven-surefire-plugin from 3.3.1 to 3.4.0 (#1653) 2024-08-19 12:11:13 -07:00
dependabot[bot] 291c12a54c chore(deps): bump org.apache.maven.plugins:maven-install-plugin from 3.1.2 to 3.1.3 (#1655) 2024-08-19 12:11:01 -07:00
dependabot[bot] 2b3413fad4 chore(deps): bump junit.version from 5.10.3 to 5.11.0 (#1652) 2024-08-19 11:16:59 -07:00
dependabot[bot] 3eab530e95 chore(deps): bump org.apache.maven.plugins:maven-gpg-plugin from 3.2.4 to 3.2.5 (#1650) 2024-08-19 11:16:40 -07:00
Max Schmitt 7d35be4f89 devops: publish Ubuntu 24.04 Docker image (#1649) 2024-08-12 19:02:17 +02:00
Yury Semikhatsky 800c2e9c71 fix: support ControlOrMeta action modifier (#1644)
Fixes: https://github.com/microsoft/playwright-java/issues/1643
2024-08-07 17:08:33 -07:00
Meir Blachman edf8174581 docs: add discord link in readme (#1642) 2024-08-06 15:54:40 -07:00
Yury Semikhatsky 8ae67204eb test: disable failing certificate tests on webkit mac (#1641) 2024-08-06 13:08:26 -07:00
Yury Semikhatsky 5f2540e556 chore: roll driver to 1.46.0 (#1640) 2024-08-05 17:46:01 -07:00
Yury Semikhatsky 776e3f26ef chore: serizlize java Exception <==> javascript Error (#1639) 2024-08-02 10:32:18 -07:00
Yury Semikhatsky 46f4ac1f33 chore: roll driver to 1.46.0-beta (#1638) 2024-07-31 13:58:01 -07:00
dependabot[bot] 73d22552e6 chore(deps): bump org.apache.maven.plugins:maven-javadoc-plugin from 3.7.0 to 3.8.0 (#1631) 2024-07-23 15:03:57 -07:00
Thomas Fowler 650419c952 fix: Replaced println with logMessage in DriverJar (#1627) 2024-07-23 10:29:15 -07:00
dependabot[bot] 1e8adde480 chore(deps): bump org.apache.maven.plugins:maven-surefire-plugin from 3.3.0 to 3.3.1 (#1625) 2024-07-15 13:45:23 -07:00
dependabot[bot] a9e1242aef chore(deps-dev): bump org.java-websocket:Java-WebSocket from 1.5.6 to 1.5.7 (#1626) 2024-07-15 13:45:11 -07:00
dependabot[bot] a222edee5a chore(deps): bump junit.version from 5.10.2 to 5.10.3 (#1616) 2024-07-11 08:59:14 -07:00
Yury Semikhatsky 7a12897be4 feat: support java.time.OffsetDateTime in post data (#1624)
Fixes https://github.com/microsoft/playwright-java/issues/1623
2024-07-11 08:58:46 -07:00
Yury Semikhatsky 7fa8081032 devops: test on java 21 which is latest lts (#1613) 2024-06-27 14:37:55 -07:00
Yury Semikhatsky 212bf981f7 chore: bump dev version to 1.46.0-SNAPSHOT (#1612) 2024-06-27 13:28:56 -07:00
Yury Semikhatsky d9ac70c66b chore: roll driver to 1.45.0-beta (#1610) 2024-06-27 13:22:47 -07:00
dependabot[bot] 4279a4ef3e chore(deps): bump org.apache.maven.plugins:maven-jar-plugin from 3.4.1 to 3.4.2 (#1604) 2024-06-27 10:15:05 -07:00
dependabot[bot] 141afb1f09 chore(deps): bump org.apache.maven.plugins:maven-clean-plugin from 3.3.2 to 3.4.0 (#1603) 2024-06-27 10:09:04 -07:00
dependabot[bot] 2eaae58659 chore(deps): bump org.apache.maven.plugins:maven-surefire-plugin from 3.2.5 to 3.3.0 (#1599) 2024-06-27 10:08:52 -07:00
Yury Semikhatsky e4828d00b6 chore: roll 1.45.0 (#1609) 2024-06-25 09:18:12 -07:00
Yury Semikhatsky a08ab2dcae devops: roll_driver script (#1605) 2024-06-25 08:34:20 -07:00
Jason Wu 626050d988 fix: Update README.md to fix wrong demo code (#1601)
Update README.md to fix wrong demo code

In Mobile and geolocation demo, code `page.click("a[data-original-title=\"Show My Location\"]");` will cause error while running.Because the website already change the attribute.
2024-06-19 09:24:45 -07:00
Yury Semikhatsky 4cc3fa3012 chore: roll driver to 1.45.0 beta, implement new features (#1600) 2024-06-18 17:08:45 -07:00
dependabot[bot] 226d075355 chore(deps): bump org.apache.maven.plugins:maven-javadoc-plugin from 3.6.3 to 3.7.0 (#1589) 2024-06-03 15:42:10 -07:00
Yury Semikhatsky 0a759e699e fix(fetch): support json objects with null values (#1587)
Fixes https://github.com/microsoft/playwright-java/issues/1585
2024-05-30 14:46:17 -07:00
Yury Semikhatsky f2a17b6255 devops: install msedge on macosx (#1581) 2024-05-20 16:21:27 -07:00
dependabot[bot] 202bc80d76 chore(deps): bump com.google.code.gson:gson from 2.10.1 to 2.11.0 (#1580) 2024-05-20 16:07:03 -07:00
Yury Semikhatsky 731d8e8dc2 chore: bump dev version to 1.45.0-SNAPSHOT (#1579) 2024-05-17 09:12:49 -07:00
Yury Semikhatsky 75062c4024 chore: roll 1.44.0 (#1575) 2024-05-08 12:10:05 -07:00
Yury Semikhatsky c9ea56a640 devops: stop producing .sha256 files, they are not required anymore (#1570) 2024-05-03 11:03:45 -07:00
Max Schmitt e4c427aa75 devops: fix ESRP publishing (#1569) 2024-05-03 10:55:40 -07:00
Max Schmitt 0471c5e86c devops: update to EsrpRelease@7 (#1566) 2024-05-03 09:03:41 -07:00
Yury Semikhatsky 5636edf69a test: ControlOrMeta modifier (#1564) 2024-05-02 16:30:27 -07:00
dependabot[bot] abfe50ce59 chore(deps): bump org.apache.maven.plugins:maven-jar-plugin from 3.3.0 to 3.4.1 (#1556) 2024-05-01 15:15:23 -07:00
Yury Semikhatsky d72364627b chore: roll driver to 1.44.0-beta-1714435420000 (#1563) 2024-04-30 08:57:03 -07:00
dependabot[bot] fe51fb4cf6 chore(deps): bump org.apache.maven.plugins:maven-install-plugin from 3.1.1 to 3.1.2 (#1562) 2024-04-29 17:02:12 -07:00
dependabot[bot] 764cc8cc8a chore(deps): bump org.apache.maven.plugins:maven-deploy-plugin from 3.1.1 to 3.1.2 (#1561) 2024-04-29 17:01:59 -07:00
Yury Semikhatsky 7b8efadc57 chore: roll driver, implement new features (#1559) 2024-04-27 08:46:04 -07:00
uchagani a654a4234e feat(junit): add ability to connect to remote browsers via fixtures (#1541) 2024-04-22 13:09:33 -07:00
dependabot[bot] 102d337b4a chore(deps): bump org.apache.maven.plugins:maven-gpg-plugin from 3.2.3 to 3.2.4 (#1555) 2024-04-22 11:29:53 -07:00
Max Schmitt f5f9b8a12d devops: migrate to OIDC for Docker publishing (#1554) 2024-04-19 00:13:33 +02:00
Max Schmitt 2f264eab76 fix(cdpSession): events without payload (#1553) 2024-04-17 09:10:55 -07:00
dependabot[bot] 5c17cc49ed chore(deps): bump org.apache.maven.plugins:maven-gpg-plugin from 3.2.2 to 3.2.3 (#1550) 2024-04-16 10:57:00 -07:00
Max Schmitt 8652942482 fix(driver): consider PLAYWRIGHT_NODEJS_PATH from host env (#1552) 2024-04-16 19:54:15 +02:00
dependabot[bot] 2829a37d58 chore(deps): bump org.apache.maven.plugins:maven-source-plugin from 3.3.0 to 3.3.1 (#1543) 2024-04-10 09:19:51 -07:00
296 changed files with 17262 additions and 4428 deletions
+46 -30
View File
@@ -1,6 +1,10 @@
trigger: none
pr: none pr: none
trigger:
tags:
include:
- '*'
resources: resources:
repositories: repositories:
- repository: 1esPipelines - repository: 1esPipelines
@@ -25,11 +29,16 @@ extends:
stages: stages:
- stage: Stage - stage: Stage
jobs: jobs:
- job: HostJob - job: Build
templateContext:
outputs:
- output: pipelineArtifact
path: $(Build.ArtifactStagingDirectory)/esrp-build
artifact: esrp-build
steps: steps:
- bash: | - bash: |
if [[ ! "$CURRENT_BRANCH" =~ ^release-.* ]]; then if [[ ! "$CURRENT_BRANCH" =~ ^v1\..* ]]; then
echo "Can only publish from a release branch." echo "Can only publish from a release tag branch (v1.*)."
echo "Unexpected branch name: $CURRENT_BRANCH" echo "Unexpected branch name: $CURRENT_BRANCH"
exit 1 exit 1
fi fi
@@ -47,35 +56,42 @@ extends:
GPG_PRIVATE_KEY_BASE64: $(GPG_PRIVATE_KEY_BASE64) # secret variable has to be mapped to an env variable GPG_PRIVATE_KEY_BASE64: $(GPG_PRIVATE_KEY_BASE64) # secret variable has to be mapped to an env variable
displayName: "Import gpg key" displayName: "Import gpg key"
- bash: ./scripts/download_driver_for_all_platforms.sh - bash: ./scripts/download_driver.sh
displayName: 'Download driver' 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 - bash: mvn -B deploy -D skipTests --no-transfer-progress --activate-profiles release -D gpg.passphrase=$GPG_PASSPHRASE -DaltDeploymentRepository=snapshot-repo::default::file:$(Build.ArtifactStagingDirectory)/esrp-build
displayName: 'Build and deploy to a local directory' displayName: 'Build and deploy to a local directory'
env: env:
GPG_PASSPHRASE: $(GPG_PASSPHRASE) # secret variable has to be mapped to an env variable GPG_PASSPHRASE: $(GPG_PASSPHRASE) # secret variable has to be mapped to an env variable
- bash: | - job: Publish
for file in $(find snapshots -type f); do dependsOn: Build
echo "processing: $file" templateContext:
if [[ $file =~ \.(md5|sha1|sha256)$ ]]; then type: releaseJob
continue isProduction: true
fi
sha256sum "$file" | cut -f1 -d \ > "$file.sha256"
done
displayName: 'Create .sha256 files'
- task: EsrpRelease@4
inputs: inputs:
ConnectedServiceName: 'Playwright-ESRP' - input: pipelineArtifact
Intent: 'PackageDistribution' artifactName: esrp-build
ContentType: 'Maven' targetPath: $(Build.ArtifactStagingDirectory)/esrp-build
ContentSource: 'Folder' steps:
FolderLocation: './local-build' - checkout: none
WaitForReleaseCompletion: true - task: EsrpRelease@9
Owners: 'yurys@microsoft.com' inputs:
Approvers: 'maxschmitt@microsoft.com' connectedservicename: 'Playwright-ESRP-PME'
ServiceEndpointUrl: 'https://api.esrp.microsoft.com' usemanagedidentity: true
MainPublisher: 'Playwright' keyvaultname: 'playwright-esrp-pme'
DomainTenantId: '72f988bf-86f1-41af-91ab-2d7cd011db47' signcertname: 'ESRP-Release-Sign'
displayName: 'ESRP Release to Maven' clientid: '13434a40-7de4-4c23-81a3-d843dc81c2c5'
intent: 'PackageDistribution'
contenttype: 'Maven'
# Keeping it commented out as a workaround for:
# https://portal.microsofticm.com/imp/v3/incidents/incident/499972482/summary
# contentsource: 'folder'
folderlocation: '$(Build.ArtifactStagingDirectory)/esrp-build'
waitforreleasecompletion: true
owners: 'yurys@microsoft.com'
approvers: 'yurys@microsoft.com'
serviceendpointurl: 'https://api.esrp.microsoft.com'
mainpublisher: 'Playwright'
domaintenantid: '975f013f-7f24-47e8-a7d3-abc4752bf346'
displayName: 'ESRP Release to Maven'
@@ -0,0 +1,76 @@
---
name: playwright-java-release
description: Prepare a Playwright Java release after the rolling PR has merged — cut the release branch, mark the Maven version, draft the GitHub release, and tick the Java boxes in the internal checklist.
---
Use this skill once the `chore: roll driver to 1.X.0` PR has merged into `main` and the upstream JS `v1.X.0` is published. The rolling work itself is covered by the [[playwright-roll]] skill.
Throughout this doc, replace `X` with the minor version (e.g. `60` for `1.60.0`) and `<user>` with the fork owner (`gh api user --jq .login`).
The full release checklist lives in the private `microsoft/playwright-internal` repo as the `v1.X checklist` issue. Find its number once:
```bash
unset GITHUB_TOKEN
ISSUE=$(gh search issues --repo microsoft/playwright-internal "v1.X checklist" --json number --jq '.[0].number')
```
Tick each Java box incrementally (one PATCH per item) so the issue reflects accurate state if the flow is interrupted:
```bash
gh api repos/microsoft/playwright-internal/issues/$ISSUE --jq '.body' > /tmp/body.md
# edit /tmp/body.md to flip "- [ ]" → "- [x]" on the relevant Java item
gh api repos/microsoft/playwright-internal/issues/$ISSUE -X PATCH --field body=@/tmp/body.md
```
## 1. Cut the release branch
Push `release-1.X` from current `upstream/main` (which now contains the merged roll commit):
```bash
git fetch upstream main
git push upstream upstream/main:refs/heads/release-1.X
```
## 2. Draft the GitHub release
Generate the release notes from the upstream docs:
```bash
cd ~/playwright
node utils/render_release_notes.mjs java 1.X > /tmp/v1.X.0-release-notes.md
```
The renderer leaves JS-isms that need fixing for Java. Apply these substitutions — the list is not exhaustive, eyeball the diff before publishing:
- `toMatchAriaSnapshot()``matchesAriaSnapshot()`
- `toHaveCSS()``hasCSS()` (and other `toHaveX` matchers → `hasX`)
- `browser.on('context')``browser.onContext()`
- `browserContext.on('download' | 'frameattached' | ...)``browserContext.onDownload()` / `onFrameAttached()` / …
Create the draft directly against `release-1.X` — drafting against `main` and retargeting later is fragile because every `gh release edit` rotates the `untagged-<hash>` ID:
```bash
gh release create v1.X.0 --repo microsoft/playwright-java --draft \
--title "v1.X.0" --notes-file /tmp/v1.X.0-release-notes.md --target release-1.X
```
## 3. Bump the Maven version on the release branch
Cut `mark-v-1.X.0` off `upstream/release-1.X`, run `set_maven_version.sh`, and PR back to the release branch:
```bash
git checkout -b mark-v-1.X.0 upstream/release-1.X
./scripts/set_maven_version.sh 1.X.0
git add -u
git commit -m "chore: mark 1.X.0"
git push -u origin mark-v-1.X.0
gh pr create --repo microsoft/playwright-java --head <user>:mark-v-1.X.0 --base release-1.X \
--title "chore: mark 1.X.0" \
--body "Updates Maven version in all modules to \`1.X.0\` for the v1.X release."
```
`set_maven_version.sh` only invokes `mvn versions:set` on `pom.xml`, `tools/*/pom.xml`, and `examples/pom.xml`, but the root invocation cascades through the reactor, so the expected diff is 11 poms: root + `driver/` + `driver-bundle/` + `playwright/` (from the reactor cascade) + 6 under `tools/` + `examples/`, all flipping `1.<prev>.0-SNAPSHOT``1.X.0`. Any other file in the diff is a red flag.
## 4. Publish
The user publishes the draft release manually once the `mark-v-1.X.0` PR is merged. After publishing, CI pushes the artifacts to Maven Central and runs the Docker workflow automatically: https://github.com/microsoft/playwright-java/actions.
+166
View File
@@ -0,0 +1,166 @@
---
name: playwright-roll
description: Roll Playwright Java to a new version
---
Help the user roll to a new version of Playwright.
ROLLING.md contains general instructions and scripts.
Start with running ./scripts/roll_driver.sh to update the version and generate the API to see the state of things.
Afterwards, walk through the upstream changes that affect the Java client and port the relevant ones.
## Determining what to port
List the upstream commits that touched a client-relevant path since the last release. The paths cover everything that can change the public Java surface or the wire protocol:
- `docs/src/api/` — the source of truth for `api.json`. Method/option additions, removals, and `langs:` filter changes flow from here.
- `packages/playwright-core/src/client/` — the JS client implementation that the Java client mirrors.
- `packages/isomorphic/` — selector engines, locator generation/parsing, and aria-snapshot logic shared between client and server. Changes here can affect client-side helpers like `getByRoleSelector`.
- `packages/playwright/src/matchers/matchers.ts` — assertion-method definitions. Changes here usually correspond to new options on `LocatorAssertions` / `PageAssertions`.
- `packages/protocol/src/protocol.yml` — the wire protocol schema. Method/event additions, parameter renames, and result-shape changes affect what the Java `*Impl` classes need to send/receive.
```bash
cd ~/playwright
PREV_TAG=$(git tag | grep -E '^v1\.[0-9]+\.[0-9]+$' | sort -V | tail -1) # e.g. v1.59.1
git log "$PREV_TAG"..HEAD --oneline -- \
'docs/src/api/' \
'packages/playwright-core/src/client/' \
'packages/isomorphic/' \
'packages/playwright/src/matchers/matchers.ts' \
'packages/protocol/src/protocol.yml'
```
Walk that list top-to-bottom (oldest-first is easier — newest is at top, so reverse). For each commit:
1. Read the commit (`git show <sha>`) to see what client/protocol/docs changed.
2. If it's JS-internal (bundling, dispatcher conventions, electron, mcp, dashboard, trace-viewer, test-runner) — skip.
3. If it touches `docs/src/api/` or types, check `langs:` annotations — features marked `langs: js`/`langs: js, python` don't apply to Java.
4. If it adds/changes a public API method or option that applies to Java, port it. The api.json regenerated by `roll_driver.sh` already contains the new types/options, so the generated Java interfaces usually pick them up automatically — what's typically missing is the `*Impl` wiring.
5. Watch for follow-up reverts — a "feat: X" commit might be undone by a later "Revert X". Check whether the change still exists in HEAD before porting.
6. Maintain a running notes file (e.g. `/tmp/roll-notes.md`) listing each upstream PR as ported / skipped / verified-already-supported, with a one-line reason. This file becomes the body of the eventual PR.
## What to include in the rolling PR
- Driver version bump
- Generated interface diffs from `roll_driver.sh`
- `*Impl` wiring for each ported feature
- Generator updates (import lists, special-cases) if new types appeared
- A small test per new public API surface — listener for new events, basic call for new methods, regression for changed return types
- PR description: list each upstream PR ported, each skipped (with reason), and each verified-already-supported
Rolling includes:
- updating client implementation to match changes in the upstream JS implementation (see ../playwright/packages/playwright-core/src/client)
- adding a couple of new tests to verify new/changed functionality
## Mimicking the JavaScript implementation
The Java client is a port of the JS client in `../playwright/packages/playwright-core/src/client/`. When implementing a new or changed method, always read the corresponding JS file first and mirror its logic:
```
../playwright/packages/playwright-core/src/client/browserContext.ts
../playwright/packages/playwright-core/src/client/page.ts
../playwright/packages/playwright-core/src/client/tracing.ts
../playwright/packages/playwright-core/src/client/video.ts
../playwright/packages/playwright-core/src/client/locator.ts
../playwright/packages/playwright-core/src/client/network.ts
...
```
Key translation rules:
**Protocol calls**`await this._channel.methodName(params)``sendMessage("methodName", params, NO_TIMEOUT)`
**Extracting a returned channel object from a result** — JS uses `SomeClass.from(result.foo)` which resolves the JS-side object for a channel reference. In Java, the object was already created when the server sent `__create__`, so extract it from the connection: `connection.getExistingObject(result.getAsJsonObject("foo").get("guid").getAsString())`
**Async/await** — all `await` calls become synchronous `sendMessage(...)` calls since the Java client is synchronous.
**`undefined` / optional params** — JS `options?.foo` checks translate to `if (options != null && options.foo != null)` null checks before adding to the params `JsonObject`.
**`_channel` fields** — the JS `this._channel.foo` maps to calling `sendMessage("foo", ...)` on `this` in the Impl class.
**Channel object references in params** — when a JS call passes a channel object as a param (e.g. `{ frame: frame._channel }`), in Java pass the guid: `params.addProperty("frame", ((FrameImpl) frame).guid)`.
## Fixing generator and compilation errors
After running `./scripts/roll_driver.sh`, the build often fails because the generated Java interfaces reference new types or methods that the generator doesn't know how to handle yet, and the `*Impl` classes don't implement new interface methods.
### ApiGenerator.java fixes (tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java)
The generator has hardcoded lists that control which imports are added to each generated file. When new classes appear in the API, add them to the relevant lists in `Interface.writeTo`:
- `options.*` import list — add new classes that use types from the options package
- `java.util.*` import list — add new classes that use `List`, `Map`, etc.
- `java.util.function.Consumer` list — add new classes with `Consumer`-typed event handlers
Type mapping: when JS-only types (like `Disposable`) are used as return types in Java-compatible methods, add a mapping in `convertBuiltinType`. For example, `Disposable``AutoCloseable`.
Event handler generation: events with `void` type generate invalid `Consumer<void>`. Handle this case in `Event.writeListenerMethods` by emitting `Runnable` instead.
After editing the generator, recompile and re-run it:
```
mvn -f tools/api-generator/pom.xml compile -q
mvn -f tools/api-generator/pom.xml exec:java -Dexec.mainClass=com.microsoft.playwright.tools.ApiGenerator
```
### Impl class fixes (playwright/src/main/java/com/microsoft/playwright/impl/)
After regenerating, compile `playwright/` to find what's missing:
```
mvn -f playwright/pom.xml compile 2>&1 | grep "ERROR"
```
Common patterns:
**Return type changed (e.g. `void` → `AutoCloseable`):** Update the method signature in the Impl class and return an appropriate `AutoCloseable`. Check the JS client to see what kind of disposable is used:
- If JS returns `DisposableObject.from(result.disposable)` — the server created a disposable channel object. Extract its guid from the protocol result and return `connection.getExistingObject(guid)` (a `DisposableObject`).
- If JS returns `new DisposableStub(() => this.someCleanup())` — it's a local callback. Return `new DisposableStub(this::someCleanup)` in Java.
- Examples: `addInitScript`/`exposeBinding`/`exposeFunction``DisposableObject`; `route(...)``DisposableStub(() -> unroute(...))`; `Tracing.group``DisposableStub(this::groupEnd)`; `Video.start``DisposableStub(this::stop)`.
**New method missing:** Add a stub implementation. Common patterns:
- Simple protocol message: `sendMessage("methodName", params, NO_TIMEOUT)`
- New property accessor (e.g. from initializer): `return initializer.get("fieldName").getAsString()`
- Delegation to mainFrame (for Page methods): `return mainFrame.locator(":root").method(...)`
**New interface entirely (e.g. `Debugger`):** Create a new `*Impl` class extending `ChannelOwner`, implement the interface, and register the type in `Connection.java`'s switch statement. Initialize the field from the parent's initializer in the parent's constructor (e.g. `connection.getExistingObject(initializer.getAsJsonObject("debugger").get("guid").getAsString())`).
**Field visibility:** If a field needs to be accessed from a sibling Impl class (e.g. setting `existingResponse` on `RequestImpl` from `BrowserContextImpl`), change it from `private` to package-private.
**`ListenerCollection` only supports `Consumer<T>`, not `Runnable`.** For void events that use `Runnable` handlers, maintain a plain `List<Runnable>` instead.
**Protocol changes that remove events** — when a method's response now returns an object directly instead of via a subsequent event, update the Impl to capture it from the `sendMessage` result and remove the old event handler. Example: `videoStart` used to fire a `"video"` page event to deliver the artifact; it now returns the artifact directly in the response. Check git history of the upstream JS client when tests hang unexpectedly.
**Protocol parameter renames** — protocol parameter names can change between versions (e.g. `wsEndpoint``endpoint` in `BrowserType.connect`). When a test fails with `expected string, got undefined` or similar validation errors from the driver, check `packages/protocol/src/protocol.yml` for the current parameter names and update the corresponding `params.addProperty(...)` call in the Impl class. Also check the JS client (`src/client/`) to see how it builds the params object.
## Rebuilding the driver-bundle after a roll
`./scripts/roll_driver.sh` does the whole roll pipeline end-to-end: bumps `DRIVER_VERSION`, downloads new driver files into `driver-bundle/src/main/resources/driver/<platform>/`, regenerates `api.json` and the Java interfaces, and updates the README. When all of that succeeds, the next `mvn` invocation that touches `driver-bundle` will pick up the new files and you don't need to think about it.
But if any step in the pipeline fails (the very common case is the API generator throwing on a new type — see *Fixing generator and compilation errors*), the run aborts before `driver-bundle/target/classes/` has been refreshed. From that point on, until you manually rebuild `driver-bundle`, the test JVM will load the **old** driver from the cached `target/classes`/installed jar even though the source resources have already been swapped to the new version.
Fix — rebuild `driver-bundle` once before re-running tests:
```
mvn -f driver-bundle/pom.xml install -DskipTests
```
## Porting and verifying tests
**Before porting an upstream test file, check the API exists in Java.** The upstream repo may have test files for brand-new APIs that haven't been added to the Java interface yet (e.g., `screencast.spec.ts` tests `page.screencast` which may not be in the generated `Page.java`). Check `git diff main --name-only` to see what interfaces were added this roll, and verify the method exists in the generated Java interface before porting.
**Java test file names don't always match upstream spec names.** `TestScreencast.java` tests `recordVideo` video-file recording (which corresponds to `video.spec.ts`), not the newer `page.screencast` streaming API (`screencast.spec.ts`). When comparing coverage, check test *content*, not just file names.
**Remove tests for behavior that was removed upstream.** When the JS client drops a client-side error check (e.g., "Page is not yet closed before saveAs", "Page did not produce any video frames"), delete the corresponding Java tests rather than trying to keep them passing. Check the upstream `tests/library/` spec to confirm the behavior is gone.
**Run the full suite to catch regressions, re-run flaky failures in isolation.** Some tests (e.g., `TestClientCertificates#shouldKeepSupportingHttp`) time out only under heavy parallel load. Run the failing test alone to confirm it's flaky before investigating further.
## Diagnosing hanging tests
When `mvn test` hangs and surefire eventually times the JVM out, it writes thread dumps to `playwright/target/surefire-reports/<timestamp>-jvmRun*.dump`. To find the stuck test:
```
grep "com.microsoft.playwright.Test" playwright/target/surefire-reports/*-jvmRun1.dump | sort -u
```
Each line is a stack frame inside a test method — typically you'll see one or two test methods blocked on a `Future.get()`, `waitForCondition`, or similar. That's the hanging test.
When you've identified a hanging test:
1. Run it in isolation: `mvn -f playwright/pom.xml test -Dtest='TestClass#testMethod'`. If it passes alone, it's a parallel-load flake — note it but move on.
2. If it still hangs in isolation, look for a recent fix in the upstream repo for the *same* test name. Use `git log --oneline tests/library/<spec>.spec.ts` in `~/playwright`. Upstream fixes for client-side hangs are often small and portable (e.g. `about:blank``server.EMPTY_PAGE` from microsoft/playwright#39840 fixed `route-web-socket.spec.ts` arraybuffer hangs — apparently some browser changed the WebSocket origin policy on `about:blank`).
3. When porting an upstream fix, mirror the helper signature change rather than hard-coding workarounds. E.g. if upstream added a `server` parameter to `setupWS`, do the same in Java by injecting `Server server` via the JUnit fixture (`@FixtureTest` already wires up `ServerLifecycle`, so adding `Server server` to the test method signature is enough — no class-level boilerplate). Watch for local-variable shadowing when you add a `Server server` parameter to a method that already has a `WebSocketRoute server` local; rename the local.
+28
View File
@@ -0,0 +1,28 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/java
{
"name": "Java",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/java:1-21-bookworm",
"features": {
"ghcr.io/devcontainers/features/java:1": {
"version": "none",
"installGradle": "false",
"installMaven": "true"
},
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
}
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "java -version",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
+18 -1
View File
@@ -3,8 +3,25 @@ updates:
- package-ecosystem: "maven" - package-ecosystem: "maven"
directory: "/" # Location of the pom.xml file directory: "/" # Location of the pom.xml file
schedule: schedule:
interval: "weekly" interval: "monthly"
open-pull-requests-limit: 10 open-pull-requests-limit: 10
groups:
# Create a group of dependencies to be updated together in one pull request
all:
applies-to: version-updates
patterns:
- "*"
update-types:
- "minor"
- "patch"
allow: allow:
- dependency-type: "direct" # Optional: Only update direct dependencies - dependency-type: "direct" # Optional: Only update direct dependencies
- dependency-type: "indirect" # Optional: Only update indirect (transitive) dependencies - dependency-type: "indirect" # Optional: Only update indirect (transitive) dependencies
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
groups:
actions:
patterns:
- "*"
+14 -15
View File
@@ -3,29 +3,28 @@ on:
release: release:
types: [published] types: [published]
workflow_dispatch: workflow_dispatch:
inputs:
is_release:
required: true
type: boolean
description: "Is this a release image?"
jobs: jobs:
publish-canary-docker: publish-canary-docker:
name: publish to DockerHub name: publish to DockerHub
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
environment: Docker
if: github.repository == 'microsoft/playwright-java' if: github.repository == 'microsoft/playwright-java'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: azure/docker-login@v1 - name: Azure login
uses: azure/login@v3
with: with:
login-server: playwright.azurecr.io client-id: ${{ secrets.AZURE_DOCKER_CLIENT_ID }}
username: playwright tenant-id: ${{ secrets.AZURE_DOCKER_TENANT_ID }}
password: ${{ secrets.DOCKER_PASSWORD }} subscription-id: ${{ secrets.AZURE_DOCKER_SUBSCRIPTION_ID }}
- name: Login to ACR via OIDC
run: az acr login --name playwright
- name: Set up Docker QEMU for arm64 docker builds - name: Set up Docker QEMU for arm64 docker builds
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
with: with:
platforms: arm64 platforms: arm64
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- run: ./utils/docker/publish_docker.sh stable - 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')
+32 -15
View File
@@ -8,6 +8,8 @@ on:
branches: branches:
- main - main
- release-* - release-*
env:
PW_MAX_RETRIES: 3
jobs: jobs:
dev: dev:
timeout-minutes: 30 timeout-minutes: 30
@@ -16,20 +18,29 @@ jobs:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
browser: [chromium, firefox, webkit] browser: [chromium, firefox, webkit]
exclude:
# macos-latest is the free M1 runner (3 vCPU / 7 GB); WebKit needs more headroom.
# Upstream's webkit matrix runs on macos-15-xlarge for the same reason.
- os: macos-latest
browser: webkit
include:
- os: macos-15-xlarge
browser: webkit
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v6
- uses: microsoft/playwright-github-action@v1
- name: Set up JDK 1.8 - name: Set up JDK 1.8
uses: actions/setup-java@v2 uses: actions/setup-java@v5
with: with:
distribution: zulu distribution: zulu
java-version: 8 java-version: 8
- name: Download drivers - name: Download drivers
shell: bash shell: bash
run: scripts/download_driver_for_all_platforms.sh run: scripts/download_driver.sh
- name: Build & Install - name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress run: mvn -B install -D skipTests --no-transfer-progress
- name: Install browsers
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps" -f playwright/pom.xml --no-transfer-progress
- name: Run tests - name: Run tests
run: mvn test --no-transfer-progress --fail-at-end -D org.slf4j.simpleLogger.showDateTime=true -D org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss run: mvn test --no-transfer-progress --fail-at-end -D org.slf4j.simpleLogger.showDateTime=true -D org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss
env: env:
@@ -62,29 +73,34 @@ jobs:
browser-channel: msedge browser-channel: msedge
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v6
- uses: microsoft/playwright-github-action@v1
- name: Install Media Pack - name: Install Media Pack
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
shell: powershell shell: powershell
run: Install-WindowsFeature Server-Media-Foundation run: Install-WindowsFeature Server-Media-Foundation
- name: Set up JDK 1.8 - name: Set up JDK 1.8
uses: actions/setup-java@v2 uses: actions/setup-java@v5
with: with:
distribution: zulu distribution: zulu
java-version: 8 java-version: 8
- name: Download drivers - name: Download drivers
shell: bash shell: bash
run: scripts/download_driver_for_all_platforms.sh run: scripts/download_driver.sh
- name: Build & Install - name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress run: mvn -B install -D skipTests --no-transfer-progress
- name: Install browsers
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps" -f playwright/pom.xml --no-transfer-progress
- name: Install MS Edge
if: matrix.browser-channel == 'msedge' && matrix.os == 'macos-latest'
shell: bash
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install msedge" -f playwright/pom.xml
- name: Run tests - name: Run tests
run: mvn test --no-transfer-progress --fail-at-end -D org.slf4j.simpleLogger.showDateTime=true -D org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss run: mvn test --no-transfer-progress --fail-at-end -D org.slf4j.simpleLogger.showDateTime=true -D org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss
env: env:
BROWSER: chromium BROWSER: chromium
BROWSER_CHANNEL: ${{ matrix.browser-channel }} BROWSER_CHANNEL: ${{ matrix.browser-channel }}
Java_17: Java_21:
timeout-minutes: 30 timeout-minutes: 30
strategy: strategy:
fail-fast: false fail-fast: false
@@ -92,18 +108,19 @@ jobs:
browser: [chromium, firefox, webkit] browser: [chromium, firefox, webkit]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v6
- uses: microsoft/playwright-github-action@v1 - name: Set up JDK 21
- name: Set up JDK 17 uses: actions/setup-java@v5
uses: actions/setup-java@v2
with: with:
distribution: adopt distribution: adopt
java-version: 17 java-version: 21
- name: Download drivers - name: Download drivers
shell: bash shell: bash
run: scripts/download_driver_for_all_platforms.sh run: scripts/download_driver.sh
- name: Build & Install - name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress run: mvn -B install -D skipTests --no-transfer-progress
- name: Install browsers
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps" -f playwright/pom.xml --no-transfer-progress
- name: Run tests - name: Run tests
run: mvn test --no-transfer-progress --fail-at-end run: mvn test --no-transfer-progress --fail-at-end
env: env:
+3 -3
View File
@@ -13,15 +13,15 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v6
- name: Cache Maven packages - name: Cache Maven packages
uses: actions/cache@v2 uses: actions/cache@v5
with: with:
path: ~/.m2 path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2 restore-keys: ${{ runner.os }}-m2
- name: Download drivers - name: Download drivers
run: scripts/download_driver_for_all_platforms.sh run: scripts/download_driver.sh
- name: Intall Playwright - name: Intall Playwright
run: mvn install -D skipTests --no-transfer-progress run: mvn install -D skipTests --no-transfer-progress
- name: Test CLI - name: Test CLI
+50 -8
View File
@@ -11,7 +11,7 @@ on:
paths: paths:
- .github/workflows/test_docker.yml - .github/workflows/test_docker.yml
- '**/Dockerfile*' - '**/Dockerfile*'
- scripts/CLI_VERSION - scripts/DRIVER_VERSION
- '**/pom.xml' - '**/pom.xml'
branches: branches:
- main - main
@@ -20,16 +20,58 @@ jobs:
test: test:
name: Test name: Test
timeout-minutes: 120 timeout-minutes: 120
runs-on: ubuntu-22.04 runs-on: ${{ matrix.runs-on }}
env:
PW_MAX_RETRIES: 3
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
flavor: [focal, jammy] flavor: [jammy, noble]
runs-on: [ubuntu-24.04, ubuntu-24.04-arm]
include:
- runs-on: ubuntu-24.04
arch: amd64
- runs-on: ubuntu-24.04-arm
arch: arm64
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v6
- name: Build Docker image - name: Build Docker image
run: bash utils/docker/build.sh --amd64 ${{ matrix.flavor }} playwright-java:localbuild-${{ matrix.flavor }}
- name: Test
run: | run: |
CONTAINER_ID="$(docker run --rm --ipc=host -v $(pwd):/root/playwright --name playwright-docker-test -d -t playwright-java:localbuild-${{ matrix.flavor }} /bin/bash)" bash utils/docker/build.sh --${{ matrix.arch }} ${{ matrix.flavor }} playwright-java:localbuild-${{ matrix.flavor }}
docker exec "${CONTAINER_ID}" /root/playwright/tools/test-local-installation/create_project_and_run_tests.sh - name: Start container
run: |
CONTAINER_ID=$(docker run \
--rm \
--name playwright-docker-test \
--platform linux/${{ matrix.arch }} \
--user=pwuser \
--workdir /home/pwuser \
--shm-size=2g \
-e CI \
-e PW_MAX_RETRIES \
-d -t \
playwright-java:localbuild-${{ matrix.flavor }} /bin/bash)
echo "CONTAINER_ID=$CONTAINER_ID" >> $GITHUB_ENV
- name: Copy repository inside docker container
run: |
docker cp . "$CONTAINER_ID":/home/pwuser/playwright
# /root/.m2 was populated as root during image build; move it to
# pwuser so the locally-installed SNAPSHOT artifacts resolve.
docker exec --user root "$CONTAINER_ID" bash -c '
chown -R pwuser /home/pwuser/playwright
mv /root/.m2 /home/pwuser/.m2
chown -R pwuser /home/pwuser/.m2
'
- name: Run smoke tests in container
run: |
docker exec "$CONTAINER_ID" /home/pwuser/playwright/tools/test-local-installation/create_project_and_run_tests.sh -Dgroups=smoke
- name: Test ClassLoader
run: |
docker exec "${CONTAINER_ID}" /home/pwuser/playwright/tools/test-spring-boot-starter/package_and_run_async_test.sh
- name: Stop container
run: |
docker stop "$CONTAINER_ID"
@@ -1,21 +0,0 @@
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 }}
+6 -3
View File
@@ -19,12 +19,15 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v6
- uses: microsoft/playwright-github-action@v1
- name: Download drivers - name: Download drivers
run: scripts/download_driver_for_all_platforms.sh run: scripts/download_driver.sh
- name: Regenerate APIs - name: Regenerate APIs
run: scripts/generate_api.sh run: scripts/generate_api.sh
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Install browsers
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps" -f playwright/pom.xml --no-transfer-progress
- name: Update browser versions in README - name: Update browser versions in README
run: scripts/update_readme.sh run: scripts/update_readme.sh
- name: Verify API is up to date - name: Verify API is up to date
+48
View File
@@ -0,0 +1,48 @@
# Playwright Java
The Java client is a port of the JavaScript client in `../playwright/packages/playwright-core/src/client/`. When implementing or changing a method, read the corresponding JS file first and mirror its logic.
Project checkouts (including the upstream `playwright` repo) live in the parent directory (`../`). Use the `gh` cli to interact with GitHub.
## Commit Convention
Semantic commit messages: `label(scope): description`
Labels: `fix`, `feat`, `chore`, `docs`, `test`, `devops`
```bash
git checkout -b fix-39562
# ... make changes ...
git add <changed-files>
git commit -m "$(cat <<'EOF'
fix(proxy): handle SOCKS proxy authentication
Fixes: https://github.com/microsoft/playwright-java/issues/39562
EOF
)"
# **Never `git push` without an explicit instruction to push.**
git push origin fix-39562
gh pr create --repo microsoft/playwright-java --head <user>:fix-39562 \
--title "fix(proxy): handle SOCKS proxy authentication" \
--body "$(cat <<'EOF'
## Summary
- <describe the change very! briefly>
Fixes https://github.com/microsoft/playwright-java/issues/39562
EOF
)"
```
Never add Co-Authored-By agents in commit message.
Never add "Generated with" in commit message.
Never add test plan to PR description. Keep PR description short — a few bullet points at most.
Branch naming for issue fixes: `fix-<issue-number>`.
**Never amend commits.** Always create a new commit for follow-up changes, even when iterating on an open PR. Amending rewrites history and forces a force-push, losing the incremental review trail. Only amend if the user explicitly says so.
**Never `git push` without an explicit instruction to push.** Applies even when a PR is already open for the branch — additional commits are immediately visible to reviewers. Commit locally, report what was committed, and wait. Only push when the user's message contains "push", "upload", "create PR", "ship it", or equivalent.
## Skills
- **playwright-roll** (`.claude/skills/playwright-roll/SKILL.md`) — roll Playwright Java to a new upstream version: bump the driver, regenerate the API, and port relevant upstream changes.
- **playwright-java-release** (`.claude/skills/playwright-java-release/SKILL.md`) — prepare a release after the rolling PR merges: cut the release branch, mark the Maven version, and draft the GitHub release.
+8 -15
View File
@@ -20,13 +20,13 @@ git clone https://github.com/microsoft/playwright-java
cd playwright-java cd playwright-java
``` ```
2. Run the following script to download playwright-cli binaries for all platforms into `driver-bundle/src/main/resources/driver/` directory (browser binaries for Chromium, Firefox and WebKit will be automatically downloaded later on first Playwright run). 2. Run the following script to download and assemble the Playwright driver for all platforms into `driver-bundle/src/main/resources/driver/` directory (browser binaries for Chromium, Firefox and WebKit will be automatically downloaded later on first Playwright run).
```bash ```bash
scripts/download_driver_for_all_platforms.sh scripts/download_driver.sh
``` ```
Names of published driver archives can be found at https://github.com/microsoft/playwright-cli/actions Each driver is assembled from the [`playwright-core`](https://www.npmjs.com/package/playwright-core) npm package (version pinned in [scripts/DRIVER_VERSION](scripts/DRIVER_VERSION)) and the matching Node.js binary from https://nodejs.org, the same way the upstream Playwright build does it.
### Building and running the tests with Maven ### Building and running the tests with Maven
@@ -34,32 +34,25 @@ Names of published driver archives can be found at https://github.com/microsoft/
mvn compile mvn compile
mvn test mvn test
# Executing a single test # Executing a single test
BROWSER=chromium mvn test --projects=playwright -Dtest=TestPageNetworkSizes#shouldHaveTheCorrectResponseBodySize BROWSER=chromium mvn test -Dtest=TestPageNetworkSizes#shouldHaveTheCorrectResponseBodySize
# Executing a single test class # Executing a single test class
BROWSER=chromium mvn test --projects=playwright -Dtest=TestPageNetworkSizes BROWSER=chromium mvn test -Dtest=TestPageNetworkSizes
``` ```
### Generating API ### Generating API
Public Java API is generated from api.json which is produced by `playwright-cli print-api-json`. To regenerate Public Java API is generated from api.json, which is generated from the upstream Playwright source at the exact commit that produced the driver version in [scripts/DRIVER_VERSION](scripts/DRIVER_VERSION) (resolved via `npm view playwright@<version> gitHead`). `scripts/generate_api.sh` fetches a minimal upstream checkout automatically; set `PW_SRC_DIR` to reuse an existing `microsoft/playwright` checkout instead. To regenerate Java interfaces for the current driver run:
Java interfaces for the current driver run the following commands:
```bash ```bash
./scripts/download_driver_for_all_platforms.sh
./scripts/generate_api.sh ./scripts/generate_api.sh
``` ```
#### Updating driver version #### 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: Versions of published driver archives can be found in [publish canary](https://github.com/microsoft/playwright/actions/workflows/publish_canary.yml) and [publish release](https://github.com/microsoft/playwright/actions/workflows/publish_release_driver.yml) actions logs. To update the driver to a particular version run the following command:
```bash ```bash
cat > scripts/CLI_VERSION scripts/roll_driver.sh [version]
<paste new version>
^D
./scripts/download_driver_for_all_platforms.sh -f
./scripts/generate_api.sh
./scripts/update_readme.sh
``` ```
### Code Style ### Code Style
+3
View File
@@ -0,0 +1,3 @@
path_classifiers:
tests:
- "playwright/src/test/**"
+14 -146
View File
@@ -2,8 +2,7 @@
[![javadoc](https://javadoc.io/badge2/com.microsoft.playwright/playwright/javadoc.svg)](https://javadoc.io/doc/com.microsoft.playwright/playwright) [![javadoc](https://javadoc.io/badge2/com.microsoft.playwright/playwright/javadoc.svg)](https://javadoc.io/doc/com.microsoft.playwright/playwright)
[![maven version](https://img.shields.io/maven-central/v/com.microsoft.playwright/playwright)](https://search.maven.org/search?q=com.microsoft.playwright) [![maven version](https://img.shields.io/maven-central/v/com.microsoft.playwright/playwright)](https://search.maven.org/search?q=com.microsoft.playwright)
[![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/https/oss.sonatype.org/com.microsoft.playwright/playwright.svg)](https://oss.sonatype.org/content/repositories/snapshots/com/microsoft/playwright/playwright/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
[![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://aka.ms/playwright-slack)
#### [Website](https://playwright.dev/java/) | [API reference](https://www.javadoc.io/doc/com.microsoft.playwright/playwright/latest/index.html) #### [Website](https://playwright.dev/java/) | [API reference](https://www.javadoc.io/doc/com.microsoft.playwright/playwright/latest/index.html)
@@ -11,59 +10,19 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| | Linux | macOS | Windows | | | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: | | :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->124.0.6367.18<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Chromium <!-- GEN:chromium-version -->149.0.7827.55<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->17.4<!-- GEN:stop --> | ✅ | ✅ | ✅ | | WebKit <!-- GEN:webkit-version -->26.5<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->124.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox <!-- GEN:firefox-version -->151.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/intro#system-requirements) for details. ## Documentation
* [Usage](#usage) [https://playwright.dev/java/docs/intro](https://playwright.dev/java/docs/intro)
- [Add Maven dependency](#add-maven-dependency)
- [Is Playwright thread-safe?](#is-playwright-thread-safe)
* [Examples](#examples)
- [Page screenshot](#page-screenshot)
- [Mobile and geolocation](#mobile-and-geolocation)
- [Evaluate JavaScript in browser](#evaluate-javascript-in-browser)
- [Intercept network requests](#intercept-network-requests)
* [Documentation](#documentation)
* [Contributing](#contributing)
* [Is Playwright for Java ready?](#is-playwright-for-java-ready)
## Usage ## API Reference
Playwright requires **Java 8** or newer. [https://playwright.dev/java/docs/api/class-playwright](https://playwright.dev/java/docs/api/class-playwright)
#### Add Maven dependency ## Example
Playwright is distributed as a set of [Maven](https://maven.apache.org/what-is-maven.html) modules. The easiest way to use it is to add one dependency to your Maven `pom.xml` file as described below. If you're not familiar with Maven please refer to its [documentation](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html).
To run Playwright simply add following dependency to your Maven project:
```xml
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.41.0</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.41.0'
}
```
#### Is Playwright thread-safe?
No, Playwright is not thread safe, i.e. all its methods as well as methods on all objects created by it (such as BrowserContext, Browser, Page etc.) are expected to be called on the same thread where Playwright object was created or proper synchronization should be implemented to ensure only one thread calls Playwright methods at any given time. Having said that it's okay to create multiple Playwright instances each on its own thread.
## Examples
You can find Maven project with the examples [here](./examples).
#### Page screenshot
This code snippet navigates to Playwright homepage 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.
@@ -95,100 +54,9 @@ public class PageScreenshot {
} }
``` ```
#### Mobile and geolocation ## Other languages
This snippet emulates Mobile Chromium on a device at a given geolocation, navigates to openstreetmap.org, performs action and takes a screenshot. More comfortable in another programming language? [Playwright](https://playwright.dev) is also available in
- [Node.js (JavaScript / TypeScript)](https://playwright.dev/docs/intro),
```java - [Python](https://playwright.dev/python/docs/intro).
import com.microsoft.playwright.options.*; - [.NET](https://playwright.dev/dotnet/docs/intro),
import com.microsoft.playwright.*;
import java.nio.file.Paths;
import static java.util.Arrays.asList;
public class MobileAndGeolocation {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setUserAgent("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36")
.setViewportSize(411, 731)
.setDeviceScaleFactor(2.625)
.setIsMobile(true)
.setHasTouch(true)
.setLocale("en-US")
.setGeolocation(41.889938, 12.492507)
.setPermissions(asList("geolocation")));
Page page = context.newPage();
page.navigate("https://www.openstreetmap.org/");
page.click("a[data-original-title=\"Show My Location\"]");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("colosseum-pixel2.png")));
}
}
}
```
#### Evaluate JavaScript in browser
This code snippet navigates to example.com in Firefox, and executes a script in the page context.
```java
import com.microsoft.playwright.*;
public class EvaluateInBrowserContext {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.firefox().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://www.example.com/");
Object dimensions = page.evaluate("() => {\n" +
" return {\n" +
" width: document.documentElement.clientWidth,\n" +
" height: document.documentElement.clientHeight,\n" +
" deviceScaleFactor: window.devicePixelRatio\n" +
" }\n" +
"}");
System.out.println(dimensions);
}
}
}
```
#### Intercept network requests
This code snippet sets up request routing for a WebKit page to log all network requests.
```java
import com.microsoft.playwright.*;
public class InterceptNetworkRequests {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.webkit().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.route("**", route -> {
System.out.println(route.request().url());
route.resume();
});
page.navigate("http://todomvc.com");
}
}
}
```
## Documentation
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).
## Contributing
Follow [the instructions](https://github.com/microsoft/playwright-java/blob/main/CONTRIBUTING.md#getting-code) to build the project from source and install the driver.
## Is Playwright for Java ready?
Yes, Playwright for Java is ready. v1.10.0 is the first stable release. Going forward we will adhere to [semantic versioning](https://semver.org/) of the API.
+2 -15
View File
@@ -2,19 +2,6 @@
* make sure to have at least Java 8 and Maven 3.6.3 * make sure to have at least Java 8 and Maven 3.6.3
* clone playwright for java: http://github.com/microsoft/playwright-java * clone playwright for java: http://github.com/microsoft/playwright-java
* set new driver version in `scripts/CLI_VERSION` * roll the driver and update generated sources: `./scripts/roll_driver.sh next`
* regenerate API: `./scripts/download_driver_for_all_platforms.sh -f && ./scripts/generate_api.sh && ./scripts/update_readme.sh` * fix any errors
* commit & send PR with the roll * commit & send PR with the roll
### Finding driver version
For development versions of Playwright, you can find the latest version by looking at [publish_canary](https://github.com/microsoft/playwright/actions/workflows/publish_canary.yml) workflow -> `publish canary NPM & Publish canary Docker` -> `build & publish driver` step -> `PACKAGE_VERSION`
<img width="960" alt="image" src="https://github.com/microsoft/playwright-java/assets/9798949/4f33a7f1-b39a-4179-8ae7-fb1d84094c75">
# Updating Version
```bash
./scripts/set_maven_version.sh 1.15.0
```
+17
View File
@@ -0,0 +1,17 @@
# Support
## How to file issues and get help
This project uses GitHub issues to track bugs and feature requests. Please search the [existing issues][gh-issues] before filing new ones to avoid duplicates. For new issues, file your bug or feature request as a new issue using corresponding template.
For help and questions about using this project, please see the [docs site for Playwright for Java][docs].
Join our community [Discord Server][discord-server] to connect with other developers using Playwright and ask questions in our 'help-playwright' forum.
## Microsoft Support Policy
Support for Playwright for Java is limited to the resources listed above.
[gh-issues]: https://github.com/microsoft/playwright-java/issues/
[docs]: https://playwright.dev/java/
[discord-server]: https://aka.ms/playwright/discord
+1 -1
View File
@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>com.microsoft.playwright</groupId> <groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId> <artifactId>parent-pom</artifactId>
<version>1.43.0-SNAPSHOT</version> <version>1.50.0-SNAPSHOT</version>
</parent> </parent>
<artifactId>driver-bundle</artifactId> <artifactId>driver-bundle</artifactId>
@@ -74,7 +74,7 @@ public class DriverJar extends Driver {
skip = System.getenv(PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD); skip = System.getenv(PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD);
} }
if (skip != null && !"0".equals(skip) && !"false".equals(skip)) { if (skip != null && !"0".equals(skip) && !"false".equals(skip)) {
System.out.println("Skipping browsers download because `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` env variable is set"); logMessage("Skipping browsers download because `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` env variable is set");
return; return;
} }
if (env.get(SELENIUM_REMOTE_URL) != null || System.getenv(SELENIUM_REMOTE_URL) != null) { if (env.get(SELENIUM_REMOTE_URL) != null || System.getenv(SELENIUM_REMOTE_URL) != null) {
@@ -114,7 +114,7 @@ public class DriverJar extends Driver {
} }
public static URI getDriverResourceURI() throws URISyntaxException { public static URI getDriverResourceURI() throws URISyntaxException {
ClassLoader classloader = Thread.currentThread().getContextClassLoader(); ClassLoader classloader = DriverJar.class.getClassLoader();
return classloader.getResource("driver/" + platformDir()).toURI(); return classloader.getResource("driver/" + platformDir()).toURI();
} }
+1 -1
View File
@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>com.microsoft.playwright</groupId> <groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId> <artifactId>parent-pom</artifactId>
<version>1.43.0-SNAPSHOT</version> <version>1.50.0-SNAPSHOT</version>
</parent> </parent>
<artifactId>driver</artifactId> <artifactId>driver</artifactId>
@@ -18,7 +18,6 @@ package com.microsoft.playwright.impl.driver;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@@ -30,7 +29,7 @@ import static com.microsoft.playwright.impl.driver.DriverLogging.logWithTimestam
* loaded from the driver-bundle module if that module is in the classpath. * loaded from the driver-bundle module if that module is in the classpath.
*/ */
public abstract class Driver { public abstract class Driver {
protected final Map<String, String> env = new LinkedHashMap<>(); protected final Map<String, String> env = new LinkedHashMap<>(System.getenv());
public static final String PLAYWRIGHT_NODEJS_PATH = "PLAYWRIGHT_NODEJS_PATH"; public static final String PLAYWRIGHT_NODEJS_PATH = "PLAYWRIGHT_NODEJS_PATH";
private static Driver instance; private static Driver instance;
+3 -2
View File
@@ -6,16 +6,17 @@
<groupId>org.example</groupId> <groupId>org.example</groupId>
<artifactId>examples</artifactId> <artifactId>examples</artifactId>
<version>1.43.0-SNAPSHOT</version> <version>1.50.0-SNAPSHOT</version>
<name>Playwright Client Examples</name> <name>Playwright Client Examples</name>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<playwright.version>1.61.0</playwright.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>com.microsoft.playwright</groupId> <groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId> <artifactId>playwright</artifactId>
<version>1.41.0</version> <version>${playwright.version}</version>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
+14 -1
View File
@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>com.microsoft.playwright</groupId> <groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId> <artifactId>parent-pom</artifactId>
<version>1.43.0-SNAPSHOT</version> <version>1.50.0-SNAPSHOT</version>
</parent> </parent>
<artifactId>playwright</artifactId> <artifactId>playwright</artifactId>
@@ -57,10 +57,23 @@
<groupId>org.java-websocket</groupId> <groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId> <artifactId>Java-WebSocket</artifactId>
</dependency> </dependency>
<!--
The following slf4j-simple dependency resolves the warning:
'SLF4J(W): No SLF4J providers were found.'
This warning is produced by the org.java-websocket library.
-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId> <artifactId>junit-jupiter-engine</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.opentest4j</groupId> <groupId>org.opentest4j</groupId>
<artifactId>opentest4j</artifactId> <artifactId>opentest4j</artifactId>
@@ -41,10 +41,32 @@ public interface APIRequest {
* </ul> * </ul>
*/ */
public String baseURL; public String baseURL;
/**
* TLS Client Authentication allows the server to request a client certificate and verify it.
*
* <p> <strong>Details</strong>
*
* <p> An array of client certificates to be used. Each certificate object must have either both {@code certPath} and {@code
* keyPath}, a single {@code pfxPath}, or their corresponding direct value equivalents ({@code cert} and {@code key}, or
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
public List<ClientCertificate> clientCertificates;
/** /**
* An object containing additional HTTP headers to be sent with every request. Defaults to none. * An object containing additional HTTP headers to be sent with every request. Defaults to none.
*/ */
public Map<String, String> extraHTTPHeaders; public Map<String, String> extraHTTPHeaders;
/**
* Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
*/
public Boolean failOnStatusCode;
/** /**
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If * 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. * no origin is specified, the username and password are sent to any servers upon unauthorized responses.
@@ -54,6 +76,12 @@ public interface APIRequest {
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
*/ */
public Boolean ignoreHTTPSErrors; public Boolean ignoreHTTPSErrors;
/**
* 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. This can be overwritten for each request
* individually.
*/
public Integer maxRedirects;
/** /**
* Network proxy settings. * Network proxy settings.
*/ */
@@ -100,6 +128,27 @@ public interface APIRequest {
this.baseURL = baseURL; this.baseURL = baseURL;
return this; return this;
} }
/**
* TLS Client Authentication allows the server to request a client certificate and verify it.
*
* <p> <strong>Details</strong>
*
* <p> An array of client certificates to be used. Each certificate object must have either both {@code certPath} and {@code
* keyPath}, a single {@code pfxPath}, or their corresponding direct value equivalents ({@code cert} and {@code key}, or
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
public NewContextOptions setClientCertificates(List<ClientCertificate> clientCertificates) {
this.clientCertificates = clientCertificates;
return this;
}
/** /**
* An object containing additional HTTP headers to be sent with every request. Defaults to none. * An object containing additional HTTP headers to be sent with every request. Defaults to none.
*/ */
@@ -107,6 +156,13 @@ public interface APIRequest {
this.extraHTTPHeaders = extraHTTPHeaders; this.extraHTTPHeaders = extraHTTPHeaders;
return this; return this;
} }
/**
* Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
*/
public NewContextOptions setFailOnStatusCode(boolean failOnStatusCode) {
this.failOnStatusCode = failOnStatusCode;
return this;
}
/** /**
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If * 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. * no origin is specified, the username and password are sent to any servers upon unauthorized responses.
@@ -129,6 +185,15 @@ public interface APIRequest {
this.ignoreHTTPSErrors = ignoreHTTPSErrors; this.ignoreHTTPSErrors = ignoreHTTPSErrors;
return this; 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. This can be overwritten for each request
* individually.
*/
public NewContextOptions setMaxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
return this;
}
/** /**
* Network proxy settings. * Network proxy settings.
*/ */
@@ -23,33 +23,57 @@ 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 * 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. * environment or the service to your e2e test.
* *
* <p> Each Playwright browser context has associated with it {@code APIRequestContext} instance which shares cookie storage * <p> Each Playwright browser context has an associated {@code APIRequestContext}, accessible via {@link
* with the browser context and can be accessed via {@link com.microsoft.playwright.BrowserContext#request * com.microsoft.playwright.BrowserContext#request BrowserContext.request()} or {@link
* BrowserContext.request()} or {@link com.microsoft.playwright.Page#request Page.request()}. It is also possible to create * com.microsoft.playwright.Page#request Page.request()} (these return the
* a new APIRequestContext instance manually by calling {@link com.microsoft.playwright.APIRequest#newContext *
* APIRequest.newContext()}. * <p> **same instance** — {@code page.request} is a shortcut for {@code page.context().request}). You can also create a
* standalone, isolated instance with {@link com.microsoft.playwright.APIRequest#newContext APIRequest.newContext()}.
* *
* <p> <strong>Cookie management</strong> * <p> <strong>Cookie management</strong>
* *
* <p> {@code APIRequestContext} returned by {@link com.microsoft.playwright.BrowserContext#request BrowserContext.request()} * <p> The {@code APIRequestContext} returned by {@link com.microsoft.playwright.BrowserContext#request
* and {@link com.microsoft.playwright.Page#request Page.request()} shares cookie storage with the corresponding {@code * BrowserContext.request()} and
* 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 * <p> {@link com.microsoft.playwright.Page#request Page.request()} uses the same cookie jar as its {@code BrowserContext}:
* calling {@link com.microsoft.playwright.APIRequest#newContext APIRequest.newContext()}. Such {@code APIRequestContext} *
* object will have its own isolated cookie storage. * <p> If you want API requests that do **not** share cookies with the browser, create an isolated context via {@link
* com.microsoft.playwright.APIRequest#newContext APIRequest.newContext()}. Such {@code APIRequestContext} object will have
* its own isolated cookie storage.
*/ */
public interface APIRequestContext { public interface APIRequestContext {
class DisposeOptions {
/**
* The reason to be reported to the operations interrupted by the context disposal.
*/
public String reason;
/**
* The reason to be reported to the operations interrupted by the context disposal.
*/
public DisposeOptions setReason(String reason) {
this.reason = reason;
return this;
}
}
class StorageStateOptions { class StorageStateOptions {
/**
* Set to {@code true} to include IndexedDB in the storage state snapshot.
*/
public Boolean indexedDB;
/** /**
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current * The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. * working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
*/ */
public Path path; public Path path;
/**
* Set to {@code true} to include IndexedDB in the storage state snapshot.
*/
public StorageStateOptions setIndexedDB(boolean indexedDB) {
this.indexedDB = indexedDB;
return this;
}
/** /**
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current * The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. * working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
@@ -88,13 +112,25 @@ public interface APIRequestContext {
* *
* @since v1.16 * @since v1.16
*/ */
void dispose(); default void dispose() {
dispose(null);
}
/**
* All responses returned by {@link com.microsoft.playwright.APIRequestContext#get APIRequestContext.get()} and similar
* methods are stored in the memory, so that you can later call {@link com.microsoft.playwright.APIResponse#body
* APIResponse.body()}.This method discards all its resources, calling any method on disposed {@code APIRequestContext}
* will throw an exception.
*
* @since v1.16
*/
void dispose(DisposeOptions options);
/** /**
* Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update * 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. JSON objects can be passed directly * context cookies from the response. The method will automatically follow redirects.
* to the request.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
*
* <p> JSON objects can be passed directly to the request:
* <pre>{@code * <pre>{@code
* Map<String, Object> data = new HashMap(); * Map<String, Object> data = new HashMap();
* data.put("title", "Book Title"); * data.put("title", "Book Title");
@@ -102,8 +138,8 @@ public interface APIRequestContext {
* request.fetch("https://example.com/api/createBook", RequestOptions.create().setMethod("post").setData(data)); * request.fetch("https://example.com/api/createBook", RequestOptions.create().setMethod("post").setData(data));
* }</pre> * }</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} * <p> The common way to send file(s) in the body of a request is to upload them as form fields with {@code
* encoding. You can achieve that with Playwright API like this: * multipart/form-data} encoding, by specifiying the {@code multipart} parameter:
* <pre>{@code * <pre>{@code
* // Pass file path to the form data constructor: * // Pass file path to the form data constructor:
* Path file = Paths.get("team.csv"); * Path file = Paths.get("team.csv");
@@ -114,7 +150,7 @@ public interface APIRequestContext {
* // Or you can pass the file content directly as FilePayload object: * // Or you can pass the file content directly as FilePayload object:
* FilePayload filePayload = new FilePayload("f.js", "text/javascript", * FilePayload filePayload = new FilePayload("f.js", "text/javascript",
* "console.log(2022);".getBytes(StandardCharsets.UTF_8)); * "console.log(2022);".getBytes(StandardCharsets.UTF_8));
* APIResponse response = request.fetch("https://example.com/api/uploadTeamList", * APIResponse response = request.fetch("https://example.com/api/uploadScript",
* RequestOptions.create().setMethod("post").setMultipart( * RequestOptions.create().setMethod("post").setMultipart(
* FormData.create().set("fileField", filePayload))); * FormData.create().set("fileField", filePayload)));
* }</pre> * }</pre>
@@ -127,10 +163,11 @@ public interface APIRequestContext {
} }
/** /**
* Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update * 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. JSON objects can be passed directly * context cookies from the response. The method will automatically follow redirects.
* to the request.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
*
* <p> JSON objects can be passed directly to the request:
* <pre>{@code * <pre>{@code
* Map<String, Object> data = new HashMap(); * Map<String, Object> data = new HashMap();
* data.put("title", "Book Title"); * data.put("title", "Book Title");
@@ -138,8 +175,8 @@ public interface APIRequestContext {
* request.fetch("https://example.com/api/createBook", RequestOptions.create().setMethod("post").setData(data)); * request.fetch("https://example.com/api/createBook", RequestOptions.create().setMethod("post").setData(data));
* }</pre> * }</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} * <p> The common way to send file(s) in the body of a request is to upload them as form fields with {@code
* encoding. You can achieve that with Playwright API like this: * multipart/form-data} encoding, by specifiying the {@code multipart} parameter:
* <pre>{@code * <pre>{@code
* // Pass file path to the form data constructor: * // Pass file path to the form data constructor:
* Path file = Paths.get("team.csv"); * Path file = Paths.get("team.csv");
@@ -150,7 +187,7 @@ public interface APIRequestContext {
* // Or you can pass the file content directly as FilePayload object: * // Or you can pass the file content directly as FilePayload object:
* FilePayload filePayload = new FilePayload("f.js", "text/javascript", * FilePayload filePayload = new FilePayload("f.js", "text/javascript",
* "console.log(2022);".getBytes(StandardCharsets.UTF_8)); * "console.log(2022);".getBytes(StandardCharsets.UTF_8));
* APIResponse response = request.fetch("https://example.com/api/uploadTeamList", * APIResponse response = request.fetch("https://example.com/api/uploadScript",
* RequestOptions.create().setMethod("post").setMultipart( * RequestOptions.create().setMethod("post").setMultipart(
* FormData.create().set("fileField", filePayload))); * FormData.create().set("fileField", filePayload)));
* }</pre> * }</pre>
@@ -162,10 +199,11 @@ public interface APIRequestContext {
APIResponse fetch(String urlOrRequest, RequestOptions params); 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 * 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. JSON objects can be passed directly * context cookies from the response. The method will automatically follow redirects.
* to the request.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
*
* <p> JSON objects can be passed directly to the request:
* <pre>{@code * <pre>{@code
* Map<String, Object> data = new HashMap(); * Map<String, Object> data = new HashMap();
* data.put("title", "Book Title"); * data.put("title", "Book Title");
@@ -173,8 +211,8 @@ public interface APIRequestContext {
* request.fetch("https://example.com/api/createBook", RequestOptions.create().setMethod("post").setData(data)); * request.fetch("https://example.com/api/createBook", RequestOptions.create().setMethod("post").setData(data));
* }</pre> * }</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} * <p> The common way to send file(s) in the body of a request is to upload them as form fields with {@code
* encoding. You can achieve that with Playwright API like this: * multipart/form-data} encoding, by specifiying the {@code multipart} parameter:
* <pre>{@code * <pre>{@code
* // Pass file path to the form data constructor: * // Pass file path to the form data constructor:
* Path file = Paths.get("team.csv"); * Path file = Paths.get("team.csv");
@@ -185,7 +223,7 @@ public interface APIRequestContext {
* // Or you can pass the file content directly as FilePayload object: * // Or you can pass the file content directly as FilePayload object:
* FilePayload filePayload = new FilePayload("f.js", "text/javascript", * FilePayload filePayload = new FilePayload("f.js", "text/javascript",
* "console.log(2022);".getBytes(StandardCharsets.UTF_8)); * "console.log(2022);".getBytes(StandardCharsets.UTF_8));
* APIResponse response = request.fetch("https://example.com/api/uploadTeamList", * APIResponse response = request.fetch("https://example.com/api/uploadScript",
* RequestOptions.create().setMethod("post").setMultipart( * RequestOptions.create().setMethod("post").setMultipart(
* FormData.create().set("fileField", filePayload))); * FormData.create().set("fileField", filePayload)));
* }</pre> * }</pre>
@@ -198,10 +236,11 @@ public interface APIRequestContext {
} }
/** /**
* Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update * 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. JSON objects can be passed directly * context cookies from the response. The method will automatically follow redirects.
* to the request.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
*
* <p> JSON objects can be passed directly to the request:
* <pre>{@code * <pre>{@code
* Map<String, Object> data = new HashMap(); * Map<String, Object> data = new HashMap();
* data.put("title", "Book Title"); * data.put("title", "Book Title");
@@ -209,8 +248,8 @@ public interface APIRequestContext {
* request.fetch("https://example.com/api/createBook", RequestOptions.create().setMethod("post").setData(data)); * request.fetch("https://example.com/api/createBook", RequestOptions.create().setMethod("post").setData(data));
* }</pre> * }</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} * <p> The common way to send file(s) in the body of a request is to upload them as form fields with {@code
* encoding. You can achieve that with Playwright API like this: * multipart/form-data} encoding, by specifiying the {@code multipart} parameter:
* <pre>{@code * <pre>{@code
* // Pass file path to the form data constructor: * // Pass file path to the form data constructor:
* Path file = Paths.get("team.csv"); * Path file = Paths.get("team.csv");
@@ -221,7 +260,7 @@ public interface APIRequestContext {
* // Or you can pass the file content directly as FilePayload object: * // Or you can pass the file content directly as FilePayload object:
* FilePayload filePayload = new FilePayload("f.js", "text/javascript", * FilePayload filePayload = new FilePayload("f.js", "text/javascript",
* "console.log(2022);".getBytes(StandardCharsets.UTF_8)); * "console.log(2022);".getBytes(StandardCharsets.UTF_8));
* APIResponse response = request.fetch("https://example.com/api/uploadTeamList", * APIResponse response = request.fetch("https://example.com/api/uploadScript",
* RequestOptions.create().setMethod("post").setMultipart( * RequestOptions.create().setMethod("post").setMultipart(
* FormData.create().set("fileField", filePayload))); * FormData.create().set("fileField", filePayload)));
* }</pre> * }</pre>
@@ -337,7 +376,8 @@ public interface APIRequestContext {
* }</pre> * }</pre>
* *
* <p> The common way to send file(s) in the body of a request is to upload them as form fields with {@code * <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: * multipart/form-data} encoding. Use {@code FormData} to construct request body and pass it to the request as {@code
* multipart} parameter:
* <pre>{@code * <pre>{@code
* // Pass file path to the form data constructor: * // Pass file path to the form data constructor:
* Path file = Paths.get("team.csv"); * Path file = Paths.get("team.csv");
@@ -346,9 +386,9 @@ public interface APIRequestContext {
* FormData.create().set("fileField", file))); * FormData.create().set("fileField", file)));
* *
* // Or you can pass the file content directly as FilePayload object: * // Or you can pass the file content directly as FilePayload object:
* FilePayload filePayload = new FilePayload("f.js", "text/javascript", * FilePayload filePayload1 = new FilePayload("f1.js", "text/javascript",
* "console.log(2022);".getBytes(StandardCharsets.UTF_8)); * "console.log(2022);".getBytes(StandardCharsets.UTF_8));
* APIResponse response = request.post("https://example.com/api/uploadTeamList", * APIResponse response = request.post("https://example.com/api/uploadScript",
* RequestOptions.create().setMultipart( * RequestOptions.create().setMultipart(
* FormData.create().set("fileField", filePayload))); * FormData.create().set("fileField", filePayload)));
* }</pre> * }</pre>
@@ -384,7 +424,8 @@ public interface APIRequestContext {
* }</pre> * }</pre>
* *
* <p> The common way to send file(s) in the body of a request is to upload them as form fields with {@code * <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: * multipart/form-data} encoding. Use {@code FormData} to construct request body and pass it to the request as {@code
* multipart} parameter:
* <pre>{@code * <pre>{@code
* // Pass file path to the form data constructor: * // Pass file path to the form data constructor:
* Path file = Paths.get("team.csv"); * Path file = Paths.get("team.csv");
@@ -393,9 +434,9 @@ public interface APIRequestContext {
* FormData.create().set("fileField", file))); * FormData.create().set("fileField", file)));
* *
* // Or you can pass the file content directly as FilePayload object: * // Or you can pass the file content directly as FilePayload object:
* FilePayload filePayload = new FilePayload("f.js", "text/javascript", * FilePayload filePayload1 = new FilePayload("f1.js", "text/javascript",
* "console.log(2022);".getBytes(StandardCharsets.UTF_8)); * "console.log(2022);".getBytes(StandardCharsets.UTF_8));
* APIResponse response = request.post("https://example.com/api/uploadTeamList", * APIResponse response = request.post("https://example.com/api/uploadScript",
* RequestOptions.create().setMultipart( * RequestOptions.create().setMultipart(
* FormData.create().set("fileField", filePayload))); * FormData.create().set("fileField", filePayload)));
* }</pre> * }</pre>
@@ -442,5 +483,11 @@ public interface APIRequestContext {
* @since v1.16 * @since v1.16
*/ */
String storageState(StorageStateOptions options); String storageState(StorageStateOptions options);
/**
*
*
* @since v1.60
*/
Tracing tracing();
} }
@@ -43,8 +43,8 @@ public interface APIResponse {
*/ */
Map<String, String> headers(); Map<String, String> headers();
/** /**
* An array with all the request HTTP headers associated with this response. Header names are not lower-cased. Headers with * An array with all the response HTTP headers associated with this response. Header names are not lower-cased. Headers
* multiple entries, such as {@code Set-Cookie}, appear in the array multiple times. * with multiple entries, such as {@code Set-Cookie}, appear in the array multiple times.
* *
* @since v1.16 * @since v1.16
*/ */
@@ -55,6 +55,20 @@ public interface APIResponse {
* @since v1.16 * @since v1.16
*/ */
boolean ok(); boolean ok();
/**
* Returns SSL and other security information. Resolves to {@code null} for non-HTTPS responses. For redirected requests,
* returns the information for the last request in the redirect chain.
*
* @since v1.61
*/
SecurityDetails securityDetails();
/**
* Returns the IP address and port of the server. Resolves to {@code null} if the server address is not available. For
* redirected requests, returns the information for the last request in the redirect chain.
*
* @since v1.61
*/
ServerAddr serverAddr();
/** /**
* Contains the status code of the response (e.g., 200 for a success). * Contains the status code of the response (e.g., 200 for a success).
* *
@@ -29,20 +29,29 @@ import java.util.regex.Pattern;
* import com.microsoft.playwright.*; * import com.microsoft.playwright.*;
* *
* public class Example { * public class Example {
* public static void main(String[] args) { * public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) { * try (Playwright playwright = Playwright.create()) {
* BrowserType firefox = playwright.firefox() * BrowserType firefox = playwright.firefox();
* Browser browser = firefox.launch(); * Browser browser = firefox.launch();
* Page page = browser.newPage(); * Page page = browser.newPage();
* page.navigate('https://example.com'); * page.navigate("https://example.com");
* browser.close(); * browser.close();
* } * }
* } * }
* } * }
* }</pre> * }</pre>
*/ */
public interface Browser extends AutoCloseable { public interface Browser extends AutoCloseable {
/**
* Emitted when a new browser context is created.
*/
void onContext(Consumer<BrowserContext> handler);
/**
* Removes handler that was previously added with {@link #onContext onContext(handler)}.
*/
void offContext(Consumer<BrowserContext> handler);
/** /**
* Emitted when Browser gets disconnected from the browser application. This might happen because of one of the following: * Emitted when Browser gets disconnected from the browser application. This might happen because of one of the following:
* <ul> * <ul>
@@ -97,11 +106,37 @@ public interface Browser extends AutoCloseable {
*/ */
public Boolean bypassCSP; public Boolean bypassCSP;
/** /**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code * TLS Client Authentication allows the server to request a client certificate and verify it.
* "no-preference"}. See {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing *
* {@code null} resets emulation to system defaults. Defaults to {@code "light"}. * <p> <strong>Details</strong>
*
* <p> An array of client certificates to be used. Each certificate object must have either both {@code certPath} and {@code
* keyPath}, a single {@code pfxPath}, or their corresponding direct value equivalents ({@code cert} and {@code key}, or
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
public List<ClientCertificate> clientCertificates;
/**
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. See {@link com.microsoft.playwright.Page#emulateMedia
* Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. Defaults to {@code
* "light"}.
*/ */
public Optional<ColorScheme> colorScheme; public Optional<ColorScheme> colorScheme;
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
* {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "no-preference"}.
*/
public Optional<Contrast> contrast;
/** /**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a * 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>. * href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
@@ -163,10 +198,6 @@ public interface Browser extends AutoCloseable {
public List<String> permissions; public List<String> permissions;
/** /**
* Network proxy settings to use with this context. Defaults to none. * Network proxy settings to use with this context. Defaults to none.
*
* <p> <strong>NOTE:</strong> For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If all contexts
* override the proxy, global proxy will be never used and can be any string, for example {@code launch({ proxy: { server:
* 'http://per-context' } })}.
*/ */
public Proxy proxy; public Proxy proxy;
/** /**
@@ -296,14 +327,46 @@ public interface Browser extends AutoCloseable {
return this; return this;
} }
/** /**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code * TLS Client Authentication allows the server to request a client certificate and verify it.
* "no-preference"}. See {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing *
* {@code null} resets emulation to system defaults. Defaults to {@code "light"}. * <p> <strong>Details</strong>
*
* <p> An array of client certificates to be used. Each certificate object must have either both {@code certPath} and {@code
* keyPath}, a single {@code pfxPath}, or their corresponding direct value equivalents ({@code cert} and {@code key}, or
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
public NewContextOptions setClientCertificates(List<ClientCertificate> clientCertificates) {
this.clientCertificates = clientCertificates;
return this;
}
/**
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. See {@link com.microsoft.playwright.Page#emulateMedia
* Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. Defaults to {@code
* "light"}.
*/ */
public NewContextOptions setColorScheme(ColorScheme colorScheme) { public NewContextOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = Optional.ofNullable(colorScheme); this.colorScheme = Optional.ofNullable(colorScheme);
return this; return this;
} }
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
* {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "no-preference"}.
*/
public NewContextOptions setContrast(Contrast contrast) {
this.contrast = Optional.ofNullable(contrast);
return this;
}
/** /**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a * 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>. * href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
@@ -411,20 +474,12 @@ public interface Browser extends AutoCloseable {
} }
/** /**
* Network proxy settings to use with this context. Defaults to none. * Network proxy settings to use with this context. Defaults to none.
*
* <p> <strong>NOTE:</strong> For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If all contexts
* override the proxy, global proxy will be never used and can be any string, for example {@code launch({ proxy: { server:
* 'http://per-context' } })}.
*/ */
public NewContextOptions setProxy(String server) { public NewContextOptions setProxy(String server) {
return setProxy(new Proxy(server)); return setProxy(new Proxy(server));
} }
/** /**
* Network proxy settings to use with this context. Defaults to none. * Network proxy settings to use with this context. Defaults to none.
*
* <p> <strong>NOTE:</strong> For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If all contexts
* override the proxy, global proxy will be never used and can be any string, for example {@code launch({ proxy: { server:
* 'http://per-context' } })}.
*/ */
public NewContextOptions setProxy(Proxy proxy) { public NewContextOptions setProxy(Proxy proxy) {
this.proxy = proxy; this.proxy = proxy;
@@ -627,11 +682,37 @@ public interface Browser extends AutoCloseable {
*/ */
public Boolean bypassCSP; public Boolean bypassCSP;
/** /**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code * TLS Client Authentication allows the server to request a client certificate and verify it.
* "no-preference"}. See {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing *
* {@code null} resets emulation to system defaults. Defaults to {@code "light"}. * <p> <strong>Details</strong>
*
* <p> An array of client certificates to be used. Each certificate object must have either both {@code certPath} and {@code
* keyPath}, a single {@code pfxPath}, or their corresponding direct value equivalents ({@code cert} and {@code key}, or
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
public List<ClientCertificate> clientCertificates;
/**
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. See {@link com.microsoft.playwright.Page#emulateMedia
* Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. Defaults to {@code
* "light"}.
*/ */
public Optional<ColorScheme> colorScheme; public Optional<ColorScheme> colorScheme;
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
* {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "no-preference"}.
*/
public Optional<Contrast> contrast;
/** /**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a * 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>. * href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
@@ -693,10 +774,6 @@ public interface Browser extends AutoCloseable {
public List<String> permissions; public List<String> permissions;
/** /**
* Network proxy settings to use with this context. Defaults to none. * Network proxy settings to use with this context. Defaults to none.
*
* <p> <strong>NOTE:</strong> For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If all contexts
* override the proxy, global proxy will be never used and can be any string, for example {@code launch({ proxy: { server:
* 'http://per-context' } })}.
*/ */
public Proxy proxy; public Proxy proxy;
/** /**
@@ -826,14 +903,46 @@ public interface Browser extends AutoCloseable {
return this; return this;
} }
/** /**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code * TLS Client Authentication allows the server to request a client certificate and verify it.
* "no-preference"}. See {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing *
* {@code null} resets emulation to system defaults. Defaults to {@code "light"}. * <p> <strong>Details</strong>
*
* <p> An array of client certificates to be used. Each certificate object must have either both {@code certPath} and {@code
* keyPath}, a single {@code pfxPath}, or their corresponding direct value equivalents ({@code cert} and {@code key}, or
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
public NewPageOptions setClientCertificates(List<ClientCertificate> clientCertificates) {
this.clientCertificates = clientCertificates;
return this;
}
/**
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. See {@link com.microsoft.playwright.Page#emulateMedia
* Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. Defaults to {@code
* "light"}.
*/ */
public NewPageOptions setColorScheme(ColorScheme colorScheme) { public NewPageOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = Optional.ofNullable(colorScheme); this.colorScheme = Optional.ofNullable(colorScheme);
return this; return this;
} }
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
* {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "no-preference"}.
*/
public NewPageOptions setContrast(Contrast contrast) {
this.contrast = Optional.ofNullable(contrast);
return this;
}
/** /**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a * 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>. * href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
@@ -941,20 +1050,12 @@ public interface Browser extends AutoCloseable {
} }
/** /**
* Network proxy settings to use with this context. Defaults to none. * Network proxy settings to use with this context. Defaults to none.
*
* <p> <strong>NOTE:</strong> For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If all contexts
* override the proxy, global proxy will be never used and can be any string, for example {@code launch({ proxy: { server:
* 'http://per-context' } })}.
*/ */
public NewPageOptions setProxy(String server) { public NewPageOptions setProxy(String server) {
return setProxy(new Proxy(server)); return setProxy(new Proxy(server));
} }
/** /**
* Network proxy settings to use with this context. Defaults to none. * Network proxy settings to use with this context. Defaults to none.
*
* <p> <strong>NOTE:</strong> For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If all contexts
* override the proxy, global proxy will be never used and can be any string, for example {@code launch({ proxy: { server:
* 'http://per-context' } })}.
*/ */
public NewPageOptions setProxy(Proxy proxy) { public NewPageOptions setProxy(Proxy proxy) {
this.proxy = proxy; this.proxy = proxy;
@@ -1130,6 +1231,44 @@ public interface Browser extends AutoCloseable {
return this; return this;
} }
} }
class BindOptions {
/**
* Host to bind the web socket server to. When specified, a web socket server is created instead of a named pipe.
*/
public String host;
/**
* Port to bind the web socket server to. When specified, a web socket server is created instead of a named pipe. Use
* {@code 0} to let the OS pick an available port.
*/
public Integer port;
/**
* Working directory associated with this browser server.
*/
public String workspaceDir;
/**
* Host to bind the web socket server to. When specified, a web socket server is created instead of a named pipe.
*/
public BindOptions setHost(String host) {
this.host = host;
return this;
}
/**
* Port to bind the web socket server to. When specified, a web socket server is created instead of a named pipe. Use
* {@code 0} to let the OS pick an available port.
*/
public BindOptions setPort(int port) {
this.port = port;
return this;
}
/**
* Working directory associated with this browser server.
*/
public BindOptions setWorkspaceDir(String workspaceDir) {
this.workspaceDir = workspaceDir;
return this;
}
}
class StartTracingOptions { class StartTracingOptions {
/** /**
* specify custom categories to use instead of default. * specify custom categories to use instead of default.
@@ -1179,10 +1318,10 @@ public interface Browser extends AutoCloseable {
* <p> In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from the * <p> In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from the
* browser server. * browser server.
* *
* <p> <strong>NOTE:</strong> This is similar to force quitting the browser. Therefore, you should call {@link * <p> <strong>NOTE:</strong> This is similar to force-quitting the browser. To close pages gracefully and ensure you receive page close events, call
* com.microsoft.playwright.BrowserContext#close BrowserContext.close()} on any {@code BrowserContext}'s you explicitly * {@link com.microsoft.playwright.BrowserContext#close BrowserContext.close()} on any {@code BrowserContext} instances you
* created earlier with {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} **before** calling {@link * explicitly created earlier using {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} **before**
* com.microsoft.playwright.Browser#close Browser.close()}. * calling {@link com.microsoft.playwright.Browser#close Browser.close()}.
* *
* <p> The {@code Browser} object itself is considered to be disposed and cannot be used anymore. * <p> The {@code Browser} object itself is considered to be disposed and cannot be used anymore.
* *
@@ -1198,10 +1337,10 @@ public interface Browser extends AutoCloseable {
* <p> In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from the * <p> In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from the
* browser server. * browser server.
* *
* <p> <strong>NOTE:</strong> This is similar to force quitting the browser. Therefore, you should call {@link * <p> <strong>NOTE:</strong> This is similar to force-quitting the browser. To close pages gracefully and ensure you receive page close events, call
* com.microsoft.playwright.BrowserContext#close BrowserContext.close()} on any {@code BrowserContext}'s you explicitly * {@link com.microsoft.playwright.BrowserContext#close BrowserContext.close()} on any {@code BrowserContext} instances you
* created earlier with {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} **before** calling {@link * explicitly created earlier using {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} **before**
* com.microsoft.playwright.Browser#close Browser.close()}. * calling {@link com.microsoft.playwright.Browser#close Browser.close()}.
* *
* <p> The {@code Browser} object itself is considered to be disposed and cannot be used anymore. * <p> The {@code Browser} object itself is considered to be disposed and cannot be used anymore.
* *
@@ -1251,7 +1390,7 @@ public interface Browser extends AutoCloseable {
* BrowserContext context = browser.newContext(); * BrowserContext context = browser.newContext();
* // Create a new page in a pristine context. * // Create a new page in a pristine context.
* Page page = context.newPage(); * Page page = context.newPage();
* page.navigate('https://example.com'); * page.navigate("https://example.com");
* *
* // Graceful close up everything * // Graceful close up everything
* context.close(); * context.close();
@@ -1278,7 +1417,7 @@ public interface Browser extends AutoCloseable {
* BrowserContext context = browser.newContext(); * BrowserContext context = browser.newContext();
* // Create a new page in a pristine context. * // Create a new page in a pristine context.
* Page page = context.newPage(); * Page page = context.newPage();
* page.navigate('https://example.com'); * page.navigate("https://example.com");
* *
* // Graceful close up everything * // Graceful close up everything
* context.close(); * context.close();
@@ -1312,6 +1451,22 @@ public interface Browser extends AutoCloseable {
* @since v1.8 * @since v1.8
*/ */
Page newPage(NewPageOptions options); Page newPage(NewPageOptions options);
/**
* Binds the browser to a named pipe or web socket, making it available for other clients to connect to.
*
* @param title Title of the browser server, used for identification.
* @since v1.59
*/
default BindResult bind(String title) {
return bind(title, null);
}
/**
* Binds the browser to a named pipe or web socket, making it available for other clients to connect to.
*
* @param title Title of the browser server, used for identification.
* @since v1.59
*/
BindResult bind(String title, BindOptions options);
/** /**
* <strong>NOTE:</strong> This API controls <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">Chromium Tracing</a> * <strong>NOTE:</strong> This API controls <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">Chromium Tracing</a>
* which is a low-level chromium-specific debugging tool. API to control <a * which is a low-level chromium-specific debugging tool. API to control <a
@@ -1326,7 +1481,7 @@ public interface Browser extends AutoCloseable {
* <pre>{@code * <pre>{@code
* browser.startTracing(page, new Browser.StartTracingOptions() * browser.startTracing(page, new Browser.StartTracingOptions()
* .setPath(Paths.get("trace.json"))); * .setPath(Paths.get("trace.json")));
* page.goto('https://www.google.com'); * page.navigate("https://www.google.com");
* browser.stopTracing(); * browser.stopTracing();
* }</pre> * }</pre>
* *
@@ -1350,7 +1505,7 @@ public interface Browser extends AutoCloseable {
* <pre>{@code * <pre>{@code
* browser.startTracing(page, new Browser.StartTracingOptions() * browser.startTracing(page, new Browser.StartTracingOptions()
* .setPath(Paths.get("trace.json"))); * .setPath(Paths.get("trace.json")));
* page.goto('https://www.google.com'); * page.navigate("https://www.google.com");
* browser.stopTracing(); * browser.stopTracing();
* }</pre> * }</pre>
* *
@@ -1373,7 +1528,7 @@ public interface Browser extends AutoCloseable {
* <pre>{@code * <pre>{@code
* browser.startTracing(page, new Browser.StartTracingOptions() * browser.startTracing(page, new Browser.StartTracingOptions()
* .setPath(Paths.get("trace.json"))); * .setPath(Paths.get("trace.json")));
* page.goto('https://www.google.com'); * page.navigate("https://www.google.com");
* browser.stopTracing(); * browser.stopTracing();
* }</pre> * }</pre>
* *
@@ -1392,6 +1547,12 @@ public interface Browser extends AutoCloseable {
* @since v1.11 * @since v1.11
*/ */
byte[] stopTracing(); byte[] stopTracing();
/**
* Unbinds the browser server previously bound with {@link com.microsoft.playwright.Browser#bind Browser.bind()}.
*
* @since v1.59
*/
void unbind();
/** /**
* Returns the browser version. * Returns the browser version.
* *
@@ -30,8 +30,9 @@ import java.util.regex.Pattern;
* <p> If a page opens another page, e.g. with a {@code window.open} call, the popup will belong to the parent page's browser * <p> If a page opens another page, e.g. with a {@code window.open} call, the popup will belong to the parent page's browser
* context. * context.
* *
* <p> Playwright allows creating "incognito" browser contexts with {@link com.microsoft.playwright.Browser#newContext * <p> Playwright allows creating isolated non-persistent browser contexts with {@link
* Browser.newContext()} method. "Incognito" browser contexts don't write any browsing data to disk. * com.microsoft.playwright.Browser#newContext Browser.newContext()} method. Non-persistent browser contexts don't write
* any browsing data to disk.
* <pre>{@code * <pre>{@code
* // Create a new incognito browser context * // Create a new incognito browser context
* BrowserContext context = browser.newContext(); * BrowserContext context = browser.newContext();
@@ -45,15 +46,7 @@ import java.util.regex.Pattern;
public interface BrowserContext extends AutoCloseable { public interface BrowserContext extends AutoCloseable {
/** /**
* <strong>NOTE:</strong> Only works with Chromium browser's persistent context. * @deprecated Background pages have been removed from Chromium together with Manifest V2 extensions.
*
* <p> Emitted when new background page is created in the context.
* <pre>{@code
* Page backgroundPage = context.waitForBackgroundPage(() -> {
* page.getByText("activate extension").click();
* });
* System.out.println(backgroundPage.evaluate("location.href"));
* }</pre>
*/ */
void onBackgroundPage(Consumer<Page> handler); void onBackgroundPage(Consumer<Page> handler);
/** /**
@@ -121,6 +114,48 @@ public interface BrowserContext extends AutoCloseable {
*/ */
void offDialog(Consumer<Dialog> handler); void offDialog(Consumer<Dialog> handler);
/**
* Emitted when attachment download started in any page belonging to this context. User can access basic file operations on
* downloaded content via the passed {@code Download} instance. See also {@link com.microsoft.playwright.Page#onDownload
* Page.onDownload()} to receive events about a specific page.
*/
void onDownload(Consumer<Download> handler);
/**
* Removes handler that was previously added with {@link #onDownload onDownload(handler)}.
*/
void offDownload(Consumer<Download> handler);
/**
* Emitted when a frame is attached in any page belonging to this context. See also {@link
* com.microsoft.playwright.Page#onFrameAttached Page.onFrameAttached()} to receive events about a specific page.
*/
void onFrameAttached(Consumer<Frame> handler);
/**
* Removes handler that was previously added with {@link #onFrameAttached onFrameAttached(handler)}.
*/
void offFrameAttached(Consumer<Frame> handler);
/**
* Emitted when a frame is detached in any page belonging to this context. See also {@link
* com.microsoft.playwright.Page#onFrameDetached Page.onFrameDetached()} to receive events about a specific page.
*/
void onFrameDetached(Consumer<Frame> handler);
/**
* Removes handler that was previously added with {@link #onFrameDetached onFrameDetached(handler)}.
*/
void offFrameDetached(Consumer<Frame> handler);
/**
* Emitted when a frame is navigated to a new url in any page belonging to this context. See also {@link
* com.microsoft.playwright.Page#onFrameNavigated Page.onFrameNavigated()} to receive events about navigations in a
* specific page.
*/
void onFrameNavigated(Consumer<Frame> handler);
/**
* Removes handler that was previously added with {@link #onFrameNavigated onFrameNavigated(handler)}.
*/
void offFrameNavigated(Consumer<Frame> handler);
/** /**
* The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event will * 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 com.microsoft.playwright.Page#onPopup Page.onPopup()} to receive events about * also fire for popup pages. See also {@link com.microsoft.playwright.Page#onPopup Page.onPopup()} to receive events about
@@ -128,7 +163,10 @@ public interface BrowserContext extends AutoCloseable {
* *
* <p> The earliest moment that page is available is when it has navigated to the initial url. For example, when opening a * <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 * 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. * "http://example.com" is done and its response has started loading in the popup. If you would like to route/listen to
* this network request, use {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} and {@link
* com.microsoft.playwright.BrowserContext#onRequest BrowserContext.onRequest()} respectively instead of similar methods on
* the {@code Page}.
* <pre>{@code * <pre>{@code
* Page newPage = context.waitForPage(() -> { * Page newPage = context.waitForPage(() -> {
* page.getByText("open new page").click(); * page.getByText("open new page").click();
@@ -145,6 +183,27 @@ public interface BrowserContext extends AutoCloseable {
*/ */
void offPage(Consumer<Page> handler); void offPage(Consumer<Page> handler);
/**
* Emitted when a page in this context is closed. See also {@link com.microsoft.playwright.Page#onClose Page.onClose()} to
* receive events about a specific page.
*/
void onPageClose(Consumer<Page> handler);
/**
* Removes handler that was previously added with {@link #onPageClose onPageClose(handler)}.
*/
void offPageClose(Consumer<Page> handler);
/**
* Emitted when the JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/Events/load">{@code load}</a> event is
* dispatched in any page belonging to this context. See also {@link com.microsoft.playwright.Page#onLoad Page.onLoad()} to
* receive events about a specific page.
*/
void onPageLoad(Consumer<Page> handler);
/**
* Removes handler that was previously added with {@link #onPageLoad onPageLoad(handler)}.
*/
void offPageLoad(Consumer<Page> handler);
/** /**
* Emitted when exception is unhandled in any of the pages in this context. To listen for errors from a particular page, * Emitted when exception is unhandled in any of the pages in this context. To listen for errors from a particular page,
* use {@link com.microsoft.playwright.Page#onPageError Page.onPageError()} instead. * use {@link com.microsoft.playwright.Page#onPageError Page.onPageError()} instead.
@@ -275,22 +334,6 @@ public interface BrowserContext extends AutoCloseable {
return this; return this;
} }
} }
class ExposeBindingOptions {
/**
* Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is
* supported. When passing by value, multiple arguments are supported.
*/
public Boolean handle;
/**
* Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is
* supported. When passing by value, multiple arguments are supported.
*/
public ExposeBindingOptions setHandle(boolean handle) {
this.handle = handle;
return this;
}
}
class GrantPermissionsOptions { class GrantPermissionsOptions {
/** /**
* The [origin] to grant permissions to, e.g. "https://example.com". * The [origin] to grant permissions to, e.g. "https://example.com".
@@ -406,12 +449,27 @@ public interface BrowserContext extends AutoCloseable {
} }
} }
class StorageStateOptions { class StorageStateOptions {
/**
* Set to {@code true} to include <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a> in
* the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase
* Authentication, enable this.
*/
public Boolean indexedDB;
/** /**
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current * The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. * working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
*/ */
public Path path; public Path path;
/**
* Set to {@code true} to include <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a> in
* the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase
* Authentication, enable this.
*/
public StorageStateOptions setIndexedDB(boolean indexedDB) {
this.indexedDB = indexedDB;
return this;
}
/** /**
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current * The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. * working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
@@ -499,6 +557,25 @@ public interface BrowserContext extends AutoCloseable {
return this; return this;
} }
} }
/**
* Playwright has ability to mock clock and passage of time.
*
* @since v1.45
*/
Clock clock();
/**
* Virtual WebAuthn authenticator for this context. Lets tests seed credentials and intercept {@code
* navigator.credentials.create()} / {@code navigator.credentials.get()} ceremonies.
*
* @since v1.61
*/
Credentials credentials();
/**
* Debugger allows to pause and resume the execution.
*
* @since v1.59
*/
Debugger debugger();
/** /**
* Adds cookies into this browser context. All pages within this context will have these cookies installed. Cookies can be * Adds cookies into this browser context. All pages within this context will have these cookies installed. Cookies can be
* obtained via {@link com.microsoft.playwright.BrowserContext#cookies BrowserContext.cookies()}. * obtained via {@link com.microsoft.playwright.BrowserContext#cookies BrowserContext.cookies()}.
@@ -508,9 +585,6 @@ public interface BrowserContext extends AutoCloseable {
* browserContext.addCookies(Arrays.asList(cookieObject1, cookieObject2)); * browserContext.addCookies(Arrays.asList(cookieObject1, cookieObject2));
* }</pre> * }</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 * @since v1.8
*/ */
void addCookies(List<Cookie> cookies); void addCookies(List<Cookie> cookies);
@@ -540,7 +614,7 @@ public interface BrowserContext extends AutoCloseable {
* @param script Script to be evaluated in all pages in the browser context. * @param script Script to be evaluated in all pages in the browser context.
* @since v1.8 * @since v1.8
*/ */
void addInitScript(String script); AutoCloseable addInitScript(String script);
/** /**
* Adds a script which would be evaluated in one of the following scenarios: * Adds a script which would be evaluated in one of the following scenarios:
* <ul> * <ul>
@@ -567,17 +641,16 @@ public interface BrowserContext extends AutoCloseable {
* @param script Script to be evaluated in all pages in the browser context. * @param script Script to be evaluated in all pages in the browser context.
* @since v1.8 * @since v1.8
*/ */
void addInitScript(Path script); AutoCloseable addInitScript(Path script);
/** /**
* <strong>NOTE:</strong> Background pages are only supported on Chromium-based browsers. * @deprecated Background pages have been removed from Chromium together with Manifest V2 extensions.
*
* <p> All existing background pages in the context.
* *
* @since v1.11 * @since v1.11
*/ */
List<Page> backgroundPages(); List<Page> backgroundPages();
/** /**
* Returns the browser instance of the context. If it was launched as a persistent context null gets returned. * Gets the browser instance that owns the context. Returns {@code null} if the context is created outside of normal
* browser, e.g. Android or Electron.
* *
* @since v1.8 * @since v1.8
*/ */
@@ -697,7 +770,7 @@ public interface BrowserContext extends AutoCloseable {
* public class Example { * public class Example {
* public static void main(String[] args) { * public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) { * try (Playwright playwright = Playwright.create()) {
* BrowserType webkit = playwright.webkit() * BrowserType webkit = playwright.webkit();
* Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false)); * Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
* BrowserContext context = browser.newContext(); * BrowserContext context = browser.newContext();
* context.exposeBinding("pageURL", (source, args) -> source.page().url()); * context.exposeBinding("pageURL", (source, args) -> source.page().url());
@@ -715,88 +788,11 @@ public interface BrowserContext extends AutoCloseable {
* } * }
* }</pre> * }</pre>
* *
* <p> An example of passing an element handle:
* <pre>{@code
* context.exposeBinding("clicked", (source, args) -> {
* ElementHandle element = (ElementHandle) args[0];
* System.out.println(element.textContent());
* return null;
* }, new BrowserContext.ExposeBindingOptions().setHandle(true));
* page.setContent("" +
* "<script>\n" +
* " document.addEventListener('click', event => window.clicked(event.target));\n" +
* "</script>\n" +
* "<div>Click me</div>\n" +
* "<div>Or click me</div>\n");
* }</pre>
*
* @param name Name of the function on the window object. * @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context. * @param callback Callback function that will be called in the Playwright's context.
* @since v1.8 * @since v1.8
*/ */
default void exposeBinding(String name, BindingCallback callback) { AutoCloseable 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
* 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> See {@link com.microsoft.playwright.Page#exposeBinding Page.exposeBinding()} for page-only version.
*
* <p> <strong>Usage</strong>
*
* <p> An example of exposing page URL to all frames in all pages in the context:
* <pre>{@code
* import com.microsoft.playwright.*;
*
* public class Example {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType webkit = playwright.webkit()
* Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
* BrowserContext context = browser.newContext();
* context.exposeBinding("pageURL", (source, args) -> source.page().url());
* Page page = context.newPage();
* page.setContent("<script>\n" +
* " async function onClick() {\n" +
* " document.querySelector('div').textContent = await window.pageURL();\n" +
* " }\n" +
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>");
* page.getByRole(AriaRole.BUTTON).click();
* }
* }
* }
* }</pre>
*
* <p> An example of passing an element handle:
* <pre>{@code
* context.exposeBinding("clicked", (source, args) -> {
* ElementHandle element = (ElementHandle) args[0];
* System.out.println(element.textContent());
* return null;
* }, new BrowserContext.ExposeBindingOptions().setHandle(true));
* page.setContent("" +
* "<script>\n" +
* " document.addEventListener('click', event => window.clicked(event.target));\n" +
* "</script>\n" +
* "<div>Click me</div>\n" +
* "<div>Or click me</div>\n");
* }</pre>
*
* @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. * 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 * When called, the function executes {@code callback} and returns a <a
@@ -823,8 +819,9 @@ public interface BrowserContext extends AutoCloseable {
* public class Example { * public class Example {
* public static void main(String[] args) { * public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) { * try (Playwright playwright = Playwright.create()) {
* BrowserType webkit = playwright.webkit() * BrowserType webkit = playwright.webkit();
* Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false)); * Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
* BrowserContext context = browser.newContext();
* context.exposeFunction("sha256", args -> { * context.exposeFunction("sha256", args -> {
* String text = (String) args[0]; * String text = (String) args[0];
* MessageDigest crypto; * MessageDigest crypto;
@@ -854,28 +851,36 @@ public interface BrowserContext extends AutoCloseable {
* @param callback Callback function that will be called in the Playwright's context. * @param callback Callback function that will be called in the Playwright's context.
* @since v1.8 * @since v1.8
*/ */
void exposeFunction(String name, FunctionCallback callback); AutoCloseable exposeFunction(String name, FunctionCallback callback);
/** /**
* Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if * Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if
* specified. * specified.
* *
* @param permissions A permission or an array of permissions to grant. Permissions can be one of the following values: * @param permissions A list of permissions to grant.
*
* <p> <strong>NOTE:</strong> Supported permissions differ between browsers, and even between different versions of the same browser. Any permission
* may stop working after an update.
*
* <p> Here are some permissions that may be supported by some browsers:
* <ul> * <ul>
* <li> {@code "geolocation"}</li>
* <li> {@code "midi"}</li>
* <li> {@code "midi-sysex"} (system-exclusive midi)</li>
* <li> {@code "notifications"}</li>
* <li> {@code "camera"}</li>
* <li> {@code "microphone"}</li>
* <li> {@code "background-sync"}</li>
* <li> {@code "ambient-light-sensor"}</li>
* <li> {@code "accelerometer"}</li> * <li> {@code "accelerometer"}</li>
* <li> {@code "gyroscope"}</li> * <li> {@code "ambient-light-sensor"}</li>
* <li> {@code "magnetometer"}</li> * <li> {@code "background-sync"}</li>
* <li> {@code "accessibility-events"}</li> * <li> {@code "camera"}</li>
* <li> {@code "clipboard-read"}</li> * <li> {@code "clipboard-read"}</li>
* <li> {@code "clipboard-write"}</li> * <li> {@code "clipboard-write"}</li>
* <li> {@code "geolocation"}</li>
* <li> {@code "gyroscope"}</li>
* <li> {@code "local-fonts"}</li>
* <li> {@code "local-network-access"}</li>
* <li> {@code "magnetometer"}</li>
* <li> {@code "microphone"}</li>
* <li> {@code "midi-sysex"} (system-exclusive midi)</li>
* <li> {@code "midi"}</li>
* <li> {@code "notifications"}</li>
* <li> {@code "payment-handler"}</li> * <li> {@code "payment-handler"}</li>
* <li> {@code "storage-access"}</li>
* <li> {@code "screen-wake-lock"}</li>
* </ul> * </ul>
* @since v1.8 * @since v1.8
*/ */
@@ -886,27 +891,41 @@ public interface BrowserContext extends AutoCloseable {
* Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if * Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if
* specified. * specified.
* *
* @param permissions A permission or an array of permissions to grant. Permissions can be one of the following values: * @param permissions A list of permissions to grant.
*
* <p> <strong>NOTE:</strong> Supported permissions differ between browsers, and even between different versions of the same browser. Any permission
* may stop working after an update.
*
* <p> Here are some permissions that may be supported by some browsers:
* <ul> * <ul>
* <li> {@code "geolocation"}</li>
* <li> {@code "midi"}</li>
* <li> {@code "midi-sysex"} (system-exclusive midi)</li>
* <li> {@code "notifications"}</li>
* <li> {@code "camera"}</li>
* <li> {@code "microphone"}</li>
* <li> {@code "background-sync"}</li>
* <li> {@code "ambient-light-sensor"}</li>
* <li> {@code "accelerometer"}</li> * <li> {@code "accelerometer"}</li>
* <li> {@code "gyroscope"}</li> * <li> {@code "ambient-light-sensor"}</li>
* <li> {@code "magnetometer"}</li> * <li> {@code "background-sync"}</li>
* <li> {@code "accessibility-events"}</li> * <li> {@code "camera"}</li>
* <li> {@code "clipboard-read"}</li> * <li> {@code "clipboard-read"}</li>
* <li> {@code "clipboard-write"}</li> * <li> {@code "clipboard-write"}</li>
* <li> {@code "geolocation"}</li>
* <li> {@code "gyroscope"}</li>
* <li> {@code "local-fonts"}</li>
* <li> {@code "local-network-access"}</li>
* <li> {@code "magnetometer"}</li>
* <li> {@code "microphone"}</li>
* <li> {@code "midi-sysex"} (system-exclusive midi)</li>
* <li> {@code "midi"}</li>
* <li> {@code "notifications"}</li>
* <li> {@code "payment-handler"}</li> * <li> {@code "payment-handler"}</li>
* <li> {@code "storage-access"}</li>
* <li> {@code "screen-wake-lock"}</li>
* </ul> * </ul>
* @since v1.8 * @since v1.8
*/ */
void grantPermissions(List<String> permissions, GrantPermissionsOptions options); void grantPermissions(List<String> permissions, GrantPermissionsOptions options);
/**
* Indicates that the browser context is in the process of closing or has already been closed.
*
* @since v1.59
*/
boolean isClosed();
/** /**
* <strong>NOTE:</strong> CDP sessions are only supported on Chromium-based browsers. * <strong>NOTE:</strong> CDP sessions are only supported on Chromium-based browsers.
* *
@@ -951,7 +970,7 @@ public interface BrowserContext extends AutoCloseable {
* *
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by * <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.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 Worker. 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"}. * Service Workers when using request interception by setting {@code serviceWorkers} to {@code "block"}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* *
@@ -992,14 +1011,14 @@ public interface BrowserContext extends AutoCloseable {
* *
* <p> <strong>NOTE:</strong> Enabling routing disables http cache. * <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 * @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* context options was provided and the passed URL is a path, it gets merged via the <a * the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor. * 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. * @param handler handler function to route the request.
* @since v1.8 * @since v1.8
*/ */
default void route(String url, Consumer<Route> handler) { default AutoCloseable route(String url, Consumer<Route> handler) {
route(url, handler, null); return route(url, handler, null);
} }
/** /**
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route * Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
@@ -1007,7 +1026,7 @@ public interface BrowserContext extends AutoCloseable {
* *
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by * <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.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 Worker. 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"}. * Service Workers when using request interception by setting {@code serviceWorkers} to {@code "block"}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* *
@@ -1048,20 +1067,20 @@ public interface BrowserContext extends AutoCloseable {
* *
* <p> <strong>NOTE:</strong> Enabling routing disables http cache. * <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 * @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* context options was provided and the passed URL is a path, it gets merged via the <a * the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor. * 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. * @param handler handler function to route the request.
* @since v1.8 * @since v1.8
*/ */
void route(String url, Consumer<Route> handler, RouteOptions options); AutoCloseable 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 * 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. * is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
* *
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by * <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.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 Worker. 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"}. * Service Workers when using request interception by setting {@code serviceWorkers} to {@code "block"}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* *
@@ -1102,14 +1121,14 @@ public interface BrowserContext extends AutoCloseable {
* *
* <p> <strong>NOTE:</strong> Enabling routing disables http cache. * <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 * @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* context options was provided and the passed URL is a path, it gets merged via the <a * the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor. * 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. * @param handler handler function to route the request.
* @since v1.8 * @since v1.8
*/ */
default void route(Pattern url, Consumer<Route> handler) { default AutoCloseable route(Pattern url, Consumer<Route> handler) {
route(url, handler, null); return route(url, handler, null);
} }
/** /**
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route * Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
@@ -1117,7 +1136,7 @@ public interface BrowserContext extends AutoCloseable {
* *
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by * <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.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 Worker. 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"}. * Service Workers when using request interception by setting {@code serviceWorkers} to {@code "block"}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* *
@@ -1158,20 +1177,20 @@ public interface BrowserContext extends AutoCloseable {
* *
* <p> <strong>NOTE:</strong> Enabling routing disables http cache. * <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 * @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* context options was provided and the passed URL is a path, it gets merged via the <a * the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor. * 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. * @param handler handler function to route the request.
* @since v1.8 * @since v1.8
*/ */
void route(Pattern url, Consumer<Route> handler, RouteOptions options); AutoCloseable 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 * 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. * is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
* *
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by * <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.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 Worker. 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"}. * Service Workers when using request interception by setting {@code serviceWorkers} to {@code "block"}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* *
@@ -1212,14 +1231,14 @@ public interface BrowserContext extends AutoCloseable {
* *
* <p> <strong>NOTE:</strong> Enabling routing disables http cache. * <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 * @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* context options was provided and the passed URL is a path, it gets merged via the <a * the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor. * 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. * @param handler handler function to route the request.
* @since v1.8 * @since v1.8
*/ */
default void route(Predicate<String> url, Consumer<Route> handler) { default AutoCloseable route(Predicate<String> url, Consumer<Route> handler) {
route(url, handler, null); return route(url, handler, null);
} }
/** /**
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route * Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
@@ -1227,7 +1246,7 @@ public interface BrowserContext extends AutoCloseable {
* *
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by * <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.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 Worker. 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"}. * Service Workers when using request interception by setting {@code serviceWorkers} to {@code "block"}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* *
@@ -1268,20 +1287,20 @@ public interface BrowserContext extends AutoCloseable {
* *
* <p> <strong>NOTE:</strong> Enabling routing disables http cache. * <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 * @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* context options was provided and the passed URL is a path, it gets merged via the <a * the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor. * 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. * @param handler handler function to route the request.
* @since v1.8 * @since v1.8
*/ */
void route(Predicate<String> url, Consumer<Route> handler, RouteOptions options); AutoCloseable 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 * 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/mock#replaying-from-har">Replaying from HAR</a>. * href="https://playwright.dev/java/docs/mock#replaying-from-har">Replaying from HAR</a>.
* *
* <p> Playwright will not serve requests intercepted by Service Worker from the HAR file. See <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 * 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"}. * using request interception by setting {@code 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 * @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. * path} is a relative path, then it is resolved relative to the current working directory.
@@ -1296,13 +1315,94 @@ public interface BrowserContext extends AutoCloseable {
* *
* <p> Playwright will not serve requests intercepted by Service Worker from the HAR file. See <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 * 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"}. * using request interception by setting {@code 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 * @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. * path} is a relative path, then it is resolved relative to the current working directory.
* @since v1.23 * @since v1.23
*/ */
void routeFromHAR(Path har, RouteFromHAROptions options); void routeFromHAR(Path har, RouteFromHAROptions options);
/**
* This method allows to modify websocket connections that are made by any page in the browser context.
*
* <p> Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this
* method before creating any pages.
*
* <p> <strong>Usage</strong>
*
* <p> Below is an example of a simple handler that blocks some websocket messages. See {@code WebSocketRoute} for more details
* and examples.
* <pre>{@code
* context.routeWebSocket("/ws", ws -> {
* ws.routeSend(message -> {
* if ("to-be-blocked".equals(message))
* return;
* ws.send(message);
* });
* ws.connect();
* });
* }</pre>
*
* @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code
* baseURL} context option.
* @param handler Handler function to route the WebSocket.
* @since v1.48
*/
void routeWebSocket(String url, Consumer<WebSocketRoute> handler);
/**
* This method allows to modify websocket connections that are made by any page in the browser context.
*
* <p> Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this
* method before creating any pages.
*
* <p> <strong>Usage</strong>
*
* <p> Below is an example of a simple handler that blocks some websocket messages. See {@code WebSocketRoute} for more details
* and examples.
* <pre>{@code
* context.routeWebSocket("/ws", ws -> {
* ws.routeSend(message -> {
* if ("to-be-blocked".equals(message))
* return;
* ws.send(message);
* });
* ws.connect();
* });
* }</pre>
*
* @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code
* baseURL} context option.
* @param handler Handler function to route the WebSocket.
* @since v1.48
*/
void routeWebSocket(Pattern url, Consumer<WebSocketRoute> handler);
/**
* This method allows to modify websocket connections that are made by any page in the browser context.
*
* <p> Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this
* method before creating any pages.
*
* <p> <strong>Usage</strong>
*
* <p> Below is an example of a simple handler that blocks some websocket messages. See {@code WebSocketRoute} for more details
* and examples.
* <pre>{@code
* context.routeWebSocket("/ws", ws -> {
* ws.routeSend(message -> {
* if ("to-be-blocked".equals(message))
* return;
* ws.send(message);
* });
* ws.connect();
* });
* }</pre>
*
* @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code
* baseURL} context option.
* @param handler Handler function to route the WebSocket.
* @since v1.48
*/
void routeWebSocket(Predicate<String> url, Consumer<WebSocketRoute> handler);
/** /**
* This setting will change the default maximum navigation time for the following methods and related shortcuts: * This setting will change the default maximum navigation time for the following methods and related shortcuts:
* <ul> * <ul>
@@ -1330,7 +1430,7 @@ public interface BrowserContext extends AutoCloseable {
* com.microsoft.playwright.BrowserContext#setDefaultNavigationTimeout BrowserContext.setDefaultNavigationTimeout()} take * com.microsoft.playwright.BrowserContext#setDefaultNavigationTimeout BrowserContext.setDefaultNavigationTimeout()} take
* priority over {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}. * priority over {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
* *
* @param timeout Maximum time in milliseconds * @param timeout Maximum time in milliseconds. Pass {@code 0} to disable timeout.
* @since v1.8 * @since v1.8
*/ */
void setDefaultTimeout(double timeout); void setDefaultTimeout(double timeout);
@@ -1369,7 +1469,7 @@ public interface BrowserContext extends AutoCloseable {
*/ */
void setOffline(boolean offline); void setOffline(boolean offline);
/** /**
* Returns storage state for this browser context, contains current cookies and local storage snapshot. * Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
* *
* @since v1.8 * @since v1.8
*/ */
@@ -1377,11 +1477,26 @@ public interface BrowserContext extends AutoCloseable {
return storageState(null); return storageState(null);
} }
/** /**
* Returns storage state for this browser context, contains current cookies and local storage snapshot. * Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
* *
* @since v1.8 * @since v1.8
*/ */
String storageState(StorageStateOptions options); String storageState(StorageStateOptions options);
/**
* Clears the existing cookies, local storage and IndexedDB entries for all origins and sets the new storage state.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* // Load storage state from a file and apply it to the context.
* context.setStorageState(Paths.get("state.json"));
* }</pre>
*
* @param storageState Populates context with given storage state. This option can be used to initialize context with logged-in information
* obtained via {@link com.microsoft.playwright.BrowserContext#storageState BrowserContext.storageState()}. Path to the
* file with saved storage state.
* @since v1.59
*/
void setStorageState(Path storageState);
/** /**
* *
* *
@@ -1399,7 +1514,7 @@ public interface BrowserContext extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code * Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code
* handler} is not specified, removes all routes for the {@code url}. * handler} is not specified, removes all routes for the {@code url}.
* *
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link * @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. * com.microsoft.playwright.BrowserContext#route BrowserContext.route()}.
* @since v1.8 * @since v1.8
*/ */
@@ -1410,7 +1525,7 @@ public interface BrowserContext extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code * Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code
* handler} is not specified, removes all routes for the {@code url}. * handler} is not specified, removes all routes for the {@code url}.
* *
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link * @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. * com.microsoft.playwright.BrowserContext#route BrowserContext.route()}.
* @param handler Optional handler function used to register a routing with {@link com.microsoft.playwright.BrowserContext#route * @param handler Optional handler function used to register a routing with {@link com.microsoft.playwright.BrowserContext#route
* BrowserContext.route()}. * BrowserContext.route()}.
@@ -1421,7 +1536,7 @@ public interface BrowserContext extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code * Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code
* handler} is not specified, removes all routes for the {@code url}. * handler} is not specified, removes all routes for the {@code url}.
* *
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link * @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. * com.microsoft.playwright.BrowserContext#route BrowserContext.route()}.
* @since v1.8 * @since v1.8
*/ */
@@ -1432,7 +1547,7 @@ public interface BrowserContext extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code * Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code
* handler} is not specified, removes all routes for the {@code url}. * handler} is not specified, removes all routes for the {@code url}.
* *
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link * @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. * com.microsoft.playwright.BrowserContext#route BrowserContext.route()}.
* @param handler Optional handler function used to register a routing with {@link com.microsoft.playwright.BrowserContext#route * @param handler Optional handler function used to register a routing with {@link com.microsoft.playwright.BrowserContext#route
* BrowserContext.route()}. * BrowserContext.route()}.
@@ -1443,7 +1558,7 @@ public interface BrowserContext extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code * Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code
* handler} is not specified, removes all routes for the {@code url}. * handler} is not specified, removes all routes for the {@code url}.
* *
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link * @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. * com.microsoft.playwright.BrowserContext#route BrowserContext.route()}.
* @since v1.8 * @since v1.8
*/ */
@@ -1454,7 +1569,7 @@ public interface BrowserContext extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code * Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code
* handler} is not specified, removes all routes for the {@code url}. * handler} is not specified, removes all routes for the {@code url}.
* *
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link * @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. * com.microsoft.playwright.BrowserContext#route BrowserContext.route()}.
* @param handler Optional handler function used to register a routing with {@link com.microsoft.playwright.BrowserContext#route * @param handler Optional handler function used to register a routing with {@link com.microsoft.playwright.BrowserContext#route
* BrowserContext.route()}. * BrowserContext.route()}.
@@ -124,10 +124,28 @@ public interface BrowserType {
} }
} }
class ConnectOverCDPOptions { class ConnectOverCDPOptions {
/**
* If specified, browser artifacts (such as traces and downloads) are saved into this directory.
*/
public Path artifactsDir;
/** /**
* Additional HTTP headers to be sent with connect request. Optional. * Additional HTTP headers to be sent with connect request. Optional.
*/ */
public Map<String, String> headers; public Map<String, String> headers;
/**
* Tells Playwright that it runs on the same host as the CDP server. It will enable certain optimizations that rely upon
* the file system being the same between Playwright and the Browser.
*/
public Boolean isLocal;
/**
* When true, Playwright will not apply its default overrides to the existing default browser context. Specifically, {@code
* acceptDownloads} is left at the browser's setting, focus emulation is not enabled, and media emulation options (such as
* {@code colorScheme}, {@code reducedMotion}, {@code forcedColors}, and {@code contrast}) are not applied. Useful when
* attaching to a user's daily-driver browser where these overrides would interfere with existing browser state. New
* contexts created via {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} are not affected. Defaults
* to {@code false}.
*/
public Boolean noDefaults;
/** /**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0. * Defaults to 0.
@@ -139,6 +157,13 @@ public interface BrowserType {
*/ */
public Double timeout; public Double timeout;
/**
* If specified, browser artifacts (such as traces and downloads) are saved into this directory.
*/
public ConnectOverCDPOptions setArtifactsDir(Path artifactsDir) {
this.artifactsDir = artifactsDir;
return this;
}
/** /**
* Additional HTTP headers to be sent with connect request. Optional. * Additional HTTP headers to be sent with connect request. Optional.
*/ */
@@ -146,6 +171,26 @@ public interface BrowserType {
this.headers = headers; this.headers = headers;
return this; return this;
} }
/**
* Tells Playwright that it runs on the same host as the CDP server. It will enable certain optimizations that rely upon
* the file system being the same between Playwright and the Browser.
*/
public ConnectOverCDPOptions setIsLocal(boolean isLocal) {
this.isLocal = isLocal;
return this;
}
/**
* When true, Playwright will not apply its default overrides to the existing default browser context. Specifically, {@code
* acceptDownloads} is left at the browser's setting, focus emulation is not enabled, and media emulation options (such as
* {@code colorScheme}, {@code reducedMotion}, {@code forcedColors}, and {@code contrast}) are not applied. Useful when
* attaching to a user's daily-driver browser where these overrides would interfere with existing browser state. New
* contexts created via {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} are not affected. Defaults
* to {@code false}.
*/
public ConnectOverCDPOptions setNoDefaults(boolean noDefaults) {
this.noDefaults = noDefaults;
return this;
}
/** /**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0. * Defaults to 0.
@@ -172,19 +217,26 @@ public interface BrowserType {
*/ */
public List<String> args; public List<String> args;
/** /**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", * If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory is not
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a * cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the browser
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>. * closes.
*/
public Path artifactsDir;
/**
* Browser distribution channel.
*
* <p> Use "chromium" to <a href="https://playwright.dev/java/docs/browsers#chromium-new-headless-mode">opt in to new headless
* mode</a>.
*
* <p> Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to
* use branded <a href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and
* Microsoft Edge</a>.
*/ */
public Object channel; public Object channel;
/** /**
* Enable Chromium sandboxing. Defaults to {@code false}. * Enable Chromium sandboxing. Defaults to {@code false}.
*/ */
public Boolean chromiumSandbox; public Boolean chromiumSandbox;
/**
* @deprecated Use <a href="https://playwright.dev/java/docs/debug">debugging tools</a> instead.
*/
public Boolean devtools;
/** /**
* If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is * If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is
* deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in * deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in
@@ -204,6 +256,9 @@ public interface BrowserType {
/** /**
* Firefox user preferences. Learn more about the Firefox user preferences at <a * Firefox user preferences. Learn more about the Firefox user preferences at <a
* href="https://support.mozilla.org/en-US/kb/about-config-editor-firefox">{@code about:config}</a>. * href="https://support.mozilla.org/en-US/kb/about-config-editor-firefox">{@code about:config}</a>.
*
* <p> You can also provide a path to a custom <a href="https://mozilla.github.io/policy-templates/">{@code policies.json}
* file</a> via {@code PLAYWRIGHT_FIREFOX_POLICIES_JSON} environment variable.
*/ */
public Map<String, Object> firefoxUserPrefs; public Map<String, Object> firefoxUserPrefs;
/** /**
@@ -221,8 +276,7 @@ public interface BrowserType {
/** /**
* Whether to run browser in headless mode. More details for <a * 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://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} * href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true}.
* unless the {@code devtools} option is {@code true}.
*/ */
public Boolean headless; public Boolean headless;
/** /**
@@ -263,20 +317,39 @@ public interface BrowserType {
this.args = args; this.args = args;
return this; return this;
} }
/**
* If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory is not
* cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the browser
* closes.
*/
public LaunchOptions setArtifactsDir(Path artifactsDir) {
this.artifactsDir = artifactsDir;
return this;
}
@Deprecated @Deprecated
/** /**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", * Browser distribution channel.
* "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>. * <p> Use "chromium" to <a href="https://playwright.dev/java/docs/browsers#chromium-new-headless-mode">opt in to new headless
* mode</a>.
*
* <p> Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to
* use branded <a href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and
* Microsoft Edge</a>.
*/ */
public LaunchOptions setChannel(BrowserChannel channel) { public LaunchOptions setChannel(BrowserChannel channel) {
this.channel = channel; this.channel = channel;
return this; return this;
} }
/** /**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", * Browser distribution channel.
* "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>. * <p> Use "chromium" to <a href="https://playwright.dev/java/docs/browsers#chromium-new-headless-mode">opt in to new headless
* mode</a>.
*
* <p> Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to
* use branded <a href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and
* Microsoft Edge</a>.
*/ */
public LaunchOptions setChannel(String channel) { public LaunchOptions setChannel(String channel) {
this.channel = channel; this.channel = channel;
@@ -289,13 +362,6 @@ public interface BrowserType {
this.chromiumSandbox = chromiumSandbox; this.chromiumSandbox = chromiumSandbox;
return this; return this;
} }
/**
* @deprecated Use <a href="https://playwright.dev/java/docs/debug">debugging tools</a> instead.
*/
public LaunchOptions setDevtools(boolean devtools) {
this.devtools = devtools;
return this;
}
/** /**
* If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is * If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is
* deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in * deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in
@@ -324,6 +390,9 @@ public interface BrowserType {
/** /**
* Firefox user preferences. Learn more about the Firefox user preferences at <a * Firefox user preferences. Learn more about the Firefox user preferences at <a
* href="https://support.mozilla.org/en-US/kb/about-config-editor-firefox">{@code about:config}</a>. * href="https://support.mozilla.org/en-US/kb/about-config-editor-firefox">{@code about:config}</a>.
*
* <p> You can also provide a path to a custom <a href="https://mozilla.github.io/policy-templates/">{@code policies.json}
* file</a> via {@code PLAYWRIGHT_FIREFOX_POLICIES_JSON} environment variable.
*/ */
public LaunchOptions setFirefoxUserPrefs(Map<String, Object> firefoxUserPrefs) { public LaunchOptions setFirefoxUserPrefs(Map<String, Object> firefoxUserPrefs) {
this.firefoxUserPrefs = firefoxUserPrefs; this.firefoxUserPrefs = firefoxUserPrefs;
@@ -353,8 +422,7 @@ public interface BrowserType {
/** /**
* Whether to run browser in headless mode. More details for <a * 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://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} * href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true}.
* unless the {@code devtools} option is {@code true}.
*/ */
public LaunchOptions setHeadless(boolean headless) { public LaunchOptions setHeadless(boolean headless) {
this.headless = headless; this.headless = headless;
@@ -424,6 +492,12 @@ public interface BrowserType {
* href="https://peter.sh/experiments/chromium-command-line-switches/">here</a>. * href="https://peter.sh/experiments/chromium-command-line-switches/">here</a>.
*/ */
public List<String> args; public List<String> args;
/**
* If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory is not
* cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the browser
* closes.
*/
public Path artifactsDir;
/** /**
* When using {@link com.microsoft.playwright.Page#navigate Page.navigate()}, {@link com.microsoft.playwright.Page#route * When using {@link com.microsoft.playwright.Page#navigate Page.navigate()}, {@link com.microsoft.playwright.Page#route
* Page.route()}, {@link com.microsoft.playwright.Page#waitForURL Page.waitForURL()}, {@link * Page.route()}, {@link com.microsoft.playwright.Page#waitForURL Page.waitForURL()}, {@link
@@ -446,9 +520,14 @@ public interface BrowserType {
*/ */
public Boolean bypassCSP; public Boolean bypassCSP;
/** /**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", * Browser distribution channel.
* "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>. * <p> Use "chromium" to <a href="https://playwright.dev/java/docs/browsers#chromium-new-headless-mode">opt in to new headless
* mode</a>.
*
* <p> Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to
* use branded <a href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and
* Microsoft Edge</a>.
*/ */
public Object channel; public Object channel;
/** /**
@@ -456,20 +535,42 @@ public interface BrowserType {
*/ */
public Boolean chromiumSandbox; public Boolean chromiumSandbox;
/** /**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code * TLS Client Authentication allows the server to request a client certificate and verify it.
* "no-preference"}. See {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing *
* {@code null} resets emulation to system defaults. Defaults to {@code "light"}. * <p> <strong>Details</strong>
*
* <p> An array of client certificates to be used. Each certificate object must have either both {@code certPath} and {@code
* keyPath}, a single {@code pfxPath}, or their corresponding direct value equivalents ({@code cert} and {@code key}, or
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
public List<ClientCertificate> clientCertificates;
/**
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. See {@link com.microsoft.playwright.Page#emulateMedia
* Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. Defaults to {@code
* "light"}.
*/ */
public Optional<ColorScheme> colorScheme; public Optional<ColorScheme> colorScheme;
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
* {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "no-preference"}.
*/
public Optional<Contrast> contrast;
/** /**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a * 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>. * href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
*/ */
public Double deviceScaleFactor; public Double deviceScaleFactor;
/**
* @deprecated Use <a href="https://playwright.dev/java/docs/debug">debugging tools</a> instead.
*/
public Boolean devtools;
/** /**
* If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is * If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is
* deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in * deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in
@@ -493,6 +594,9 @@ public interface BrowserType {
/** /**
* Firefox user preferences. Learn more about the Firefox user preferences at <a * Firefox user preferences. Learn more about the Firefox user preferences at <a
* href="https://support.mozilla.org/en-US/kb/about-config-editor-firefox">{@code about:config}</a>. * href="https://support.mozilla.org/en-US/kb/about-config-editor-firefox">{@code about:config}</a>.
*
* <p> You can also provide a path to a custom <a href="https://mozilla.github.io/policy-templates/">{@code policies.json}
* file</a> via {@code PLAYWRIGHT_FIREFOX_POLICIES_JSON} environment variable.
*/ */
public Map<String, Object> firefoxUserPrefs; public Map<String, Object> firefoxUserPrefs;
/** /**
@@ -522,8 +626,7 @@ public interface BrowserType {
/** /**
* Whether to run browser in headless mode. More details for <a * 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://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} * href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true}.
* unless the {@code devtools} option is {@code true}.
*/ */
public Boolean headless; public Boolean headless;
/** /**
@@ -689,6 +792,15 @@ public interface BrowserType {
this.args = args; this.args = args;
return this; return this;
} }
/**
* If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory is not
* cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the browser
* closes.
*/
public LaunchPersistentContextOptions setArtifactsDir(Path artifactsDir) {
this.artifactsDir = artifactsDir;
return this;
}
/** /**
* When using {@link com.microsoft.playwright.Page#navigate Page.navigate()}, {@link com.microsoft.playwright.Page#route * When using {@link com.microsoft.playwright.Page#navigate Page.navigate()}, {@link com.microsoft.playwright.Page#route
* Page.route()}, {@link com.microsoft.playwright.Page#waitForURL Page.waitForURL()}, {@link * Page.route()}, {@link com.microsoft.playwright.Page#waitForURL Page.waitForURL()}, {@link
@@ -718,18 +830,28 @@ public interface BrowserType {
} }
@Deprecated @Deprecated
/** /**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", * Browser distribution channel.
* "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>. * <p> Use "chromium" to <a href="https://playwright.dev/java/docs/browsers#chromium-new-headless-mode">opt in to new headless
* mode</a>.
*
* <p> Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to
* use branded <a href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and
* Microsoft Edge</a>.
*/ */
public LaunchPersistentContextOptions setChannel(BrowserChannel channel) { public LaunchPersistentContextOptions setChannel(BrowserChannel channel) {
this.channel = channel; this.channel = channel;
return this; return this;
} }
/** /**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", * Browser distribution channel.
* "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>. * <p> Use "chromium" to <a href="https://playwright.dev/java/docs/browsers#chromium-new-headless-mode">opt in to new headless
* mode</a>.
*
* <p> Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to
* use branded <a href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and
* Microsoft Edge</a>.
*/ */
public LaunchPersistentContextOptions setChannel(String channel) { public LaunchPersistentContextOptions setChannel(String channel) {
this.channel = channel; this.channel = channel;
@@ -743,14 +865,46 @@ public interface BrowserType {
return this; return this;
} }
/** /**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code * TLS Client Authentication allows the server to request a client certificate and verify it.
* "no-preference"}. See {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing *
* {@code null} resets emulation to system defaults. Defaults to {@code "light"}. * <p> <strong>Details</strong>
*
* <p> An array of client certificates to be used. Each certificate object must have either both {@code certPath} and {@code
* keyPath}, a single {@code pfxPath}, or their corresponding direct value equivalents ({@code cert} and {@code key}, or
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
public LaunchPersistentContextOptions setClientCertificates(List<ClientCertificate> clientCertificates) {
this.clientCertificates = clientCertificates;
return this;
}
/**
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. See {@link com.microsoft.playwright.Page#emulateMedia
* Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. Defaults to {@code
* "light"}.
*/ */
public LaunchPersistentContextOptions setColorScheme(ColorScheme colorScheme) { public LaunchPersistentContextOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = Optional.ofNullable(colorScheme); this.colorScheme = Optional.ofNullable(colorScheme);
return this; return this;
} }
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
* {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "no-preference"}.
*/
public LaunchPersistentContextOptions setContrast(Contrast contrast) {
this.contrast = Optional.ofNullable(contrast);
return this;
}
/** /**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a * 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>. * href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
@@ -759,13 +913,6 @@ public interface BrowserType {
this.deviceScaleFactor = deviceScaleFactor; this.deviceScaleFactor = deviceScaleFactor;
return this; return this;
} }
/**
* @deprecated Use <a href="https://playwright.dev/java/docs/debug">debugging tools</a> instead.
*/
public LaunchPersistentContextOptions setDevtools(boolean devtools) {
this.devtools = devtools;
return this;
}
/** /**
* If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is * If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is
* deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in * deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in
@@ -801,6 +948,9 @@ public interface BrowserType {
/** /**
* Firefox user preferences. Learn more about the Firefox user preferences at <a * Firefox user preferences. Learn more about the Firefox user preferences at <a
* href="https://support.mozilla.org/en-US/kb/about-config-editor-firefox">{@code about:config}</a>. * href="https://support.mozilla.org/en-US/kb/about-config-editor-firefox">{@code about:config}</a>.
*
* <p> You can also provide a path to a custom <a href="https://mozilla.github.io/policy-templates/">{@code policies.json}
* file</a> via {@code PLAYWRIGHT_FIREFOX_POLICIES_JSON} environment variable.
*/ */
public LaunchPersistentContextOptions setFirefoxUserPrefs(Map<String, Object> firefoxUserPrefs) { public LaunchPersistentContextOptions setFirefoxUserPrefs(Map<String, Object> firefoxUserPrefs) {
this.firefoxUserPrefs = firefoxUserPrefs; this.firefoxUserPrefs = firefoxUserPrefs;
@@ -854,8 +1004,7 @@ public interface BrowserType {
/** /**
* Whether to run browser in headless mode. More details for <a * 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://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} * href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true}.
* unless the {@code devtools} option is {@code true}.
*/ */
public LaunchPersistentContextOptions setHeadless(boolean headless) { public LaunchPersistentContextOptions setHeadless(boolean headless) {
this.headless = headless; this.headless = headless;
@@ -1132,25 +1281,27 @@ public interface BrowserType {
} }
} }
/** /**
* This method attaches Playwright to an existing browser instance. When connecting to another browser launched via {@code * This method attaches Playwright to an existing browser instance created via {@code BrowserType.launchServer} in Node.js.
* 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. * <p> <strong>NOTE:</strong> The major and minor version of the Playwright instance that connects needs to match the version of Playwright that
* launches the browser (1.2.3 → is compatible with 1.2.x).
*
* @param endpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}.
* @since v1.8 * @since v1.8
*/ */
default Browser connect(String wsEndpoint) { default Browser connect(String endpoint) {
return connect(wsEndpoint, null); return connect(endpoint, null);
} }
/** /**
* This method attaches Playwright to an existing browser instance. When connecting to another browser launched via {@code * This method attaches Playwright to an existing browser instance created via {@code BrowserType.launchServer} in Node.js.
* 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. * <p> <strong>NOTE:</strong> The major and minor version of the Playwright instance that connects needs to match the version of Playwright that
* launches the browser (1.2.3 → is compatible with 1.2.x).
*
* @param endpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}.
* @since v1.8 * @since v1.8
*/ */
Browser connect(String wsEndpoint, ConnectOptions options); Browser connect(String endpoint, ConnectOptions options);
/** /**
* This method 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.
* *
@@ -1158,6 +1309,11 @@ public interface BrowserType {
* *
* <p> <strong>NOTE:</strong> Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers. * <p> <strong>NOTE:</strong> Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
* *
* <p> <strong>NOTE:</strong> This connection is significantly lower fidelity than the Playwright protocol connection via {@link
* com.microsoft.playwright.BrowserType#connect BrowserType.connect()}. If you are experiencing issues or attempting to use
* advanced functionality, you probably want to use {@link com.microsoft.playwright.BrowserType#connect
* BrowserType.connect()}.
*
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* Browser browser = playwright.chromium().connectOverCDP("http://localhost:9222"); * Browser browser = playwright.chromium().connectOverCDP("http://localhost:9222");
@@ -1179,6 +1335,11 @@ public interface BrowserType {
* *
* <p> <strong>NOTE:</strong> Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers. * <p> <strong>NOTE:</strong> Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
* *
* <p> <strong>NOTE:</strong> This connection is significantly lower fidelity than the Playwright protocol connection via {@link
* com.microsoft.playwright.BrowserType#connect BrowserType.connect()}. If you are experiencing issues or attempting to use
* advanced functionality, you probably want to use {@link com.microsoft.playwright.BrowserType#connect
* BrowserType.connect()}.
*
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* Browser browser = playwright.chromium().connectOverCDP("http://localhost:9222"); * Browser browser = playwright.chromium().connectOverCDP("http://localhost:9222");
@@ -1273,11 +1434,20 @@ public interface BrowserType {
* <p> Launches browser that uses persistent storage located at {@code userDataDir} and returns the only context. Closing this * <p> Launches browser that uses persistent storage located at {@code userDataDir} and returns the only context. Closing this
* context will automatically close the browser. * context will automatically close the browser.
* *
* @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for <a * @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. Pass an empty string to
* create a temporary directory.
*
* <p> More details for <a
* href="https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction">Chromium</a> and <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 * href="https://wiki.mozilla.org/Firefox/CommandLineOptions#User_profile">Firefox</a>. Chromium's user data directory is
* Chromium's user data directory is the **parent** directory of the "Profile Path" seen at {@code chrome://version}. Pass * the **parent** directory of the "Profile Path" seen at {@code chrome://version}.
* an empty string to use a temporary directory instead. *
* <p> Note that browsers do not allow launching multiple instances with the same User Data Directory.
*
* <p> <strong>NOTE:</strong> Chromium/Chrome: Due to recent Chrome policy changes, automating the default Chrome user profile is not supported.
* Pointing {@code userDataDir} to Chrome's main "User Data" directory (the profile used for your regular browsing) may
* result in pages not loading or the browser exiting. Create and use a separate directory (for example, an empty folder)
* as your automation profile instead. See https://developer.chrome.com/blog/remote-debugging-port for details.
* @since v1.8 * @since v1.8
*/ */
default BrowserContext launchPersistentContext(Path userDataDir) { default BrowserContext launchPersistentContext(Path userDataDir) {
@@ -1289,11 +1459,20 @@ public interface BrowserType {
* <p> Launches browser that uses persistent storage located at {@code userDataDir} and returns the only context. Closing this * <p> Launches browser that uses persistent storage located at {@code userDataDir} and returns the only context. Closing this
* context will automatically close the browser. * context will automatically close the browser.
* *
* @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for <a * @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. Pass an empty string to
* create a temporary directory.
*
* <p> More details for <a
* href="https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction">Chromium</a> and <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 * href="https://wiki.mozilla.org/Firefox/CommandLineOptions#User_profile">Firefox</a>. Chromium's user data directory is
* Chromium's user data directory is the **parent** directory of the "Profile Path" seen at {@code chrome://version}. Pass * the **parent** directory of the "Profile Path" seen at {@code chrome://version}.
* an empty string to use a temporary directory instead. *
* <p> Note that browsers do not allow launching multiple instances with the same User Data Directory.
*
* <p> <strong>NOTE:</strong> Chromium/Chrome: Due to recent Chrome policy changes, automating the default Chrome user profile is not supported.
* Pointing {@code userDataDir} to Chrome's main "User Data" directory (the profile used for your regular browsing) may
* result in pages not loading or the browser exiting. Create and use a separate directory (for example, an empty folder)
* as your automation profile instead. See https://developer.chrome.com/blog/remote-debugging-port for details.
* @since v1.8 * @since v1.8
*/ */
BrowserContext launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options); BrowserContext launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options);
@@ -48,6 +48,16 @@ import com.google.gson.JsonObject;
* }</pre> * }</pre>
*/ */
public interface CDPSession { public interface CDPSession {
/**
* Emitted when the session is closed, either because the target was closed or {@code session.detach()} was called.
*/
void onClose(Consumer<CDPSession> handler);
/**
* Removes handler that was previously added with {@link #onClose onClose(handler)}.
*/
void offClose(Consumer<CDPSession> handler);
/** /**
* Detaches the CDPSession from the target. Once detached, the CDPSession object won't emit any events and can't be used to * Detaches the CDPSession from the target. Once detached, the CDPSession object won't emit any events and can't be used to
* send messages. * send messages.
@@ -0,0 +1,366 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import java.util.Date;
/**
* Accurately simulating time-dependent behavior is essential for verifying the correctness of applications. Learn more
* about <a href="https://playwright.dev/java/docs/clock">clock emulation</a>.
*
* <p> Note that clock is installed for the entire {@code BrowserContext}, so the time in all the pages and iframes is
* controlled by the same clock.
*/
public interface Clock {
class InstallOptions {
/**
* Time to initialize with, current system time by default.
*/
public Object time;
/**
* Time to initialize with, current system time by default.
*/
public InstallOptions setTime(long time) {
this.time = time;
return this;
}
/**
* Time to initialize with, current system time by default.
*/
public InstallOptions setTime(String time) {
this.time = time;
return this;
}
/**
* Time to initialize with, current system time by default.
*/
public InstallOptions setTime(Date time) {
this.time = time;
return this;
}
}
/**
* Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the
* laptop lid for a while and reopening it later, after given time.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.clock().fastForward(1000);
* page.clock().fastForward("30:00");
* }</pre>
*
* @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08"
* for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
* @since v1.45
*/
void fastForward(long ticks);
/**
* Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the
* laptop lid for a while and reopening it later, after given time.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.clock().fastForward(1000);
* page.clock().fastForward("30:00");
* }</pre>
*
* @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08"
* for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
* @since v1.45
*/
void fastForward(String ticks);
/**
* Install fake implementations for the following time-related functions:
* <ul>
* <li> {@code Date}</li>
* <li> {@code setTimeout}</li>
* <li> {@code clearTimeout}</li>
* <li> {@code setInterval}</li>
* <li> {@code clearInterval}</li>
* <li> {@code requestAnimationFrame}</li>
* <li> {@code cancelAnimationFrame}</li>
* <li> {@code requestIdleCallback}</li>
* <li> {@code cancelIdleCallback}</li>
* <li> {@code performance}</li>
* </ul>
*
* <p> Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and
* control the behavior of time-dependent functions. See {@link com.microsoft.playwright.Clock#runFor Clock.runFor()} and
* {@link com.microsoft.playwright.Clock#fastForward Clock.fastForward()} for more information.
*
* @since v1.45
*/
default void install() {
install(null);
}
/**
* Install fake implementations for the following time-related functions:
* <ul>
* <li> {@code Date}</li>
* <li> {@code setTimeout}</li>
* <li> {@code clearTimeout}</li>
* <li> {@code setInterval}</li>
* <li> {@code clearInterval}</li>
* <li> {@code requestAnimationFrame}</li>
* <li> {@code cancelAnimationFrame}</li>
* <li> {@code requestIdleCallback}</li>
* <li> {@code cancelIdleCallback}</li>
* <li> {@code performance}</li>
* </ul>
*
* <p> Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and
* control the behavior of time-dependent functions. See {@link com.microsoft.playwright.Clock#runFor Clock.runFor()} and
* {@link com.microsoft.playwright.Clock#fastForward Clock.fastForward()} for more information.
*
* @since v1.45
*/
void install(InstallOptions options);
/**
* Advance the clock, firing all the time-related callbacks.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.clock().runFor(1000);
* page.clock().runFor("30:00");
* }</pre>
*
* @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08"
* for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
* @since v1.45
*/
void runFor(long ticks);
/**
* Advance the clock, firing all the time-related callbacks.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.clock().runFor(1000);
* page.clock().runFor("30:00");
* }</pre>
*
* @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08"
* for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
* @since v1.45
*/
void runFor(String ticks);
/**
* Advance the clock by jumping forward in time and pause the time. Once this method is called, no timers are fired unless
* {@link com.microsoft.playwright.Clock#runFor Clock.runFor()}, {@link com.microsoft.playwright.Clock#fastForward
* Clock.fastForward()}, {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} or {@link
* com.microsoft.playwright.Clock#resume Clock.resume()} is called.
*
* <p> Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and reopening it at
* the specified time and pausing.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd");
* page.clock().pauseAt(format.parse("2020-02-02"));
* page.clock().pauseAt("2020-02-02");
* }</pre>
*
* <p> For best results, install the clock before navigating the page and set it to a time slightly before the intended test
* time. This ensures that all timers run normally during page loading, preventing the page from getting stuck. Once the
* page has fully loaded, you can safely use {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} to pause the
* clock.
* <pre>{@code
* // Initialize clock with some time before the test time and let the page load
* // naturally. `Date.now` will progress as the timers fire.
* SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
* page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-12-10T08:00:00")));
* page.navigate("http://localhost:3333");
* page.clock().pauseAt(format.parse("2024-12-10T10:00:00"));
* }</pre>
*
* @param time Time to pause at.
* @since v1.45
*/
void pauseAt(long time);
/**
* Advance the clock by jumping forward in time and pause the time. Once this method is called, no timers are fired unless
* {@link com.microsoft.playwright.Clock#runFor Clock.runFor()}, {@link com.microsoft.playwright.Clock#fastForward
* Clock.fastForward()}, {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} or {@link
* com.microsoft.playwright.Clock#resume Clock.resume()} is called.
*
* <p> Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and reopening it at
* the specified time and pausing.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd");
* page.clock().pauseAt(format.parse("2020-02-02"));
* page.clock().pauseAt("2020-02-02");
* }</pre>
*
* <p> For best results, install the clock before navigating the page and set it to a time slightly before the intended test
* time. This ensures that all timers run normally during page loading, preventing the page from getting stuck. Once the
* page has fully loaded, you can safely use {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} to pause the
* clock.
* <pre>{@code
* // Initialize clock with some time before the test time and let the page load
* // naturally. `Date.now` will progress as the timers fire.
* SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
* page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-12-10T08:00:00")));
* page.navigate("http://localhost:3333");
* page.clock().pauseAt(format.parse("2024-12-10T10:00:00"));
* }</pre>
*
* @param time Time to pause at.
* @since v1.45
*/
void pauseAt(String time);
/**
* Advance the clock by jumping forward in time and pause the time. Once this method is called, no timers are fired unless
* {@link com.microsoft.playwright.Clock#runFor Clock.runFor()}, {@link com.microsoft.playwright.Clock#fastForward
* Clock.fastForward()}, {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} or {@link
* com.microsoft.playwright.Clock#resume Clock.resume()} is called.
*
* <p> Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and reopening it at
* the specified time and pausing.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd");
* page.clock().pauseAt(format.parse("2020-02-02"));
* page.clock().pauseAt("2020-02-02");
* }</pre>
*
* <p> For best results, install the clock before navigating the page and set it to a time slightly before the intended test
* time. This ensures that all timers run normally during page loading, preventing the page from getting stuck. Once the
* page has fully loaded, you can safely use {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} to pause the
* clock.
* <pre>{@code
* // Initialize clock with some time before the test time and let the page load
* // naturally. `Date.now` will progress as the timers fire.
* SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
* page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-12-10T08:00:00")));
* page.navigate("http://localhost:3333");
* page.clock().pauseAt(format.parse("2024-12-10T10:00:00"));
* }</pre>
*
* @param time Time to pause at.
* @since v1.45
*/
void pauseAt(Date time);
/**
* Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual.
*
* @since v1.45
*/
void resume();
/**
* Makes {@code Date.now} and {@code new Date()} return fixed fake time at all times, keeps all the timers running.
*
* <p> Use this method for simple scenarios where you only need to test with a predefined time. For more advanced scenarios,
* use {@link com.microsoft.playwright.Clock#install Clock.install()} instead. Read docs on <a
* href="https://playwright.dev/java/docs/clock">clock emulation</a> to learn more.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.clock().setFixedTime(new Date());
* page.clock().setFixedTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
* page.clock().setFixedTime("2020-02-02");
* }</pre>
*
* @param time Time to be set in milliseconds.
* @since v1.45
*/
void setFixedTime(long time);
/**
* Makes {@code Date.now} and {@code new Date()} return fixed fake time at all times, keeps all the timers running.
*
* <p> Use this method for simple scenarios where you only need to test with a predefined time. For more advanced scenarios,
* use {@link com.microsoft.playwright.Clock#install Clock.install()} instead. Read docs on <a
* href="https://playwright.dev/java/docs/clock">clock emulation</a> to learn more.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.clock().setFixedTime(new Date());
* page.clock().setFixedTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
* page.clock().setFixedTime("2020-02-02");
* }</pre>
*
* @param time Time to be set in milliseconds.
* @since v1.45
*/
void setFixedTime(String time);
/**
* Makes {@code Date.now} and {@code new Date()} return fixed fake time at all times, keeps all the timers running.
*
* <p> Use this method for simple scenarios where you only need to test with a predefined time. For more advanced scenarios,
* use {@link com.microsoft.playwright.Clock#install Clock.install()} instead. Read docs on <a
* href="https://playwright.dev/java/docs/clock">clock emulation</a> to learn more.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.clock().setFixedTime(new Date());
* page.clock().setFixedTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
* page.clock().setFixedTime("2020-02-02");
* }</pre>
*
* @param time Time to be set in milliseconds.
* @since v1.45
*/
void setFixedTime(Date time);
/**
* Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for example
* switching from summer to winter time, or changing time zones.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.clock().setSystemTime(new Date());
* page.clock().setSystemTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
* page.clock().setSystemTime("2020-02-02");
* }</pre>
*
* @param time Time to be set in milliseconds.
* @since v1.45
*/
void setSystemTime(long time);
/**
* Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for example
* switching from summer to winter time, or changing time zones.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.clock().setSystemTime(new Date());
* page.clock().setSystemTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
* page.clock().setSystemTime("2020-02-02");
* }</pre>
*
* @param time Time to be set in milliseconds.
* @since v1.45
*/
void setSystemTime(String time);
/**
* Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for example
* switching from summer to winter time, or changing time zones.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.clock().setSystemTime(new Date());
* page.clock().setSystemTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
* page.clock().setSystemTime("2020-02-02");
* }</pre>
*
* @param time Time to be set in milliseconds.
* @since v1.45
*/
void setSystemTime(Date time);
}
@@ -20,7 +20,7 @@ import java.util.*;
/** /**
* {@code ConsoleMessage} objects are dispatched by page via the {@link com.microsoft.playwright.Page#onConsoleMessage * {@code ConsoleMessage} objects are dispatched by page via the {@link com.microsoft.playwright.Page#onConsoleMessage
* Page.onConsoleMessage()} event. For each console messages logged in the page there will be corresponding event in the * Page.onConsoleMessage()} event. For each console message logged in the page there will be corresponding event in the
* Playwright context. * Playwright context.
* <pre>{@code * <pre>{@code
* // Listen for all console messages and print them to the standard output. * // Listen for all console messages and print them to the standard output.
@@ -39,8 +39,8 @@ import java.util.*;
* }); * });
* *
* // Deconstruct console.log arguments * // Deconstruct console.log arguments
* msg.args().get(0).jsonValue() // hello * msg.args().get(0).jsonValue(); // hello
* msg.args().get(1).jsonValue() // 42 * msg.args().get(1).jsonValue(); // 42
* }</pre> * }</pre>
*/ */
public interface ConsoleMessage { public interface ConsoleMessage {
@@ -69,6 +69,12 @@ public interface ConsoleMessage {
* @since v1.8 * @since v1.8
*/ */
String text(); String text();
/**
* The timestamp of the console message in milliseconds since the Unix epoch.
*
* @since v1.59
*/
double timestamp();
/** /**
* One of the following values: {@code "log"}, {@code "debug"}, {@code "info"}, {@code "error"}, {@code "warning"}, {@code * 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 * "dir"}, {@code "dirxml"}, {@code "table"}, {@code "trace"}, {@code "clear"}, {@code "startGroup"}, {@code
@@ -78,5 +84,12 @@ public interface ConsoleMessage {
* @since v1.8 * @since v1.8
*/ */
String type(); String type();
/**
* The web worker or service worker that produced this console message, if any. Note that console messages from web workers
* also have non-null {@link com.microsoft.playwright.ConsoleMessage#page ConsoleMessage.page()}.
*
* @since v1.57
*/
Worker worker();
} }
@@ -0,0 +1,238 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* {@code Credentials} is a virtual WebAuthn authenticator scoped to a {@code BrowserContext}. It lets tests register
* passkeys and answer {@code navigator.credentials.create()} / {@code navigator.credentials.get()} ceremonies in the page,
* without a real authenticator or hardware security key.
*
* <p> There are two common ways to use it:
*
* <p> <strong>Usage: seed a known credential</strong>
* <pre>{@code
* BrowserContext context = browser.newContext();
*
* // A passkey your backend already provisioned for a test user.
* context.credentials().create("example.com", new Credentials.CreateOptions()
* .setId(knownCredentialId) // base64url
* .setUserHandle(knownUserHandle) // base64url
* .setPrivateKey(knownPrivateKey) // base64url PKCS#8 (DER)
* .setPublicKey(knownPublicKey)); // base64url SPKI (DER)
* context.credentials().install();
*
* Page page = context.newPage();
* page.navigate("https://example.com/login");
* // The page's navigator.credentials.get() is answered with the seeded passkey.
* }</pre>
*
* <p> <strong>Usage: capture a passkey, then reuse it</strong>
* <pre>{@code
* // setup test: let the app register a passkey, then save it.
* BrowserContext context = browser.newContext();
* context.credentials().install();
*
* Page page = context.newPage();
* page.navigate("https://example.com/register");
* page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Create a passkey")).click();
*
* // Read back the passkey the page registered — it includes the private key.
* VirtualCredential credential = context.credentials().get(
* new Credentials.GetOptions().setRpId("example.com")).get(0);
* Files.writeString(Paths.get("playwright/.auth/passkey.json"), new Gson().toJson(credential));
* }</pre>
* <pre>{@code
* // later test: seed the captured passkey so the app starts already enrolled.
* VirtualCredential credential = new Gson().fromJson(
* Files.readString(Paths.get("playwright/.auth/passkey.json")), VirtualCredential.class);
* BrowserContext context = browser.newContext();
* context.credentials().create(credential.rpId, new Credentials.CreateOptions()
* .setId(credential.id)
* .setUserHandle(credential.userHandle)
* .setPrivateKey(credential.privateKey)
* .setPublicKey(credential.publicKey));
* context.credentials().install();
*
* Page page = context.newPage();
* page.navigate("https://example.com/login");
* // navigator.credentials.get() resolves the captured passkey — already signed in.
* }</pre>
*
* <p> <strong>Defaults</strong>
*/
public interface Credentials {
class CreateOptions {
/**
* Base64url-encoded credential id. Auto-generated if omitted.
*/
public String id;
/**
* Base64url-encoded PKCS#8 (DER) private key. Auto-generated if omitted.
*/
public String privateKey;
/**
* Base64url-encoded SPKI (DER) public key. Auto-generated if omitted.
*/
public String publicKey;
/**
* Base64url-encoded user handle. Auto-generated if omitted.
*/
public String userHandle;
/**
* Base64url-encoded credential id. Auto-generated if omitted.
*/
public CreateOptions setId(String id) {
this.id = id;
return this;
}
/**
* Base64url-encoded PKCS#8 (DER) private key. Auto-generated if omitted.
*/
public CreateOptions setPrivateKey(String privateKey) {
this.privateKey = privateKey;
return this;
}
/**
* Base64url-encoded SPKI (DER) public key. Auto-generated if omitted.
*/
public CreateOptions setPublicKey(String publicKey) {
this.publicKey = publicKey;
return this;
}
/**
* Base64url-encoded user handle. Auto-generated if omitted.
*/
public CreateOptions setUserHandle(String userHandle) {
this.userHandle = userHandle;
return this;
}
}
class GetOptions {
/**
* Only return the credential with this base64url-encoded id.
*/
public String id;
/**
* Only return credentials for this relying party id.
*/
public String rpId;
/**
* Only return the credential with this base64url-encoded id.
*/
public GetOptions setId(String id) {
this.id = id;
return this;
}
/**
* Only return credentials for this relying party id.
*/
public GetOptions setRpId(String rpId) {
this.rpId = rpId;
return this;
}
}
/**
* Installs the virtual WebAuthn authenticator into the context, overriding {@code navigator.credentials.create()} and
* {@code navigator.credentials.get()} in all current and future pages. Call this before the page first touches {@code
* navigator.credentials}.
*
* <p> Required: until {@link com.microsoft.playwright.Credentials#install Credentials.install()} is called, no interception is
* in place and the page sees the platform's native (or absent) WebAuthn behaviour. Seeding credentials with {@link
* com.microsoft.playwright.Credentials#create Credentials.create()} without installing populates the authenticator, but
* the page will never see those credentials.
*
* @since v1.61
*/
void install();
/**
* Seeds a virtual WebAuthn credential and returns it.
*
* <p> With only {@code rpId}, generates a fresh **ECDSA P-256** keypair, credential id and user handle. The seeded credential
* is discoverable (resident), so the page can resolve it from both username-then-passkey and usernameless passkey flows.
* The returned object carries the private and public keys, so it can be persisted to disk and re-seeded in a later test.
*
* <p> To **import a known credential**, supply all four of {@code id}, {@code userHandle}, {@code privateKey} and {@code
* publicKey} together.
*
* <p> Call {@link com.microsoft.playwright.Credentials#install Credentials.install()} before navigating to a page that uses
* WebAuthn.
*
* @param rpId Relying party id (typically the site's effective domain).
* @since v1.61
*/
default VirtualCredential create(String rpId) {
return create(rpId, null);
}
/**
* Seeds a virtual WebAuthn credential and returns it.
*
* <p> With only {@code rpId}, generates a fresh **ECDSA P-256** keypair, credential id and user handle. The seeded credential
* is discoverable (resident), so the page can resolve it from both username-then-passkey and usernameless passkey flows.
* The returned object carries the private and public keys, so it can be persisted to disk and re-seeded in a later test.
*
* <p> To **import a known credential**, supply all four of {@code id}, {@code userHandle}, {@code privateKey} and {@code
* publicKey} together.
*
* <p> Call {@link com.microsoft.playwright.Credentials#install Credentials.install()} before navigating to a page that uses
* WebAuthn.
*
* @param rpId Relying party id (typically the site's effective domain).
* @since v1.61
*/
VirtualCredential create(String rpId, CreateOptions options);
/**
* Removes a credential from the authenticator by its id. Works for any credential currently held — both those seeded with
* {@link com.microsoft.playwright.Credentials#create Credentials.create()} and those the page registered itself by calling
* {@code navigator.credentials.create()}.
*
* @param id Base64url-encoded credential id.
* @since v1.61
*/
void delete(String id);
/**
* Returns every credential currently held by the authenticator, optionally filtered by {@code rpId} or {@code id}. This
* includes both credentials seeded with {@link com.microsoft.playwright.Credentials#create Credentials.create()} and
* credentials the page registered itself by calling {@code navigator.credentials.create()}.
*
* <p> Each returned credential includes its private and public keys, so a passkey the app just registered can be saved and
* re-seeded into a later test with {@link com.microsoft.playwright.Credentials#create Credentials.create()} — see the
* second example in the class overview.
*
* @since v1.61
*/
default List<VirtualCredential> get() {
return get(null);
}
/**
* Returns every credential currently held by the authenticator, optionally filtered by {@code rpId} or {@code id}. This
* includes both credentials seeded with {@link com.microsoft.playwright.Credentials#create Credentials.create()} and
* credentials the page registered itself by calling {@code navigator.credentials.create()}.
*
* <p> Each returned credential includes its private and public keys, so a passkey the app just registered can be saved and
* re-seeded into a later test with {@link com.microsoft.playwright.Credentials#create Credentials.create()} — see the
* second example in the class overview.
*
* @since v1.61
*/
List<VirtualCredential> get(GetOptions options);
}
@@ -0,0 +1,78 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* API for controlling the Playwright debugger. The debugger allows pausing script execution and inspecting the page.
* Obtain the debugger instance via {@link com.microsoft.playwright.BrowserContext#debugger BrowserContext.debugger()}.
*/
public interface Debugger {
/**
* Emitted when the debugger pauses or resumes.
*/
void onPausedStateChanged(Runnable handler);
/**
* Removes handler that was previously added with {@link #onPausedStateChanged onPausedStateChanged(handler)}.
*/
void offPausedStateChanged(Runnable handler);
/**
* Returns details about the currently paused call. Returns {@code null} if the debugger is not paused.
*
* @since v1.59
*/
DebuggerPausedDetails pausedDetails();
/**
* Configures the debugger to pause before the next action is executed.
*
* <p> Throws if the debugger is already paused. Use {@link com.microsoft.playwright.Debugger#next Debugger.next()} or {@link
* com.microsoft.playwright.Debugger#runTo Debugger.runTo()} to step while paused.
*
* <p> Note that {@link com.microsoft.playwright.Page#pause Page.pause()} is equivalent to a "debugger" statement — it pauses
* execution at the call site immediately. On the contrary, {@link com.microsoft.playwright.Debugger#requestPause
* Debugger.requestPause()} is equivalent to "pause on next statement" — it configures the debugger to pause before the
* next action is executed.
*
* @since v1.59
*/
void requestPause();
/**
* Resumes script execution. Throws if the debugger is not paused.
*
* @since v1.59
*/
void resume();
/**
* Resumes script execution and pauses again before the next action. Throws if the debugger is not paused.
*
* @since v1.59
*/
void next();
/**
* Resumes script execution and pauses when an action originates from the given source location. Throws if the debugger is
* not paused.
*
* @param location The source location to pause at.
* @since v1.59
*/
void runTo(Location location);
}
@@ -48,6 +48,9 @@ public interface Download {
/** /**
* Returns a readable stream for a successful download, or throws for a failed/canceled download. * Returns a readable stream for a successful download, or throws for a failed/canceled download.
* *
* <p> <strong>NOTE:</strong> If you don't need a readable stream, it's usually simpler to read the file from disk after the download completed. See
* {@link com.microsoft.playwright.Download#path Download.path()}.
*
* @since v1.8 * @since v1.8
*/ */
InputStream createReadStream(); InputStream createReadStream();
@@ -65,9 +65,7 @@ public interface ElementHandle extends JSHandle {
*/ */
public Boolean force; public Boolean force;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -98,9 +96,7 @@ public interface ElementHandle extends JSHandle {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public CheckOptions setNoWaitAfter(boolean noWaitAfter) { public CheckOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -161,13 +157,12 @@ public interface ElementHandle extends JSHandle {
public Boolean force; public Boolean force;
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public List<KeyboardModifier> modifiers; public List<KeyboardModifier> modifiers;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option will default to {@code true} in the future.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -175,6 +170,12 @@ public interface ElementHandle extends JSHandle {
* element. * element.
*/ */
public Position position; public Position position;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public Integer steps;
/** /**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default * 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 com.microsoft.playwright.BrowserContext#setDefaultTimeout * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -220,16 +221,15 @@ public interface ElementHandle extends JSHandle {
} }
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public ClickOptions setModifiers(List<KeyboardModifier> modifiers) { public ClickOptions setModifiers(List<KeyboardModifier> modifiers) {
this.modifiers = modifiers; this.modifiers = modifiers;
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option will default to {@code true} in the future.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public ClickOptions setNoWaitAfter(boolean noWaitAfter) { public ClickOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -250,6 +250,15 @@ public interface ElementHandle extends JSHandle {
this.position = position; this.position = position;
return this; return this;
} }
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public ClickOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/** /**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default * 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 com.microsoft.playwright.BrowserContext#setDefaultTimeout * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -286,13 +295,12 @@ public interface ElementHandle extends JSHandle {
public Boolean force; public Boolean force;
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public List<KeyboardModifier> modifiers; public List<KeyboardModifier> modifiers;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -300,6 +308,12 @@ public interface ElementHandle extends JSHandle {
* element. * element.
*/ */
public Position position; public Position position;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public Integer steps;
/** /**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default * 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 com.microsoft.playwright.BrowserContext#setDefaultTimeout * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -338,16 +352,15 @@ public interface ElementHandle extends JSHandle {
} }
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public DblclickOptions setModifiers(List<KeyboardModifier> modifiers) { public DblclickOptions setModifiers(List<KeyboardModifier> modifiers) {
this.modifiers = modifiers; this.modifiers = modifiers;
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public DblclickOptions setNoWaitAfter(boolean noWaitAfter) { public DblclickOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -368,6 +381,15 @@ public interface ElementHandle extends JSHandle {
this.position = position; this.position = position;
return this; return this;
} }
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public DblclickOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/** /**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default * 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 com.microsoft.playwright.BrowserContext#setDefaultTimeout * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -395,9 +417,7 @@ public interface ElementHandle extends JSHandle {
*/ */
public Boolean force; public Boolean force;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -417,9 +437,7 @@ public interface ElementHandle extends JSHandle {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public FillOptions setNoWaitAfter(boolean noWaitAfter) { public FillOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -444,13 +462,12 @@ public interface ElementHandle extends JSHandle {
public Boolean force; public Boolean force;
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public List<KeyboardModifier> modifiers; public List<KeyboardModifier> modifiers;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -482,16 +499,15 @@ public interface ElementHandle extends JSHandle {
} }
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public HoverOptions setModifiers(List<KeyboardModifier> modifiers) { public HoverOptions setModifiers(List<KeyboardModifier> modifiers) {
this.modifiers = modifiers; this.modifiers = modifiers;
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public HoverOptions setNoWaitAfter(boolean noWaitAfter) { public HoverOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -558,9 +574,7 @@ public interface ElementHandle extends JSHandle {
*/ */
public Double delay; public Double delay;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option will default to {@code true} in the future.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -579,9 +593,7 @@ public interface ElementHandle extends JSHandle {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option will default to {@code true} in the future.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public PressOptions setNoWaitAfter(boolean noWaitAfter) { public PressOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -617,7 +629,9 @@ public interface ElementHandle extends JSHandle {
public ScreenshotCaret caret; public ScreenshotCaret caret;
/** /**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box * Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
* invisible elements, see <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
* visible elements</a> to disable that.
*/ */
public List<Locator> mask; public List<Locator> mask;
/** /**
@@ -691,7 +705,9 @@ public interface ElementHandle extends JSHandle {
} }
/** /**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box * Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
* invisible elements, see <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
* visible elements</a> to disable that.
*/ */
public ScreenshotOptions setMask(List<Locator> mask) { public ScreenshotOptions setMask(List<Locator> mask) {
this.mask = mask; this.mask = mask;
@@ -795,9 +811,7 @@ public interface ElementHandle extends JSHandle {
*/ */
public Boolean force; public Boolean force;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -817,9 +831,7 @@ public interface ElementHandle extends JSHandle {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public SelectOptionOptions setNoWaitAfter(boolean noWaitAfter) { public SelectOptionOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -876,9 +888,7 @@ public interface ElementHandle extends JSHandle {
*/ */
public Boolean force; public Boolean force;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -909,9 +919,7 @@ public interface ElementHandle extends JSHandle {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public SetCheckedOptions setNoWaitAfter(boolean noWaitAfter) { public SetCheckedOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -954,9 +962,7 @@ public interface ElementHandle extends JSHandle {
} }
class SetInputFilesOptions { class SetInputFilesOptions {
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -968,9 +974,7 @@ public interface ElementHandle extends JSHandle {
public Double timeout; public Double timeout;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public SetInputFilesOptions setNoWaitAfter(boolean noWaitAfter) { public SetInputFilesOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -995,13 +999,12 @@ public interface ElementHandle extends JSHandle {
public Boolean force; public Boolean force;
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public List<KeyboardModifier> modifiers; public List<KeyboardModifier> modifiers;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -1033,16 +1036,15 @@ public interface ElementHandle extends JSHandle {
} }
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public TapOptions setModifiers(List<KeyboardModifier> modifiers) { public TapOptions setModifiers(List<KeyboardModifier> modifiers) {
this.modifiers = modifiers; this.modifiers = modifiers;
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public TapOptions setNoWaitAfter(boolean noWaitAfter) { public TapOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -1089,9 +1091,7 @@ public interface ElementHandle extends JSHandle {
*/ */
public Double delay; public Double delay;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -1110,9 +1110,7 @@ public interface ElementHandle extends JSHandle {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public TypeOptions setNoWaitAfter(boolean noWaitAfter) { public TypeOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -1136,9 +1134,7 @@ public interface ElementHandle extends JSHandle {
*/ */
public Boolean force; public Boolean force;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -1169,9 +1165,7 @@ public interface ElementHandle extends JSHandle {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public UncheckOptions setNoWaitAfter(boolean noWaitAfter) { public UncheckOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -1324,7 +1318,6 @@ public interface ElementHandle extends JSHandle {
* force} option is set.</li> * force} option is set.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li> * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* <li> Ensure that the element is now checked. If not, this method throws.</li> * <li> Ensure that the element is now checked. If not, this method throws.</li>
* </ol> * </ol>
* *
@@ -1347,7 +1340,6 @@ public interface ElementHandle extends JSHandle {
* force} option is set.</li> * force} option is set.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li> * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* <li> Ensure that the element is now checked. If not, this method throws.</li> * <li> Ensure that the element is now checked. If not, this method throws.</li>
* </ol> * </ol>
* *
@@ -1413,8 +1405,6 @@ public interface ElementHandle extends JSHandle {
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to double click in the center of the element, or the * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to double click in the center of the element, or the
* specified {@code position}.</li> * specified {@code position}.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set. Note that if the
* first click of the {@code dblclick()} triggers a navigation event, this method will throw.</li>
* </ol> * </ol>
* *
* <p> If the element is detached from the DOM at any moment during the action, this method throws. * <p> If the element is detached from the DOM at any moment during the action, this method throws.
@@ -1437,8 +1427,6 @@ public interface ElementHandle extends JSHandle {
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to double click in the center of the element, or the * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to double click in the center of the element, or the
* specified {@code position}.</li> * specified {@code position}.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set. Note that if the
* first click of the {@code dblclick()} triggers a navigation event, this method will throw.</li>
* </ol> * </ol>
* *
* <p> If the element is detached from the DOM at any moment during the action, this method throws. * <p> If the element is detached from the DOM at any moment during the action, this method throws.
@@ -1695,7 +1683,6 @@ public interface ElementHandle extends JSHandle {
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to hover over the center of the element, or the specified * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to hover over the center of the element, or the specified
* {@code position}.</li> * {@code position}.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* </ol> * </ol>
* *
* <p> If the element is detached from the DOM at any moment during the action, this method throws. * <p> If the element is detached from the DOM at any moment during the action, this method throws.
@@ -1716,7 +1703,6 @@ public interface ElementHandle extends JSHandle {
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to hover over the center of the element, or the specified * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to hover over the center of the element, or the specified
* {@code position}.</li> * {@code position}.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* </ol> * </ol>
* *
* <p> If the element is detached from the DOM at any moment during the action, this method throws. * <p> If the element is detached from the DOM at any moment during the action, this method throws.
@@ -1820,7 +1806,7 @@ public interface ElementHandle extends JSHandle {
* ArrowUp}, etc. * ArrowUp}, etc.
* *
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code * <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code
* ShiftLeft}. * ShiftLeft}, {@code ControlOrMeta}.
* *
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case. * <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
* *
@@ -1851,7 +1837,7 @@ public interface ElementHandle extends JSHandle {
* ArrowUp}, etc. * ArrowUp}, etc.
* *
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code * <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code
* ShiftLeft}. * ShiftLeft}, {@code ControlOrMeta}.
* *
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case. * <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
* *
@@ -1918,6 +1904,8 @@ public interface ElementHandle extends JSHandle {
* <p> Throws when {@code elementHandle} does not point to an element <a * <p> Throws when {@code elementHandle} does not point to an element <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected">connected</a> to a Document or a ShadowRoot. * href="https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected">connected</a> to a Document or a ShadowRoot.
* *
* <p> See <a href="https://playwright.dev/java/docs/input#scrolling">scrolling</a> for alternative ways to scroll.
*
* @since v1.8 * @since v1.8
*/ */
default void scrollIntoViewIfNeeded() { default void scrollIntoViewIfNeeded() {
@@ -1932,6 +1920,8 @@ public interface ElementHandle extends JSHandle {
* <p> Throws when {@code elementHandle} does not point to an element <a * <p> Throws when {@code elementHandle} does not point to an element <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected">connected</a> to a Document or a ShadowRoot. * href="https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected">connected</a> to a Document or a ShadowRoot.
* *
* <p> See <a href="https://playwright.dev/java/docs/input#scrolling">scrolling</a> for alternative ways to scroll.
*
* @since v1.8 * @since v1.8
*/ */
void scrollIntoViewIfNeeded(ScrollIntoViewIfNeededOptions options); void scrollIntoViewIfNeeded(ScrollIntoViewIfNeededOptions options);
@@ -2328,7 +2318,6 @@ public interface ElementHandle extends JSHandle {
* unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li> * unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li> * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* <li> Ensure that the element is now checked or unchecked. If not, this method throws.</li> * <li> Ensure that the element is now checked or unchecked. If not, this method throws.</li>
* </ol> * </ol>
* *
@@ -2350,7 +2339,6 @@ public interface ElementHandle extends JSHandle {
* unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li> * unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li> * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* <li> Ensure that the element is now checked or unchecked. If not, this method throws.</li> * <li> Ensure that the element is now checked or unchecked. If not, this method throws.</li>
* </ol> * </ol>
* *
@@ -2363,7 +2351,8 @@ public interface ElementHandle extends JSHandle {
void setChecked(boolean checked, SetCheckedOptions options); void setChecked(boolean checked, SetCheckedOptions options);
/** /**
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then * Sets the value of the file input to these file paths or files. 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. * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with
* a {@code [webkitdirectory]} attribute, only a single directory path is supported.
* *
* <p> This method expects {@code ElementHandle} to point to an <a * <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is * href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
@@ -2378,7 +2367,8 @@ public interface ElementHandle extends JSHandle {
} }
/** /**
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then * Sets the value of the file input to these file paths or files. 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. * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with
* a {@code [webkitdirectory]} attribute, only a single directory path is supported.
* *
* <p> This method expects {@code ElementHandle} to point to an <a * <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is * href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
@@ -2391,7 +2381,8 @@ public interface ElementHandle extends JSHandle {
void setInputFiles(Path files, SetInputFilesOptions options); void setInputFiles(Path files, SetInputFilesOptions options);
/** /**
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then * Sets the value of the file input to these file paths or files. 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. * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with
* a {@code [webkitdirectory]} attribute, only a single directory path is supported.
* *
* <p> This method expects {@code ElementHandle} to point to an <a * <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is * href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
@@ -2406,7 +2397,8 @@ public interface ElementHandle extends JSHandle {
} }
/** /**
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then * Sets the value of the file input to these file paths or files. 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. * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with
* a {@code [webkitdirectory]} attribute, only a single directory path is supported.
* *
* <p> This method expects {@code ElementHandle} to point to an <a * <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is * href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
@@ -2419,7 +2411,8 @@ public interface ElementHandle extends JSHandle {
void setInputFiles(Path[] files, SetInputFilesOptions options); void setInputFiles(Path[] files, SetInputFilesOptions options);
/** /**
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then * Sets the value of the file input to these file paths or files. 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. * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with
* a {@code [webkitdirectory]} attribute, only a single directory path is supported.
* *
* <p> This method expects {@code ElementHandle} to point to an <a * <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is * href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
@@ -2434,7 +2427,8 @@ public interface ElementHandle extends JSHandle {
} }
/** /**
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then * Sets the value of the file input to these file paths or files. 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. * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with
* a {@code [webkitdirectory]} attribute, only a single directory path is supported.
* *
* <p> This method expects {@code ElementHandle} to point to an <a * <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is * href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
@@ -2447,7 +2441,8 @@ public interface ElementHandle extends JSHandle {
void setInputFiles(FilePayload files, SetInputFilesOptions options); void setInputFiles(FilePayload files, SetInputFilesOptions options);
/** /**
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then * Sets the value of the file input to these file paths or files. 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. * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with
* a {@code [webkitdirectory]} attribute, only a single directory path is supported.
* *
* <p> This method expects {@code ElementHandle} to point to an <a * <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is * href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
@@ -2462,7 +2457,8 @@ public interface ElementHandle extends JSHandle {
} }
/** /**
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then * Sets the value of the file input to these file paths or files. 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. * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with
* a {@code [webkitdirectory]} attribute, only a single directory path is supported.
* *
* <p> This method expects {@code ElementHandle} to point to an <a * <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is * href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
@@ -2481,7 +2477,6 @@ public interface ElementHandle extends JSHandle {
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#touchscreen Page.touchscreen()} to tap the center of the element, or the * <li> Use {@link com.microsoft.playwright.Page#touchscreen Page.touchscreen()} to tap the center of the element, or the
* specified {@code position}.</li> * specified {@code position}.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* </ol> * </ol>
* *
* <p> If the element is detached from the DOM at any moment during the action, this method throws. * <p> If the element is detached from the DOM at any moment during the action, this method throws.
@@ -2504,7 +2499,6 @@ public interface ElementHandle extends JSHandle {
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#touchscreen Page.touchscreen()} to tap the center of the element, or the * <li> Use {@link com.microsoft.playwright.Page#touchscreen Page.touchscreen()} to tap the center of the element, or the
* specified {@code position}.</li> * specified {@code position}.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* </ol> * </ol>
* *
* <p> If the element is detached from the DOM at any moment during the action, this method throws. * <p> If the element is detached from the DOM at any moment during the action, this method throws.
@@ -2552,7 +2546,6 @@ public interface ElementHandle extends JSHandle {
* force} option is set.</li> * force} option is set.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li> * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* <li> Ensure that the element is now unchecked. If not, this method throws.</li> * <li> Ensure that the element is now unchecked. If not, this method throws.</li>
* </ol> * </ol>
* *
@@ -2575,7 +2568,6 @@ public interface ElementHandle extends JSHandle {
* force} option is set.</li> * force} option is set.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li> * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* <li> Ensure that the element is now unchecked. If not, this method throws.</li> * <li> Ensure that the element is now unchecked. If not, this method throws.</li>
* </ol> * </ol>
* *
@@ -30,9 +30,7 @@ import java.nio.file.Path;
public interface FileChooser { public interface FileChooser {
class SetFilesOptions { class SetFilesOptions {
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -44,9 +42,7 @@ public interface FileChooser {
public Double timeout; public Double timeout;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public SetFilesOptions setNoWaitAfter(boolean noWaitAfter) { public SetFilesOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -72,7 +72,7 @@ public interface Frame {
*/ */
public Path path; public Path path;
/** /**
* Script type. Use 'module' in order to load a Javascript ES6 module. See <a * Script type. Use 'module' in order to load a JavaScript ES6 module. See <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script">script</a> for more details. * href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script">script</a> for more details.
*/ */
public String type; public String type;
@@ -97,7 +97,7 @@ public interface Frame {
return this; return this;
} }
/** /**
* Script type. Use 'module' in order to load a Javascript ES6 module. See <a * Script type. Use 'module' in order to load a JavaScript ES6 module. See <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script">script</a> for more details. * href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script">script</a> for more details.
*/ */
public AddScriptTagOptions setType(String type) { public AddScriptTagOptions setType(String type) {
@@ -157,9 +157,7 @@ public interface Frame {
*/ */
public Boolean force; public Boolean force;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -195,9 +193,7 @@ public interface Frame {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public CheckOptions setNoWaitAfter(boolean noWaitAfter) { public CheckOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -266,13 +262,12 @@ public interface Frame {
public Boolean force; public Boolean force;
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public List<KeyboardModifier> modifiers; public List<KeyboardModifier> modifiers;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option will default to {@code true} in the future.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -295,7 +290,8 @@ public interface Frame {
/** /**
* When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a> * When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a>
* checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
* performing it. * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
* elements which are only visible when those keys are pressed.
*/ */
public Boolean trial; public Boolean trial;
@@ -330,16 +326,15 @@ public interface Frame {
} }
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public ClickOptions setModifiers(List<KeyboardModifier> modifiers) { public ClickOptions setModifiers(List<KeyboardModifier> modifiers) {
this.modifiers = modifiers; this.modifiers = modifiers;
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option will default to {@code true} in the future.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public ClickOptions setNoWaitAfter(boolean noWaitAfter) { public ClickOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -381,7 +376,8 @@ public interface Frame {
/** /**
* When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a> * When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a>
* checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
* performing it. * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
* elements which are only visible when those keys are pressed.
*/ */
public ClickOptions setTrial(boolean trial) { public ClickOptions setTrial(boolean trial) {
this.trial = trial; this.trial = trial;
@@ -404,13 +400,12 @@ public interface Frame {
public Boolean force; public Boolean force;
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public List<KeyboardModifier> modifiers; public List<KeyboardModifier> modifiers;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -433,7 +428,8 @@ public interface Frame {
/** /**
* When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a> * When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a>
* checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
* performing it. * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
* elements which are only visible when those keys are pressed.
*/ */
public Boolean trial; public Boolean trial;
@@ -461,16 +457,15 @@ public interface Frame {
} }
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public DblclickOptions setModifiers(List<KeyboardModifier> modifiers) { public DblclickOptions setModifiers(List<KeyboardModifier> modifiers) {
this.modifiers = modifiers; this.modifiers = modifiers;
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public DblclickOptions setNoWaitAfter(boolean noWaitAfter) { public DblclickOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -512,7 +507,8 @@ public interface Frame {
/** /**
* When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a> * When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a>
* checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
* performing it. * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
* elements which are only visible when those keys are pressed.
*/ */
public DblclickOptions setTrial(boolean trial) { public DblclickOptions setTrial(boolean trial) {
this.trial = trial; this.trial = trial;
@@ -559,9 +555,7 @@ public interface Frame {
*/ */
public Boolean force; public Boolean force;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -569,6 +563,11 @@ public interface Frame {
* specified, some visible point of the element is used. * specified, some visible point of the element is used.
*/ */
public Position sourcePosition; public Position sourcePosition;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public Integer steps;
/** /**
* When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
* element, the call throws an exception. * element, the call throws an exception.
@@ -602,9 +601,7 @@ public interface Frame {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public DragAndDropOptions setNoWaitAfter(boolean noWaitAfter) { public DragAndDropOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -625,6 +622,14 @@ public interface Frame {
this.sourcePosition = sourcePosition; this.sourcePosition = sourcePosition;
return this; return this;
} }
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public DragAndDropOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/** /**
* When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
* element, the call throws an exception. * element, the call throws an exception.
@@ -691,9 +696,7 @@ public interface Frame {
*/ */
public Boolean force; public Boolean force;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -718,9 +721,7 @@ public interface Frame {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public FillOptions setNoWaitAfter(boolean noWaitAfter) { public FillOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -866,6 +867,13 @@ public interface Frame {
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>. * <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>.
*/ */
public Boolean checked; public Boolean checked;
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</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-description">accessible description</a>.
*/
public Object description;
/** /**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
* *
@@ -874,8 +882,8 @@ public interface Frame {
*/ */
public Boolean disabled; public Boolean disabled;
/** /**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* is a regular expression. Note that exact match still trims whitespace. * Ignored when the value is a regular expression. Note that exact match still trims whitespace.
*/ */
public Boolean exact; public Boolean exact;
/** /**
@@ -927,6 +935,26 @@ public interface Frame {
this.checked = checked; this.checked = checked;
return this; return this;
} }
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</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-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(String description) {
this.description = description;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</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-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(Pattern description) {
this.description = description;
return this;
}
/** /**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
* *
@@ -938,8 +966,8 @@ public interface Frame {
return this; return this;
} }
/** /**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* is a regular expression. Note that exact match still trims whitespace. * Ignored when the value is a regular expression. Note that exact match still trims whitespace.
*/ */
public GetByRoleOptions setExact(boolean exact) { public GetByRoleOptions setExact(boolean exact) {
this.exact = exact; this.exact = exact;
@@ -1115,13 +1143,12 @@ public interface Frame {
public Boolean force; public Boolean force;
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public List<KeyboardModifier> modifiers; public List<KeyboardModifier> modifiers;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -1144,7 +1171,8 @@ public interface Frame {
/** /**
* When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a> * When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a>
* checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
* performing it. * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
* elements which are only visible when those keys are pressed.
*/ */
public Boolean trial; public Boolean trial;
@@ -1158,16 +1186,15 @@ public interface Frame {
} }
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public HoverOptions setModifiers(List<KeyboardModifier> modifiers) { public HoverOptions setModifiers(List<KeyboardModifier> modifiers) {
this.modifiers = modifiers; this.modifiers = modifiers;
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public HoverOptions setNoWaitAfter(boolean noWaitAfter) { public HoverOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -1209,7 +1236,8 @@ public interface Frame {
/** /**
* When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a> * When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a>
* checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
* performing it. * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
* elements which are only visible when those keys are pressed.
*/ */
public HoverOptions setTrial(boolean trial) { public HoverOptions setTrial(boolean trial) {
this.trial = trial; this.trial = trial;
@@ -1607,9 +1635,7 @@ public interface Frame {
*/ */
public Double delay; public Double delay;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option will default to {@code true} in the future.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -1633,9 +1659,7 @@ public interface Frame {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option will default to {@code true} in the future.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public PressOptions setNoWaitAfter(boolean noWaitAfter) { public PressOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -1683,9 +1707,7 @@ public interface Frame {
*/ */
public Boolean force; public Boolean force;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -1710,9 +1732,7 @@ public interface Frame {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public SelectOptionOptions setNoWaitAfter(boolean noWaitAfter) { public SelectOptionOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -1744,9 +1764,7 @@ public interface Frame {
*/ */
public Boolean force; public Boolean force;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -1782,9 +1800,7 @@ public interface Frame {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public SetCheckedOptions setNoWaitAfter(boolean noWaitAfter) { public SetCheckedOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -1884,9 +1900,7 @@ public interface Frame {
} }
class SetInputFilesOptions { class SetInputFilesOptions {
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -1903,9 +1917,7 @@ public interface Frame {
public Double timeout; public Double timeout;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public SetInputFilesOptions setNoWaitAfter(boolean noWaitAfter) { public SetInputFilesOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -1938,13 +1950,12 @@ public interface Frame {
public Boolean force; public Boolean force;
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public List<KeyboardModifier> modifiers; public List<KeyboardModifier> modifiers;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -1967,7 +1978,8 @@ public interface Frame {
/** /**
* When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a> * When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a>
* checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
* performing it. * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
* elements which are only visible when those keys are pressed.
*/ */
public Boolean trial; public Boolean trial;
@@ -1981,16 +1993,15 @@ public interface Frame {
} }
/** /**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current
* modifiers back. If not specified, currently pressed modifiers are used. * modifiers back. If not specified, currently pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows
* and Linux and to "Meta" on macOS.
*/ */
public TapOptions setModifiers(List<KeyboardModifier> modifiers) { public TapOptions setModifiers(List<KeyboardModifier> modifiers) {
this.modifiers = modifiers; this.modifiers = modifiers;
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public TapOptions setNoWaitAfter(boolean noWaitAfter) { public TapOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -2032,7 +2043,8 @@ public interface Frame {
/** /**
* When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a> * When set, this method only performs the <a href="https://playwright.dev/java/docs/actionability">actionability</a>
* checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
* performing it. * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
* elements which are only visible when those keys are pressed.
*/ */
public TapOptions setTrial(boolean trial) { public TapOptions setTrial(boolean trial) {
this.trial = trial; this.trial = trial;
@@ -2078,9 +2090,7 @@ public interface Frame {
*/ */
public Double delay; public Double delay;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -2104,9 +2114,7 @@ public interface Frame {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public TypeOptions setNoWaitAfter(boolean noWaitAfter) { public TypeOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -2138,9 +2146,7 @@ public interface Frame {
*/ */
public Boolean force; public Boolean force;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public Boolean noWaitAfter; public Boolean noWaitAfter;
/** /**
@@ -2176,9 +2182,7 @@ public interface Frame {
return this; return this;
} }
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can * @deprecated This option has no effect.
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
* inaccessible pages. Defaults to {@code false}.
*/ */
public UncheckOptions setNoWaitAfter(boolean noWaitAfter) { public UncheckOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter; this.noWaitAfter = noWaitAfter;
@@ -2295,7 +2299,7 @@ public interface Frame {
*/ */
public Double timeout; public Double timeout;
/** /**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the * A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string. * the string.
*/ */
@@ -2325,7 +2329,7 @@ public interface Frame {
return this; return this;
} }
/** /**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the * A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string. * the string.
*/ */
@@ -2334,7 +2338,7 @@ public interface Frame {
return this; return this;
} }
/** /**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the * A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string. * the string.
*/ */
@@ -2343,7 +2347,7 @@ public interface Frame {
return this; return this;
} }
/** /**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the * A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string. * the string.
*/ */
@@ -2523,7 +2527,6 @@ public interface Frame {
* unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li> * unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li> * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* <li> Ensure that the element is now checked. If not, this method throws.</li> * <li> Ensure that the element is now checked. If not, this method throws.</li>
* </ol> * </ol>
* *
@@ -2546,7 +2549,6 @@ public interface Frame {
* unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li> * unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li> * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* <li> Ensure that the element is now checked. If not, this method throws.</li> * <li> Ensure that the element is now checked. If not, this method throws.</li>
* </ol> * </ol>
* *
@@ -2617,9 +2619,8 @@ public interface Frame {
* unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li> * unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to double click in the center of the element, or the * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to double click in the center of the element, or the
* specified {@code position}.</li> * specified {@code position}. if the first click of the {@code dblclick()} triggers a navigation event, this method will
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set. Note that if the * throw.</li>
* first click of the {@code dblclick()} triggers a navigation event, this method will throw.</li>
* </ol> * </ol>
* *
* <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code * <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code
@@ -2641,9 +2642,8 @@ public interface Frame {
* unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li> * unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to double click in the center of the element, or the * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to double click in the center of the element, or the
* specified {@code position}.</li> * specified {@code position}. if the first click of the {@code dblclick()} triggers a navigation event, this method will
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set. Note that if the * throw.</li>
* first click of the {@code dblclick()} triggers a navigation event, this method will throw.</li>
* </ol> * </ol>
* *
* <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code * <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code
@@ -2965,7 +2965,7 @@ public interface Frame {
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Frame#evaluate * <p> {@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Frame#evaluate
* Frame.evaluate()}: * Frame.evaluate()}:
* <pre>{@code * <pre>{@code
* ElementHandle bodyHandle = frame.evaluate("document.body"); * ElementHandle bodyHandle = frame.evaluateHandle("document.body");
* String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello")); * String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose(); * bodyHandle.dispose();
* }</pre> * }</pre>
@@ -3005,7 +3005,7 @@ public interface Frame {
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Frame#evaluate * <p> {@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Frame#evaluate
* Frame.evaluate()}: * Frame.evaluate()}:
* <pre>{@code * <pre>{@code
* ElementHandle bodyHandle = frame.evaluate("document.body"); * ElementHandle bodyHandle = frame.evaluateHandle("document.body");
* String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello")); * String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose(); * bodyHandle.dispose();
* }</pre> * }</pre>
@@ -3407,7 +3407,7 @@ public interface Frame {
* *
* <p> Consider the following DOM structure. * <p> Consider the following DOM structure.
* *
* <p> You can locate each element by it's implicit role: * <p> You can locate each element by its implicit role:
* <pre>{@code * <pre>{@code
* assertThat(page * assertThat(page
* .getByRole(AriaRole.HEADING, * .getByRole(AriaRole.HEADING,
@@ -3450,7 +3450,7 @@ public interface Frame {
* *
* <p> Consider the following DOM structure. * <p> Consider the following DOM structure.
* *
* <p> You can locate each element by it's implicit role: * <p> You can locate each element by its implicit role:
* <pre>{@code * <pre>{@code
* assertThat(page * assertThat(page
* .getByRole(AriaRole.HEADING, * .getByRole(AriaRole.HEADING,
@@ -3489,7 +3489,7 @@ public interface Frame {
* *
* <p> Consider the following DOM structure. * <p> Consider the following DOM structure.
* *
* <p> You can locate the element by it's test id: * <p> You can locate the element by its test id:
* <pre>{@code * <pre>{@code
* page.getByTestId("directions").click(); * page.getByTestId("directions").click();
* }</pre> * }</pre>
@@ -3511,7 +3511,7 @@ public interface Frame {
* *
* <p> Consider the following DOM structure. * <p> Consider the following DOM structure.
* *
* <p> You can locate the element by it's test id: * <p> You can locate the element by its test id:
* <pre>{@code * <pre>{@code
* page.getByTestId("directions").click(); * page.getByTestId("directions").click();
* }</pre> * }</pre>
@@ -3539,19 +3539,19 @@ public interface Frame {
* <p> You can locate by text substring, exact string, or a regular expression: * <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code * <pre>{@code
* // Matches <span> * // Matches <span>
* page.getByText("world") * page.getByText("world");
* *
* // Matches first <div> * // Matches first <div>
* page.getByText("Hello world") * page.getByText("Hello world");
* *
* // Matches second <div> * // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true)) * page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
* *
* // Matches both <div>s * // Matches both <div>s
* page.getByText(Pattern.compile("Hello")) * page.getByText(Pattern.compile("Hello"));
* *
* // Matches second <div> * // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE)) * page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre> * }</pre>
* *
* <p> <strong>Details</strong> * <p> <strong>Details</strong>
@@ -3581,19 +3581,19 @@ public interface Frame {
* <p> You can locate by text substring, exact string, or a regular expression: * <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code * <pre>{@code
* // Matches <span> * // Matches <span>
* page.getByText("world") * page.getByText("world");
* *
* // Matches first <div> * // Matches first <div>
* page.getByText("Hello world") * page.getByText("Hello world");
* *
* // Matches second <div> * // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true)) * page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
* *
* // Matches both <div>s * // Matches both <div>s
* page.getByText(Pattern.compile("Hello")) * page.getByText(Pattern.compile("Hello"));
* *
* // Matches second <div> * // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE)) * page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre> * }</pre>
* *
* <p> <strong>Details</strong> * <p> <strong>Details</strong>
@@ -3621,19 +3621,19 @@ public interface Frame {
* <p> You can locate by text substring, exact string, or a regular expression: * <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code * <pre>{@code
* // Matches <span> * // Matches <span>
* page.getByText("world") * page.getByText("world");
* *
* // Matches first <div> * // Matches first <div>
* page.getByText("Hello world") * page.getByText("Hello world");
* *
* // Matches second <div> * // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true)) * page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
* *
* // Matches both <div>s * // Matches both <div>s
* page.getByText(Pattern.compile("Hello")) * page.getByText(Pattern.compile("Hello"));
* *
* // Matches second <div> * // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE)) * page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre> * }</pre>
* *
* <p> <strong>Details</strong> * <p> <strong>Details</strong>
@@ -3663,19 +3663,19 @@ public interface Frame {
* <p> You can locate by text substring, exact string, or a regular expression: * <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code * <pre>{@code
* // Matches <span> * // Matches <span>
* page.getByText("world") * page.getByText("world");
* *
* // Matches first <div> * // Matches first <div>
* page.getByText("Hello world") * page.getByText("Hello world");
* *
* // Matches second <div> * // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true)) * page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
* *
* // Matches both <div>s * // Matches both <div>s
* page.getByText(Pattern.compile("Hello")) * page.getByText(Pattern.compile("Hello"));
* *
* // Matches second <div> * // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE)) * page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre> * }</pre>
* *
* <p> <strong>Details</strong> * <p> <strong>Details</strong>
@@ -3823,7 +3823,6 @@ public interface Frame {
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to hover over the center of the element, or the specified * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to hover over the center of the element, or the specified
* {@code position}.</li> * {@code position}.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* </ol> * </ol>
* *
* <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code * <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code
@@ -3844,7 +3843,6 @@ public interface Frame {
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to hover over the center of the element, or the specified * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to hover over the center of the element, or the specified
* {@code position}.</li> * {@code position}.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* </ol> * </ol>
* *
* <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code * <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code
@@ -4082,7 +4080,8 @@ public interface Frame {
* ArrowUp}, etc. * ArrowUp}, etc.
* *
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code * <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code
* ShiftLeft}. * ShiftLeft}, {@code ControlOrMeta}. {@code ControlOrMeta} resolves to {@code Control} on Windows and Linux and to {@code
* Meta} on macOS.
* *
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case. * <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
* *
@@ -4111,7 +4110,8 @@ public interface Frame {
* ArrowUp}, etc. * ArrowUp}, etc.
* *
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code * <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code
* ShiftLeft}. * ShiftLeft}, {@code ControlOrMeta}. {@code ControlOrMeta} resolves to {@code Control} on Windows and Linux and to {@code
* Meta} on macOS.
* *
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case. * <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
* *
@@ -4558,7 +4558,6 @@ public interface Frame {
* unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li> * unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li> * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* <li> Ensure that the element is now checked or unchecked. If not, this method throws.</li> * <li> Ensure that the element is now checked or unchecked. If not, this method throws.</li>
* </ol> * </ol>
* *
@@ -4582,7 +4581,6 @@ public interface Frame {
* unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li> * unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li> * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* <li> Ensure that the element is now checked or unchecked. If not, this method throws.</li> * <li> Ensure that the element is now checked or unchecked. If not, this method throws.</li>
* </ol> * </ol>
* *
@@ -4743,7 +4741,6 @@ public interface Frame {
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#touchscreen Page.touchscreen()} to tap the center of the element, or the * <li> Use {@link com.microsoft.playwright.Page#touchscreen Page.touchscreen()} to tap the center of the element, or the
* specified {@code position}.</li> * specified {@code position}.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* </ol> * </ol>
* *
* <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code * <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code
@@ -4766,7 +4763,6 @@ public interface Frame {
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#touchscreen Page.touchscreen()} to tap the center of the element, or the * <li> Use {@link com.microsoft.playwright.Page#touchscreen Page.touchscreen()} to tap the center of the element, or the
* specified {@code position}.</li> * specified {@code position}.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* </ol> * </ol>
* *
* <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code * <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code
@@ -4832,7 +4828,6 @@ public interface Frame {
* unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li> * unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li> * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* <li> Ensure that the element is now unchecked. If not, this method throws.</li> * <li> Ensure that the element is now unchecked. If not, this method throws.</li>
* </ol> * </ol>
* *
@@ -4855,7 +4850,6 @@ public interface Frame {
* unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li> * unless {@code force} option is set. If the element is detached during the checks, the whole action is retried.</li>
* <li> Scroll the element into view if needed.</li> * <li> Scroll the element into view if needed.</li>
* <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li> * <li> Use {@link com.microsoft.playwright.Page#mouse Page.mouse()} to click in the center of the element.</li>
* <li> Wait for initiated navigations to either succeed or fail, unless {@code noWaitAfter} option is set.</li>
* <li> Ensure that the element is now unchecked. If not, this method throws.</li> * <li> Ensure that the element is now unchecked. If not, this method throws.</li>
* </ol> * </ol>
* *
@@ -4989,6 +4983,9 @@ public interface Frame {
* <p> This returns when the frame reaches a required load state, {@code load} by default. The navigation must have been * <p> This returns when the frame reaches a required load state, {@code load} by default. The navigation must have been
* committed when this method is called. If current document has already reached the required state, resolves immediately. * committed when this method is called. If current document has already reached the required state, resolves immediately.
* *
* <p> <strong>NOTE:</strong> Most of the time, this method is not needed because Playwright <a
* href="https://playwright.dev/java/docs/actionability">auto-waits before every action</a>.
*
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* frame.click("button"); // Click triggers navigation. * frame.click("button"); // Click triggers navigation.
@@ -5014,6 +5011,9 @@ public interface Frame {
* <p> This returns when the frame reaches a required load state, {@code load} by default. The navigation must have been * <p> This returns when the frame reaches a required load state, {@code load} by default. The navigation must have been
* committed when this method is called. If current document has already reached the required state, resolves immediately. * committed when this method is called. If current document has already reached the required state, resolves immediately.
* *
* <p> <strong>NOTE:</strong> Most of the time, this method is not needed because Playwright <a
* href="https://playwright.dev/java/docs/actionability">auto-waits before every action</a>.
*
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* frame.click("button"); // Click triggers navigation. * frame.click("button"); // Click triggers navigation.
@@ -5031,6 +5031,9 @@ public interface Frame {
* <p> This returns when the frame reaches a required load state, {@code load} by default. The navigation must have been * <p> This returns when the frame reaches a required load state, {@code load} by default. The navigation must have been
* committed when this method is called. If current document has already reached the required state, resolves immediately. * committed when this method is called. If current document has already reached the required state, resolves immediately.
* *
* <p> <strong>NOTE:</strong> Most of the time, this method is not needed because Playwright <a
* href="https://playwright.dev/java/docs/actionability">auto-waits before every action</a>.
*
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* frame.click("button"); // Click triggers navigation. * frame.click("button"); // Click triggers navigation.
@@ -5163,7 +5166,7 @@ public interface Frame {
* frame.waitForURL("**\/target.html"); * frame.waitForURL("**\/target.html");
* }</pre> * }</pre>
* *
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the * @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string. * the string.
* @since v1.11 * @since v1.11
@@ -5180,7 +5183,7 @@ public interface Frame {
* frame.waitForURL("**\/target.html"); * frame.waitForURL("**\/target.html");
* }</pre> * }</pre>
* *
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the * @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string. * the string.
* @since v1.11 * @since v1.11
@@ -5195,7 +5198,7 @@ public interface Frame {
* frame.waitForURL("**\/target.html"); * frame.waitForURL("**\/target.html");
* }</pre> * }</pre>
* *
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the * @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string. * the string.
* @since v1.11 * @since v1.11
@@ -5212,7 +5215,7 @@ public interface Frame {
* frame.waitForURL("**\/target.html"); * frame.waitForURL("**\/target.html");
* }</pre> * }</pre>
* *
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the * @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string. * the string.
* @since v1.11 * @since v1.11
@@ -5227,7 +5230,7 @@ public interface Frame {
* frame.waitForURL("**\/target.html"); * frame.waitForURL("**\/target.html");
* }</pre> * }</pre>
* *
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the * @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string. * the string.
* @since v1.11 * @since v1.11
@@ -5244,7 +5247,7 @@ public interface Frame {
* frame.waitForURL("**\/target.html"); * frame.waitForURL("**\/target.html");
* }</pre> * }</pre>
* *
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the * @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string. * the string.
* @since v1.11 * @since v1.11
@@ -22,10 +22,10 @@ import java.util.regex.Pattern;
/** /**
* FrameLocator represents a view to the {@code iframe} on the page. It captures the logic sufficient to retrieve the * 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 * {@code iframe} and locate elements in that iframe. FrameLocator can be created with either {@link
* com.microsoft.playwright.Page#frameLocator Page.frameLocator()} or {@link com.microsoft.playwright.Locator#frameLocator * com.microsoft.playwright.Locator#contentFrame Locator.contentFrame()}, {@link com.microsoft.playwright.Page#frameLocator
* Locator.frameLocator()} method. * Page.frameLocator()} or {@link com.microsoft.playwright.Locator#frameLocator Locator.frameLocator()} method.
* <pre>{@code * <pre>{@code
* Locator locator = page.frameLocator("#my-frame").getByText("Submit"); * Locator locator = page.locator("#my-frame").contentFrame().getByText("Submit");
* locator.click(); * locator.click();
* }</pre> * }</pre>
* *
@@ -35,10 +35,10 @@ import java.util.regex.Pattern;
* a given selector. * a given selector.
* <pre>{@code * <pre>{@code
* // Throws if there are several frames in DOM: * // Throws if there are several frames in DOM:
* page.frame_locator(".result-frame").getByRole(AriaRole.BUTTON).click(); * page.locator(".result-frame").contentFrame().getByRole(AriaRole.BUTTON).click();
* *
* // Works because we explicitly tell locator to pick the first frame: * // Works because we explicitly tell locator to pick the first frame:
* page.frame_locator(".result-frame").first().getByRole(AriaRole.BUTTON).click(); * page.locator(".result-frame").first().contentFrame().getByRole(AriaRole.BUTTON).click();
* }</pre> * }</pre>
* *
* <p> <strong>Converting Locator to FrameLocator</strong> * <p> <strong>Converting Locator to FrameLocator</strong>
@@ -107,6 +107,13 @@ public interface FrameLocator {
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>. * <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>.
*/ */
public Boolean checked; public Boolean checked;
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</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-description">accessible description</a>.
*/
public Object description;
/** /**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
* *
@@ -115,8 +122,8 @@ public interface FrameLocator {
*/ */
public Boolean disabled; public Boolean disabled;
/** /**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* is a regular expression. Note that exact match still trims whitespace. * Ignored when the value is a regular expression. Note that exact match still trims whitespace.
*/ */
public Boolean exact; public Boolean exact;
/** /**
@@ -168,6 +175,26 @@ public interface FrameLocator {
this.checked = checked; this.checked = checked;
return this; return this;
} }
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</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-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(String description) {
this.description = description;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</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-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(Pattern description) {
this.description = description;
return this;
}
/** /**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
* *
@@ -179,8 +206,8 @@ public interface FrameLocator {
return this; return this;
} }
/** /**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* is a regular expression. Note that exact match still trims whitespace. * Ignored when the value is a regular expression. Note that exact match still trims whitespace.
*/ */
public GetByRoleOptions setExact(boolean exact) { public GetByRoleOptions setExact(boolean exact) {
this.exact = exact; this.exact = exact;
@@ -383,7 +410,8 @@ public interface FrameLocator {
} }
} }
/** /**
* Returns locator to the first matching frame. * @deprecated Use {@link com.microsoft.playwright.Locator#first Locator.first()} followed by {@link
* com.microsoft.playwright.Locator#contentFrame Locator.contentFrame()} instead.
* *
* @since v1.17 * @since v1.17
*/ */
@@ -601,7 +629,7 @@ public interface FrameLocator {
* *
* <p> Consider the following DOM structure. * <p> Consider the following DOM structure.
* *
* <p> You can locate each element by it's implicit role: * <p> You can locate each element by its implicit role:
* <pre>{@code * <pre>{@code
* assertThat(page * assertThat(page
* .getByRole(AriaRole.HEADING, * .getByRole(AriaRole.HEADING,
@@ -644,7 +672,7 @@ public interface FrameLocator {
* *
* <p> Consider the following DOM structure. * <p> Consider the following DOM structure.
* *
* <p> You can locate each element by it's implicit role: * <p> You can locate each element by its implicit role:
* <pre>{@code * <pre>{@code
* assertThat(page * assertThat(page
* .getByRole(AriaRole.HEADING, * .getByRole(AriaRole.HEADING,
@@ -683,7 +711,7 @@ public interface FrameLocator {
* *
* <p> Consider the following DOM structure. * <p> Consider the following DOM structure.
* *
* <p> You can locate the element by it's test id: * <p> You can locate the element by its test id:
* <pre>{@code * <pre>{@code
* page.getByTestId("directions").click(); * page.getByTestId("directions").click();
* }</pre> * }</pre>
@@ -705,7 +733,7 @@ public interface FrameLocator {
* *
* <p> Consider the following DOM structure. * <p> Consider the following DOM structure.
* *
* <p> You can locate the element by it's test id: * <p> You can locate the element by its test id:
* <pre>{@code * <pre>{@code
* page.getByTestId("directions").click(); * page.getByTestId("directions").click();
* }</pre> * }</pre>
@@ -733,19 +761,19 @@ public interface FrameLocator {
* <p> You can locate by text substring, exact string, or a regular expression: * <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code * <pre>{@code
* // Matches <span> * // Matches <span>
* page.getByText("world") * page.getByText("world");
* *
* // Matches first <div> * // Matches first <div>
* page.getByText("Hello world") * page.getByText("Hello world");
* *
* // Matches second <div> * // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true)) * page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
* *
* // Matches both <div>s * // Matches both <div>s
* page.getByText(Pattern.compile("Hello")) * page.getByText(Pattern.compile("Hello"));
* *
* // Matches second <div> * // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE)) * page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre> * }</pre>
* *
* <p> <strong>Details</strong> * <p> <strong>Details</strong>
@@ -775,19 +803,19 @@ public interface FrameLocator {
* <p> You can locate by text substring, exact string, or a regular expression: * <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code * <pre>{@code
* // Matches <span> * // Matches <span>
* page.getByText("world") * page.getByText("world");
* *
* // Matches first <div> * // Matches first <div>
* page.getByText("Hello world") * page.getByText("Hello world");
* *
* // Matches second <div> * // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true)) * page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
* *
* // Matches both <div>s * // Matches both <div>s
* page.getByText(Pattern.compile("Hello")) * page.getByText(Pattern.compile("Hello"));
* *
* // Matches second <div> * // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE)) * page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre> * }</pre>
* *
* <p> <strong>Details</strong> * <p> <strong>Details</strong>
@@ -815,19 +843,19 @@ public interface FrameLocator {
* <p> You can locate by text substring, exact string, or a regular expression: * <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code * <pre>{@code
* // Matches <span> * // Matches <span>
* page.getByText("world") * page.getByText("world");
* *
* // Matches first <div> * // Matches first <div>
* page.getByText("Hello world") * page.getByText("Hello world");
* *
* // Matches second <div> * // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true)) * page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
* *
* // Matches both <div>s * // Matches both <div>s
* page.getByText(Pattern.compile("Hello")) * page.getByText(Pattern.compile("Hello"));
* *
* // Matches second <div> * // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE)) * page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre> * }</pre>
* *
* <p> <strong>Details</strong> * <p> <strong>Details</strong>
@@ -857,19 +885,19 @@ public interface FrameLocator {
* <p> You can locate by text substring, exact string, or a regular expression: * <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code * <pre>{@code
* // Matches <span> * // Matches <span>
* page.getByText("world") * page.getByText("world");
* *
* // Matches first <div> * // Matches first <div>
* page.getByText("Hello world") * page.getByText("Hello world");
* *
* // Matches second <div> * // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true)) * page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
* *
* // Matches both <div>s * // Matches both <div>s
* page.getByText(Pattern.compile("Hello")) * page.getByText(Pattern.compile("Hello"));
* *
* // Matches second <div> * // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE)) * page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre> * }</pre>
* *
* <p> <strong>Details</strong> * <p> <strong>Details</strong>
@@ -953,7 +981,8 @@ public interface FrameLocator {
*/ */
Locator getByTitle(Pattern text, GetByTitleOptions options); Locator getByTitle(Pattern text, GetByTitleOptions options);
/** /**
* Returns locator to the last matching frame. * @deprecated Use {@link com.microsoft.playwright.Locator#last Locator.last()} followed by {@link
* com.microsoft.playwright.Locator#contentFrame Locator.contentFrame()} instead.
* *
* @since v1.17 * @since v1.17
*/ */
@@ -1003,7 +1032,8 @@ public interface FrameLocator {
*/ */
Locator locator(Locator selectorOrLocator, LocatorOptions options); 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. * @deprecated Use {@link com.microsoft.playwright.Locator#nth Locator.nth()} followed by {@link
* com.microsoft.playwright.Locator#contentFrame Locator.contentFrame()} instead.
* *
* @since v1.17 * @since v1.17
*/ */
@@ -1018,7 +1048,7 @@ public interface FrameLocator {
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* FrameLocator frameLocator = page.frameLocator("iframe[name=\"embedded\"]"); * FrameLocator frameLocator = page.locator("iframe[name=\"embedded\"]").contentFrame();
* // ... * // ...
* Locator locator = frameLocator.owner(); * Locator locator = frameLocator.owner();
* assertThat(locator).isVisible(); * assertThat(locator).isVisible();
@@ -48,10 +48,7 @@ import com.microsoft.playwright.options.*;
* *
* <p> An example to trigger select-all with the keyboard * <p> An example to trigger select-all with the keyboard
* <pre>{@code * <pre>{@code
* // on Windows and Linux * page.keyboard().press("ControlOrMeta+A");
* page.keyboard().press("Control+A");
* // on macOS
* page.keyboard().press("Meta+A");
* }</pre> * }</pre>
*/ */
public interface Keyboard { public interface Keyboard {
@@ -97,7 +94,8 @@ public interface Keyboard {
* ArrowUp}, etc. * ArrowUp}, etc.
* *
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code * <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code
* ShiftLeft}. * ShiftLeft}, {@code ControlOrMeta}. {@code ControlOrMeta} resolves to {@code Control} on Windows and Linux and to {@code
* Meta} on macOS.
* *
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case. * <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
* *
@@ -147,7 +145,8 @@ public interface Keyboard {
* ArrowUp}, etc. * ArrowUp}, etc.
* *
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code * <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code
* ShiftLeft}. * ShiftLeft}, {@code ControlOrMeta}. {@code ControlOrMeta} resolves to {@code Control} on Windows and Linux and to {@code
* Meta} on macOS.
* *
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case. * <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
* *
@@ -162,7 +161,7 @@ public interface Keyboard {
* Page page = browser.newPage(); * Page page = browser.newPage();
* page.navigate("https://keycode.info"); * page.navigate("https://keycode.info");
* page.keyboard().press("A"); * page.keyboard().press("A");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png")); * page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png")));
* page.keyboard().press("ArrowLeft"); * page.keyboard().press("ArrowLeft");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("ArrowLeft.png"))); * page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("ArrowLeft.png")));
* page.keyboard().press("Shift+O"); * page.keyboard().press("Shift+O");
@@ -193,7 +192,8 @@ public interface Keyboard {
* ArrowUp}, etc. * ArrowUp}, etc.
* *
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code * <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code
* ShiftLeft}. * ShiftLeft}, {@code ControlOrMeta}. {@code ControlOrMeta} resolves to {@code Control} on Windows and Linux and to {@code
* Meta} on macOS.
* *
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case. * <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
* *
@@ -208,7 +208,7 @@ public interface Keyboard {
* Page page = browser.newPage(); * Page page = browser.newPage();
* page.navigate("https://keycode.info"); * page.navigate("https://keycode.info");
* page.keyboard().press("A"); * page.keyboard().press("A");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png")); * page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png")));
* page.keyboard().press("ArrowLeft"); * page.keyboard().press("ArrowLeft");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("ArrowLeft.png"))); * page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("ArrowLeft.png")));
* page.keyboard().press("Shift+O"); * page.keyboard().press("Shift+O");
File diff suppressed because it is too large Load Diff
@@ -21,6 +21,11 @@ import com.microsoft.playwright.options.*;
/** /**
* The Mouse class operates in main-frame CSS pixels relative to the top-left corner of the viewport. * The Mouse class operates in main-frame CSS pixels relative to the top-left corner of the viewport.
* *
* <p> <strong>NOTE:</strong> If you want to debug where the mouse moved, you can use the <a
* href="https://playwright.dev/java/docs/trace-viewer-intro">Trace viewer</a> or <a
* href="https://playwright.dev/java/docs/running-tests">Playwright Inspector</a>. A red dot showing the location of the
* mouse will be shown for every mouse action.
*
* <p> Every {@code page} object has its own Mouse, accessible with {@link com.microsoft.playwright.Page#mouse Page.mouse()}. * <p> Every {@code page} object has its own Mouse, accessible with {@link com.microsoft.playwright.Page#mouse Page.mouse()}.
* <pre>{@code * <pre>{@code
* // Using page.mouse to trace a 100x100 square. * // Using page.mouse to trace a 100x100 square.
@@ -122,12 +127,16 @@ public interface Mouse {
} }
class MoveOptions { class MoveOptions {
/** /**
* Defaults to 1. Sends intermediate {@code mousemove} events. * Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/ */
public Integer steps; public Integer steps;
/** /**
* Defaults to 1. Sends intermediate {@code mousemove} events. * Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/ */
public MoveOptions setSteps(int steps) { public MoveOptions setSteps(int steps) {
this.steps = steps; this.steps = steps;
@@ -163,6 +172,8 @@ public interface Mouse {
* Shortcut for {@link com.microsoft.playwright.Mouse#move Mouse.move()}, {@link com.microsoft.playwright.Mouse#down * Shortcut for {@link com.microsoft.playwright.Mouse#move Mouse.move()}, {@link com.microsoft.playwright.Mouse#down
* Mouse.down()}, {@link com.microsoft.playwright.Mouse#up Mouse.up()}. * Mouse.down()}, {@link com.microsoft.playwright.Mouse#up Mouse.up()}.
* *
* @param x X coordinate relative to the main frame's viewport in CSS pixels.
* @param y Y coordinate relative to the main frame's viewport in CSS pixels.
* @since v1.8 * @since v1.8
*/ */
default void click(double x, double y) { default void click(double x, double y) {
@@ -172,6 +183,8 @@ public interface Mouse {
* Shortcut for {@link com.microsoft.playwright.Mouse#move Mouse.move()}, {@link com.microsoft.playwright.Mouse#down * Shortcut for {@link com.microsoft.playwright.Mouse#move Mouse.move()}, {@link com.microsoft.playwright.Mouse#down
* Mouse.down()}, {@link com.microsoft.playwright.Mouse#up Mouse.up()}. * Mouse.down()}, {@link com.microsoft.playwright.Mouse#up Mouse.up()}.
* *
* @param x X coordinate relative to the main frame's viewport in CSS pixels.
* @param y Y coordinate relative to the main frame's viewport in CSS pixels.
* @since v1.8 * @since v1.8
*/ */
void click(double x, double y, ClickOptions options); void click(double x, double y, ClickOptions options);
@@ -180,6 +193,8 @@ public interface Mouse {
* Mouse.down()}, {@link com.microsoft.playwright.Mouse#up Mouse.up()}, {@link com.microsoft.playwright.Mouse#down * Mouse.down()}, {@link com.microsoft.playwright.Mouse#up Mouse.up()}, {@link com.microsoft.playwright.Mouse#down
* Mouse.down()} and {@link com.microsoft.playwright.Mouse#up Mouse.up()}. * Mouse.down()} and {@link com.microsoft.playwright.Mouse#up Mouse.up()}.
* *
* @param x X coordinate relative to the main frame's viewport in CSS pixels.
* @param y Y coordinate relative to the main frame's viewport in CSS pixels.
* @since v1.8 * @since v1.8
*/ */
default void dblclick(double x, double y) { default void dblclick(double x, double y) {
@@ -190,6 +205,8 @@ public interface Mouse {
* Mouse.down()}, {@link com.microsoft.playwright.Mouse#up Mouse.up()}, {@link com.microsoft.playwright.Mouse#down * Mouse.down()}, {@link com.microsoft.playwright.Mouse#up Mouse.up()}, {@link com.microsoft.playwright.Mouse#down
* Mouse.down()} and {@link com.microsoft.playwright.Mouse#up Mouse.up()}. * Mouse.down()} and {@link com.microsoft.playwright.Mouse#up Mouse.up()}.
* *
* @param x X coordinate relative to the main frame's viewport in CSS pixels.
* @param y Y coordinate relative to the main frame's viewport in CSS pixels.
* @since v1.8 * @since v1.8
*/ */
void dblclick(double x, double y, DblclickOptions options); void dblclick(double x, double y, DblclickOptions options);
@@ -210,6 +227,8 @@ public interface Mouse {
/** /**
* Dispatches a {@code mousemove} event. * Dispatches a {@code mousemove} event.
* *
* @param x X coordinate relative to the main frame's viewport in CSS pixels.
* @param y Y coordinate relative to the main frame's viewport in CSS pixels.
* @since v1.8 * @since v1.8
*/ */
default void move(double x, double y) { default void move(double x, double y) {
@@ -218,6 +237,8 @@ public interface Mouse {
/** /**
* Dispatches a {@code mousemove} event. * Dispatches a {@code mousemove} event.
* *
* @param x X coordinate relative to the main frame's viewport in CSS pixels.
* @param y Y coordinate relative to the main frame's viewport in CSS pixels.
* @since v1.8 * @since v1.8
*/ */
void move(double x, double y, MoveOptions options); void move(double x, double y, MoveOptions options);
@@ -236,7 +257,8 @@ public interface Mouse {
*/ */
void up(UpOptions options); void up(UpOptions options);
/** /**
* Dispatches a {@code wheel} event. * Dispatches a {@code wheel} event. This method is usually used to manually scroll the page. See <a
* href="https://playwright.dev/java/docs/input#scrolling">scrolling</a> for alternative ways to scroll.
* *
* <p> <strong>NOTE:</strong> Wheel events may cause scrolling if they are not handled, and this method does not wait for the scrolling to finish * <p> <strong>NOTE:</strong> Wheel events may cause scrolling if they are not handled, and this method does not wait for the scrolling to finish
* before returning. * before returning.
File diff suppressed because it is too large Load Diff
@@ -99,7 +99,7 @@ public interface Request {
*/ */
List<HttpHeader> headersArray(); List<HttpHeader> headersArray();
/** /**
* Returns the value of the header matching the name. The name is case insensitive. * Returns the value of the header matching the name. The name is case-insensitive.
* *
* @param name Name of the header. * @param name Name of the header.
* @since v1.15 * @since v1.15
@@ -183,6 +183,16 @@ public interface Request {
* @since v1.8 * @since v1.8
*/ */
Response response(); Response response();
/**
* Returns the {@code Response} object if the response has already been received, {@code null} otherwise.
*
* <p> Unlike {@link com.microsoft.playwright.Request#response Request.response()}, this method does not wait for the response
* to arrive. It returns immediately with the response object if the response has been received, or {@code null} if the
* response has not been received yet.
*
* @since v1.59
*/
Response existingResponse();
/** /**
* Returns resource size information for given request. * Returns resource size information for given request.
* *
@@ -71,7 +71,7 @@ public interface Response {
*/ */
List<HttpHeader> headersArray(); List<HttpHeader> headersArray();
/** /**
* Returns the value of the header matching the name. The name is case insensitive. If multiple headers have the same name * 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} * (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. * separator is used. If no headers are found, {@code null} is returned.
* *
@@ -80,12 +80,18 @@ public interface Response {
*/ */
String headerValue(String name); String headerValue(String name);
/** /**
* Returns all values of the headers matching the name, for example {@code set-cookie}. The name is case insensitive. * 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. * @param name Name of the header.
* @since v1.15 * @since v1.15
*/ */
List<String> headerValues(String name); List<String> headerValues(String name);
/**
* Returns the http version used by the response.
*
* @since v1.59
*/
String httpVersion();
/** /**
* Contains a boolean stating whether the response was successful (status in the range 200-299) or not. * Contains a boolean stating whether the response was successful (status in the range 200-299) or not.
* *
@@ -147,6 +147,12 @@ public interface Route {
* exceeded. Defaults to {@code 20}. Pass {@code 0} to not follow redirects. * exceeded. Defaults to {@code 20}. Pass {@code 0} to not follow redirects.
*/ */
public Integer maxRedirects; public Integer maxRedirects;
/**
* Maximum number of times network errors should be retried. Currently only {@code ECONNRESET} error is retried. Does not
* retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to {@code 0} - no
* retries.
*/
public Integer maxRetries;
/** /**
* If set changes the request method (e.g. GET or POST). * If set changes the request method (e.g. GET or POST).
*/ */
@@ -179,6 +185,15 @@ public interface Route {
this.maxRedirects = maxRedirects; this.maxRedirects = maxRedirects;
return this; return this;
} }
/**
* Maximum number of times network errors should be retried. Currently only {@code ECONNRESET} error is retried. Does not
* retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to {@code 0} - no
* retries.
*/
public FetchOptions setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
return this;
}
/** /**
* If set changes the request method (e.g. GET or POST). * If set changes the request method (e.g. GET or POST).
*/ */
@@ -333,7 +348,7 @@ public interface Route {
*/ */
void abort(String errorCode); void abort(String errorCode);
/** /**
* Continues route's request with optional overrides. * Sends route's request to the network with optional overrides.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
@@ -348,10 +363,17 @@ public interface Route {
* *
* <p> <strong>Details</strong> * <p> <strong>Details</strong>
* *
* <p> Note that any overrides such as {@code url} or {@code headers} only apply to the request being routed. If this request * <p> The {@code headers} option applies to both the routed request and any redirects it initiates. However, {@code url},
* results in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header * {@code method}, and {@code postData} only apply to the original request and are not carried over to redirected requests.
* through redirects, use the combination of {@link com.microsoft.playwright.Route#fetch Route.fetch()} and {@link *
* com.microsoft.playwright.Route#fulfill Route.fulfill()} instead. * <p> {@link com.microsoft.playwright.Route#resume Route.resume()} will immediately send the request to the network, other
* matching handlers won't be invoked. Use {@link com.microsoft.playwright.Route#fallback Route.fallback()} If you want
* next matching handler in the chain to be invoked.
*
* <p> <strong>NOTE:</strong> Some request headers are **forbidden** and cannot be overridden (for example, {@code Cookie}, {@code Host}, {@code
* Content-Length} and others, see <a
* href="https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header">this MDN page</a> for full list). If
* an override is provided for a forbidden header, it will be ignored and the original request header will be used.To set custom cookies, use {@link com.microsoft.playwright.BrowserContext#addCookies BrowserContext.addCookies()}.
* *
* @since v1.8 * @since v1.8
*/ */
@@ -359,7 +381,7 @@ public interface Route {
resume(null); resume(null);
} }
/** /**
* Continues route's request with optional overrides. * Sends route's request to the network with optional overrides.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
@@ -374,21 +396,31 @@ public interface Route {
* *
* <p> <strong>Details</strong> * <p> <strong>Details</strong>
* *
* <p> Note that any overrides such as {@code url} or {@code headers} only apply to the request being routed. If this request * <p> The {@code headers} option applies to both the routed request and any redirects it initiates. However, {@code url},
* results in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header * {@code method}, and {@code postData} only apply to the original request and are not carried over to redirected requests.
* through redirects, use the combination of {@link com.microsoft.playwright.Route#fetch Route.fetch()} and {@link *
* com.microsoft.playwright.Route#fulfill Route.fulfill()} instead. * <p> {@link com.microsoft.playwright.Route#resume Route.resume()} will immediately send the request to the network, other
* matching handlers won't be invoked. Use {@link com.microsoft.playwright.Route#fallback Route.fallback()} If you want
* next matching handler in the chain to be invoked.
*
* <p> <strong>NOTE:</strong> Some request headers are **forbidden** and cannot be overridden (for example, {@code Cookie}, {@code Host}, {@code
* Content-Length} and others, see <a
* href="https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header">this MDN page</a> for full list). If
* an override is provided for a forbidden header, it will be ignored and the original request header will be used.To set custom cookies, use {@link com.microsoft.playwright.BrowserContext#addCookies BrowserContext.addCookies()}.
* *
* @since v1.8 * @since v1.8
*/ */
void resume(ResumeOptions options); void resume(ResumeOptions options);
/** /**
* When several routes match the given pattern, they run in the order opposite to their registration. That way the last * Continues route's request with optional overrides. The method is similar to {@link com.microsoft.playwright.Route#resume
* Route.resume()} with the difference that other matching handlers will be invoked before sending the request.
*
* <p> <strong>Usage</strong>
*
* <p> 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 * 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 * 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. * registered route.
*
* <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* page.route("**\/*", route -> { * page.route("**\/*", route -> {
* // Runs last. * // Runs last.
@@ -442,18 +474,24 @@ public interface Route {
* }); * });
* }</pre> * }</pre>
* *
* <p> Use {@link com.microsoft.playwright.Route#resume Route.resume()} to immediately send the request to the network, other
* matching handlers won't be invoked in that case.
*
* @since v1.23 * @since v1.23
*/ */
default void fallback() { default void fallback() {
fallback(null); fallback(null);
} }
/** /**
* When several routes match the given pattern, they run in the order opposite to their registration. That way the last * Continues route's request with optional overrides. The method is similar to {@link com.microsoft.playwright.Route#resume
* Route.resume()} with the difference that other matching handlers will be invoked before sending the request.
*
* <p> <strong>Usage</strong>
*
* <p> 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 * 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 * 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. * registered route.
*
* <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* page.route("**\/*", route -> { * page.route("**\/*", route -> {
* // Runs last. * // Runs last.
@@ -507,6 +545,9 @@ public interface Route {
* }); * });
* }</pre> * }</pre>
* *
* <p> Use {@link com.microsoft.playwright.Route#resume Route.resume()} to immediately send the request to the network, other
* matching handlers won't be invoked in that case.
*
* @since v1.23 * @since v1.23
*/ */
void fallback(FallbackOptions options); void fallback(FallbackOptions options);
@@ -0,0 +1,276 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
/**
* Interface for capturing screencast frames from a page.
*/
public interface Screencast {
class StartOptions {
/**
* Callback that receives JPEG-encoded frame data along with the page viewport size at the time of capture.
*/
public Consumer<ScreencastFrame> onFrame;
/**
* Path where the video should be saved when the screencast is stopped. When provided, video recording is started.
*/
public Path path;
/**
* The quality of the image, between 0-100.
*/
public Integer quality;
/**
* Specifies the dimensions of screencast frames. The actual frame is scaled to preserve the page's aspect ratio and may be
* smaller than these bounds. If a screencast is already active (e.g. started by tracing or video recording), the existing
* configuration takes precedence and the frame size may exceed these bounds or this option may be ignored. If not
* specified the size will be equal to page viewport scaled down to fit into 800×800.
*/
public Size size;
/**
* Callback that receives JPEG-encoded frame data along with the page viewport size at the time of capture.
*/
public StartOptions setOnFrame(Consumer<ScreencastFrame> onFrame) {
this.onFrame = onFrame;
return this;
}
/**
* Path where the video should be saved when the screencast is stopped. When provided, video recording is started.
*/
public StartOptions setPath(Path path) {
this.path = path;
return this;
}
/**
* The quality of the image, between 0-100.
*/
public StartOptions setQuality(int quality) {
this.quality = quality;
return this;
}
/**
* Specifies the dimensions of screencast frames. The actual frame is scaled to preserve the page's aspect ratio and may be
* smaller than these bounds. If a screencast is already active (e.g. started by tracing or video recording), the existing
* configuration takes precedence and the frame size may exceed these bounds or this option may be ignored. If not
* specified the size will be equal to page viewport scaled down to fit into 800×800.
*/
public StartOptions setSize(int width, int height) {
return setSize(new Size(width, height));
}
/**
* Specifies the dimensions of screencast frames. The actual frame is scaled to preserve the page's aspect ratio and may be
* smaller than these bounds. If a screencast is already active (e.g. started by tracing or video recording), the existing
* configuration takes precedence and the frame size may exceed these bounds or this option may be ignored. If not
* specified the size will be equal to page viewport scaled down to fit into 800×800.
*/
public StartOptions setSize(Size size) {
this.size = size;
return this;
}
}
class ShowOverlayOptions {
/**
* Duration in milliseconds after which the overlay is automatically removed. Overlay stays until dismissed if not
* provided.
*/
public Double duration;
/**
* Duration in milliseconds after which the overlay is automatically removed. Overlay stays until dismissed if not
* provided.
*/
public ShowOverlayOptions setDuration(double duration) {
this.duration = duration;
return this;
}
}
class ShowChapterOptions {
/**
* Optional description text displayed below the title.
*/
public String description;
/**
* Duration in milliseconds after which the overlay is automatically removed. Defaults to {@code 2000}.
*/
public Double duration;
/**
* Optional description text displayed below the title.
*/
public ShowChapterOptions setDescription(String description) {
this.description = description;
return this;
}
/**
* Duration in milliseconds after which the overlay is automatically removed. Defaults to {@code 2000}.
*/
public ShowChapterOptions setDuration(double duration) {
this.duration = duration;
return this;
}
}
class ShowActionsOptions {
/**
* Cursor decoration shown for pointer actions. {@code "pointer"} (the default) renders a mouse pointer that animates from
* the previous action point to the next one. {@code "none"} disables the cursor decoration.
*/
public ScreencastCursor cursor;
/**
* How long each annotation is displayed in milliseconds. Defaults to {@code 500}.
*/
public Double duration;
/**
* Font size of the action title in pixels. Defaults to {@code 24}.
*/
public Integer fontSize;
/**
* Position of the action title overlay. Defaults to {@code "top-right"}.
*/
public AnnotatePosition position;
/**
* Cursor decoration shown for pointer actions. {@code "pointer"} (the default) renders a mouse pointer that animates from
* the previous action point to the next one. {@code "none"} disables the cursor decoration.
*/
public ShowActionsOptions setCursor(ScreencastCursor cursor) {
this.cursor = cursor;
return this;
}
/**
* How long each annotation is displayed in milliseconds. Defaults to {@code 500}.
*/
public ShowActionsOptions setDuration(double duration) {
this.duration = duration;
return this;
}
/**
* Font size of the action title in pixels. Defaults to {@code 24}.
*/
public ShowActionsOptions setFontSize(int fontSize) {
this.fontSize = fontSize;
return this;
}
/**
* Position of the action title overlay. Defaults to {@code "top-right"}.
*/
public ShowActionsOptions setPosition(AnnotatePosition position) {
this.position = position;
return this;
}
}
/**
* Starts the screencast. When {@code path} is provided, it saves video recording to the specified file. When {@code
* onFrame} is provided, delivers JPEG-encoded frames to the callback. Both can be used together.
*
* <p> <strong>Usage</strong>
*
* @since v1.59
*/
default AutoCloseable start() {
return start(null);
}
/**
* Starts the screencast. When {@code path} is provided, it saves video recording to the specified file. When {@code
* onFrame} is provided, delivers JPEG-encoded frames to the callback. Both can be used together.
*
* <p> <strong>Usage</strong>
*
* @since v1.59
*/
AutoCloseable start(StartOptions options);
/**
* Stops the screencast and video recording if active. If a video was being recorded, saves it to the path specified in
* {@link com.microsoft.playwright.Screencast#start Screencast.start()}.
*
* @since v1.59
*/
void stop();
/**
* Adds an overlay with the given HTML content. The overlay is displayed on top of the page until removed. Returns a
* disposable that removes the overlay when disposed.
*
* @param html HTML content for the overlay.
* @since v1.59
*/
default AutoCloseable showOverlay(String html) {
return showOverlay(html, null);
}
/**
* Adds an overlay with the given HTML content. The overlay is displayed on top of the page until removed. Returns a
* disposable that removes the overlay when disposed.
*
* @param html HTML content for the overlay.
* @since v1.59
*/
AutoCloseable showOverlay(String html, ShowOverlayOptions options);
/**
* Shows a chapter overlay with a title and optional description, centered on the page with a blurred backdrop. Useful for
* narrating video recordings. The overlay is removed after the specified duration, or 2000ms.
*
* @param title Title text displayed prominently in the overlay.
* @since v1.59
*/
default void showChapter(String title) {
showChapter(title, null);
}
/**
* Shows a chapter overlay with a title and optional description, centered on the page with a blurred backdrop. Useful for
* narrating video recordings. The overlay is removed after the specified duration, or 2000ms.
*
* @param title Title text displayed prominently in the overlay.
* @since v1.59
*/
void showChapter(String title, ShowChapterOptions options);
/**
* Enables visual annotations on interacted elements. Returns a disposable that stops showing actions when disposed.
*
* @since v1.59
*/
default AutoCloseable showActions() {
return showActions(null);
}
/**
* Enables visual annotations on interacted elements. Returns a disposable that stops showing actions when disposed.
*
* @since v1.59
*/
AutoCloseable showActions(ShowActionsOptions options);
/**
* Shows overlays.
*
* @since v1.59
*/
void showOverlays();
/**
* Removes action decorations.
*
* @since v1.59
*/
void hideActions();
/**
* Hides overlays without removing them.
*
* @since v1.59
*/
void hideOverlays();
}
@@ -0,0 +1,39 @@
/*
* 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;
public interface ScreencastFrame {
/**
* JPEG-encoded frame data.
*/
byte[] data();
/**
* The timestamp of when the frame was presented by the browser, in milliseconds since the Unix epoch.
*/
double timestamp();
/**
* Width of the page viewport at the time the frame was captured.
*/
int viewportWidth();
/**
* Height of the page viewport at the time the frame was captured.
*/
int viewportHeight();
}
@@ -201,7 +201,8 @@ public interface Selectors {
* Defines custom attribute name to be used in {@link com.microsoft.playwright.Page#getByTestId Page.getByTestId()}. {@code * Defines custom attribute name to be used in {@link com.microsoft.playwright.Page#getByTestId Page.getByTestId()}. {@code
* data-testid} is used by default. * data-testid} is used by default.
* *
* @param attributeName Test id attribute name. * @param attributeName Test id attribute name. To match elements with any of several attributes, pass them as a comma-separated list, e.g.
* {@code "data-pw,data-ti"}.
* @since v1.27 * @since v1.27
*/ */
void setTestIdAttribute(String attributeName); void setTestIdAttribute(String attributeName);
@@ -20,14 +20,19 @@ package com.microsoft.playwright;
/** /**
* The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the * The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the
* touchscreen can only be used in browser contexts that have been initialized with {@code hasTouch} set to true. * touchscreen can only be used in browser contexts that have been initialized with {@code hasTouch} set to true.
*
* <p> This class is limited to emulating tap gestures. For examples of other gestures simulated by manually dispatching touch
* events, see the <a href="https://playwright.dev/java/docs/touch-events">emulating legacy touch events</a> page.
*/ */
public interface Touchscreen { public interface Touchscreen {
/** /**
* Dispatches a {@code touchstart} and {@code touchend} event with a single touch at the position ({@code x},{@code y}). * Dispatches a {@code touchstart} and {@code touchend} event with a single touch at the position ({@code x},{@code y}).
* *
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Page#tap Page.tap()} the method will throw if {@code hasTouch} option of the browser * <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Touchscreen#tap Touchscreen.tap()} will throw if the {@code hasTouch} option of the
* context is false. * browser context is false.
* *
* @param x X coordinate relative to the main frame's viewport in CSS pixels.
* @param y Y coordinate relative to the main frame's viewport in CSS pixels.
* @since v1.8 * @since v1.8
*/ */
void tap(double x, double y); void tap(double x, double y);
@@ -16,12 +16,20 @@
package com.microsoft.playwright; package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.regex.Pattern;
/** /**
* API for collecting and saving Playwright traces. Playwright traces can be opened in <a * 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> <strong>NOTE:</strong> You probably want to <a href="https://playwright.dev/docs/api/class-testoptions#test-options-trace">enable tracing in
* your config file</a> instead of using {@code context.tracing}.The {@code context.tracing} API captures browser operations and network activity, but it doesn't record test assertions
* (like {@code expect} calls). We recommend <a
* href="https://playwright.dev/docs/api/class-testoptions#test-options-trace">enabling tracing through Playwright Test
* configuration</a>, which includes those assertions and provides a more complete trace for debugging test failures.
*
* <p> Start recording a trace before performing actions. At the end, stop tracing and save it to a file. * <p> Start recording a trace before performing actions. At the end, stop tracing and save it to a file.
* <pre>{@code * <pre>{@code
* Browser browser = chromium.launch(); * Browser browser = chromium.launch();
@@ -37,10 +45,16 @@ import java.nio.file.Path;
*/ */
public interface Tracing { public interface Tracing {
class StartOptions { class StartOptions {
/**
* When enabled, the trace is written to an unarchived file that is updated in real time as actions occur, instead of
* caching changes and archiving them into a zip file at the end. This is useful for live trace viewing during test
* execution.
*/
public Boolean live;
/** /**
* If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code * If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code
* tracesDir} folder specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify the * tracesDir} directory specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify
* final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stop * the final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stop
* Tracing.stop()} instead. * Tracing.stop()} instead.
*/ */
public String name; public String name;
@@ -67,10 +81,19 @@ public interface Tracing {
*/ */
public String title; public String title;
/**
* When enabled, the trace is written to an unarchived file that is updated in real time as actions occur, instead of
* caching changes and archiving them into a zip file at the end. This is useful for live trace viewing during test
* execution.
*/
public StartOptions setLive(boolean live) {
this.live = live;
return this;
}
/** /**
* If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code * If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code
* tracesDir} folder specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify the * tracesDir} directory specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify
* final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stop * the final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stop
* Tracing.stop()} instead. * Tracing.stop()} instead.
*/ */
public StartOptions setName(String name) { public StartOptions setName(String name) {
@@ -115,8 +138,8 @@ public interface Tracing {
class StartChunkOptions { class StartChunkOptions {
/** /**
* If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code * If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code
* tracesDir} folder specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify the * tracesDir} directory specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify
* final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stopChunk * the final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stopChunk
* Tracing.stopChunk()} instead. * Tracing.stopChunk()} instead.
*/ */
public String name; public String name;
@@ -127,8 +150,8 @@ public interface Tracing {
/** /**
* If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code * If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code
* tracesDir} folder specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify the * tracesDir} directory specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify
* final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stopChunk * the final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stopChunk
* Tracing.stopChunk()} instead. * Tracing.stopChunk()} instead.
*/ */
public StartChunkOptions setName(String name) { public StartChunkOptions setName(String name) {
@@ -143,6 +166,82 @@ public interface Tracing {
return this; return this;
} }
} }
class StartHarOptions {
/**
* 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 or entries in the ZIP archive. If {@code embed}
* is specified, content is stored inline the HAR file as per HAR specification. Defaults to {@code attach} for {@code
* .zip} output files and to {@code embed} for all other file extensions.
*/
public HarContentPolicy content;
/**
* 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 mode;
/**
* A glob or regex pattern to filter requests that are stored in the HAR. Defaults to none.
*/
public Object urlFilter;
/**
* 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 or entries in the ZIP archive. If {@code embed}
* is specified, content is stored inline the HAR file as per HAR specification. Defaults to {@code attach} for {@code
* .zip} output files and to {@code embed} for all other file extensions.
*/
public StartHarOptions setContent(HarContentPolicy content) {
this.content = content;
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 StartHarOptions setMode(HarMode mode) {
this.mode = mode;
return this;
}
/**
* A glob or regex pattern to filter requests that are stored in the HAR. Defaults to none.
*/
public StartHarOptions setUrlFilter(String urlFilter) {
this.urlFilter = urlFilter;
return this;
}
/**
* A glob or regex pattern to filter requests that are stored in the HAR. Defaults to none.
*/
public StartHarOptions setUrlFilter(Pattern urlFilter) {
this.urlFilter = urlFilter;
return this;
}
}
class GroupOptions {
/**
* Specifies a custom location for the group to be shown in the trace viewer. Defaults to the location of the {@link
* com.microsoft.playwright.Tracing#group Tracing.group()} call.
*/
public Location location;
/**
* Specifies a custom location for the group to be shown in the trace viewer. Defaults to the location of the {@link
* com.microsoft.playwright.Tracing#group Tracing.group()} call.
*/
public GroupOptions setLocation(String file) {
return setLocation(new Location(file));
}
/**
* Specifies a custom location for the group to be shown in the trace viewer. Defaults to the location of the {@link
* com.microsoft.playwright.Tracing#group Tracing.group()} call.
*/
public GroupOptions setLocation(Location location) {
this.location = location;
return this;
}
}
class StopOptions { class StopOptions {
/** /**
* Export trace into the file with the given path. * Export trace into the file with the given path.
@@ -176,6 +275,12 @@ public interface Tracing {
/** /**
* Start tracing. * Start tracing.
* *
* <p> <strong>NOTE:</strong> You probably want to <a href="https://playwright.dev/docs/api/class-testoptions#test-options-trace">enable tracing in
* your config file</a> instead of using {@code Tracing.start}.The {@code context.tracing} API captures browser operations and network activity, but it doesn't record test assertions
* (like {@code expect} calls). We recommend <a
* href="https://playwright.dev/docs/api/class-testoptions#test-options-trace">enabling tracing through Playwright Test
* configuration</a>, which includes those assertions and provides a more complete trace for debugging test failures.
*
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* context.tracing().start(new Tracing.StartOptions() * context.tracing().start(new Tracing.StartOptions()
@@ -195,6 +300,12 @@ public interface Tracing {
/** /**
* Start tracing. * Start tracing.
* *
* <p> <strong>NOTE:</strong> You probably want to <a href="https://playwright.dev/docs/api/class-testoptions#test-options-trace">enable tracing in
* your config file</a> instead of using {@code Tracing.start}.The {@code context.tracing} API captures browser operations and network activity, but it doesn't record test assertions
* (like {@code expect} calls). We recommend <a
* href="https://playwright.dev/docs/api/class-testoptions#test-options-trace">enabling tracing through Playwright Test
* configuration</a>, which includes those assertions and provides a more complete trace for debugging test failures.
*
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* context.tracing().start(new Tracing.StartOptions() * context.tracing().start(new Tracing.StartOptions()
@@ -271,6 +382,98 @@ public interface Tracing {
* @since v1.15 * @since v1.15
*/ */
void startChunk(StartChunkOptions options); void startChunk(StartChunkOptions options);
/**
* Start recording a HAR (HTTP Archive) of network activity in this context. The HAR file is written to disk when {@link
* com.microsoft.playwright.Tracing#stopHar Tracing.stopHar()} is called, or when the returned {@code Disposable} is
* disposed.
*
* <p> Only one HAR recording can be active at a time per {@code BrowserContext}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* context.tracing().startHar(Paths.get("trace.har"));
* Page page = context.newPage();
* page.navigate("https://playwright.dev");
* context.tracing().stopHar();
* }</pre>
*
* @param path Path on the filesystem to write the HAR file to. If the file name ends with {@code .zip}, the HAR is saved as a zip
* archive with response bodies attached as separate files.
* @since v1.60
*/
default AutoCloseable startHar(Path path) {
return startHar(path, null);
}
/**
* Start recording a HAR (HTTP Archive) of network activity in this context. The HAR file is written to disk when {@link
* com.microsoft.playwright.Tracing#stopHar Tracing.stopHar()} is called, or when the returned {@code Disposable} is
* disposed.
*
* <p> Only one HAR recording can be active at a time per {@code BrowserContext}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* context.tracing().startHar(Paths.get("trace.har"));
* Page page = context.newPage();
* page.navigate("https://playwright.dev");
* context.tracing().stopHar();
* }</pre>
*
* @param path Path on the filesystem to write the HAR file to. If the file name ends with {@code .zip}, the HAR is saved as a zip
* archive with response bodies attached as separate files.
* @since v1.60
*/
AutoCloseable startHar(Path path, StartHarOptions options);
/**
* <strong>NOTE:</strong> Use {@code test.step} instead when available.
*
* <p> Creates a new group within the trace, assigning any subsequent API calls to this group, until {@link
* com.microsoft.playwright.Tracing#groupEnd Tracing.groupEnd()} is called. Groups can be nested and will be visible in the
* trace viewer.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* // All actions between group and groupEnd
* // will be shown in the trace viewer as a group.
* page.context().tracing().group("Open Playwright.dev > API");
* page.navigate("https://playwright.dev/");
* page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("API")).click();
* page.context().tracing().groupEnd();
* }</pre>
*
* @param name Group name shown in the trace viewer.
* @since v1.49
*/
default AutoCloseable group(String name) {
return group(name, null);
}
/**
* <strong>NOTE:</strong> Use {@code test.step} instead when available.
*
* <p> Creates a new group within the trace, assigning any subsequent API calls to this group, until {@link
* com.microsoft.playwright.Tracing#groupEnd Tracing.groupEnd()} is called. Groups can be nested and will be visible in the
* trace viewer.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* // All actions between group and groupEnd
* // will be shown in the trace viewer as a group.
* page.context().tracing().group("Open Playwright.dev > API");
* page.navigate("https://playwright.dev/");
* page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("API")).click();
* page.context().tracing().groupEnd();
* }</pre>
*
* @param name Group name shown in the trace viewer.
* @since v1.49
*/
AutoCloseable group(String name, GroupOptions options);
/**
* Closes the last group created by {@link com.microsoft.playwright.Tracing#group Tracing.group()}.
*
* @since v1.49
*/
void groupEnd();
/** /**
* Stop tracing. * Stop tracing.
* *
@@ -301,5 +504,12 @@ public interface Tracing {
* @since v1.15 * @since v1.15
*/ */
void stopChunk(StopChunkOptions options); void stopChunk(StopChunkOptions options);
/**
* Stop HAR recording and save the HAR file to the path given to {@link com.microsoft.playwright.Tracing#startHar
* Tracing.startHar()}.
*
* @since v1.60
*/
void stopHar();
} }
@@ -16,6 +16,7 @@
package com.microsoft.playwright; package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.nio.file.Path; import java.nio.file.Path;
/** /**
@@ -16,6 +16,7 @@
package com.microsoft.playwright; package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
/** /**
* {@code WebError} class represents an unhandled exception thrown in the page. It is dispatched via the {@link * {@code WebError} class represents an unhandled exception thrown in the page. It is dispatched via the {@link
@@ -43,5 +44,11 @@ public interface WebError {
* @since v1.38 * @since v1.38
*/ */
String error(); String error();
/**
*
*
* @since v1.60
*/
WebErrorLocation location();
} }
@@ -20,7 +20,10 @@ import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
/** /**
* The {@code WebSocket} class represents websocket connections in the page. * The {@code WebSocket} class represents WebSocket connections within a page. It provides the ability to inspect and
* manipulate the data being transmitted and received.
*
* <p> If you want to intercept or modify WebSocket frames, consider using {@code WebSocketRoute}.
*/ */
public interface WebSocket { public interface WebSocket {
@@ -0,0 +1,245 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
* Whenever a <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket">{@code WebSocket}</a> route is set up
* with {@link com.microsoft.playwright.Page#routeWebSocket Page.routeWebSocket()} or {@link
* com.microsoft.playwright.BrowserContext#routeWebSocket BrowserContext.routeWebSocket()}, the {@code WebSocketRoute}
* object allows to handle the WebSocket, like an actual server would do.
*
* <p> <strong>Mocking</strong>
*
* <p> By default, the routed WebSocket will not connect to the server. This way, you can mock entire communication over the
* WebSocket. Here is an example that responds to a {@code "request"} with a {@code "response"}.
* <pre>{@code
* page.routeWebSocket("wss://example.com/ws", ws -> {
* ws.onMessage(frame -> {
* if ("request".equals(frame.text()))
* ws.send("response");
* });
* });
* }</pre>
*
* <p> Since we do not call {@link com.microsoft.playwright.WebSocketRoute#connectToServer WebSocketRoute.connectToServer()}
* inside the WebSocket route handler, Playwright assumes that WebSocket will be mocked, and opens the WebSocket inside the
* page automatically.
*
* <p> Here is another example that handles JSON messages:
* <pre>{@code
* page.routeWebSocket("wss://example.com/ws", ws -> {
* ws.onMessage(frame -> {
* JsonObject json = new JsonParser().parse(frame.text()).getAsJsonObject();
* if ("question".equals(json.get("request").getAsString())) {
* Map<String, String> result = new HashMap();
* result.put("response", "answer");
* ws.send(gson.toJson(result));
* }
* });
* });
* }</pre>
*
* <p> <strong>Intercepting</strong>
*
* <p> Alternatively, you may want to connect to the actual server, but intercept messages in-between and modify or block them.
* Calling {@link com.microsoft.playwright.WebSocketRoute#connectToServer WebSocketRoute.connectToServer()} returns a
* server-side {@code WebSocketRoute} instance that you can send messages to, or handle incoming messages.
*
* <p> Below is an example that modifies some messages sent by the page to the server. Messages sent from the server to the
* page are left intact, relying on the default forwarding.
* <pre>{@code
* page.routeWebSocket("/ws", ws -> {
* WebSocketRoute server = ws.connectToServer();
* ws.onMessage(frame -> {
* if ("request".equals(frame.text()))
* server.send("request2");
* else
* server.send(frame.text());
* });
* });
* }</pre>
*
* <p> After connecting to the server, all **messages are forwarded** between the page and the server by default.
*
* <p> However, if you call {@link com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} on the
* original route, messages from the page to the server **will not be forwarded** anymore, but should instead be handled by
* the {@code handler}.
*
* <p> Similarly, calling {@link com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} on the
* server-side WebSocket will **stop forwarding messages** from the server to the page, and {@code handler} should take
* care of them.
*
* <p> The following example blocks some messages in both directions. Since it calls {@link
* com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} in both directions, there is no automatic
* forwarding at all.
* <pre>{@code
* page.routeWebSocket("/ws", ws -> {
* WebSocketRoute server = ws.connectToServer();
* ws.onMessage(frame -> {
* if (!"blocked-from-the-page".equals(frame.text()))
* server.send(frame.text());
* });
* server.onMessage(frame -> {
* if (!"blocked-from-the-server".equals(frame.text()))
* ws.send(frame.text());
* });
* });
* }</pre>
*/
public interface WebSocketRoute {
class CloseOptions {
/**
* Optional <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#code">close code</a>.
*/
public Integer code;
/**
* Optional <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#reason">close reason</a>.
*/
public String reason;
/**
* Optional <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#code">close code</a>.
*/
public CloseOptions setCode(int code) {
this.code = code;
return this;
}
/**
* Optional <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#reason">close reason</a>.
*/
public CloseOptions setReason(String reason) {
this.reason = reason;
return this;
}
}
/**
* Closes one side of the WebSocket connection.
*
* @since v1.48
*/
default void close() {
close(null);
}
/**
* Closes one side of the WebSocket connection.
*
* @since v1.48
*/
void close(CloseOptions options);
/**
* By default, routed WebSocket does not connect to the server, so you can mock entire WebSocket communication. This method
* connects to the actual WebSocket server, and returns the server-side {@code WebSocketRoute} instance, giving the ability
* to send and receive messages from the server.
*
* <p> Once connected to the server:
* <ul>
* <li> Messages received from the server will be **automatically forwarded** to the WebSocket in the page, unless {@link
* com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} is called on the server-side {@code
* WebSocketRoute}.</li>
* <li> Messages sent by the <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send">{@code
* WebSocket.send()}</a> call in the page will be **automatically forwarded** to the server, unless {@link
* com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} is called on the original {@code
* WebSocketRoute}.</li>
* </ul>
*
* <p> See examples at the top for more details.
*
* @since v1.48
*/
WebSocketRoute connectToServer();
/**
* Allows to handle <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close">{@code WebSocket.close}</a>.
*
* <p> By default, closing one side of the connection, either in the page or on the server, will close the other side. However,
* when {@link com.microsoft.playwright.WebSocketRoute#onClose WebSocketRoute.onClose()} handler is set up, the default
* forwarding of closure is disabled, and handler should take care of it.
*
* @param handler Function that will handle WebSocket closure. Received an optional <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#code">close code</a> and an optional <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#reason">close reason</a>.
* @since v1.48
*/
void onClose(BiConsumer<Integer, String> handler);
/**
* This method allows to handle messages that are sent by the WebSocket, either from the page or from the server.
*
* <p> When called on the original WebSocket route, this method handles messages sent from the page. You can handle this
* messages by responding to them with {@link com.microsoft.playwright.WebSocketRoute#send WebSocketRoute.send()},
* forwarding them to the server-side connection returned by {@link com.microsoft.playwright.WebSocketRoute#connectToServer
* WebSocketRoute.connectToServer()} or do something else.
*
* <p> Once this method is called, messages are not automatically forwarded to the server or to the page - you should do that
* manually by calling {@link com.microsoft.playwright.WebSocketRoute#send WebSocketRoute.send()}. See examples at the top
* for more details.
*
* <p> Calling this method again will override the handler with a new one.
*
* @param handler Function that will handle messages.
* @since v1.48
*/
void onMessage(Consumer<WebSocketFrame> handler);
/**
* Sends a message to the WebSocket. When called on the original WebSocket, sends the message to the page. When called on
* the result of {@link com.microsoft.playwright.WebSocketRoute#connectToServer WebSocketRoute.connectToServer()}, sends
* the message to the server. See examples at the top for more details.
*
* @param message Message to send.
* @since v1.48
*/
void send(String message);
/**
* Sends a message to the WebSocket. When called on the original WebSocket, sends the message to the page. When called on
* the result of {@link com.microsoft.playwright.WebSocketRoute#connectToServer WebSocketRoute.connectToServer()}, sends
* the message to the server. See examples at the top for more details.
*
* @param message Message to send.
* @since v1.48
*/
void send(byte[] message);
/**
* The list of WebSocket subprotocols requested by the page, as passed via the second argument to the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket">{@code WebSocket} constructor</a>.
* Corresponds to the {@code Sec-WebSocket-Protocol} request header.
*
* <p> Returns an empty array if no protocols were specified.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.routeWebSocket("wss://example.com/ws", ws -> {
* if (ws.protocols().contains("chat.v2")) {
* ws.onMessage(frame -> ws.send("v2:" + frame.text()));
* } else {
* ws.close(1002, "Unsupported protocol");
* }
* });
* }</pre>
*
* @since v1.60
*/
List<String> protocols();
/**
* URL of the WebSocket created in the page.
*
* @since v1.48
*/
String url();
}
@@ -0,0 +1,73 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* WebStorage exposes the page's {@code localStorage} or {@code sessionStorage} for the current origin via an async, <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Storage">browser-consistent</a> API.
*
* <p> Instances are accessed through {@link com.microsoft.playwright.Page#localStorage Page.localStorage()} and {@link
* com.microsoft.playwright.Page#sessionStorage Page.sessionStorage()}.
* <pre>{@code
* page.navigate("https://example.com");
* page.localStorage().setItem("token", "abc");
* String token = page.localStorage().getItem("token");
* List<WebStorageItem> all = page.localStorage().items();
* page.localStorage().removeItem("token");
* page.localStorage().clear();
* }</pre>
*/
public interface WebStorage {
/**
* Returns all items in the storage as name/value pairs.
*
* @since v1.61
*/
List<WebStorageItem> items();
/**
* Returns the value for the given {@code name} if present.
*
* @param name Name of the item to retrieve.
* @since v1.61
*/
String getItem(String name);
/**
* Sets the value for the given {@code name}. Overwrites any existing value for that name.
*
* @param name Name of the item to set.
* @param value New value for the item.
* @since v1.61
*/
void setItem(String name, String value);
/**
* Removes the item with the given {@code name}. No-op if the item is absent.
*
* @param name Name of the item to remove.
* @since v1.61
*/
void removeItem(String name);
/**
* Removes all items from the storage.
*
* @since v1.61
*/
void clear();
}
@@ -17,6 +17,7 @@
package com.microsoft.playwright; package com.microsoft.playwright;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate;
/** /**
* The Worker class represents a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">WebWorker</a>. * The Worker class represents a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">WebWorker</a>.
@@ -44,6 +45,16 @@ public interface Worker {
*/ */
void offClose(Consumer<Worker> handler); void offClose(Consumer<Worker> handler);
/**
* Emitted when JavaScript within the worker calls one of console API methods, e.g. {@code console.log} or {@code
* console.dir}.
*/
void onConsole(Consumer<ConsoleMessage> handler);
/**
* Removes handler that was previously added with {@link #onConsole onConsole(handler)}.
*/
void offConsole(Consumer<ConsoleMessage> handler);
class WaitForCloseOptions { class WaitForCloseOptions {
/** /**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The * Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
@@ -62,6 +73,35 @@ public interface Worker {
return this; return this;
} }
} }
class WaitForConsoleMessageOptions {
/**
* Receives the {@code ConsoleMessage} object and resolves to true 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 com.microsoft.playwright.BrowserContext#setDefaultTimeout
* BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
/**
* Receives the {@code ConsoleMessage} object and resolves to true 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 com.microsoft.playwright.BrowserContext#setDefaultTimeout
* BrowserContext.setDefaultTimeout()}.
*/
public WaitForConsoleMessageOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
/** /**
* Returns the return value of {@code expression}. * Returns the return value of {@code expression}.
* *
@@ -158,5 +198,21 @@ public interface Worker {
* @since v1.10 * @since v1.10
*/ */
Worker waitForClose(WaitForCloseOptions options, Runnable callback); Worker waitForClose(WaitForCloseOptions options, Runnable callback);
/**
* Performs action and waits for a console message.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.57
*/
default ConsoleMessage waitForConsoleMessage(Runnable callback) {
return waitForConsoleMessage(null, callback);
}
/**
* Performs action and waits for a console message.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.57
*/
ConsoleMessage waitForConsoleMessage(WaitForConsoleMessageOptions options, Runnable callback);
} }
@@ -21,15 +21,15 @@ package com.microsoft.playwright.assertions;
* The {@code APIResponseAssertions} class provides assertion methods that can be used to make assertions about the {@code * The {@code APIResponseAssertions} class provides assertion methods that can be used to make assertions about the {@code
* APIResponse} in the tests. * APIResponse} in the tests.
* <pre>{@code * <pre>{@code
* ... * // ...
* import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; * import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
* *
* public class TestPage { * public class TestPage {
* ... * // ...
* @Test * @Test
* void navigatesToLoginPage() { * void navigatesToLoginPage() {
* ... * // ...
* APIResponse response = page.request().get('https://playwright.dev'); * APIResponse response = page.request().get("https://playwright.dev");
* assertThat(response).isOK(); * assertThat(response).isOK();
* } * }
* } * }
@@ -37,8 +37,11 @@ package com.microsoft.playwright.assertions;
*/ */
public interface APIResponseAssertions { public interface APIResponseAssertions {
/** /**
* Makes the assertion check for the opposite condition. For example, this code tests that the response status is not * Makes the assertion check for the opposite condition.
* successful: *
* <p> <strong>Usage</strong>
*
* <p> For example, this code tests that the response status is not successful:
* <pre>{@code * <pre>{@code
* assertThat(response).not().isOK(); * assertThat(response).not().isOK();
* }</pre> * }</pre>
@@ -16,20 +16,23 @@
package com.microsoft.playwright.assertions; package com.microsoft.playwright.assertions;
import java.util.*;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import com.microsoft.playwright.options.AriaRole;
import com.microsoft.playwright.options.PseudoElement;
/** /**
* The {@code LocatorAssertions} class provides assertion methods that can be used to make assertions about the {@code * The {@code LocatorAssertions} class provides assertion methods that can be used to make assertions about the {@code
* Locator} state in the tests. * Locator} state in the tests.
* <pre>{@code * <pre>{@code
* ... * // ...
* import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; * import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
* *
* public class TestLocator { * public class TestLocator {
* ... * // ...
* @Test * @Test
* void statusBecomesSubmitted() { * void statusBecomesSubmitted() {
* ... * // ...
* page.getByRole(AriaRole.BUTTON).click(); * page.getByRole(AriaRole.BUTTON).click();
* assertThat(page.locator(".status")).hasText("Submitted"); * assertThat(page.locator(".status")).hasText("Submitted");
* } * }
@@ -57,16 +60,37 @@ public interface LocatorAssertions {
} }
} }
class IsCheckedOptions { class IsCheckedOptions {
/**
* Provides state to assert for. Asserts for input to be checked by default. This option can't be used when {@code
* indeterminate} is set to true.
*/
public Boolean checked; public Boolean checked;
/**
* Asserts that the element is in the indeterminate (mixed) state. Only supported for checkboxes and radio buttons. This
* option can't be true when {@code checked} is provided.
*/
public Boolean indeterminate;
/** /**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/ */
public Double timeout; public Double timeout;
/**
* Provides state to assert for. Asserts for input to be checked by default. This option can't be used when {@code
* indeterminate} is set to true.
*/
public IsCheckedOptions setChecked(boolean checked) { public IsCheckedOptions setChecked(boolean checked) {
this.checked = checked; this.checked = checked;
return this; return this;
} }
/**
* Asserts that the element is in the indeterminate (mixed) state. Only supported for checkboxes and radio buttons. This
* option can't be true when {@code checked} is provided.
*/
public IsCheckedOptions setIndeterminate(boolean indeterminate) {
this.indeterminate = indeterminate;
return this;
}
/** /**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/ */
@@ -215,6 +239,20 @@ public interface LocatorAssertions {
return this; return this;
} }
} }
class ContainsClassOptions {
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public Double timeout;
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public ContainsClassOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class ContainsTextOptions { class ContainsTextOptions {
/** /**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
@@ -253,6 +291,87 @@ public interface LocatorAssertions {
return this; return this;
} }
} }
class HasAccessibleDescriptionOptions {
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
*/
public Boolean ignoreCase;
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public Double timeout;
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
*/
public HasAccessibleDescriptionOptions setIgnoreCase(boolean ignoreCase) {
this.ignoreCase = ignoreCase;
return this;
}
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public HasAccessibleDescriptionOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class HasAccessibleErrorMessageOptions {
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
*/
public Boolean ignoreCase;
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public Double timeout;
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
*/
public HasAccessibleErrorMessageOptions setIgnoreCase(boolean ignoreCase) {
this.ignoreCase = ignoreCase;
return this;
}
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public HasAccessibleErrorMessageOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class HasAccessibleNameOptions {
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
*/
public Boolean ignoreCase;
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public Double timeout;
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
*/
public HasAccessibleNameOptions setIgnoreCase(boolean ignoreCase) {
this.ignoreCase = ignoreCase;
return this;
}
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public HasAccessibleNameOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class HasAttributeOptions { class HasAttributeOptions {
/** /**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
@@ -309,11 +428,22 @@ public interface LocatorAssertions {
} }
} }
class HasCSSOptions { class HasCSSOptions {
/**
* Pseudo-element to read computed styles from.
*/
public PseudoElement pseudo;
/** /**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/ */
public Double timeout; public Double timeout;
/**
* Pseudo-element to read computed styles from.
*/
public HasCSSOptions setPseudo(PseudoElement pseudo) {
this.pseudo = pseudo;
return this;
}
/** /**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/ */
@@ -350,6 +480,20 @@ public interface LocatorAssertions {
return this; return this;
} }
} }
class HasRoleOptions {
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public Double timeout;
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public HasRoleOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class HasTextOptions { class HasTextOptions {
/** /**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
@@ -416,9 +560,26 @@ public interface LocatorAssertions {
return this; return this;
} }
} }
class MatchesAriaSnapshotOptions {
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public Double timeout;
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public MatchesAriaSnapshotOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
/** /**
* Makes the assertion check for the opposite condition. For example, this code tests that the Locator doesn't contain text * Makes the assertion check for the opposite condition.
* {@code "error"}: *
* <p> <strong>Usage</strong>
*
* <p> For example, this code tests that the Locator doesn't contain text {@code "error"}:
* <pre>{@code * <pre>{@code
* assertThat(locator).not().containsText("error"); * assertThat(locator).not().containsText("error");
* }</pre> * }</pre>
@@ -683,10 +844,10 @@ public interface LocatorAssertions {
* assertThat(page.getByText("Welcome")).isVisible(); * assertThat(page.getByText("Welcome")).isVisible();
* *
* // At least one item in the list is visible. * // At least one item in the list is visible.
* asserThat(page.getByTestId("todo-item").first()).isVisible(); * assertThat(page.getByTestId("todo-item").first()).isVisible();
* *
* // At least one of the two elements is visible, possibly both. * // At least one of the two elements is visible, possibly both.
* asserThat( * assertThat(
* page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")) * page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in"))
* .or(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign up"))) * .or(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign up")))
* .first() * .first()
@@ -711,10 +872,10 @@ public interface LocatorAssertions {
* assertThat(page.getByText("Welcome")).isVisible(); * assertThat(page.getByText("Welcome")).isVisible();
* *
* // At least one item in the list is visible. * // At least one item in the list is visible.
* asserThat(page.getByTestId("todo-item").first()).isVisible(); * assertThat(page.getByTestId("todo-item").first()).isVisible();
* *
* // At least one of the two elements is visible, possibly both. * // At least one of the two elements is visible, possibly both.
* asserThat( * assertThat(
* page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")) * page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in"))
* .or(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign up"))) * .or(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign up")))
* .first() * .first()
@@ -724,6 +885,98 @@ public interface LocatorAssertions {
* @since v1.20 * @since v1.20
*/ */
void isVisible(IsVisibleOptions options); void isVisible(IsVisibleOptions options);
/**
* Ensures the {@code Locator} points to an element with given CSS classes. All classes from the asserted value, separated
* by spaces, must be present in the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/classList">Element.classList</a> in any order.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).containsClass("middle selected row");
* assertThat(page.locator("#component")).containsClass("selected");
* assertThat(page.locator("#component")).containsClass("row middle");
* }</pre>
*
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class lists. Each element's class attribute is matched against the corresponding class in the array:
* <pre>{@code
* assertThat(page.locator(".list > .component")).containsClass(Arrays.asList("inactive", "active", "inactive"));
* }</pre>
*
* @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
* @since v1.52
*/
default void containsClass(String expected) {
containsClass(expected, null);
}
/**
* Ensures the {@code Locator} points to an element with given CSS classes. All classes from the asserted value, separated
* by spaces, must be present in the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/classList">Element.classList</a> in any order.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).containsClass("middle selected row");
* assertThat(page.locator("#component")).containsClass("selected");
* assertThat(page.locator("#component")).containsClass("row middle");
* }</pre>
*
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class lists. Each element's class attribute is matched against the corresponding class in the array:
* <pre>{@code
* assertThat(page.locator(".list > .component")).containsClass(Arrays.asList("inactive", "active", "inactive"));
* }</pre>
*
* @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
* @since v1.52
*/
void containsClass(String expected, ContainsClassOptions options);
/**
* Ensures the {@code Locator} points to an element with given CSS classes. All classes from the asserted value, separated
* by spaces, must be present in the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/classList">Element.classList</a> in any order.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).containsClass("middle selected row");
* assertThat(page.locator("#component")).containsClass("selected");
* assertThat(page.locator("#component")).containsClass("row middle");
* }</pre>
*
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class lists. Each element's class attribute is matched against the corresponding class in the array:
* <pre>{@code
* assertThat(page.locator(".list > .component")).containsClass(Arrays.asList("inactive", "active", "inactive"));
* }</pre>
*
* @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
* @since v1.52
*/
default void containsClass(List<String> expected) {
containsClass(expected, null);
}
/**
* Ensures the {@code Locator} points to an element with given CSS classes. All classes from the asserted value, separated
* by spaces, must be present in the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/classList">Element.classList</a> in any order.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).containsClass("middle selected row");
* assertThat(page.locator("#component")).containsClass("selected");
* assertThat(page.locator("#component")).containsClass("row middle");
* }</pre>
*
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class lists. Each element's class attribute is matched against the corresponding class in the array:
* <pre>{@code
* assertThat(page.locator(".list > .component")).containsClass(Arrays.asList("inactive", "active", "inactive"));
* }</pre>
*
* @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
* @since v1.52
*/
void containsClass(List<String> expected, ContainsClassOptions options);
/** /**
* Ensures the {@code Locator} points to an element that contains the given text. All nested elements will be considered * Ensures the {@code Locator} points to an element that contains the given text. All nested elements will be considered
* when computing the text content of the element. You can use regular expressions for the value as well. * when computing the text content of the element. You can use regular expressions for the value as well.
@@ -751,7 +1004,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion: * <p> Let's see how we can use the assertion:
* <pre>{@code * <pre>{@code
* // Contains the right items in the right order * // Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
* *
* // Wrong order * // Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -796,7 +1049,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion: * <p> Let's see how we can use the assertion:
* <pre>{@code * <pre>{@code
* // Contains the right items in the right order * // Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
* *
* // Wrong order * // Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -839,7 +1092,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion: * <p> Let's see how we can use the assertion:
* <pre>{@code * <pre>{@code
* // Contains the right items in the right order * // Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
* *
* // Wrong order * // Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -884,7 +1137,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion: * <p> Let's see how we can use the assertion:
* <pre>{@code * <pre>{@code
* // Contains the right items in the right order * // Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
* *
* // Wrong order * // Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -927,7 +1180,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion: * <p> Let's see how we can use the assertion:
* <pre>{@code * <pre>{@code
* // Contains the right items in the right order * // Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
* *
* // Wrong order * // Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -972,7 +1225,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion: * <p> Let's see how we can use the assertion:
* <pre>{@code * <pre>{@code
* // Contains the right items in the right order * // Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
* *
* // Wrong order * // Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -1015,7 +1268,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion: * <p> Let's see how we can use the assertion:
* <pre>{@code * <pre>{@code
* // Contains the right items in the right order * // Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
* *
* // Wrong order * // Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -1060,7 +1313,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion: * <p> Let's see how we can use the assertion:
* <pre>{@code * <pre>{@code
* // Contains the right items in the right order * // Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
* *
* // Wrong order * // Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"}); * assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -1076,6 +1329,186 @@ public interface LocatorAssertions {
* @since v1.20 * @since v1.20
*/ */
void containsText(Pattern[] expected, ContainsTextOptions options); void containsText(Pattern[] expected, ContainsTextOptions options);
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("save-button");
* assertThat(locator).hasAccessibleDescription("Save results to disk");
* }</pre>
*
* @param description Expected accessible description.
* @since v1.44
*/
default void hasAccessibleDescription(String description) {
hasAccessibleDescription(description, null);
}
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("save-button");
* assertThat(locator).hasAccessibleDescription("Save results to disk");
* }</pre>
*
* @param description Expected accessible description.
* @since v1.44
*/
void hasAccessibleDescription(String description, HasAccessibleDescriptionOptions options);
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("save-button");
* assertThat(locator).hasAccessibleDescription("Save results to disk");
* }</pre>
*
* @param description Expected accessible description.
* @since v1.44
*/
default void hasAccessibleDescription(Pattern description) {
hasAccessibleDescription(description, null);
}
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("save-button");
* assertThat(locator).hasAccessibleDescription("Save results to disk");
* }</pre>
*
* @param description Expected accessible description.
* @since v1.44
*/
void hasAccessibleDescription(Pattern description, HasAccessibleDescriptionOptions options);
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/aria/#aria-errormessage">aria errormessage</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("username-input");
* assertThat(locator).hasAccessibleErrorMessage("Username is required.");
* }</pre>
*
* @param errorMessage Expected accessible error message.
* @since v1.50
*/
default void hasAccessibleErrorMessage(String errorMessage) {
hasAccessibleErrorMessage(errorMessage, null);
}
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/aria/#aria-errormessage">aria errormessage</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("username-input");
* assertThat(locator).hasAccessibleErrorMessage("Username is required.");
* }</pre>
*
* @param errorMessage Expected accessible error message.
* @since v1.50
*/
void hasAccessibleErrorMessage(String errorMessage, HasAccessibleErrorMessageOptions options);
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/aria/#aria-errormessage">aria errormessage</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("username-input");
* assertThat(locator).hasAccessibleErrorMessage("Username is required.");
* }</pre>
*
* @param errorMessage Expected accessible error message.
* @since v1.50
*/
default void hasAccessibleErrorMessage(Pattern errorMessage) {
hasAccessibleErrorMessage(errorMessage, null);
}
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/aria/#aria-errormessage">aria errormessage</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("username-input");
* assertThat(locator).hasAccessibleErrorMessage("Username is required.");
* }</pre>
*
* @param errorMessage Expected accessible error message.
* @since v1.50
*/
void hasAccessibleErrorMessage(Pattern errorMessage, HasAccessibleErrorMessageOptions options);
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/accname/#dfn-accessible-name">accessible name</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("save-button");
* assertThat(locator).hasAccessibleName("Save to disk");
* }</pre>
*
* @param name Expected accessible name.
* @since v1.44
*/
default void hasAccessibleName(String name) {
hasAccessibleName(name, null);
}
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/accname/#dfn-accessible-name">accessible name</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("save-button");
* assertThat(locator).hasAccessibleName("Save to disk");
* }</pre>
*
* @param name Expected accessible name.
* @since v1.44
*/
void hasAccessibleName(String name, HasAccessibleNameOptions options);
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/accname/#dfn-accessible-name">accessible name</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("save-button");
* assertThat(locator).hasAccessibleName("Save to disk");
* }</pre>
*
* @param name Expected accessible name.
* @since v1.44
*/
default void hasAccessibleName(Pattern name) {
hasAccessibleName(name, null);
}
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/accname/#dfn-accessible-name">accessible name</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("save-button");
* assertThat(locator).hasAccessibleName("Save to disk");
* }</pre>
*
* @param name Expected accessible name.
* @since v1.44
*/
void hasAccessibleName(Pattern name, HasAccessibleNameOptions options);
/** /**
* Ensures the {@code Locator} points to an element with given attribute. * Ensures the {@code Locator} points to an element with given attribute.
* *
@@ -1133,18 +1566,21 @@ public interface LocatorAssertions {
*/ */
void hasAttribute(String name, Pattern value, HasAttributeOptions options); void hasAttribute(String name, Pattern value, HasAttributeOptions options);
/** /**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* relaxed regular expression. * the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected")); * assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass("selected row"); * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre> * }</pre>
* *
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted: * <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code * <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"}); * assertThat(page.locator(".list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre> * }</pre>
* *
* @param expected Expected class or RegExp or a list of those. * @param expected Expected class or RegExp or a list of those.
@@ -1154,18 +1590,21 @@ public interface LocatorAssertions {
hasClass(expected, null); hasClass(expected, null);
} }
/** /**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* relaxed regular expression. * the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected")); * assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass("selected row"); * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre> * }</pre>
* *
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted: * <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code * <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"}); * assertThat(page.locator(".list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre> * }</pre>
* *
* @param expected Expected class or RegExp or a list of those. * @param expected Expected class or RegExp or a list of those.
@@ -1173,18 +1612,21 @@ public interface LocatorAssertions {
*/ */
void hasClass(String expected, HasClassOptions options); void hasClass(String expected, HasClassOptions options);
/** /**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* relaxed regular expression. * the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected")); * assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass("selected row"); * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre> * }</pre>
* *
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted: * <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code * <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"}); * assertThat(page.locator(".list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre> * }</pre>
* *
* @param expected Expected class or RegExp or a list of those. * @param expected Expected class or RegExp or a list of those.
@@ -1194,18 +1636,21 @@ public interface LocatorAssertions {
hasClass(expected, null); hasClass(expected, null);
} }
/** /**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* relaxed regular expression. * the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected")); * assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass("selected row"); * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre> * }</pre>
* *
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted: * <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code * <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"}); * assertThat(page.locator(".list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre> * }</pre>
* *
* @param expected Expected class or RegExp or a list of those. * @param expected Expected class or RegExp or a list of those.
@@ -1213,18 +1658,21 @@ public interface LocatorAssertions {
*/ */
void hasClass(Pattern expected, HasClassOptions options); void hasClass(Pattern expected, HasClassOptions options);
/** /**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* relaxed regular expression. * the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected")); * assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass("selected row"); * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre> * }</pre>
* *
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted: * <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code * <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"}); * assertThat(page.locator(".list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre> * }</pre>
* *
* @param expected Expected class or RegExp or a list of those. * @param expected Expected class or RegExp or a list of those.
@@ -1234,18 +1682,21 @@ public interface LocatorAssertions {
hasClass(expected, null); hasClass(expected, null);
} }
/** /**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* relaxed regular expression. * the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected")); * assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass("selected row"); * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre> * }</pre>
* *
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted: * <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code * <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"}); * assertThat(page.locator(".list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre> * }</pre>
* *
* @param expected Expected class or RegExp or a list of those. * @param expected Expected class or RegExp or a list of those.
@@ -1253,18 +1704,21 @@ public interface LocatorAssertions {
*/ */
void hasClass(String[] expected, HasClassOptions options); void hasClass(String[] expected, HasClassOptions options);
/** /**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* relaxed regular expression. * the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected")); * assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass("selected row"); * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre> * }</pre>
* *
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted: * <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code * <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"}); * assertThat(page.locator(".list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre> * }</pre>
* *
* @param expected Expected class or RegExp or a list of those. * @param expected Expected class or RegExp or a list of those.
@@ -1274,18 +1728,21 @@ public interface LocatorAssertions {
hasClass(expected, null); hasClass(expected, null);
} }
/** /**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* relaxed regular expression. * the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
* *
* <p> <strong>Usage</strong> * <p> <strong>Usage</strong>
* <pre>{@code * <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected")); * assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass("selected row"); * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre> * }</pre>
* *
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted: * <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code * <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"}); * assertThat(page.locator(".list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre> * }</pre>
* *
* @param expected Expected class or RegExp or a list of those. * @param expected Expected class or RegExp or a list of those.
@@ -1456,6 +1913,42 @@ public interface LocatorAssertions {
* @since v1.20 * @since v1.20
*/ */
void hasJSProperty(String name, Object value, HasJSPropertyOptions options); void hasJSProperty(String name, Object value, HasJSPropertyOptions options);
/**
* Ensures the {@code Locator} points to an element with a given <a href="https://www.w3.org/TR/wai-aria-1.2/#roles">ARIA
* role</a>.
*
* <p> Note that role is matched as a string, disregarding the ARIA role hierarchy. For example, asserting a superclass role
* {@code "checkbox"} on an element with a subclass role {@code "switch"} will fail.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("save-button");
* assertThat(locator).hasRole(AriaRole.BUTTON);
* }</pre>
*
* @param role Required aria role.
* @since v1.44
*/
default void hasRole(AriaRole role) {
hasRole(role, null);
}
/**
* Ensures the {@code Locator} points to an element with a given <a href="https://www.w3.org/TR/wai-aria-1.2/#roles">ARIA
* role</a>.
*
* <p> Note that role is matched as a string, disregarding the ARIA role hierarchy. For example, asserting a superclass role
* {@code "checkbox"} on an element with a subclass role {@code "switch"} will fail.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("save-button");
* assertThat(locator).hasRole(AriaRole.BUTTON);
* }</pre>
*
* @param role Required aria role.
* @since v1.44
*/
void hasRole(AriaRole role, HasRoleOptions options);
/** /**
* Ensures the {@code Locator} points to an element with the given text. All nested elements will be considered when * Ensures the {@code Locator} points to an element with the given text. All nested elements will be considered when
* computing the text content of the element. You can use regular expressions for the value as well. * computing the text content of the element. You can use regular expressions for the value as well.
@@ -1872,7 +2365,7 @@ public interface LocatorAssertions {
* *
* <p> For example, given the following element: * <p> For example, given the following element:
* <pre>{@code * <pre>{@code
* page.locator("id=favorite-colors").selectOption(["R", "G"]); * page.locator("id=favorite-colors").selectOption(new String[]{"R", "G"});
* assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") }); * assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
* }</pre> * }</pre>
* *
@@ -1890,7 +2383,7 @@ public interface LocatorAssertions {
* *
* <p> For example, given the following element: * <p> For example, given the following element:
* <pre>{@code * <pre>{@code
* page.locator("id=favorite-colors").selectOption(["R", "G"]); * page.locator("id=favorite-colors").selectOption(new String[]{"R", "G"});
* assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") }); * assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
* }</pre> * }</pre>
* *
@@ -1906,7 +2399,7 @@ public interface LocatorAssertions {
* *
* <p> For example, given the following element: * <p> For example, given the following element:
* <pre>{@code * <pre>{@code
* page.locator("id=favorite-colors").selectOption(["R", "G"]); * page.locator("id=favorite-colors").selectOption(new String[]{"R", "G"});
* assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") }); * assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
* }</pre> * }</pre>
* *
@@ -1924,7 +2417,7 @@ public interface LocatorAssertions {
* *
* <p> For example, given the following element: * <p> For example, given the following element:
* <pre>{@code * <pre>{@code
* page.locator("id=favorite-colors").selectOption(["R", "G"]); * page.locator("id=favorite-colors").selectOption(new String[]{"R", "G"});
* assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") }); * assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
* }</pre> * }</pre>
* *
@@ -1932,5 +2425,39 @@ public interface LocatorAssertions {
* @since v1.23 * @since v1.23
*/ */
void hasValues(Pattern[] values, HasValuesOptions options); void hasValues(Pattern[] values, HasValuesOptions options);
/**
* Asserts that the target element matches the given <a
* href="https://playwright.dev/java/docs/aria-snapshots">accessibility snapshot</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.navigate("https://demo.playwright.dev/todomvc/");
* assertThat(page.locator("body")).matchesAriaSnapshot("""
* - heading "todos"
* - textbox "What needs to be done?"
* """);
* }</pre>
*
* @since v1.49
*/
default void matchesAriaSnapshot(String expected) {
matchesAriaSnapshot(expected, null);
}
/**
* Asserts that the target element matches the given <a
* href="https://playwright.dev/java/docs/aria-snapshots">accessibility snapshot</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.navigate("https://demo.playwright.dev/todomvc/");
* assertThat(page.locator("body")).matchesAriaSnapshot("""
* - heading "todos"
* - textbox "What needs to be done?"
* """);
* }</pre>
*
* @since v1.49
*/
void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions options);
} }
@@ -22,14 +22,14 @@ import java.util.regex.Pattern;
* The {@code PageAssertions} class provides assertion methods that can be used to make assertions about the {@code Page} * The {@code PageAssertions} class provides assertion methods that can be used to make assertions about the {@code Page}
* state in the tests. * state in the tests.
* <pre>{@code * <pre>{@code
* ... * // ...
* import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; * import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
* *
* public class TestPage { * public class TestPage {
* ... * // ...
* @Test * @Test
* void navigatesToLoginPage() { * void navigatesToLoginPage() {
* ... * // ...
* page.getByText("Sign in").click(); * page.getByText("Sign in").click();
* assertThat(page).hasURL(Pattern.compile(".*\/login")); * assertThat(page).hasURL(Pattern.compile(".*\/login"));
* } * }
@@ -37,6 +37,20 @@ import java.util.regex.Pattern;
* }</pre> * }</pre>
*/ */
public interface PageAssertions { public interface PageAssertions {
class MatchesAriaSnapshotOptions {
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public Double timeout;
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public MatchesAriaSnapshotOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class HasTitleOptions { class HasTitleOptions {
/** /**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
@@ -52,11 +66,24 @@ public interface PageAssertions {
} }
} }
class HasURLOptions { class HasURLOptions {
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression parameter if specified. A provided predicate ignores this flag.
*/
public Boolean ignoreCase;
/** /**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/ */
public Double timeout; public Double timeout;
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression parameter if specified. A provided predicate ignores this flag.
*/
public HasURLOptions setIgnoreCase(boolean ignoreCase) {
this.ignoreCase = ignoreCase;
return this;
}
/** /**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/ */
@@ -66,8 +93,11 @@ public interface PageAssertions {
} }
} }
/** /**
* Makes the assertion check for the opposite condition. For example, this code tests that the page URL doesn't contain * Makes the assertion check for the opposite condition.
* {@code "error"}: *
* <p> <strong>Usage</strong>
*
* <p> For example, this code tests that the page URL doesn't contain {@code "error"}:
* <pre>{@code * <pre>{@code
* assertThat(page).not().hasURL("error"); * assertThat(page).not().hasURL("error");
* }</pre> * }</pre>
@@ -75,6 +105,40 @@ public interface PageAssertions {
* @since v1.20 * @since v1.20
*/ */
PageAssertions not(); PageAssertions not();
/**
* Asserts that the page body matches the given <a href="https://playwright.dev/java/docs/aria-snapshots">accessibility
* snapshot</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.navigate("https://demo.playwright.dev/todomvc/");
* assertThat(page).matchesAriaSnapshot("""
* - heading "todos"
* - textbox "What needs to be done?"
* """);
* }</pre>
*
* @since v1.60
*/
default void matchesAriaSnapshot(String expected) {
matchesAriaSnapshot(expected, null);
}
/**
* Asserts that the page body matches the given <a href="https://playwright.dev/java/docs/aria-snapshots">accessibility
* snapshot</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.navigate("https://demo.playwright.dev/todomvc/");
* assertThat(page).matchesAriaSnapshot("""
* - heading "todos"
* - textbox "What needs to be done?"
* """);
* }</pre>
*
* @since v1.60
*/
void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions options);
/** /**
* Ensures the page has the given title. * Ensures the page has the given title.
* *
@@ -30,14 +30,13 @@ import com.microsoft.playwright.impl.PageAssertionsImpl;
* *
* <p> Consider the following example: * <p> Consider the following example:
* <pre>{@code * <pre>{@code
* ...
* import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; * import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
* *
* public class TestExample { * public class TestExample {
* ... * // ...
* @Test * @Test
* void statusBecomesSubmitted() { * void statusBecomesSubmitted() {
* ... * // ...
* page.locator("#submit-button").click(); * page.locator("#submit-button").click();
* assertThat(page.locator(".status")).hasText("Submitted"); * assertThat(page.locator(".status")).hasText("Submitted");
* } * }
@@ -29,6 +29,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Base64; import java.util.Base64;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import static com.microsoft.playwright.impl.Serialization.*; import static com.microsoft.playwright.impl.Serialization.*;
@@ -36,25 +37,38 @@ import static com.microsoft.playwright.impl.Utils.toFilePayload;
class APIRequestContextImpl extends ChannelOwner implements APIRequestContext { class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
private final TracingImpl tracing; private final TracingImpl tracing;
private String disposeReason;
protected TimeoutSettings timeoutSettings = new TimeoutSettings();
APIRequestContextImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { APIRequestContextImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
this.tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString()); this.tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString());
} }
@Override
public com.microsoft.playwright.Tracing tracing() {
return tracing;
}
@Override @Override
public APIResponse delete(String url, RequestOptions options) { public APIResponse delete(String url, RequestOptions options) {
return fetch(url, ensureOptions(options, "DELETE")); return fetch(url, ensureOptions(options, "DELETE"));
} }
@Override @Override
public void dispose() { public void dispose(DisposeOptions options) {
withLogging("APIRequestContext.dispose", () -> sendMessage("dispose")); if (options == null) {
options = new DisposeOptions();
}
disposeReason = options.reason;
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("dispose", params, NO_TIMEOUT);
} }
@Override @Override
public APIResponse fetch(String urlOrRequest, RequestOptions options) { public APIResponse fetch(String urlOrRequest, RequestOptions options) {
return withLogging("APIRequestContext.fetch", () -> fetchImpl(urlOrRequest, (RequestOptionsImpl) options)); return fetchImpl(urlOrRequest, (RequestOptionsImpl) options);
} }
@Override @Override
@@ -76,9 +90,13 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
} }
private APIResponse fetchImpl(String url, RequestOptionsImpl options) { private APIResponse fetchImpl(String url, RequestOptionsImpl options) {
if (disposeReason != null) {
throw new PlaywrightException(disposeReason);
}
if (options == null) { if (options == null) {
options = new RequestOptionsImpl(); options = new RequestOptionsImpl();
} }
options.timeout = timeoutSettings.timeout(options.timeout);
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("url", url); params.addProperty("url", url);
if (options.params != null) { if (options.params != null) {
@@ -86,7 +104,7 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
for (Map.Entry<String, ?> e : options.params.entrySet()) { for (Map.Entry<String, ?> e : options.params.entrySet()) {
queryParams.put(e.getKey(), "" + e.getValue()); queryParams.put(e.getKey(), "" + e.getValue());
} }
params.add("params", toNameValueArray(queryParams)); params.add("params", toNameValueArray(queryParams.entrySet()));
} }
if (options.method != null) { if (options.method != null) {
params.addProperty("method", options.method); params.addProperty("method", options.method);
@@ -106,7 +124,7 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
} }
} }
if (bytes == null) { if (bytes == null) {
params.addProperty("jsonData", gson().toJson(options.data)); params.addProperty("jsonData", jsonDataSerializer.toJson(options.data));
} else { } else {
String base64 = Base64.getEncoder().encodeToString(bytes); String base64 = Base64.getEncoder().encodeToString(bytes);
params.addProperty("postData", base64); params.addProperty("postData", base64);
@@ -118,9 +136,6 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
if (options.multipart != null) { if (options.multipart != null) {
params.add("multipartData", serializeMultipartData(options.multipart.fields)); params.add("multipartData", serializeMultipartData(options.multipart.fields));
} }
if (options.timeout != null) {
params.addProperty("timeout", options.timeout);
}
if (options.failOnStatusCode != null) { if (options.failOnStatusCode != null) {
params.addProperty("failOnStatusCode", options.failOnStatusCode); params.addProperty("failOnStatusCode", options.failOnStatusCode);
} }
@@ -133,7 +148,13 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
} }
params.addProperty("maxRedirects", options.maxRedirects); params.addProperty("maxRedirects", options.maxRedirects);
} }
JsonObject json = sendMessage("fetch", params).getAsJsonObject(); if (options.maxRetries != null) {
if (options.maxRetries < 0) {
throw new PlaywrightException("'maxRetries' must be greater than or equal to '0'");
}
params.addProperty("maxRetries", options.maxRetries);
}
JsonObject json = sendMessage("fetch", params, timeoutSettings.timeout(options.timeout)).getAsJsonObject();
return new APIResponseImpl(this, json.getAsJsonObject("response")); return new APIResponseImpl(this, json.getAsJsonObject("response"));
} }
@@ -149,9 +170,9 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
return false; return false;
} }
private static JsonArray serializeMultipartData(Map<String, Object> data) { private static JsonArray serializeMultipartData(List<? extends Map.Entry<String, Object>> data) {
JsonArray result = new JsonArray(); JsonArray result = new JsonArray();
for (Map.Entry<String, Object> e : data.entrySet()) { for (Map.Entry<String, ?> e : data) {
FilePayload filePayload = null; FilePayload filePayload = null;
if (e.getValue() instanceof FilePayload) { if (e.getValue() instanceof FilePayload) {
filePayload = (FilePayload) e.getValue(); filePayload = (FilePayload) e.getValue();
@@ -199,14 +220,12 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
@Override @Override
public String storageState(StorageStateOptions options) { public String storageState(StorageStateOptions options) {
return withLogging("APIRequestContext.storageState", () -> { JsonElement json = sendMessage("storageState");
JsonElement json = sendMessage("storageState"); String storageState = json.toString();
String storageState = json.toString(); if (options != null && options.path != null) {
if (options != null && options.path != null) { Utils.writeToFile(storageState.getBytes(StandardCharsets.UTF_8), options.path);
Utils.writeToFile(storageState.getBytes(StandardCharsets.UTF_8), options.path); }
} return storageState;
return storageState;
});
} }
private static RequestOptionsImpl ensureOptions(RequestOptions options, String method) { private static RequestOptionsImpl ensureOptions(RequestOptions options, String method) {
@@ -20,12 +20,16 @@ import com.google.gson.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.microsoft.playwright.APIRequest; import com.microsoft.playwright.APIRequest;
import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.ClientCertificate;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.List;
import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT;
import static com.microsoft.playwright.impl.Serialization.gson; import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.addToProtocol;
class APIRequestImpl implements APIRequest { class APIRequestImpl implements APIRequest {
private final PlaywrightImpl playwright; private final PlaywrightImpl playwright;
@@ -36,12 +40,10 @@ class APIRequestImpl implements APIRequest {
@Override @Override
public APIRequestContextImpl newContext(NewContextOptions options) { public APIRequestContextImpl newContext(NewContextOptions options) {
return playwright.withLogging("APIRequest.newContext", () -> newContextImpl(options));
}
private APIRequestContextImpl newContextImpl(NewContextOptions options) {
if (options == null) { if (options == null) {
options = new NewContextOptions(); options = new NewContextOptions();
} else {
options = Utils.clone(options);
} }
if (options.storageStatePath != null) { if (options.storageStatePath != null) {
try { try {
@@ -57,13 +59,19 @@ class APIRequestImpl implements APIRequest {
storageState = new Gson().fromJson(options.storageState, JsonObject.class); storageState = new Gson().fromJson(options.storageState, JsonObject.class);
options.storageState = null; options.storageState = null;
} }
List<ClientCertificate> clientCertificateList = options.clientCertificates;
options.clientCertificates = null;
Double timeout = options.timeout;
// Timeout is handled on the client.
options.timeout = null;
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
if (storageState != null) { if (storageState != null) {
params.add("storageState", storageState); params.add("storageState", storageState);
} }
addToProtocol(params, clientCertificateList);
JsonObject result = playwright.sendMessage("newRequest", params).getAsJsonObject(); JsonObject result = playwright.sendMessage("newRequest", params, NO_TIMEOUT).getAsJsonObject();
APIRequestContextImpl context = playwright.connection.getExistingObject(result.getAsJsonObject("request").get("guid").getAsString()); APIRequestContextImpl context = playwright.connection.getExistingObject(result.getAsJsonObject("request").get("guid").getAsString());
context.timeoutSettings.setDefaultTimeout(timeout);
return context; return context;
} }
} }
@@ -17,18 +17,20 @@
package com.microsoft.playwright.impl; package com.microsoft.playwright.impl;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.microsoft.playwright.APIResponse; import com.microsoft.playwright.APIResponse;
import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.HttpHeader; import com.microsoft.playwright.options.HttpHeader;
import com.microsoft.playwright.options.SecurityDetails;
import com.microsoft.playwright.options.ServerAddr;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT;
import static com.microsoft.playwright.impl.Serialization.gson; import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError; import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
@@ -46,31 +48,27 @@ class APIResponseImpl implements APIResponse {
@Override @Override
public byte[] body() { public byte[] body() {
return context.withLogging("APIResponse.body", () -> { try {
try { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("fetchUid", fetchUid());
params.addProperty("fetchUid", fetchUid()); JsonObject json = context.sendMessage("fetchResponseBody", params, NO_TIMEOUT).getAsJsonObject();
JsonObject json = context.sendMessage("fetchResponseBody", params).getAsJsonObject(); if (!json.has("binary")) {
if (!json.has("binary")) { throw new PlaywrightException("Response has been disposed");
throw new PlaywrightException("Response has been disposed");
}
return Base64.getDecoder().decode(json.get("binary").getAsString());
} catch (PlaywrightException e) {
if (isSafeCloseError(e)) {
throw new PlaywrightException("Response has been disposed");
}
throw e;
} }
}); return Base64.getDecoder().decode(json.get("binary").getAsString());
} catch (PlaywrightException e) {
if (isSafeCloseError(e)) {
throw new PlaywrightException("Response has been disposed");
}
throw e;
}
} }
@Override @Override
public void dispose() { public void dispose() {
context.withLogging("APIResponse.dispose", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("fetchUid", fetchUid());
params.addProperty("fetchUid", fetchUid()); context.sendMessage("disposeAPIResponse", params, NO_TIMEOUT);
context.sendMessage("disposeAPIResponse", params);
});
} }
@Override @Override
@@ -89,6 +87,22 @@ class APIResponseImpl implements APIResponse {
return status == 0 || (status >= 200 && status <= 299); return status == 0 || (status >= 200 && status <= 299);
} }
@Override
public SecurityDetails securityDetails() {
if (!initializer.has("securityDetails")) {
return null;
}
return gson().fromJson(initializer.get("securityDetails"), SecurityDetails.class);
}
@Override
public ServerAddr serverAddr() {
if (!initializer.has("serverAddr")) {
return null;
}
return gson().fromJson(initializer.get("serverAddr"), ServerAddr.class);
}
@Override @Override
public int status() { public int status() {
return initializer.get("status").getAsInt(); return initializer.get("status").getAsInt();
@@ -116,7 +130,7 @@ class APIResponseImpl implements APIResponse {
List<String> fetchLog() { List<String> fetchLog() {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("fetchUid", fetchUid()); params.addProperty("fetchUid", fetchUid());
JsonObject json = context.sendMessage("fetchLog", params).getAsJsonObject(); JsonObject json = context.sendMessage("fetchLog", params, NO_TIMEOUT).getAsJsonObject();
JsonArray log = json.get("log").getAsJsonArray(); JsonArray log = json.get("log").getAsJsonArray();
return gson().fromJson(log, new TypeToken<List<String>>() {}.getType()); return gson().fromJson(log, new TypeToken<List<String>>() {}.getType());
} }
@@ -86,6 +86,6 @@ class ArtifactImpl extends ChannelOwner {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("path", path.toString()); params.addProperty("path", path.toString());
sendMessage("saveAs", params); sendMessage("saveAs", params, NO_TIMEOUT);
} }
} }
@@ -16,10 +16,10 @@
package com.microsoft.playwright.impl; package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import org.opentest4j.AssertionFailedError; import org.opentest4j.AssertionFailedError;
import org.opentest4j.ValueWrapper; import org.opentest4j.ValueWrapper;
import java.lang.reflect.Field;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -28,53 +28,63 @@ import java.util.stream.Collectors;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags; import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
class AssertionsBase { abstract class AssertionsBase {
final LocatorImpl actualLocator;
final boolean isNot; final boolean isNot;
AssertionsBase(LocatorImpl actual, boolean isNot) { AssertionsBase(boolean isNot) {
this.actualLocator = actual;
this.isNot = isNot; this.isNot = isNot;
} }
void expectImpl(String expression, ExpectedTextValue textValue, Object expected, String message, FrameExpectOptions options) { void expectImpl(String expression, ExpectedTextValue textValue, Object expected, String message, FrameExpectOptions options, String title) {
expectImpl(expression, asList(textValue), expected, message, options); expectImpl(expression, asList(textValue), expected, message, options, title);
} }
void expectImpl(String expression, List<ExpectedTextValue> expectedText, Object expected, String message, FrameExpectOptions options) { void expectImpl(String expression, List<ExpectedTextValue> expectedText, Object expected, String message, FrameExpectOptions options, String title) {
if (options == null) { if (options == null) {
options = new FrameExpectOptions(); options = new FrameExpectOptions();
} }
options.expectedText = expectedText; options.expectedText = expectedText;
options.isNot = isNot; expectImpl(expression, options, expected, message, title);
expectImpl(expression, options, expected, message);
} }
void expectImpl(String expression, FrameExpectOptions expectOptions, Object expected, String message) { void expectImpl(String expression, FrameExpectOptions expectOptions, Object expected, String message, String title) {
if (expectOptions.timeout == null) { if (expectOptions.timeout == null) {
expectOptions.timeout = AssertionsTimeout.defaultTimeout; expectOptions.timeout = AssertionsTimeout.defaultTimeout;
} }
if (expectOptions.isNot) { expectOptions.isNot = isNot;
if (isNot) {
message = message.replace("expected to", "expected not to"); message = message.replace("expected to", "expected not to");
} }
FrameExpectResult result = actualLocator.expect(expression, expectOptions); FrameExpectResult result = doExpect(expression, expectOptions, title);
if (result.matches == isNot) { if (result.matches == isNot) {
Object actual = result.received == null ? null : Serialization.deserialize(result.received); Object actual;
String log = String.join("\n", result.log); if (result.received == null) {
actual = null;
} else if (result.received.value != null) {
actual = Serialization.deserialize(result.received.value);
} else {
actual = result.received.ariaSnapshot;
}
String log = (result.log == null) ? "" : String.join("\n", result.log);
if (!log.isEmpty()) { if (!log.isEmpty()) {
log = "\nCall log:\n" + log; log = "\nCall log:\n" + log;
} }
if (result.errorMessage != null) {
message += "\n" + result.errorMessage;
}
if (expected == null) { if (expected == null) {
throw new AssertionFailedError(message + log); throw new AssertionFailedError(message + log);
} }
ValueWrapper expectedValue = formatValue(expected); ValueWrapper expectedValue = formatValue(expected);
ValueWrapper actualValue = formatValue(actual); ValueWrapper actualValue = formatValue(actual);
message += ": " + expectedValue.getStringRepresentation() + "\nReceived: " + actualValue.getStringRepresentation() + "\n"; message += "\nExpected: " + expectedValue.getStringRepresentation() + "\nReceived: " + actualValue.getStringRepresentation() + "\n";
throw new AssertionFailedError(message + log, expectedValue, actualValue); throw new AssertionFailedError(message + log, expectedValue, actualValue);
} }
} }
private static ValueWrapper formatValue(Object value) { abstract FrameExpectResult doExpect(String expression, FrameExpectOptions expectOptions, String title);
protected static ValueWrapper formatValue(Object value) {
if (value == null || !value.getClass().isArray()) { if (value == null || !value.getClass().isArray()) {
return ValueWrapper.create(value); return ValueWrapper.create(value);
} }
@@ -91,4 +101,17 @@ class AssertionsBase {
} }
return expected; return expected;
} }
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;
}
}
} }
@@ -78,11 +78,11 @@ class BindingCall extends ChannelOwner {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.add("result", gson().toJsonTree(serializeArgument(result))); params.add("result", gson().toJsonTree(serializeArgument(result)));
sendMessage("resolve", params); sendMessage("resolve", params, NO_TIMEOUT);
} catch (RuntimeException exception) { } catch (RuntimeException exception) {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.add("error", gson().toJsonTree(serializeError(exception))); params.add("error", gson().toJsonTree(serializeError(exception)));
sendMessage("reject", params); sendMessage("reject", params, NO_TIMEOUT);
} }
} }
} }
@@ -34,23 +34,24 @@ import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter; import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Serialization.gson; import static com.microsoft.playwright.impl.Utils.*;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes; import static java.nio.file.Files.readAllBytes;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
class BrowserContextImpl extends ChannelOwner implements BrowserContext { class BrowserContextImpl extends ChannelOwner implements BrowserContext {
private final BrowserImpl browser; protected BrowserImpl browser;
private final TracingImpl tracing; private final TracingImpl tracing;
private final DebuggerImpl debugger;
private final APIRequestContextImpl request; private final APIRequestContextImpl request;
private final ClockImpl clock;
private final CredentialsImpl credentials;
final List<PageImpl> pages = new ArrayList<>(); final List<PageImpl> pages = new ArrayList<>();
final List<PageImpl> backgroundPages = new ArrayList<>();
final Router routes = new Router(); final Router routes = new Router();
private boolean closeWasCalled; final WebSocketRouter webSocketRoutes = new WebSocketRouter();
private boolean closingOrClosed;
private final WaitableEvent<EventType, ?> closePromise; private final WaitableEvent<EventType, ?> closePromise;
final Map<String, BindingCallback> bindings = new HashMap<>(); final Map<String, BindingCallback> bindings = new HashMap<>();
PageImpl ownerPage; PageImpl ownerPage;
@@ -68,26 +69,18 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} }
private final ListenerCollection<EventType> listeners = new ListenerCollection<>(eventSubscriptions(), this); private final ListenerCollection<EventType> listeners = new ListenerCollection<>(eventSubscriptions(), this);
final TimeoutSettings timeoutSettings = new TimeoutSettings(); final TimeoutSettings timeoutSettings = new TimeoutSettings();
Path videosDir;
URL baseUrl;
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 { enum EventType {
BACKGROUNDPAGE,
CLOSE, CLOSE,
CONSOLE, CONSOLE,
DIALOG, DIALOG,
DOWNLOAD,
FRAMEATTACHED,
FRAMEDETACHED,
FRAMENAVIGATED,
PAGE, PAGE,
PAGECLOSE,
PAGELOAD,
WEBERROR, WEBERROR,
REQUEST, REQUEST,
REQUESTFAILED, REQUESTFAILED,
@@ -97,28 +90,32 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
BrowserContextImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { BrowserContextImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
if (parent instanceof BrowserImpl) {
browser = (BrowserImpl) parent;
} else {
browser = null;
}
tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString()); tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString());
debugger = connection.getExistingObject(initializer.getAsJsonObject("debugger").get("guid").getAsString());
request = connection.getExistingObject(initializer.getAsJsonObject("requestContext").get("guid").getAsString()); request = connection.getExistingObject(initializer.getAsJsonObject("requestContext").get("guid").getAsString());
request.timeoutSettings = timeoutSettings;
clock = new ClockImpl(this);
credentials = new CredentialsImpl(this);
closePromise = new WaitableEvent<>(listeners, EventType.CLOSE); closePromise = new WaitableEvent<>(listeners, EventType.CLOSE);
} }
void setRecordHar(Path path, HarContentPolicy policy) { Path videosDir() {
if (path != null) { JsonObject recordVideo = initializer.getAsJsonObject("options").getAsJsonObject("recordVideo");
harRecorders.put("", new HarRecorder(path, policy)); if (recordVideo == null) {
return null;
} }
return Paths.get(recordVideo.get("dir").getAsString());
} }
void setBaseUrl(String spec) { URL baseUrl() {
try { JsonElement url = initializer.getAsJsonObject("options").get("baseURL");
this.baseUrl = new URL(spec); if (url != null) {
} catch (MalformedURLException e) { try {
this.baseUrl = null; return new URL(url.getAsString());
} catch (MalformedURLException e) {
}
} }
return null;
} }
String effectiveCloseReason() { String effectiveCloseReason() {
@@ -133,12 +130,24 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override @Override
public void onBackgroundPage(Consumer<Page> handler) { public void onBackgroundPage(Consumer<Page> handler) {
listeners.add(EventType.BACKGROUNDPAGE, handler);
} }
@Override @Override
public void offBackgroundPage(Consumer<Page> handler) { public void offBackgroundPage(Consumer<Page> handler) {
listeners.remove(EventType.BACKGROUNDPAGE, handler); }
@Override
public void onDownload(Consumer<Download> handler) {
listeners.add(EventType.DOWNLOAD, handler);
}
@Override
public void offDownload(Consumer<Download> handler) {
listeners.remove(EventType.DOWNLOAD, handler);
}
void notifyDownload(Download download) {
listeners.notify(EventType.DOWNLOAD, download);
} }
@Override @Override
@@ -181,6 +190,76 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
listeners.remove(EventType.PAGE, handler); listeners.remove(EventType.PAGE, handler);
} }
@Override
public void onFrameAttached(Consumer<Frame> handler) {
listeners.add(EventType.FRAMEATTACHED, handler);
}
@Override
public void offFrameAttached(Consumer<Frame> handler) {
listeners.remove(EventType.FRAMEATTACHED, handler);
}
void notifyFrameAttached(FrameImpl frame) {
listeners.notify(EventType.FRAMEATTACHED, frame);
}
@Override
public void onFrameDetached(Consumer<Frame> handler) {
listeners.add(EventType.FRAMEDETACHED, handler);
}
@Override
public void offFrameDetached(Consumer<Frame> handler) {
listeners.remove(EventType.FRAMEDETACHED, handler);
}
void notifyFrameDetached(FrameImpl frame) {
listeners.notify(EventType.FRAMEDETACHED, frame);
}
@Override
public void onFrameNavigated(Consumer<Frame> handler) {
listeners.add(EventType.FRAMENAVIGATED, handler);
}
@Override
public void offFrameNavigated(Consumer<Frame> handler) {
listeners.remove(EventType.FRAMENAVIGATED, handler);
}
void notifyFrameNavigated(FrameImpl frame) {
listeners.notify(EventType.FRAMENAVIGATED, frame);
}
@Override
public void onPageClose(Consumer<Page> handler) {
listeners.add(EventType.PAGECLOSE, handler);
}
@Override
public void offPageClose(Consumer<Page> handler) {
listeners.remove(EventType.PAGECLOSE, handler);
}
void notifyPageClose(PageImpl page) {
listeners.notify(EventType.PAGECLOSE, page);
}
@Override
public void onPageLoad(Consumer<Page> handler) {
listeners.add(EventType.PAGELOAD, handler);
}
@Override
public void offPageLoad(Consumer<Page> handler) {
listeners.remove(EventType.PAGELOAD, handler);
}
void notifyPageLoad(PageImpl page) {
listeners.notify(EventType.PAGELOAD, page);
}
@Override @Override
public void onWebError(Consumer<WebError> handler) { public void onWebError(Consumer<WebError> handler) {
listeners.add(EventType.WEBERROR, handler); listeners.add(EventType.WEBERROR, handler);
@@ -231,6 +310,16 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
listeners.remove(EventType.RESPONSE, handler); listeners.remove(EventType.RESPONSE, handler);
} }
@Override
public ClockImpl clock() {
return clock;
}
@Override
public Credentials credentials() {
return credentials;
}
private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Predicate<T> predicate, Double timeout) { private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Predicate<T> predicate, Double timeout) {
List<Waitable<T>> waitables = new ArrayList<>(); List<Waitable<T>> waitables = new ArrayList<>();
waitables.add(new WaitableEvent<>(listeners, eventType, predicate)); waitables.add(new WaitableEvent<>(listeners, eventType, predicate));
@@ -255,7 +344,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
public CDPSession newCDPSession(Page page) { public CDPSession newCDPSession(Page page) {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.add("page", ((PageImpl) page).toProtocolRef()); params.add("page", ((PageImpl) page).toProtocolRef());
JsonObject result = sendMessage("newCDPSession", params).getAsJsonObject(); JsonObject result = sendMessage("newCDPSession", params, NO_TIMEOUT).getAsJsonObject();
return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString()); return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
} }
@@ -263,13 +352,29 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
public CDPSession newCDPSession(Frame frame) { public CDPSession newCDPSession(Frame frame) {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.add("frame", ((FrameImpl) frame).toProtocolRef()); params.add("frame", ((FrameImpl) frame).toProtocolRef());
JsonObject result = sendMessage("newCDPSession", params).getAsJsonObject(); JsonObject result = sendMessage("newCDPSession", params, NO_TIMEOUT).getAsJsonObject();
return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString()); return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
} }
@Override
public boolean isClosed() {
return closingOrClosed;
}
@Override @Override
public void close(CloseOptions options) { public void close(CloseOptions options) {
withLogging("BrowserContext.close", () -> closeImpl(options)); if (!closingOrClosed) {
closingOrClosed = true;
if (options == null) {
options = new CloseOptions();
}
closeReason = options.reason;
request.dispose(convertType(options, APIRequestContext.DisposeOptions.class));
tracing.exportAllHars();
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("close", params, NO_TIMEOUT);
}
runUntil(() -> {}, closePromise);
} }
@Override @Override
@@ -277,75 +382,35 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
return cookies(url == null ? new ArrayList<>() : Collections.singletonList(url)); return cookies(url == null ? new ArrayList<>() : Collections.singletonList(url));
} }
private void closeImpl(CloseOptions options) {
if (!closeWasCalled) {
closeWasCalled = true;
if (options == null) {
options = new CloseOptions();
}
closeReason = options.reason;
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());
// Server side will compress artifact if content is attach or if file is .zip.
HarRecorder harParams = entry.getValue();
boolean isCompressed = harParams.contentPolicy == HarContentPolicy.ATTACH || harParams.path.toString().endsWith(".zip");
boolean needCompressed = harParams.path.toString().endsWith(".zip");
if (isCompressed && !needCompressed) {
String tmpPath = harParams.path + ".tmp";
artifact.saveAs(Paths.get(tmpPath));
JsonObject unzipParams = new JsonObject();
unzipParams.addProperty("zipFile", tmpPath);
unzipParams.addProperty("harFile", harParams.path.toString());
connection.localUtils.sendMessage("harUnzip", unzipParams);
} else {
artifact.saveAs(harParams.path);
}
artifact.delete();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("close", params);
}
runUntil(() -> {}, closePromise);
}
@Override @Override
public void addCookies(List<Cookie> cookies) { public void addCookies(List<Cookie> cookies) {
withLogging("BrowserContext.addCookies", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.add("cookies", gson().toJsonTree(cookies));
params.add("cookies", gson().toJsonTree(cookies)); sendMessage("addCookies", params, NO_TIMEOUT);
sendMessage("addCookies", params);
});
} }
@Override @Override
public void addInitScript(String script) { public AutoCloseable addInitScript(String script) {
withLogging("BrowserContext.addInitScript", () -> addInitScriptImpl(script)); JsonObject params = new JsonObject();
params.addProperty("source", script);
JsonObject result = sendMessage("addInitScript", params, NO_TIMEOUT).getAsJsonObject();
return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString());
} }
@Override @Override
public void addInitScript(Path path) { public AutoCloseable addInitScript(Path path) {
withLogging("BrowserContext.addInitScript", () -> { try {
try { byte[] bytes = readAllBytes(path);
byte[] bytes = readAllBytes(path); return addInitScript(new String(bytes, UTF_8));
addInitScriptImpl(new String(bytes, UTF_8)); } catch (IOException e) {
} catch (IOException e) { throw new PlaywrightException("Failed to read script from file", e);
throw new PlaywrightException("Failed to read script from file", e); }
}
});
} }
@Override @Override
public List<Page> backgroundPages() { public List<Page> backgroundPages() {
return new ArrayList<>(backgroundPages); return Collections.emptyList();
}
private void addInitScriptImpl(String script) {
JsonObject params = new JsonObject();
params.addProperty("source", script);
sendMessage("addInitScript", params);
} }
@Override @Override
@@ -355,10 +420,6 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override @Override
public void clearCookies(ClearCookiesOptions options) { public void clearCookies(ClearCookiesOptions options) {
withLogging("BrowserContext.clearCookies", () -> clearCookiesImpl(options));
}
private void clearCookiesImpl(ClearCookiesOptions options) {
if (options == null) { if (options == null) {
options = new ClearCookiesOptions(); options = new ClearCookiesOptions();
} }
@@ -366,7 +427,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
setStringOrRegex(params, "name", options.name); setStringOrRegex(params, "name", options.name);
setStringOrRegex(params, "domain", options.domain); setStringOrRegex(params, "domain", options.domain);
setStringOrRegex(params, "path", options.path); setStringOrRegex(params, "path", options.path);
sendMessage("clearCookies", params); sendMessage("clearCookies", params, NO_TIMEOUT);
} }
private static void setStringOrRegex(JsonObject params, String name, Object value) { private static void setStringOrRegex(JsonObject params, String name, Object value) {
@@ -381,31 +442,27 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override @Override
public void clearPermissions() { public void clearPermissions() {
withLogging("BrowserContext.clearPermissions", () -> sendMessage("clearPermissions")); sendMessage("clearPermissions");
} }
@Override @Override
public List<Cookie> cookies(List<String> urls) { public List<Cookie> cookies(List<String> urls) {
return withLogging("BrowserContext.cookies", () -> cookiesImpl(urls));
}
private List<Cookie> cookiesImpl(List<String> urls) {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
if (urls == null) { if (urls == null) {
urls = new ArrayList<>(); urls = new ArrayList<>();
} }
params.add("urls", gson().toJsonTree(urls)); params.add("urls", gson().toJsonTree(urls));
JsonObject json = sendMessage("cookies", params).getAsJsonObject(); JsonObject json = sendMessage("cookies", params, NO_TIMEOUT).getAsJsonObject();
Cookie[] cookies = gson().fromJson(json.getAsJsonArray("cookies"), Cookie[].class); Cookie[] cookies = gson().fromJson(json.getAsJsonArray("cookies"), Cookie[].class);
return asList(cookies); return asList(cookies);
} }
@Override @Override
public void exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) { public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding) {
withLogging("BrowserContext.exposeBinding", () -> exposeBindingImpl(name, playwrightBinding, options)); return exposeBindingImpl(name, playwrightBinding);
} }
private void exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) { private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding) {
if (bindings.containsKey(name)) { if (bindings.containsKey(name)) {
throw new PlaywrightException("Function \"" + name + "\" has been already registered"); throw new PlaywrightException("Function \"" + name + "\" has been already registered");
} }
@@ -418,24 +475,17 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("name", name); params.addProperty("name", name);
if (options != null && options.handle != null && options.handle) { JsonObject result = sendMessage("exposeBinding", params, NO_TIMEOUT).getAsJsonObject();
params.addProperty("needsHandle", true); return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString());
}
sendMessage("exposeBinding", params);
} }
@Override @Override
public void exposeFunction(String name, FunctionCallback playwrightFunction) { public AutoCloseable exposeFunction(String name, FunctionCallback playwrightFunction) {
withLogging("BrowserContext.exposeFunction", return exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args));
() -> exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args), null));
} }
@Override @Override
public void grantPermissions(List<String> permissions, GrantPermissionsOptions options) { public void grantPermissions(List<String> permissions, GrantPermissionsOptions options) {
withLogging("BrowserContext.grantPermissions", () -> grantPermissionsImpl(permissions, options));
}
private void grantPermissionsImpl(List<String> permissions, GrantPermissionsOptions options) {
if (options == null) { if (options == null) {
options = new GrantPermissionsOptions(); options = new GrantPermissionsOptions();
} }
@@ -444,15 +494,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.add("permissions", gson().toJsonTree(permissions)); params.add("permissions", gson().toJsonTree(permissions));
sendMessage("grantPermissions", params); sendMessage("grantPermissions", params, NO_TIMEOUT);
} }
@Override @Override
public PageImpl newPage() { public PageImpl newPage() {
return withLogging("BrowserContext.newPage", () -> newPageImpl());
}
private PageImpl newPageImpl() {
if (ownerPage != null) { if (ownerPage != null) {
throw new PlaywrightException("Please use browser.newContext()"); throw new PlaywrightException("Please use browser.newContext()");
} }
@@ -471,18 +517,21 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} }
@Override @Override
public void route(String url, Consumer<Route> handler, RouteOptions options) { public AutoCloseable route(String url, Consumer<Route> handler, RouteOptions options) {
route(new UrlMatcher(baseUrl, url), handler, options); route(UrlMatcher.forGlob(baseUrl(), url, this.connection.localUtils, false), handler, options);
return new DisposableStub(() -> unroute(url, handler));
} }
@Override @Override
public void route(Pattern url, Consumer<Route> handler, RouteOptions options) { public AutoCloseable route(Pattern url, Consumer<Route> handler, RouteOptions options) {
route(new UrlMatcher(url), handler, options); route(new UrlMatcher(url), handler, options);
return new DisposableStub(() -> unroute(url, handler));
} }
@Override @Override
public void route(Predicate<String> url, Consumer<Route> handler, RouteOptions options) { public AutoCloseable route(Predicate<String> url, Consumer<Route> handler, RouteOptions options) {
route(new UrlMatcher(url), handler, options); route(new UrlMatcher(url), handler, options);
return new DisposableStub(() -> unroute(url, handler));
} }
@Override @Override
@@ -491,116 +540,118 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
options = new RouteFromHAROptions(); options = new RouteFromHAROptions();
} }
if (options.update != null && options.update) { if (options.update != null && options.update) {
recordIntoHar(null, har, options); recordIntoHar(null, har, options, null);
return; return;
} }
UrlMatcher matcher = UrlMatcher.forOneOf(baseUrl, options.url); UrlMatcher matcher = UrlMatcher.forOneOf(baseUrl(), options.url, this.connection.localUtils, false);
HARRouter harRouter = new HARRouter(connection.localUtils, har, options.notFound); HARRouter harRouter = new HARRouter(connection.localUtils, har, options.notFound);
onClose(context -> harRouter.dispose()); onClose(context -> harRouter.dispose());
route(matcher, route -> harRouter.handle(route), null); route(matcher, route -> harRouter.handle(route), null);
} }
private void route(UrlMatcher matcher, Consumer<Route> handler, RouteOptions options) { private void route(UrlMatcher matcher, Consumer<Route> handler, RouteOptions options) {
withLogging("BrowserContext.route", () -> { routes.add(matcher, handler, options == null ? null : options.times);
routes.add(matcher, handler, options == null ? null : options.times); updateInterceptionPatterns();
updateInterceptionPatterns();
});
} }
void recordIntoHar(PageImpl page, Path har, RouteFromHAROptions options) { @Override
JsonObject params = new JsonObject(); public void routeWebSocket(String url, Consumer<WebSocketRoute> handler) {
if (page != null) { routeWebSocketImpl(UrlMatcher.forGlob(baseUrl(), url, this.connection.localUtils, true), handler);
params.add("page", page.toProtocolRef()); }
@Override
public void routeWebSocket(Pattern pattern, Consumer<WebSocketRoute> handler) {
routeWebSocketImpl(new UrlMatcher(pattern), handler);
}
@Override
public void routeWebSocket(Predicate<String> predicate, Consumer<WebSocketRoute> handler) {
routeWebSocketImpl(new UrlMatcher(predicate), handler);
}
private void routeWebSocketImpl(UrlMatcher matcher, Consumer<WebSocketRoute> handler) {
webSocketRoutes.add(matcher, handler);
updateWebSocketInterceptionPatterns();
}
void recordIntoHar(PageImpl page, Path har, RouteFromHAROptions options, HarContentPolicy contentPolicy) {
if (contentPolicy == null) {
contentPolicy = Utils.convertType(options.updateContent, HarContentPolicy.class);
} }
JsonObject jsonOptions = new JsonObject(); tracing.recordIntoHar(page, har, options.url, contentPolicy, options.updateMode, null);
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 @Override
public void setDefaultNavigationTimeout(double timeout) { public void setDefaultNavigationTimeout(double timeout) {
setDefaultNavigationTimeoutImpl(timeout); timeoutSettings.setDefaultNavigationTimeout(timeout);
}
void setDefaultNavigationTimeoutImpl(Double timeout) {
withLogging("BrowserContext.setDefaultNavigationTimeout", () -> {
timeoutSettings.setDefaultNavigationTimeout(timeout);
JsonObject params = new JsonObject();
params.addProperty("timeout", timeout);
sendMessage("setDefaultNavigationTimeoutNoReply", params);
});
} }
@Override @Override
public void setDefaultTimeout(double timeout) { public void setDefaultTimeout(double timeout) {
setDefaultTimeoutImpl(timeout); timeoutSettings.setDefaultTimeout(timeout);
}
void setDefaultTimeoutImpl(Double timeout) {
withLogging("BrowserContext.setDefaultTimeout", () -> {
timeoutSettings.setDefaultTimeout(timeout);
JsonObject params = new JsonObject();
params.addProperty("timeout", timeout);
sendMessage("setDefaultTimeoutNoReply", params);
});
} }
@Override @Override
public void setExtraHTTPHeaders(Map<String, String> headers) { public void setExtraHTTPHeaders(Map<String, String> headers) {
withLogging("BrowserContext.setExtraHTTPHeaders", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); JsonArray jsonHeaders = new JsonArray();
JsonArray jsonHeaders = new JsonArray(); for (Map.Entry<String, String> e : headers.entrySet()) {
for (Map.Entry<String, String> e : headers.entrySet()) { JsonObject header = new JsonObject();
JsonObject header = new JsonObject(); header.addProperty("name", e.getKey());
header.addProperty("name", e.getKey()); header.addProperty("value", e.getValue());
header.addProperty("value", e.getValue()); jsonHeaders.add(header);
jsonHeaders.add(header); }
} params.add("headers", jsonHeaders);
params.add("headers", jsonHeaders); sendMessage("setExtraHTTPHeaders", params, NO_TIMEOUT);
sendMessage("setExtraHTTPHeaders", params);
});
} }
@Override @Override
public void setGeolocation(Geolocation geolocation) { public void setGeolocation(Geolocation geolocation) {
withLogging("BrowserContext.setGeolocation", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); if (geolocation != null) {
if (geolocation != null) { params.add("geolocation", gson().toJsonTree(geolocation));
params.add("geolocation", gson().toJsonTree(geolocation)); }
} sendMessage("setGeolocation", params, NO_TIMEOUT);
sendMessage("setGeolocation", params);
});
} }
@Override @Override
public void setOffline(boolean offline) { public void setOffline(boolean offline) {
withLogging("BrowserContext.setOffline", () -> { JsonObject params = new JsonObject();
params.addProperty("offline", offline);
sendMessage("setOffline", params, NO_TIMEOUT);
}
@Override
public void setStorageState(Path storageState) {
try {
String state = new String(readAllBytes(storageState), UTF_8);
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("offline", offline); params.addProperty("storageState", state);
sendMessage("setOffline", params); sendMessage("setStorageState", params, NO_TIMEOUT);
}); } catch (IOException e) {
throw new PlaywrightException("Failed to read storage state from file", e);
}
} }
@Override @Override
public String storageState(StorageStateOptions options) { public String storageState(StorageStateOptions options) {
return withLogging("BrowserContext.storageState", () -> { if (options == null) {
JsonElement json = sendMessage("storageState"); options = new StorageStateOptions();
String storageState = json.toString(); }
if (options != null && options.path != null) { JsonObject params = gson().toJsonTree(options).getAsJsonObject();
Utils.writeToFile(storageState.getBytes(StandardCharsets.UTF_8), options.path); params.remove("path");
} JsonElement json = sendMessage("storageState", params, NO_TIMEOUT);
return storageState;
}); String storageState = json.toString();
if (options.path != null) {
Utils.writeToFile(storageState.getBytes(StandardCharsets.UTF_8), options.path);
}
return storageState;
}
@Override
public DebuggerImpl debugger() {
return debugger;
} }
@Override @Override
@@ -610,15 +661,13 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override @Override
public void unrouteAll() { public void unrouteAll() {
withLogging("BrowserContext.unrouteAll", () -> { routes.removeAll();
routes.removeAll(); updateInterceptionPatterns();
updateInterceptionPatterns();
});
} }
@Override @Override
public void unroute(String url, Consumer<Route> handler) { public void unroute(String url, Consumer<Route> handler) {
unroute(new UrlMatcher(this.baseUrl, url), handler); unroute(UrlMatcher.forGlob(this.baseUrl(), url, this.connection.localUtils, false), handler);
} }
@Override @Override
@@ -664,14 +713,16 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} }
private void unroute(UrlMatcher matcher, Consumer<Route> handler) { private void unroute(UrlMatcher matcher, Consumer<Route> handler) {
withLogging("BrowserContext.unroute", () -> { routes.remove(matcher, handler);
routes.remove(matcher, handler); updateInterceptionPatterns();
updateInterceptionPatterns();
});
} }
private void updateInterceptionPatterns() { private void updateInterceptionPatterns() {
sendMessage("setNetworkInterceptionPatterns", routes.interceptionPatterns()); sendMessage("setNetworkInterceptionPatterns", routes.interceptionPatterns(), NO_TIMEOUT);
}
private void updateWebSocketInterceptionPatterns() {
sendMessage("setWebSocketInterceptionPatterns", webSocketRoutes.interceptionPatterns(), NO_TIMEOUT);
} }
void handleRoute(RouteImpl route) { void handleRoute(RouteImpl route) {
@@ -684,6 +735,12 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} }
} }
void handleWebSocketRoute(WebSocketRouteImpl route) {
if (!webSocketRoutes.handle(route)) {
route.connectToServer();
}
}
WaitableResult<JsonElement> pause() { WaitableResult<JsonElement> pause() {
return sendMessageAsync("pause", new JsonObject()); return sendMessageAsync("pause", new JsonObject());
} }
@@ -723,6 +780,9 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
RouteImpl route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString()); RouteImpl route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
route.browserContext = this; route.browserContext = this;
handleRoute(route); handleRoute(route);
} else if ("webSocketRoute".equals(event)) {
WebSocketRouteImpl route = connection.getExistingObject(params.getAsJsonObject("webSocketRoute").get("guid").getAsString());
handleWebSocketRoute(route);
} else if ("page".equals(event)) { } else if ("page".equals(event)) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString()); PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
pages.add(page); pages.add(page);
@@ -730,10 +790,6 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
if (page.opener() != null && !page.opener().isClosed()) { if (page.opener() != null && !page.opener().isClosed()) {
page.opener().notifyPopup(page); page.opener().notifyPopup(page);
} }
} else if ("backgroundPage".equals(event)) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
backgroundPages.add(page);
listeners.notify(EventType.BACKGROUNDPAGE, page);
} else if ("bindingCall".equals(event)) { } else if ("bindingCall".equals(event)) {
BindingCall bindingCall = connection.getExistingObject(params.getAsJsonObject("binding").get("guid").getAsString()); BindingCall bindingCall = connection.getExistingObject(params.getAsJsonObject("binding").get("guid").getAsString());
BindingCallback binding = bindings.get(bindingCall.name()); BindingCallback binding = bindings.get(bindingCall.name());
@@ -741,9 +797,19 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
bindingCall.call(binding); bindingCall.call(binding);
} }
} else if ("console".equals(event)) { } else if ("console".equals(event)) {
ConsoleMessageImpl message = new ConsoleMessageImpl(connection, params); PageImpl page = null;
if (params.has("page")) {
page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
}
WorkerImpl worker = null;
if (params.has("worker")) {
worker = connection.getExistingObject(params.getAsJsonObject("worker").get("guid").getAsString());
}
ConsoleMessageImpl message = new ConsoleMessageImpl(connection, params, page, worker);
listeners.notify(BrowserContextImpl.EventType.CONSOLE, message); listeners.notify(BrowserContextImpl.EventType.CONSOLE, message);
PageImpl page = message.page(); if (worker != null) {
worker.listeners.notify(WorkerImpl.EventType.CONSOLE, message);
}
if (page != null) { if (page != null) {
page.listeners.notify(PageImpl.EventType.CONSOLE, message); page.listeners.notify(PageImpl.EventType.CONSOLE, message);
} }
@@ -784,28 +850,25 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} }
} else if ("response".equals(event)) { } else if ("response".equals(event)) {
String guid = params.getAsJsonObject("response").get("guid").getAsString(); String guid = params.getAsJsonObject("response").get("guid").getAsString();
Response response = connection.getExistingObject(guid); ResponseImpl response = connection.getExistingObject(guid);
listeners.notify(EventType.RESPONSE, response); listeners.notify(EventType.RESPONSE, response);
if (params.has("page")) { if (params.has("page")) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString()); PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
page.listeners.notify(PageImpl.EventType.RESPONSE, response); page.listeners.notify(PageImpl.EventType.RESPONSE, response);
} }
} else if ("pageError".equals(event)) { } else if ("pageError".equals(event)) {
SerializedError error = gson().fromJson(params.getAsJsonObject("error"), SerializedError.class); String errorStr = parseError(params.getAsJsonObject("error"));
String errorStr = "";
if (error.error != null) {
errorStr = error.error.name + ": " + error.error.message;
if (error.error.stack != null && !error.error.stack.isEmpty()) {
errorStr += "\n" + error.error.stack;
}
}
PageImpl page; PageImpl page;
try { try {
page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString()); page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
} catch (PlaywrightException e) { } catch (PlaywrightException e) {
page = null; page = null;
} }
listeners.notify(BrowserContextImpl.EventType.WEBERROR, new WebErrorImpl(page, errorStr)); WebErrorLocation location = null;
if (params.has("location")) {
location = gson().fromJson(params.getAsJsonObject("location"), WebErrorLocation.class);
}
listeners.notify(BrowserContextImpl.EventType.WEBERROR, new WebErrorImpl(page, errorStr, location));
if (page != null) { if (page != null) {
page.listeners.notify(PageImpl.EventType.PAGEERROR, errorStr); page.listeners.notify(PageImpl.EventType.PAGEERROR, errorStr);
} }
@@ -815,8 +878,10 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} }
void didClose() { void didClose() {
closingOrClosed = true;
if (browser != null) { if (browser != null) {
browser.contexts.remove(this); browser.contexts.remove(this);
browser.browserType.playwright.selectors.contextsForSelectors.remove(this);
} }
listeners.notify(EventType.CLOSE, this); listeners.notify(EventType.CLOSE, this);
} }
@@ -825,7 +890,49 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("name", name); params.addProperty("name", name);
params.addProperty("lastModifiedMs", lastModifiedMs); params.addProperty("lastModifiedMs", lastModifiedMs);
JsonObject json = sendMessage("createTempFile", params).getAsJsonObject(); JsonObject json = sendMessage("createTempFile", params, NO_TIMEOUT).getAsJsonObject();
return connection.getExistingObject(json.getAsJsonObject("writableStream").get("guid").getAsString()); return connection.getExistingObject(json.getAsJsonObject("writableStream").get("guid").getAsString());
} }
protected void initializeHarFromOptions(Browser.NewContextOptions options) {
if (options.recordHarPath == null) {
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");
}
return;
}
HarContentPolicy contentPolicy = options.recordHarContent;
if (contentPolicy == null && options.recordHarOmitContent != null && options.recordHarOmitContent == true) {
contentPolicy = HarContentPolicy.OMIT;
}
if (contentPolicy == null) {
contentPolicy = options.recordHarPath.endsWith(".zip") ? HarContentPolicy.ATTACH : HarContentPolicy.EMBED;
}
RouteFromHAROptions routeFromHAROptions = new RouteFromHAROptions();
if (options.recordHarUrlFilter instanceof String) {
routeFromHAROptions.setUrl((String) options.recordHarUrlFilter);
} else if (options.recordHarUrlFilter instanceof Pattern) {
routeFromHAROptions.setUrl((Pattern) options.recordHarUrlFilter);
}
if (options.recordHarMode != null) {
routeFromHAROptions.updateMode = options.recordHarMode;
} else {
routeFromHAROptions.updateMode = HarMode.FULL;
}
routeFromHAROptions.url = options.recordHarUrlFilter;
recordIntoHar(null, options.recordHarPath, routeFromHAROptions, contentPolicy);
}
} }
@@ -20,7 +20,7 @@ import com.google.gson.Gson;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.microsoft.playwright.*; import com.microsoft.playwright.*;
import com.microsoft.playwright.options.HarContentPolicy; import com.microsoft.playwright.options.BindResult;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -29,10 +29,8 @@ import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.function.Consumer; 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.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.*; import static com.microsoft.playwright.impl.Utils.*;
import static com.microsoft.playwright.impl.Utils.convertType;
class BrowserImpl extends ChannelOwner implements Browser { class BrowserImpl extends ChannelOwner implements Browser {
final Set<BrowserContextImpl> contexts = new HashSet<>(); final Set<BrowserContextImpl> contexts = new HashSet<>();
@@ -45,6 +43,7 @@ class BrowserImpl extends ChannelOwner implements Browser {
String closeReason; String closeReason;
enum EventType { enum EventType {
CONTEXT,
DISCONNECTED, DISCONNECTED,
} }
@@ -52,6 +51,16 @@ class BrowserImpl extends ChannelOwner implements Browser {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
} }
@Override
public void onContext(Consumer<BrowserContext> handler) {
listeners.add(EventType.CONTEXT, handler);
}
@Override
public void offContext(Consumer<BrowserContext> handler) {
listeners.remove(EventType.CONTEXT, handler);
}
@Override @Override
public void onDisconnected(Consumer<Browser> handler) { public void onDisconnected(Consumer<Browser> handler) {
listeners.add(EventType.DISCONNECTED, handler); listeners.add(EventType.DISCONNECTED, handler);
@@ -69,10 +78,6 @@ class BrowserImpl extends ChannelOwner implements Browser {
@Override @Override
public void close(CloseOptions options) { public void close(CloseOptions options) {
withLogging("Browser.close", () -> closeImpl(options));
}
private void closeImpl(CloseOptions options) {
if (options == null) { if (options == null) {
options = new CloseOptions(); options = new CloseOptions();
} }
@@ -117,16 +122,20 @@ class BrowserImpl extends ChannelOwner implements Browser {
@Override @Override
public BrowserContextImpl newContext(NewContextOptions options) { public BrowserContextImpl newContext(NewContextOptions options) {
return withLogging("Browser.newContext", () -> newContextImpl(options));
}
private BrowserContextImpl newContextImpl(NewContextOptions options) {
if (options == null) { if (options == null) {
options = new NewContextOptions(); options = new NewContextOptions();
} else { } else {
// Make a copy so that we can nullify some fields below. // Make a copy so that we can nullify some fields below.
options = convertType(options, NewContextOptions.class); options = convertType(options, NewContextOptions.class);
} }
NewContextOptions harOptions = Utils.clone(options);
options.recordHarContent = null;
options.recordHarMode = null;
options.recordHarPath = null;
options.recordHarOmitContent = null;
options.recordHarUrlFilter = null;
if (options.storageStatePath != null) { if (options.storageStatePath != null) {
try { try {
byte[] bytes = Files.readAllBytes(options.storageStatePath); byte[] bytes = Files.readAllBytes(options.storageStatePath);
@@ -141,51 +150,10 @@ class BrowserImpl extends ChannelOwner implements Browser {
storageState = new Gson().fromJson(options.storageState, JsonObject.class); storageState = new Gson().fromJson(options.storageState, JsonObject.class);
options.storageState = null; 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(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
if (storageState != null) { if (storageState != null) {
params.add("storageState", storageState); params.add("storageState", storageState);
} }
if (recordHar != null) {
params.add("recordHar", recordHar);
}
if (options.recordVideoDir != null) { if (options.recordVideoDir != null) {
JsonObject recordVideo = new JsonObject(); JsonObject recordVideo = new JsonObject();
recordVideo.addProperty("dir", options.recordVideoDir.toAbsolutePath().toString()); recordVideo.addProperty("dir", options.recordVideoDir.toAbsolutePath().toString());
@@ -208,35 +176,26 @@ class BrowserImpl extends ChannelOwner implements Browser {
params.addProperty("noDefaultViewport", true); params.addProperty("noDefaultViewport", true);
} }
} }
addToProtocol(params, options.clientCertificates);
params.remove("acceptDownloads"); params.remove("acceptDownloads");
if (options.acceptDownloads != null) { if (options.acceptDownloads != null) {
params.addProperty("acceptDownloads", options.acceptDownloads ? "accept" : "deny"); params.addProperty("acceptDownloads", options.acceptDownloads ? "accept" : "deny");
} }
JsonElement result = sendMessage("newContext", params); params.add("selectorEngines", gson().toJsonTree(browserType.playwright.selectors.selectorEngines));
params.addProperty("testIdAttributeName", browserType.playwright.selectors.testIdAttributeName);
JsonElement result = sendMessage("newContext", params, NO_TIMEOUT);
BrowserContextImpl context = connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("context").get("guid").getAsString()); BrowserContextImpl context = connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("context").get("guid").getAsString());
context.videosDir = options.recordVideoDir; context.initializeHarFromOptions(harOptions);
if (options.baseURL != null) {
context.setBaseUrl(options.baseURL);
}
context.setRecordHar(recordHarPath, harContentPolicy);
if (launchOptions != null) {
context.tracing().setTracesDir(launchOptions.tracesDir);
}
contexts.add(context);
return context; return context;
} }
@Override @Override
public Page newPage(NewPageOptions options) { public Page newPage(NewPageOptions options) {
return withLogging("Browser.newPage", () -> newPageImpl(options)); return withTitle("Create Page", () -> newPageImpl(options));
} }
@Override @Override
public void startTracing(Page page, StartTracingOptions options) { public void startTracing(Page page, StartTracingOptions options) {
withLogging("Browser.startTracing", () -> startTracingImpl(page, options));
}
private void startTracingImpl(Page page, StartTracingOptions options) {
if (options == null) { if (options == null) {
options = new StartTracingOptions(); options = new StartTracingOptions();
} }
@@ -245,15 +204,37 @@ class BrowserImpl extends ChannelOwner implements Browser {
if (page != null) { if (page != null) {
params.add("page", ((PageImpl) page).toProtocolRef()); params.add("page", ((PageImpl) page).toProtocolRef());
} }
sendMessage("startTracing", params); sendMessage("startTracing", params, NO_TIMEOUT);
}
@Override
public BindResult bind(String title, BindOptions options) {
JsonObject params = new JsonObject();
params.addProperty("title", title);
if (options != null) {
if (options.host != null) {
params.addProperty("host", options.host);
}
if (options.port != null) {
params.addProperty("port", options.port);
}
if (options.workspaceDir != null) {
params.addProperty("workspaceDir", options.workspaceDir);
}
}
JsonObject result = sendMessage("startServer", params, NO_TIMEOUT).getAsJsonObject();
BindResult bindResult = new BindResult();
bindResult.endpoint = result.get("endpoint").getAsString();
return bindResult;
}
@Override
public void unbind() {
sendMessage("stopServer", new JsonObject(), NO_TIMEOUT);
} }
@Override @Override
public byte[] stopTracing() { public byte[] stopTracing() {
return withLogging("Browser.stopTracing", () -> stopTracingImpl());
}
private byte[] stopTracingImpl() {
JsonObject json = sendMessage("stopTracing").getAsJsonObject(); JsonObject json = sendMessage("stopTracing").getAsJsonObject();
ArtifactImpl artifact = connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("artifact").get("guid").getAsString()); ArtifactImpl artifact = connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("artifact").get("guid").getAsString());
byte[] data = artifact.readAllBytes(); byte[] data = artifact.readAllBytes();
@@ -294,18 +275,47 @@ class BrowserImpl extends ChannelOwner implements Browser {
@Override @Override
void handleEvent(String event, JsonObject parameters) { void handleEvent(String event, JsonObject parameters) {
if ("close".equals(event)) { switch (event) {
didClose(); case "context":
didCreateContext(connection.getExistingObject(parameters.getAsJsonObject("context").get("guid").getAsString()));
break;
case "close":
didClose();
break;
} }
} }
@Override @Override
public CDPSession newBrowserCDPSession() { public CDPSession newBrowserCDPSession() {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
JsonObject result = sendMessage("newBrowserCDPSession", params).getAsJsonObject(); JsonObject result = sendMessage("newBrowserCDPSession", params, NO_TIMEOUT).getAsJsonObject();
return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString()); return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
} }
protected void connectToBrowserType(BrowserTypeImpl browserType, Path tracesDir){
// Note: when using connect(), `browserType` is different from `this.parent`.
// This is why browser type is not wired up in the constructor, and instead this separate method is called later on.
this.browserType = browserType;
this.tracePath = tracesDir;
for (BrowserContextImpl context : contexts) {
context.tracing().setTracesDir(tracesDir);
browserType.playwright.selectors.contextsForSelectors.add(context);
}
}
private void didCreateContext(BrowserContextImpl context) {
context.browser = this;
contexts.add(context);
// Note: when connecting to a browser, initial contexts arrive before `_browserType` is set,
// and will be configured later in `ConnectToBrowserType`.
if (browserType != null) {
context.tracing().setTracesDir(tracePath);
browserType.playwright.selectors.contextsForSelectors.add(context);
}
listeners.notify(EventType.CONTEXT, context);
}
private void didClose() { private void didClose() {
isConnected = false; isConnected = false;
listeners.notify(EventType.DISCONNECTED, this); listeners.notify(EventType.DISCONNECTED, this);
@@ -22,32 +22,30 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.Browser; import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserType; import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.HarContentPolicy;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.function.Consumer; 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.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.addToProtocol;
import static com.microsoft.playwright.impl.Utils.convertType; import static com.microsoft.playwright.impl.Utils.convertType;
class BrowserTypeImpl extends ChannelOwner implements BrowserType { class BrowserTypeImpl extends ChannelOwner implements BrowserType {
protected PlaywrightImpl playwright;
BrowserTypeImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { BrowserTypeImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
} }
@Override @Override
public BrowserImpl launch(LaunchOptions options) { public BrowserImpl launch(LaunchOptions options) {
return withLogging("BrowserType.launch", () -> launchImpl(options));
}
private BrowserImpl launchImpl(LaunchOptions options) {
if (options == null) { if (options == null) {
options = new LaunchOptions(); options = new LaunchOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
JsonElement result = sendMessage("launch", params); JsonElement result = sendMessage("launch", params, TimeoutSettings.launchTimeout(options.timeout));
BrowserImpl browser = connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("browser").get("guid").getAsString()); BrowserImpl browser = connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("browser").get("guid").getAsString());
browser.browserType = this; browser.browserType = this;
browser.launchOptions = options; browser.launchOptions = options;
@@ -56,16 +54,12 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
@Override @Override
public Browser connect(String wsEndpoint, ConnectOptions options) { public Browser connect(String wsEndpoint, ConnectOptions options) {
return withLogging("BrowserType.connect", () -> connectImpl(wsEndpoint, options));
}
private Browser connectImpl(String wsEndpoint, ConnectOptions options) {
if (options == null) { if (options == null) {
options = new ConnectOptions(); options = new ConnectOptions();
} }
// We don't use gson() here as the headers map should be serialized to a json object. // We don't use gson() here as the headers map should be serialized to a json object.
JsonObject params = new Gson().toJsonTree(options).getAsJsonObject(); JsonObject params = new Gson().toJsonTree(options).getAsJsonObject();
params.addProperty("wsEndpoint", wsEndpoint); params.addProperty("endpoint", wsEndpoint);
if (!params.has("headers")) { if (!params.has("headers")) {
params.add("headers", new JsonObject()); params.add("headers", new JsonObject());
@@ -82,7 +76,12 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
headers.addProperty("x-playwright-browser", name()); headers.addProperty("x-playwright-browser", name());
} }
JsonObject json = connection.localUtils().sendMessage("connect", params).getAsJsonObject(); Double timeout = options.timeout;
if (timeout == null) {
timeout = 0.0;
}
JsonObject json = connection.localUtils().sendMessage("connect", params, timeout).getAsJsonObject();
JsonPipe pipe = connection.getExistingObject(json.getAsJsonObject("pipe").get("guid").getAsString()); JsonPipe pipe = connection.getExistingObject(json.getAsJsonObject("pipe").get("guid").getAsString());
Connection connection = new Connection(pipe, this.connection.env, this.connection.localUtils); Connection connection = new Connection(pipe, this.connection.env, this.connection.localUtils);
PlaywrightImpl playwright = connection.initializePlaywright(); PlaywrightImpl playwright = connection.initializePlaywright();
@@ -94,14 +93,13 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
} }
throw new PlaywrightException("Malformed endpoint. Did you use launchServer method?"); throw new PlaywrightException("Malformed endpoint. Did you use launchServer method?");
} }
playwright.initSharedSelectors(this.connection.getExistingObject("Playwright")); playwright.selectors = this.playwright.selectors;
BrowserImpl browser = connection.getExistingObject(playwright.initializer.getAsJsonObject("preLaunchedBrowser").get("guid").getAsString()); BrowserImpl browser = connection.getExistingObject(playwright.initializer.getAsJsonObject("preLaunchedBrowser").get("guid").getAsString());
browser.isConnectedOverWebSocket = true; browser.isConnectedOverWebSocket = true;
browser.browserType = this; browser.connectToBrowserType(this, null);
Consumer<JsonPipe> connectionCloseListener = t -> browser.notifyRemoteClosed(); Consumer<JsonPipe> connectionCloseListener = t -> browser.notifyRemoteClosed();
pipe.onClose(connectionCloseListener); pipe.onClose(connectionCloseListener);
browser.onDisconnected(b -> { browser.onDisconnected(b -> {
playwright.unregisterSelectors();
pipe.offClose(connectionCloseListener); pipe.offClose(connectionCloseListener);
try { try {
connection.close(); connection.close();
@@ -114,28 +112,19 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
@Override @Override
public Browser connectOverCDP(String endpointURL, ConnectOverCDPOptions options) { public Browser connectOverCDP(String endpointURL, ConnectOverCDPOptions options) {
if (!"chromium".equals(name())) { if (!"chromium".equals(name()) && !"webkit".equals(name())) {
throw new PlaywrightException("Connecting over CDP is only supported in Chromium."); throw new PlaywrightException("Connecting over CDP is only supported in Chromium and WebKit.");
} }
return withLogging("BrowserType.connectOverCDP", () -> connectOverCDPImpl(endpointURL, options));
}
private Browser connectOverCDPImpl(String endpointURL, ConnectOverCDPOptions options) {
if (options == null) { if (options == null) {
options = new ConnectOverCDPOptions(); options = new ConnectOverCDPOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("endpointURL", endpointURL); params.addProperty("endpointURL", endpointURL);
JsonObject json = sendMessage("connectOverCDP", params).getAsJsonObject(); JsonObject json = sendMessage("connectOverCDP", params, TimeoutSettings.launchTimeout(options.timeout)).getAsJsonObject();
BrowserImpl browser = connection.getExistingObject(json.getAsJsonObject("browser").get("guid").getAsString()); BrowserImpl browser = connection.getExistingObject(json.getAsJsonObject("browser").get("guid").getAsString());
browser.browserType = this; browser.connectToBrowserType(this, null);
if (json.has("defaultContext")) {
String contextId = json.getAsJsonObject("defaultContext").get("guid").getAsString();
BrowserContextImpl defaultContext = connection.getExistingObject(contextId);
browser.contexts.add(defaultContext);
}
return browser; return browser;
} }
@@ -145,60 +134,26 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
@Override @Override
public BrowserContextImpl launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options) { public BrowserContextImpl launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options) {
return withLogging("BrowserType.launchPersistentContext",
() -> launchPersistentContextImpl(userDataDir, options));
}
private BrowserContextImpl launchPersistentContextImpl(Path userDataDir, LaunchPersistentContextOptions options) {
if (options == null) { if (options == null) {
options = new LaunchPersistentContextOptions(); options = new LaunchPersistentContextOptions();
} else { } else {
// Make a copy so that we can nullify some fields below. // Make a copy so that we can nullify some fields below.
options = convertType(options, LaunchPersistentContextOptions.class); options = convertType(options, LaunchPersistentContextOptions.class);
} }
JsonObject recordHar = null;
Path recordHarPath = options.recordHarPath; Browser.NewContextOptions harOptions = convertType(options, Browser.NewContextOptions.class);
HarContentPolicy harContentPolicy = null; options.recordHarContent = null;
if (options.recordHarPath != null) { options.recordHarMode = null;
recordHar = new JsonObject(); options.recordHarPath = null;
recordHar.addProperty("path", options.recordHarPath.toString()); options.recordHarOmitContent = null;
if (options.recordHarContent != null) { options.recordHarUrlFilter = 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(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("userDataDir", userDataDir.toString()); if (!userDataDir.isAbsolute() && !userDataDir.toString().isEmpty()) {
if (recordHar != null) { Path cwd = Paths.get("").toAbsolutePath();
params.add("recordHar", recordHar); userDataDir = cwd.resolve(userDataDir);
} }
params.addProperty("userDataDir", userDataDir.toString());
if (options.recordVideoDir != null) { if (options.recordVideoDir != null) {
JsonObject recordVideo = new JsonObject(); JsonObject recordVideo = new JsonObject();
recordVideo.addProperty("dir", options.recordVideoDir.toAbsolutePath().toString()); recordVideo.addProperty("dir", options.recordVideoDir.toAbsolutePath().toString());
@@ -221,17 +176,18 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
params.addProperty("noDefaultViewport", true); params.addProperty("noDefaultViewport", true);
} }
} }
addToProtocol(params, options.clientCertificates);
params.remove("acceptDownloads"); params.remove("acceptDownloads");
if (options.acceptDownloads != null) { if (options.acceptDownloads != null) {
params.addProperty("acceptDownloads", options.acceptDownloads ? "accept" : "deny"); params.addProperty("acceptDownloads", options.acceptDownloads ? "accept" : "deny");
} }
JsonObject json = sendMessage("launchPersistentContext", params).getAsJsonObject(); params.add("selectorEngines", gson().toJsonTree(playwright.selectors.selectorEngines));
params.addProperty("testIdAttributeName", playwright.selectors.testIdAttributeName);
JsonObject json = sendMessage("launchPersistentContext", params, TimeoutSettings.launchTimeout(options.timeout)).getAsJsonObject();
BrowserImpl browser = connection.getExistingObject(json.getAsJsonObject("browser").get("guid").getAsString());
browser.connectToBrowserType(this, options.tracesDir);
BrowserContextImpl context = connection.getExistingObject(json.getAsJsonObject("context").get("guid").getAsString()); BrowserContextImpl context = connection.getExistingObject(json.getAsJsonObject("context").get("guid").getAsString());
context.videosDir = options.recordVideoDir; context.initializeHarFromOptions(harOptions);
if (options.baseURL != null) {
context.setBaseUrl(options.baseURL);
}
context.setRecordHar(recordHarPath, harContentPolicy);
context.tracing().setTracesDir(options.tracesDir); context.tracing().setTracesDir(options.tracesDir);
return context; return context;
} }
@@ -25,6 +25,11 @@ import java.util.function.Consumer;
public class CDPSessionImpl extends ChannelOwner implements CDPSession { public class CDPSessionImpl extends ChannelOwner implements CDPSession {
private final ListenerCollection<String> listeners = new ListenerCollection<>(new HashMap<>(), this); private final ListenerCollection<String> listeners = new ListenerCollection<>(new HashMap<>(), this);
private final ListenerCollection<EventType> typedListeners = new ListenerCollection<>(new HashMap<>(), this);
enum EventType {
CLOSE,
}
protected CDPSessionImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { protected CDPSessionImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
@@ -35,11 +40,27 @@ public class CDPSessionImpl extends ChannelOwner implements CDPSession {
super.handleEvent(event, parameters); super.handleEvent(event, parameters);
if ("event".equals(event)) { if ("event".equals(event)) {
String method = parameters.get("method").getAsString(); String method = parameters.get("method").getAsString();
JsonObject params = parameters.get("params").getAsJsonObject(); JsonObject params = null;
if (parameters.has("params")) {
params = parameters.get("params").getAsJsonObject();
}
listeners.notify(method, params); listeners.notify(method, params);
listeners.notify("event", parameters);
} else if ("close".equals(event)) {
typedListeners.notify(EventType.CLOSE, this);
} }
} }
@Override
public void onClose(Consumer<CDPSession> handler) {
typedListeners.add(EventType.CLOSE, handler);
}
@Override
public void offClose(Consumer<CDPSession> handler) {
typedListeners.remove(EventType.CLOSE, handler);
}
public JsonObject send(String method) { public JsonObject send(String method) {
return send(method, null); return send(method, null);
} }
@@ -36,6 +36,8 @@ class ChannelOwner extends LoggingSupport {
final JsonObject initializer; final JsonObject initializer;
private boolean wasCollected; private boolean wasCollected;
static Double NO_TIMEOUT = null;
protected ChannelOwner(ChannelOwner parent, String type, String guid, JsonObject initializer) { protected ChannelOwner(ChannelOwner parent, String type, String guid, JsonObject initializer) {
this(parent.connection, parent, type, guid, initializer); this(parent.connection, parent, type, guid, initializer);
} }
@@ -82,27 +84,51 @@ class ChannelOwner extends LoggingSupport {
return new WaitForEventLogger<>(this, apiName, code).get(); return new WaitForEventLogger<>(this, apiName, code).get();
} }
@Override
<T> T withLogging(String apiName, Supplier<T> code) { void withTitle(String title, Runnable code) {
String previousApiName = connection.setApiName(apiName); withTitle(title, () -> {
code.run();
return null;
});
}
<T> T withTitle(String title, Supplier<T> code) {
String previousTitle = connection.setTitle(title);
try { try {
return super.withLogging(apiName, code); return code.get();
} finally { } finally {
connection.setApiName(previousApiName); connection.setTitle(previousTitle);
} }
} }
WaitableResult<JsonElement> sendMessageAsync(String method) {
return sendMessageAsync(method, new JsonObject());
}
WaitableResult<JsonElement> sendMessageAsync(String method, JsonObject params) { WaitableResult<JsonElement> sendMessageAsync(String method, JsonObject params) {
checkNotCollected(); checkNotCollected();
return connection.sendMessageAsync(guid, method, params); return connection.sendMessageAsync(guid, method, params);
} }
JsonElement sendMessage(String method) { // Fire-and-forget: silently drop if the object was collected.
return sendMessage(method, new JsonObject()); void sendMessageNoReply(String method, JsonObject params) {
if (wasCollected) {
return;
}
connection.sendMessageNoReply(guid, method, params);
} }
JsonElement sendMessage(String method, JsonObject params) { JsonElement sendMessage(String method) {
return sendMessage(method, new JsonObject(), NO_TIMEOUT);
}
JsonElement sendMessage(String method, JsonObject params, Double timeout) {
checkNotCollected(); checkNotCollected();
if (timeout != null) {
params.addProperty("timeout", timeout);
} else if (params.has("timeout")) {
throw new PlaywrightException("Internal error: timeout must be passed explicitly.");
}
return connection.sendMessage(guid, method, params); return connection.sendMessage(guid, method, params);
} }
@@ -0,0 +1,136 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Clock;
import java.util.Date;
import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT;
class ClockImpl implements Clock {
private final ChannelOwner browserContext;
ClockImpl(BrowserContextImpl browserContext) {
this.browserContext = browserContext;
}
private void sendMessageWithLogging(String method, JsonObject params) {
String capitalizedMethod = method.substring(0, 1).toUpperCase() + method.substring(1);
browserContext.sendMessage("clock" + capitalizedMethod, params, NO_TIMEOUT);
}
@Override
public void fastForward(long ticks) {
JsonObject params = new JsonObject();
params.addProperty("ticksNumber", ticks);
sendMessageWithLogging("fastForward", params);
}
@Override
public void fastForward(String ticks) {
JsonObject params = new JsonObject();
params.addProperty("ticksString", ticks);
sendMessageWithLogging("fastForward", params);
}
@Override
public void install(InstallOptions options) {
JsonObject params = new JsonObject();
if (options != null) {
parseTime(options.time, params);
}
sendMessageWithLogging("install", params);
}
@Override
public void runFor(long ticks) {
JsonObject params = new JsonObject();
params.addProperty("ticksNumber", ticks);
sendMessageWithLogging("runFor", params);
}
@Override
public void runFor(String ticks) {
JsonObject params = new JsonObject();
params.addProperty("ticksString", ticks);
sendMessageWithLogging("runFor", params);
}
@Override
public void pauseAt(long time) {
JsonObject params = new JsonObject();
params.addProperty("timeNumber", time);
sendMessageWithLogging("pauseAt", params);
}
@Override
public void pauseAt(String time) {
JsonObject params = new JsonObject();
params.addProperty("timeString", time);
sendMessageWithLogging("pauseAt", params);
}
@Override
public void pauseAt(Date time) {
JsonObject params = new JsonObject();
params.addProperty("timeNumber", time.getTime());
sendMessageWithLogging("pauseAt", params);
}
@Override
public void resume() {
sendMessageWithLogging("resume", new JsonObject());
}
@Override
public void setFixedTime(long time) {
JsonObject params = new JsonObject();
params.addProperty("timeNumber", time);
sendMessageWithLogging("setFixedTime", params);
}
@Override
public void setFixedTime(String time) {
JsonObject params = new JsonObject();
params.addProperty("timeString", time);
sendMessageWithLogging("setFixedTime", params);
}
@Override
public void setFixedTime(Date time) {
JsonObject params = new JsonObject();
params.addProperty("timeNumber", time.getTime());
sendMessageWithLogging("setFixedTime", params);
}
@Override
public void setSystemTime(long time) {
JsonObject params = new JsonObject();
params.addProperty("timeNumber", time);
sendMessageWithLogging("setSystemTime", params);
}
@Override
public void setSystemTime(String time) {
JsonObject params = new JsonObject();
params.addProperty("timeString", time);
sendMessageWithLogging("setSystemTime", params);
}
@Override
public void setSystemTime(Date time) {
JsonObject params = new JsonObject();
params.addProperty("timeNumber", time.getTime());
sendMessageWithLogging("setSystemTime", params);
}
private static void parseTime(Object time, JsonObject params) {
if (time instanceof Long) {
params.addProperty("timeNumber", (Long) time);
} else if (time instanceof Date) {
params.addProperty("timeNumber", ((Date) time).getTime());
} else if (time instanceof String) {
params.addProperty("timeString", (String) time);
}
}
}
@@ -19,7 +19,6 @@ import com.google.gson.Gson;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.TimeoutError; import com.microsoft.playwright.TimeoutError;
@@ -40,6 +39,7 @@ class Message {
JsonObject params; JsonObject params;
JsonElement result; JsonElement result;
SerializedError error; SerializedError error;
JsonObject errorDetails;
JsonArray log; JsonArray log;
@Override @Override
@@ -64,7 +64,8 @@ public class Connection {
private int lastId = 0; private int lastId = 0;
private final StackTraceCollector stackTraceCollector; private final StackTraceCollector stackTraceCollector;
private final Map<Integer, WaitableResult<JsonElement>> callbacks = new HashMap<>(); private final Map<Integer, WaitableResult<JsonElement>> callbacks = new HashMap<>();
private String apiName; private String title;
private boolean titleReported = false;
private static final boolean isLogging; private static final boolean isLogging;
static { static {
String debug = System.getenv("DEBUG"); String debug = System.getenv("DEBUG");
@@ -83,7 +84,7 @@ public class Connection {
PlaywrightImpl initialize() { PlaywrightImpl initialize() {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("sdkLanguage", "java"); params.addProperty("sdkLanguage", "java");
JsonElement result = sendMessage("initialize", params.getAsJsonObject()); JsonElement result = sendMessage("initialize", params.getAsJsonObject(), NO_TIMEOUT);
return this.connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("playwright").get("guid").getAsString()); return this.connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("playwright").get("guid").getAsString());
} }
} }
@@ -116,9 +117,10 @@ public class Connection {
} }
} }
String setApiName(String name) { String setTitle(String newTitle) {
String previous = apiName; String previous = title;
apiName = name; titleReported = false;
title = newTitle;
return previous; return previous;
} }
@@ -131,13 +133,20 @@ public class Connection {
} }
public WaitableResult<JsonElement> sendMessageAsync(String guid, String method, JsonObject params) { public WaitableResult<JsonElement> sendMessageAsync(String guid, String method, JsonObject params) {
return internalSendMessage(guid, method, params, true); return internalSendMessage(guid, method, params, true, true);
} }
private WaitableResult<JsonElement> internalSendMessage(String guid, String method, JsonObject params, boolean sendStack) { // Fire-and-forget: the server never replies.
public void sendMessageNoReply(String guid, String method, JsonObject params) {
internalSendMessage(guid, method, params, false, false);
}
private WaitableResult<JsonElement> internalSendMessage(String guid, String method, JsonObject params, boolean sendStack, boolean expectsReply) {
int id = ++lastId; int id = ++lastId;
WaitableResult<JsonElement> result = new WaitableResult<>(); WaitableResult<JsonElement> result = new WaitableResult<>();
callbacks.put(id, result); if (expectsReply) {
callbacks.put(id, result);
}
JsonObject message = new JsonObject(); JsonObject message = new JsonObject();
message.addProperty("id", id); message.addProperty("id", id);
message.addProperty("guid", guid); message.addProperty("guid", guid);
@@ -146,12 +155,14 @@ public class Connection {
JsonObject metadata = new JsonObject(); JsonObject metadata = new JsonObject();
metadata.addProperty("wallTime", currentTimeMillis()); metadata.addProperty("wallTime", currentTimeMillis());
JsonArray stack = null; JsonArray stack = null;
if (apiName == null) { if (titleReported) {
metadata.addProperty("internal", true); metadata.addProperty("internal", true);
} else { } else {
metadata.addProperty("apiName", apiName); if (title != null) {
// All but first message in an API call are considered internal and will be hidden from the inspector. metadata.addProperty("title", title);
apiName = null; // All but first message in a custom-titled API call are considered internal and will be hidden from the inspector.
titleReported = true;
}
if (stackTraceCollector != null) { if (stackTraceCollector != null) {
stack = stackTraceCollector.currentStackTrace(); stack = stackTraceCollector.currentStackTrace();
if (!stack.isEmpty()) { if (!stack.isEmpty()) {
@@ -172,7 +183,7 @@ public class Connection {
callData.add("stack", stack); callData.add("stack", stack);
JsonObject stackParams = new JsonObject(); JsonObject stackParams = new JsonObject();
stackParams.add("callData", callData); stackParams.add("callData", callData);
internalSendMessage(localUtils.guid,"addStackToTracingNoReply", stackParams, false); internalSendMessage(localUtils.guid,"addStackToTracingNoReply", stackParams, false, true);
} }
return result; return result;
} }
@@ -248,16 +259,20 @@ public class Connection {
callback.complete(message.result); callback.complete(message.result);
} else { } else {
String callLog = formatCallLog(message.log); String callLog = formatCallLog(message.log);
PlaywrightException exception;
if (message.error.error == null) { if (message.error.error == null) {
callback.completeExceptionally(new PlaywrightException(message.error + callLog)); exception = new PlaywrightException(message.error + callLog);
} else if ("TimeoutError".equals(message.error.error.name)) { } else if ("TimeoutError".equals(message.error.error.name)) {
callback.completeExceptionally(new TimeoutError(message.error.error + callLog)); exception = new TimeoutError(message.error.error + callLog);
} else if ("TargetClosedError".equals(message.error.error.name)) { } else if ("TargetClosedError".equals(message.error.error.name)) {
callback.completeExceptionally(new TargetClosedError(message.error.error + callLog)); exception = new TargetClosedError(message.error.error + callLog);
} else { } else {
callback.completeExceptionally(new DriverException(message.error.error + callLog)); exception = new DriverException(message.error.error + callLog);
} }
if (message.errorDetails != null) {
exception = new ServerErrorWithDetails(exception, message.errorDetails, message.log);
}
callback.completeExceptionally(exception);
} }
return; return;
} }
@@ -330,6 +345,9 @@ public class Connection {
case "Dialog": case "Dialog":
result = new DialogImpl(parent, type, guid, initializer); result = new DialogImpl(parent, type, guid, initializer);
break; break;
case "Disposable":
result = new DisposableObject(parent, type, guid, initializer);
break;
case "Electron": case "Electron":
// result = new Playwright(parent, type, guid, initializer); // result = new Playwright(parent, type, guid, initializer);
break; break;
@@ -373,17 +391,20 @@ public class Connection {
case "Stream": case "Stream":
result = new Stream(parent, type, guid, initializer); result = new Stream(parent, type, guid, initializer);
break; break;
case "Selectors":
result = new SelectorsImpl(parent, type, guid, initializer);
break;
case "SocksSupport": case "SocksSupport":
break; break;
case "Debugger":
result = new DebuggerImpl(parent, type, guid, initializer);
break;
case "Tracing": case "Tracing":
result = new TracingImpl(parent, type, guid, initializer); result = new TracingImpl(parent, type, guid, initializer);
break; break;
case "WebSocket": case "WebSocket":
result = new WebSocketImpl(parent, type, guid, initializer); result = new WebSocketImpl(parent, type, guid, initializer);
break; break;
case "WebSocketRoute":
result = new WebSocketRouteImpl(parent, type, guid, initializer);
break;
case "Worker": case "Worker":
result = new WorkerImpl(parent, type, guid, initializer); result = new WorkerImpl(parent, type, guid, initializer);
break; break;
@@ -20,33 +20,38 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.microsoft.playwright.ConsoleMessage; import com.microsoft.playwright.ConsoleMessage;
import com.microsoft.playwright.JSHandle; import com.microsoft.playwright.JSHandle;
import com.microsoft.playwright.Page; import com.microsoft.playwright.Worker;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static com.microsoft.playwright.impl.Serialization.gson;
public class ConsoleMessageImpl implements ConsoleMessage { public class ConsoleMessageImpl implements ConsoleMessage {
private final Connection connection; private final Connection connection;
private PageImpl page; private final PageImpl page;
private final WorkerImpl worker;
private final JsonObject initializer; private final JsonObject initializer;
public ConsoleMessageImpl(Connection connection, JsonObject initializer) { public ConsoleMessageImpl(Connection connection, JsonObject initializer, PageImpl page, WorkerImpl worker) {
this.connection = connection; this.connection = connection;
// Note: currently, we only report console messages for pages and they always have a page. this.page = page;
// However, in the future we might report console messages for service workers or something else, this.worker = worker;
// where page() would be null.
if (initializer.has("page")) {
page = connection.getExistingObject(initializer.getAsJsonObject("page").get("guid").getAsString());
}
this.initializer = initializer; this.initializer = initializer;
} }
@Override
public double timestamp() {
return initializer.get("timestamp").getAsDouble();
}
public String type() { public String type() {
return initializer.get("type").getAsString(); return initializer.get("type").getAsString();
} }
@Override
public Worker worker() {
return worker;
}
public String text() { public String text() {
return initializer.get("text").getAsString(); return initializer.get("text").getAsString();
} }
@@ -0,0 +1,62 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Credentials;
import com.microsoft.playwright.options.VirtualCredential;
import java.util.List;
import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT;
import static com.microsoft.playwright.impl.Serialization.gson;
import static java.util.Arrays.asList;
class CredentialsImpl implements Credentials {
private final BrowserContextImpl context;
CredentialsImpl(BrowserContextImpl context) {
this.context = context;
}
@Override
public void install() {
context.sendMessage("credentialsInstall", new JsonObject(), NO_TIMEOUT);
}
@Override
public VirtualCredential create(String rpId, CreateOptions options) {
JsonObject params = options == null ? new JsonObject() : gson().toJsonTree(options).getAsJsonObject();
params.addProperty("rpId", rpId);
JsonObject json = context.sendMessage("credentialsCreate", params, NO_TIMEOUT).getAsJsonObject();
return gson().fromJson(json.get("credential"), VirtualCredential.class);
}
@Override
public void delete(String id) {
JsonObject params = new JsonObject();
params.addProperty("id", id);
context.sendMessage("credentialsDelete", params, NO_TIMEOUT);
}
@Override
public List<VirtualCredential> get(GetOptions options) {
JsonObject params = options == null ? new JsonObject() : gson().toJsonTree(options).getAsJsonObject();
JsonObject json = context.sendMessage("credentialsGet", params, NO_TIMEOUT).getAsJsonObject();
return asList(gson().fromJson(json.getAsJsonArray("credentials"), VirtualCredential[].class));
}
}
@@ -0,0 +1,86 @@
/*
* 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.Debugger;
import com.microsoft.playwright.options.Location;
import com.microsoft.playwright.options.DebuggerPausedDetails;
import java.util.ArrayList;
import java.util.List;
import static com.microsoft.playwright.impl.Serialization.gson;
class DebuggerImpl extends ChannelOwner implements Debugger {
private final List<Runnable> pausedStateChangedHandlers = new ArrayList<>();
private DebuggerPausedDetails pausedDetails;
DebuggerImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@Override
protected void handleEvent(String event, JsonObject params) {
if ("pausedStateChanged".equals(event)) {
if (params.has("pausedDetails") && !params.get("pausedDetails").isJsonNull()) {
pausedDetails = gson().fromJson(params.get("pausedDetails"), DebuggerPausedDetails.class);
} else {
pausedDetails = null;
}
for (Runnable handler : new ArrayList<>(pausedStateChangedHandlers)) {
handler.run();
}
}
}
@Override
public void onPausedStateChanged(Runnable handler) {
pausedStateChangedHandlers.add(handler);
}
@Override
public void offPausedStateChanged(Runnable handler) {
pausedStateChangedHandlers.remove(handler);
}
@Override
public DebuggerPausedDetails pausedDetails() {
return pausedDetails;
}
@Override
public void requestPause() {
sendMessage("requestPause", new JsonObject(), NO_TIMEOUT);
}
@Override
public void resume() {
sendMessage("resume", new JsonObject(), NO_TIMEOUT);
}
@Override
public void next() {
sendMessage("next", new JsonObject(), NO_TIMEOUT);
}
@Override
public void runTo(Location location) {
JsonObject params = gson().toJsonTree(location).getAsJsonObject();
sendMessage("runTo", params, NO_TIMEOUT);
}
}
@@ -34,18 +34,20 @@ class DialogImpl extends ChannelOwner implements Dialog {
@Override @Override
public void accept(String promptText) { public void accept(String promptText) {
withLogging("Dialog.accept", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); if (promptText != null) {
if (promptText != null) { params.addProperty("promptText", promptText);
params.addProperty("promptText", promptText); }
} sendMessage("accept", params, NO_TIMEOUT);
sendMessage("accept", params);
});
} }
@Override @Override
public void dismiss() { public void dismiss() {
withLogging("Dialog.dismiss", () -> sendMessage("dismiss")); try {
sendMessage("dismiss");
} catch (TargetClosedError e) {
// Swallow TargetClosedErrors for beforeunload dialogs.
}
} }
@Override @Override
@@ -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.google.gson.JsonObject;
class DisposableObject extends ChannelOwner implements AutoCloseable {
DisposableObject(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@Override
public void close() {
sendMessage("dispose", new JsonObject(), NO_TIMEOUT);
}
}
@@ -0,0 +1,34 @@
/*
* 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;
class DisposableStub implements AutoCloseable {
private Runnable dispose;
DisposableStub(Runnable dispose) {
this.dispose = dispose;
}
@Override
public void close() {
if (dispose == null)
return;
Runnable d = dispose;
dispose = null;
d.run();
}
}
@@ -46,22 +46,22 @@ class DownloadImpl implements Download {
@Override @Override
public void cancel() { public void cancel() {
page.withLogging("Download.cancel", () -> artifact.cancel()); artifact.cancel();
} }
@Override @Override
public InputStream createReadStream() { public InputStream createReadStream() {
return page.withLogging("Download.createReadStream", () -> artifact.createReadStream()); return artifact.createReadStream();
} }
@Override @Override
public void delete() { public void delete() {
page.withLogging("Download.delete", () -> artifact.delete()); artifact.delete();
} }
@Override @Override
public String failure() { public String failure() {
return page.withLogging("Download.failure", () -> artifact.failure()); return artifact.failure();
} }
@Override @Override
@@ -71,11 +71,11 @@ class DownloadImpl implements Download {
@Override @Override
public Path path() { public Path path() {
return page.withLogging("Download.path", () -> artifact.pathAfterFinished()); return artifact.pathAfterFinished();
} }
@Override @Override
public void saveAs(Path path) { public void saveAs(Path path) {
page.withLogging("Download.saveAs", () -> artifact.saveAs(path)); artifact.saveAs(path);
} }
} }
@@ -21,11 +21,13 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.microsoft.playwright.ElementHandle; import com.microsoft.playwright.ElementHandle;
import com.microsoft.playwright.Frame; import com.microsoft.playwright.Frame;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.BoundingBox; import com.microsoft.playwright.options.BoundingBox;
import com.microsoft.playwright.options.ElementState; import com.microsoft.playwright.options.ElementState;
import com.microsoft.playwright.options.FilePayload; import com.microsoft.playwright.options.FilePayload;
import com.microsoft.playwright.options.SelectOption; import com.microsoft.playwright.options.SelectOption;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
@@ -38,8 +40,11 @@ import static com.microsoft.playwright.options.ScreenshotType.JPEG;
import static com.microsoft.playwright.options.ScreenshotType.PNG; import static com.microsoft.playwright.options.ScreenshotType.PNG;
public class ElementHandleImpl extends JSHandleImpl implements ElementHandle { public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
private final FrameImpl frame;
ElementHandleImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { ElementHandleImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
this.frame = (FrameImpl)parent;
} }
@Override @Override
@@ -49,105 +54,83 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override @Override
public ElementHandle querySelector(String selector) { public ElementHandle querySelector(String selector) {
return withLogging("ElementHandle.querySelector", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("selector", selector);
params.addProperty("selector", selector); JsonElement json = sendMessage("querySelector", params, NO_TIMEOUT);
JsonElement json = sendMessage("querySelector", params); JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
JsonObject element = json.getAsJsonObject().getAsJsonObject("element"); if (element == null) {
if (element == null) { return null;
return null; }
} return connection.getExistingObject(element.get("guid").getAsString());
return connection.getExistingObject(element.get("guid").getAsString());
});
} }
@Override @Override
public List<ElementHandle> querySelectorAll(String selector) { public List<ElementHandle> querySelectorAll(String selector) {
return withLogging("ElementHandle.<", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("selector", selector);
params.addProperty("selector", selector); JsonElement json = sendMessage("querySelectorAll", params, NO_TIMEOUT);
JsonElement json = sendMessage("querySelectorAll", params); JsonArray elements = json.getAsJsonObject().getAsJsonArray("elements");
JsonArray elements = json.getAsJsonObject().getAsJsonArray("elements"); if (elements == null) {
if (elements == null) { return null;
return null; }
} List<ElementHandle> handles = new ArrayList<>();
List<ElementHandle> handles = new ArrayList<>(); for (JsonElement item : elements) {
for (JsonElement item : elements) { handles.add(connection.getExistingObject(item.getAsJsonObject().get("guid").getAsString()));
handles.add(connection.getExistingObject(item.getAsJsonObject().get("guid").getAsString())); }
} return handles;
return handles;
});
} }
@Override @Override
public Object evalOnSelector(String selector, String pageFunction, Object arg) { public Object evalOnSelector(String selector, String pageFunction, Object arg) {
return withLogging("ElementHandle.evalOnSelector", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("selector", selector);
params.addProperty("selector", selector); params.addProperty("expression", pageFunction);
params.addProperty("expression", pageFunction); params.add("arg", gson().toJsonTree(serializeArgument(arg)));
params.add("arg", gson().toJsonTree(serializeArgument(arg))); JsonElement json = sendMessage("evalOnSelector", params, NO_TIMEOUT);
JsonElement json = sendMessage("evalOnSelector", params); SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class); return deserialize(value);
return deserialize(value);
});
} }
@Override @Override
public Object evalOnSelectorAll(String selector, String pageFunction, Object arg) { public Object evalOnSelectorAll(String selector, String pageFunction, Object arg) {
return withLogging("ElementHandle.evalOnSelectorAll", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("selector", selector);
params.addProperty("selector", selector); params.addProperty("expression", pageFunction);
params.addProperty("expression", pageFunction); params.add("arg", gson().toJsonTree(serializeArgument(arg)));
params.add("arg", gson().toJsonTree(serializeArgument(arg))); JsonElement json = sendMessage("evalOnSelectorAll", params, NO_TIMEOUT);
JsonElement json = sendMessage("evalOnSelectorAll", params); SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class); return deserialize(value);
return deserialize(value);
});
} }
@Override @Override
public BoundingBox boundingBox() { public BoundingBox boundingBox() {
return withLogging("ElementHandle.boundingBox", () -> { JsonObject json = sendMessage("boundingBox").getAsJsonObject();
JsonObject json = sendMessage("boundingBox").getAsJsonObject(); if (!json.has("value")) {
if (!json.has("value")) { return null;
return null; }
} return gson().fromJson(json.get("value"), BoundingBox.class);
return gson().fromJson(json.get("value"), BoundingBox.class);
});
} }
@Override @Override
public void check(CheckOptions options) { public void check(CheckOptions options) {
withLogging("ElementHandle.check", () -> checkImpl(options));
}
private void checkImpl(CheckOptions options) {
if (options == null) { if (options == null) {
options = new CheckOptions(); options = new CheckOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("check", params); sendMessage("check", params, frame.timeout(options.timeout));
} }
@Override @Override
public void click(ClickOptions options) { public void click(ClickOptions options) {
withLogging("ElementHandle.click", () -> clickImpl(options));
}
private void clickImpl(ClickOptions options) {
if (options == null) { if (options == null) {
options = new ClickOptions(); options = new ClickOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("click", params); sendMessage("click", params, frame.timeout(options.timeout));
} }
@Override @Override
public Frame contentFrame() { public Frame contentFrame() {
return withLogging("ElementHandle.contentFrame", () -> contentFrameImpl());
}
private Frame contentFrameImpl() {
JsonObject json = sendMessage("contentFrame").getAsJsonObject(); JsonObject json = sendMessage("contentFrame").getAsJsonObject();
if (!json.has("frame")) { if (!json.has("frame")) {
return null; return null;
@@ -157,177 +140,132 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override @Override
public void dblclick(DblclickOptions options) { public void dblclick(DblclickOptions options) {
withLogging("ElementHandle.dblclick", () -> dblclickImpl(options));
}
private void dblclickImpl(DblclickOptions options) {
if (options == null) { if (options == null) {
options = new DblclickOptions(); options = new DblclickOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("dblclick", params); sendMessage("dblclick", params, frame.timeout(options.timeout));
} }
@Override @Override
public void dispatchEvent(String type, Object eventInit) { public void dispatchEvent(String type, Object eventInit) {
withLogging("ElementHandle.dispatchEvent", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("type", type);
params.addProperty("type", type); params.add("eventInit", gson().toJsonTree(serializeArgument(eventInit)));
params.add("eventInit", gson().toJsonTree(serializeArgument(eventInit))); sendMessage("dispatchEvent", params, NO_TIMEOUT);
sendMessage("dispatchEvent", params);
});
} }
@Override @Override
public void fill(String value, FillOptions options) { public void fill(String value, FillOptions options) {
withLogging("ElementHandle.fill", () -> fillImpl(value, options));
}
private void fillImpl(String value, FillOptions options) {
if (options == null) { if (options == null) {
options = new FillOptions(); options = new FillOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("value", value); params.addProperty("value", value);
sendMessage("fill", params); sendMessage("fill", params, frame.timeout(options.timeout));
} }
@Override @Override
public void focus() { public void focus() {
withLogging("ElementHandle.focus", () -> sendMessage("focus")); sendMessage("focus");
} }
@Override @Override
public String getAttribute(String name) { public String getAttribute(String name) {
return withLogging("ElementHandle.getAttribute", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("name", name);
params.addProperty("name", name); JsonObject json = sendMessage("getAttribute", params, NO_TIMEOUT).getAsJsonObject();
JsonObject json = sendMessage("getAttribute", params).getAsJsonObject(); return json.has("value") ? json.get("value").getAsString() : null;
return json.has("value") ? json.get("value").getAsString() : null;
});
} }
@Override @Override
public void hover(HoverOptions options) { public void hover(HoverOptions options) {
withLogging("ElementHandle.hover", () -> hoverImpl(options));
}
private void hoverImpl(HoverOptions options) {
if (options == null) { if (options == null) {
options = new HoverOptions(); options = new HoverOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("hover", params); sendMessage("hover", params, frame.timeout(options.timeout));
} }
@Override @Override
public String innerHTML() { public String innerHTML() {
return withLogging("ElementHandle.innerHTML", () -> { JsonObject json = sendMessage("innerHTML").getAsJsonObject();
JsonObject json = sendMessage("innerHTML").getAsJsonObject(); return json.get("value").getAsString();
return json.get("value").getAsString();
});
} }
@Override @Override
public String innerText() { public String innerText() {
return withLogging("ElementHandle.innerText", () -> { JsonObject json = sendMessage("innerText").getAsJsonObject();
JsonObject json = sendMessage("innerText").getAsJsonObject(); return json.get("value").getAsString();
return json.get("value").getAsString();
});
} }
@Override @Override
public String inputValue(InputValueOptions options) { public String inputValue(InputValueOptions options) {
return withLogging("ElementHandle.inputValue", () -> inputValueImpl(options));
}
private String inputValueImpl(InputValueOptions options) {
if (options == null) { if (options == null) {
options = new InputValueOptions(); options = new InputValueOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
JsonObject json = sendMessage("inputValue", params).getAsJsonObject(); JsonObject json = sendMessage("inputValue", params, NO_TIMEOUT).getAsJsonObject();
return json.get("value").getAsString(); return json.get("value").getAsString();
} }
@Override @Override
public boolean isChecked() { public boolean isChecked() {
return withLogging("ElementHandle.isChecked", () -> { JsonObject json = sendMessage("isChecked").getAsJsonObject();
JsonObject json = sendMessage("isChecked").getAsJsonObject(); return json.get("value").getAsBoolean();
return json.get("value").getAsBoolean();
});
} }
@Override @Override
public boolean isDisabled() { public boolean isDisabled() {
return withLogging("ElementHandle.isDisabled", () -> { JsonObject json = sendMessage("isDisabled").getAsJsonObject();
JsonObject json = sendMessage("isDisabled").getAsJsonObject(); return json.get("value").getAsBoolean();
return json.get("value").getAsBoolean();
});
} }
@Override @Override
public boolean isEditable() { public boolean isEditable() {
return withLogging("ElementHandle.isEditable", () -> { JsonObject json = sendMessage("isEditable").getAsJsonObject();
JsonObject json = sendMessage("isEditable").getAsJsonObject(); return json.get("value").getAsBoolean();
return json.get("value").getAsBoolean();
});
} }
@Override @Override
public boolean isEnabled() { public boolean isEnabled() {
return withLogging("ElementHandle.isEnabled", () -> { JsonObject json = sendMessage("isEnabled").getAsJsonObject();
JsonObject json = sendMessage("isEnabled").getAsJsonObject(); return json.get("value").getAsBoolean();
return json.get("value").getAsBoolean();
});
} }
@Override @Override
public boolean isHidden() { public boolean isHidden() {
return withLogging("ElementHandle.isHidden", () -> { JsonObject json = sendMessage("isHidden").getAsJsonObject();
JsonObject json = sendMessage("isHidden").getAsJsonObject(); return json.get("value").getAsBoolean();
return json.get("value").getAsBoolean();
});
} }
@Override @Override
public boolean isVisible() { public boolean isVisible() {
return withLogging("ElementHandle.isVisible", () -> { JsonObject json = sendMessage("isVisible").getAsJsonObject();
JsonObject json = sendMessage("isVisible").getAsJsonObject(); return json.get("value").getAsBoolean();
return json.get("value").getAsBoolean();
});
} }
@Override @Override
public FrameImpl ownerFrame() { public FrameImpl ownerFrame() {
return withLogging("ElementHandle.ownerFrame", () -> { JsonObject json = sendMessage("ownerFrame").getAsJsonObject();
JsonObject json = sendMessage("ownerFrame").getAsJsonObject(); if (!json.has("frame")) {
if (!json.has("frame")) { return null;
return null; }
} return connection.getExistingObject(json.getAsJsonObject("frame").get("guid").getAsString());
return connection.getExistingObject(json.getAsJsonObject("frame").get("guid").getAsString());
});
} }
@Override @Override
public void press(String key, PressOptions options) { public void press(String key, PressOptions options) {
withLogging("ElementHandle.press", () -> pressImpl(key, options));
}
private void pressImpl(String key, PressOptions options) {
if (options == null) { if (options == null) {
options = new PressOptions(); options = new PressOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("key", key); params.addProperty("key", key);
sendMessage("press", params); sendMessage("press", params, frame.timeout(options.timeout));
} }
@Override @Override
public byte[] screenshot(ScreenshotOptions options) { public byte[] screenshot(ScreenshotOptions options) {
return withLogging("ElementHandle.screenshot", () -> screenshotImpl(options));
}
private byte[] screenshotImpl(ScreenshotOptions options) {
if (options == null) { if (options == null) {
options = new ScreenshotOptions(); options = new ScreenshotOptions();
} }
@@ -346,7 +284,7 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("path"); params.remove("path");
JsonObject json = sendMessage("screenshot", params).getAsJsonObject(); JsonObject json = sendMessage("screenshot", params, frame.timeout(options.timeout)).getAsJsonObject();
byte[] buffer = Base64.getDecoder().decode(json.get("binary").getAsString()); byte[] buffer = Base64.getDecoder().decode(json.get("binary").getAsString());
if (options.path != null) { if (options.path != null) {
@@ -357,7 +295,11 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override @Override
public void scrollIntoViewIfNeeded(ScrollIntoViewIfNeededOptions options) { public void scrollIntoViewIfNeeded(ScrollIntoViewIfNeededOptions options) {
withLogging("ElementHandle.scrollIntoViewIfNeeded", () -> scrollIntoViewIfNeededImpl(options)); if (options == null) {
options = new ScrollIntoViewIfNeededOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("scrollIntoViewIfNeeded", params, frame.timeout(options.timeout));
} }
@Override @Override
@@ -381,7 +323,7 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
if (values != null) { if (values != null) {
params.add("options", toSelectValueOrLabel(values)); params.add("options", toSelectValueOrLabel(values));
} }
return selectOption(params); return selectOption(params, options.timeout);
} }
@Override @Override
@@ -390,13 +332,6 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
return selectOption(values, options); return selectOption(values, options);
} }
private void scrollIntoViewIfNeededImpl(ScrollIntoViewIfNeededOptions options) {
if (options == null) {
options = new ScrollIntoViewIfNeededOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("scrollIntoViewIfNeeded", params);
}
@Override @Override
public List<String> selectOption(SelectOption[] values, SelectOptionOptions options) { public List<String> selectOption(SelectOption[] values, SelectOptionOptions options) {
@@ -407,7 +342,7 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
if (values != null) { if (values != null) {
params.add("options", gson().toJsonTree(values)); params.add("options", gson().toJsonTree(values));
} }
return selectOption(params); return selectOption(params, options.timeout);
} }
@Override @Override
@@ -419,19 +354,21 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
if (values != null) { if (values != null) {
params.add("elements", Serialization.toProtocol(values)); params.add("elements", Serialization.toProtocol(values));
} }
return selectOption(params); return selectOption(params, options.timeout);
} }
private List<String> selectOption(JsonObject params) { private List<String> selectOption(JsonObject params, Double timeout) {
return withLogging("SelectOption", () -> { JsonObject json = sendMessage("selectOption", params, frame.timeout(timeout)).getAsJsonObject();
JsonObject json = sendMessage("selectOption", params).getAsJsonObject(); return parseStringList(json.getAsJsonArray("values"));
return parseStringList(json.getAsJsonArray("values"));
});
} }
@Override @Override
public void selectText(SelectTextOptions options) { public void selectText(SelectTextOptions options) {
withLogging("ElementHandle.selectText", () -> selectTextImpl(options)); if (options == null) {
options = new SelectTextOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("selectText", params, frame.timeout(options.timeout));
} }
@Override @Override
@@ -448,20 +385,9 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
setInputFiles(new Path[]{files}, options); setInputFiles(new Path[]{files}, options);
} }
private void selectTextImpl(SelectTextOptions options) {
if (options == null) {
options = new SelectTextOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("selectText", params);
}
@Override @Override
public void setInputFiles(Path[] files, SetInputFilesOptions options) { public void setInputFiles(Path[] files, SetInputFilesOptions options) {
withLogging("ElementHandle.setInputFiles", () -> setInputFilesImpl(files, options));
}
void setInputFilesImpl(Path[] files, SetInputFilesOptions options) {
FrameImpl frame = ownerFrame(); FrameImpl frame = ownerFrame();
if (frame == null) { if (frame == null) {
throw new Error("Cannot set input files to detached element"); throw new Error("Cannot set input files to detached element");
@@ -471,7 +397,7 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
addFilePathUploadParams(files, params, frame.page().context()); addFilePathUploadParams(files, params, frame.page().context());
sendMessage("setInputFiles", params); sendMessage("setInputFiles", params, frame.timeout(options.timeout));
} }
@Override @Override
@@ -481,77 +407,51 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override @Override
public void setInputFiles(FilePayload[] files, SetInputFilesOptions options) { public void setInputFiles(FilePayload[] files, SetInputFilesOptions options) {
withLogging("ElementHandle.setInputFiles", () -> setInputFilesImpl(files, options));
}
void setInputFilesImpl(FilePayload[] files, SetInputFilesOptions options) {
checkFilePayloadSize(files); checkFilePayloadSize(files);
if (options == null) { if (options == null) {
options = new SetInputFilesOptions(); options = new SetInputFilesOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.add("payloads", Serialization.toJsonArray(files)); params.add("payloads", Serialization.toJsonArray(files));
sendMessage("setInputFiles", params); sendMessage("setInputFiles", params, frame.timeout(options.timeout));
} }
@Override @Override
public void tap(TapOptions options) { public void tap(TapOptions options) {
withLogging("ElementHandle.tap", () -> tapImpl(options));
}
private void tapImpl(TapOptions options) {
if (options == null) { if (options == null) {
options = new TapOptions(); options = new TapOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("tap", params); sendMessage("tap", params, frame.timeout(options.timeout));
} }
@Override @Override
public String textContent() { public String textContent() {
return withLogging("ElementHandle.textContent", () -> textContentImpl()); JsonObject json = sendMessage("textContent").getAsJsonObject();
} return json.has("value") ? json.get("value").getAsString() : null;
private String textContentImpl() {
return withLogging("ElementHandle.textContent", () -> {
JsonObject json = sendMessage("textContent").getAsJsonObject();
return json.has("value") ? json.get("value").getAsString() : null;
});
} }
@Override @Override
public void type(String text, TypeOptions options) { public void type(String text, TypeOptions options) {
withLogging("ElementHandle.type", () -> typeImpl(text, options));
}
private void typeImpl(String text, TypeOptions options) {
if (options == null) { if (options == null) {
options = new TypeOptions(); options = new TypeOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("text", text); params.addProperty("text", text);
sendMessage("type", params); sendMessage("type", params, frame.timeout(options.timeout));
} }
@Override @Override
public void uncheck(UncheckOptions options) { public void uncheck(UncheckOptions options) {
withLogging("ElementHandle.uncheck", () -> uncheckImpl(options));
}
private void uncheckImpl(UncheckOptions options) {
if (options == null) { if (options == null) {
options = new UncheckOptions(); options = new UncheckOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("uncheck", params); sendMessage("uncheck", params, frame.timeout(options.timeout));
} }
@Override @Override
public void waitForElementState(ElementState state, WaitForElementStateOptions options) { public void waitForElementState(ElementState state, WaitForElementStateOptions options) {
withLogging("ElementHandle.waitForElementState", () -> waitForElementStateImpl(state, options));
}
private void waitForElementStateImpl(ElementState state, WaitForElementStateOptions options) {
if (options == null) { if (options == null) {
options = new WaitForElementStateOptions(); options = new WaitForElementStateOptions();
} }
@@ -560,7 +460,7 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("state", toProtocol(state)); params.addProperty("state", toProtocol(state));
sendMessage("waitForElementState", params); sendMessage("waitForElementState", params, frame.timeout(options.timeout));
} }
private static String toProtocol(ElementState state) { private static String toProtocol(ElementState state) {
@@ -569,16 +469,12 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override @Override
public ElementHandle waitForSelector(String selector, WaitForSelectorOptions options) { public ElementHandle waitForSelector(String selector, WaitForSelectorOptions options) {
return withLogging("ElementHandle.waitForSelector", () -> waitForSelectorImpl(selector, options));
}
private ElementHandle waitForSelectorImpl(String selector, WaitForSelectorOptions options) {
if (options == null) { if (options == null) {
options = new WaitForSelectorOptions(); options = new WaitForSelectorOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
JsonElement json = sendMessage("waitForSelector", params); JsonElement json = sendMessage("waitForSelector", params, frame.timeout(options.timeout)).getAsJsonObject();
JsonObject element = json.getAsJsonObject().getAsJsonObject("element"); JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
if (element == null) { if (element == null) {
return null; return null;
@@ -58,8 +58,7 @@ class FileChooserImpl implements FileChooser {
@Override @Override
public void setFiles(Path[] files, SetFilesOptions options) { public void setFiles(Path[] files, SetFilesOptions options) {
page.withLogging("FileChooser.setInputFiles", element.setInputFiles(files, convertType(options, ElementHandle.SetInputFilesOptions.class));
() -> element.setInputFilesImpl(files, convertType(options, ElementHandle.SetInputFilesOptions.class)));
} }
@Override @Override
@@ -69,7 +68,6 @@ class FileChooserImpl implements FileChooser {
@Override @Override
public void setFiles(FilePayload[] files, SetFilesOptions options) { public void setFiles(FilePayload[] files, SetFilesOptions options) {
page.withLogging("FileChooser.setInputFiles", element.setInputFiles(files, convertType(options, ElementHandle.SetInputFilesOptions.class));
() -> element.setInputFilesImpl(files, convertType(options, ElementHandle.SetInputFilesOptions.class)));
} }
} }
@@ -20,39 +20,93 @@ import com.microsoft.playwright.options.FilePayload;
import com.microsoft.playwright.options.FormData; import com.microsoft.playwright.options.FormData;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.LinkedHashMap; import java.util.*;
import java.util.Map; import java.util.stream.Collectors;
public class FormDataImpl implements FormData { public class FormDataImpl implements FormData {
Map<String, Object> fields = new LinkedHashMap<>(); static class Field implements Map.Entry<String, Object> {
final String name;
final Object value;
private Field(String name, Object value) {
this.name = name;
this.value = value;
}
@Override
public String getKey() {
return name;
}
@Override
public Object getValue() {
return value;
}
@Override
public Object setValue(Object value) {
throw new UnsupportedOperationException();
}
}
List<Field> fields = new ArrayList();
@Override
public FormData append(String name, String value) {
return appendImpl(name, value);
}
@Override
public FormData append(String name, boolean value) {
return appendImpl(name, value);
}
@Override
public FormData append(String name, int value) {
return appendImpl(name, value);
}
@Override
public FormData append(String name, Path value) {
return appendImpl(name, value);
}
@Override
public FormData append(String name, FilePayload value) {
return appendImpl(name, value);
}
@Override @Override
public FormData set(String name, String value) { public FormData set(String name, String value) {
fields.put(name, value); return setImpl(name, value);
return this;
} }
@Override @Override
public FormData set(String name, boolean value) { public FormData set(String name, boolean value) {
fields.put(name, value); return setImpl(name, value);
return this;
} }
@Override @Override
public FormData set(String name, int value) { public FormData set(String name, int value) {
fields.put(name, value); return setImpl(name, value);
return this;
} }
@Override @Override
public FormData set(String name, Path value) { public FormData set(String name, Path value) {
fields.put(name, value); return setImpl(name, value);
return this;
} }
@Override @Override
public FormData set(String name, FilePayload value) { public FormData set(String name, FilePayload value) {
fields.put(name, value); return setImpl(name, value);
}
private FormData setImpl(String name, Object value) {
fields = fields.stream().filter(f -> !name.equals(f.name)).collect(Collectors.toList());
return appendImpl(name, value);
}
private FormData appendImpl(String name, Object value) {
fields.add(new Field(name, value));
return this; return this;
} }
} }
@@ -74,16 +74,12 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override @Override
public ElementHandle querySelector(String selector, QuerySelectorOptions options) { public ElementHandle querySelector(String selector, QuerySelectorOptions options) {
return withLogging("Frame.querySelector", () -> querySelectorImpl(selector, options));
}
ElementHandleImpl querySelectorImpl(String selector, QuerySelectorOptions options) {
if (options == null) { if (options == null) {
options = new QuerySelectorOptions(); options = new QuerySelectorOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
JsonElement json = sendMessage("querySelector", params); JsonElement json = sendMessage("querySelector", params, NO_TIMEOUT);
JsonObject element = json.getAsJsonObject().getAsJsonObject("element"); JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
if (element == null) { if (element == null) {
return null; return null;
@@ -93,7 +89,18 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override @Override
public List<ElementHandle> querySelectorAll(String selector) { public List<ElementHandle> querySelectorAll(String selector) {
return withLogging("Frame.querySelectorAll", () -> querySelectorAllImpl(selector)); JsonObject params = new JsonObject();
params.addProperty("selector", selector);
JsonElement json = sendMessage("querySelectorAll", params, NO_TIMEOUT);
JsonArray elements = json.getAsJsonObject().getAsJsonArray("elements");
if (elements == null) {
return null;
}
List<ElementHandle> handles = new ArrayList<>();
for (JsonElement item : elements) {
handles.add(connection.getExistingObject(item.getAsJsonObject().get("guid").getAsString()));
}
return handles;
} }
@Override @Override
@@ -110,7 +117,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override @Override
public List<String> selectOption(String selector, String[] values, SelectOptionOptions options) { public List<String> selectOption(String selector, String[] values, SelectOptionOptions options) {
return withLogging("Frame.selectOption", () -> selectOptionImpl(selector, values, options)); return selectOptionImpl(selector, values, options);
} }
@Override @Override
@@ -119,24 +126,10 @@ public class FrameImpl extends ChannelOwner implements Frame {
return selectOption(selector, values, options); return selectOption(selector, values, options);
} }
List<ElementHandle> querySelectorAllImpl(String selector) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
JsonElement json = sendMessage("querySelectorAll", params);
JsonArray elements = json.getAsJsonObject().getAsJsonArray("elements");
if (elements == null) {
return null;
}
List<ElementHandle> handles = new ArrayList<>();
for (JsonElement item : elements) {
handles.add(connection.getExistingObject(item.getAsJsonObject().get("guid").getAsString()));
}
return handles;
}
@Override @Override
public Object evalOnSelector(String selector, String pageFunction, Object arg, EvalOnSelectorOptions options) { public Object evalOnSelector(String selector, String pageFunction, Object arg, EvalOnSelectorOptions options) {
return withLogging("Frame.evalOnSelector", () -> evalOnSelectorImpl(selector, pageFunction, arg, options)); return evalOnSelectorImpl(selector, pageFunction, arg, options);
} }
Object evalOnSelectorImpl(String selector, String pageFunction, Object arg, EvalOnSelectorOptions options) { Object evalOnSelectorImpl(String selector, String pageFunction, Object arg, EvalOnSelectorOptions options) {
@@ -147,14 +140,14 @@ public class FrameImpl extends ChannelOwner implements Frame {
params.addProperty("selector", selector); params.addProperty("selector", selector);
params.addProperty("expression", pageFunction); params.addProperty("expression", pageFunction);
params.add("arg", gson().toJsonTree(serializeArgument(arg))); params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evalOnSelector", params); JsonElement json = sendMessage("evalOnSelector", params, NO_TIMEOUT);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class); SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
return deserialize(value); return deserialize(value);
} }
@Override @Override
public Object evalOnSelectorAll(String selector, String pageFunction, Object arg) { public Object evalOnSelectorAll(String selector, String pageFunction, Object arg) {
return withLogging("Frame.evalOnSelectorAll", () -> evalOnSelectorAllImpl(selector, pageFunction, arg)); return evalOnSelectorAllImpl(selector, pageFunction, arg);
} }
Object evalOnSelectorAllImpl(String selector, String pageFunction, Object arg) { Object evalOnSelectorAllImpl(String selector, String pageFunction, Object arg) {
@@ -162,14 +155,14 @@ public class FrameImpl extends ChannelOwner implements Frame {
params.addProperty("selector", selector); params.addProperty("selector", selector);
params.addProperty("expression", pageFunction); params.addProperty("expression", pageFunction);
params.add("arg", gson().toJsonTree(serializeArgument(arg))); params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evalOnSelectorAll", params); JsonElement json = sendMessage("evalOnSelectorAll", params, NO_TIMEOUT);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class); SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
return deserialize(value); return deserialize(value);
} }
@Override @Override
public ElementHandle addScriptTag(AddScriptTagOptions options){ public ElementHandle addScriptTag(AddScriptTagOptions options){
return withLogging("Frame.addScriptTag", () -> addScriptTagImpl(options)); return addScriptTagImpl(options);
} }
ElementHandle addScriptTagImpl(AddScriptTagOptions options) { ElementHandle addScriptTagImpl(AddScriptTagOptions options) {
@@ -189,13 +182,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
content = addSourceUrlToScript(content, options.path); content = addSourceUrlToScript(content, options.path);
jsonOptions.addProperty("content", content); jsonOptions.addProperty("content", content);
} }
JsonElement json = sendMessage("addScriptTag", jsonOptions); JsonElement json = sendMessage("addScriptTag", jsonOptions, NO_TIMEOUT);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("element").get("guid").getAsString()); return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("element").get("guid").getAsString());
} }
@Override @Override
public ElementHandle addStyleTag(AddStyleTagOptions options){ public ElementHandle addStyleTag(AddStyleTagOptions options){
return withLogging("Frame.addStyleTag", () -> addStyleTagImpl(options)); return addStyleTagImpl(options);
} }
ElementHandle addStyleTagImpl(AddStyleTagOptions options) { ElementHandle addStyleTagImpl(AddStyleTagOptions options) {
@@ -215,22 +208,18 @@ public class FrameImpl extends ChannelOwner implements Frame {
content += "/*# sourceURL=" + options.path.toString().replace("\n", "") + "*/"; content += "/*# sourceURL=" + options.path.toString().replace("\n", "") + "*/";
jsonOptions.addProperty("content", content); jsonOptions.addProperty("content", content);
} }
JsonElement json = sendMessage("addStyleTag", jsonOptions); JsonElement json = sendMessage("addStyleTag", jsonOptions, NO_TIMEOUT);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("element").get("guid").getAsString()); return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("element").get("guid").getAsString());
} }
@Override @Override
public void check(String selector, CheckOptions options){ public void check(String selector, CheckOptions options){
withLogging("Frame.check", () -> checkImpl(selector, options));
}
void checkImpl(String selector, CheckOptions options) {
if (options == null) { if (options == null) {
options = new CheckOptions(); options = new CheckOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
sendMessage("check", params); sendMessage("check", params, timeout(options.timeout));
} }
@Override @Override
@@ -240,47 +229,45 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override @Override
public void click(String selector, ClickOptions options) { public void click(String selector, ClickOptions options) {
withLogging("Frame.click", () -> clickImpl(selector, options)); clickImpl(selector, options, null);
} }
void clickImpl(String selector, ClickOptions options) { void clickImpl(String selector, ClickOptions options, Integer steps) {
if (options == null) { if (options == null) {
options = new ClickOptions(); options = new ClickOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
sendMessage("click", params); if (steps != null) {
params.addProperty("steps", steps);
}
sendMessage("click", params, timeout(options.timeout));
} }
@Override @Override
public String content() { public String content() {
return withLogging("Frame.content", () -> contentImpl());
}
String contentImpl() {
return sendMessage("content").getAsJsonObject().get("value").getAsString(); return sendMessage("content").getAsJsonObject().get("value").getAsString();
} }
@Override @Override
public void dblclick(String selector, DblclickOptions options) { public void dblclick(String selector, DblclickOptions options) {
withLogging("Frame.dblclick", () -> dblclickImpl(selector, options)); dblclickImpl(selector, options, null);
} }
void dblclickImpl(String selector, DblclickOptions options) { void dblclickImpl(String selector, DblclickOptions options, Integer steps) {
if (options == null) { if (options == null) {
options = new DblclickOptions(); options = new DblclickOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
sendMessage("dblclick", params); if (steps != null) {
params.addProperty("steps", steps);
}
sendMessage("dblclick", params, timeout(options.timeout));
} }
@Override @Override
public void dispatchEvent(String selector, String type, Object eventInit, DispatchEventOptions options) { public void dispatchEvent(String selector, String type, Object eventInit, DispatchEventOptions options) {
withLogging("Frame.dispatchEvent", () -> dispatchEventImpl(selector, type, eventInit, options));
}
void dispatchEventImpl(String selector, String type, Object eventInit, DispatchEventOptions options) {
if (options == null) { if (options == null) {
options = new DispatchEventOptions(); options = new DispatchEventOptions();
} }
@@ -288,71 +275,55 @@ public class FrameImpl extends ChannelOwner implements Frame {
params.addProperty("selector", selector); params.addProperty("selector", selector);
params.addProperty("type", type); params.addProperty("type", type);
params.add("eventInit", gson().toJsonTree(serializeArgument(eventInit))); params.add("eventInit", gson().toJsonTree(serializeArgument(eventInit)));
sendMessage("dispatchEvent", params); sendMessage("dispatchEvent", params, timeout(options.timeout));
} }
@Override @Override
public Object evaluate(String expression, Object arg) { public Object evaluate(String expression, Object arg) {
return withLogging("Frame.evaluate", () -> evaluateImpl(expression, arg));
}
Object evaluateImpl(String expression, Object arg) {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("expression", expression); params.addProperty("expression", expression);
params.addProperty("world", "main"); params.addProperty("world", "main");
params.add("arg", gson().toJsonTree(serializeArgument(arg))); params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpression", params); JsonElement json = sendMessage("evaluateExpression", params, NO_TIMEOUT);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class); SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
return deserialize(value); return deserialize(value);
} }
@Override @Override
public JSHandle evaluateHandle(String pageFunction, Object arg) { public JSHandle evaluateHandle(String pageFunction, Object arg) {
return withLogging("Frame.evaluateHandle", () -> evaluateHandleImpl(pageFunction, arg));
}
JSHandle evaluateHandleImpl(String pageFunction, Object arg) {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction); params.addProperty("expression", pageFunction);
params.addProperty("world", "main"); params.addProperty("world", "main");
params.add("arg", gson().toJsonTree(serializeArgument(arg))); params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpressionHandle", params); JsonElement json = sendMessage("evaluateExpressionHandle", params, NO_TIMEOUT);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("handle").get("guid").getAsString()); return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("handle").get("guid").getAsString());
} }
@Override @Override
public void fill(String selector, String value, FillOptions options) { public void fill(String selector, String value, FillOptions options) {
withLogging("Frame.fill", () -> fillImpl(selector, value, options));
}
void fillImpl(String selector, String value, FillOptions options) {
if (options == null) { if (options == null) {
options = new FillOptions(); options = new FillOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
params.addProperty("value", value); params.addProperty("value", value);
sendMessage("fill", params); sendMessage("fill", params, timeout(options.timeout));
} }
@Override @Override
public void focus(String selector, FocusOptions options) { public void focus(String selector, FocusOptions options) {
withLogging("Frame.focus", () -> focusImpl(selector, options));
}
void focusImpl(String selector, FocusOptions options) {
if (options == null) { if (options == null) {
options = new FocusOptions(); options = new FocusOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
sendMessage("focus", params); sendMessage("focus", params, timeout(options.timeout));
} }
@Override @Override
public ElementHandle frameElement() { public ElementHandle frameElement() {
return withLogging("Frame.frameElement", () -> frameElementImpl()); JsonObject json = sendMessage("frameElement").getAsJsonObject();
return connection.getExistingObject(json.getAsJsonObject("element").get("guid").getAsString());
} }
@Override @Override
@@ -360,14 +331,9 @@ public class FrameImpl extends ChannelOwner implements Frame {
return new FrameLocatorImpl(this, selector); return new FrameLocatorImpl(this, selector);
} }
ElementHandle frameElementImpl() {
JsonObject json = sendMessage("frameElement").getAsJsonObject();
return connection.getExistingObject(json.getAsJsonObject("element").get("guid").getAsString());
}
@Override @Override
public String getAttribute(String selector, String name, GetAttributeOptions options) { public String getAttribute(String selector, String name, GetAttributeOptions options) {
return withLogging("Frame.getAttribute", () -> getAttributeImpl(selector, name, options)); return getAttributeImpl(selector, name, options);
} }
@Override @Override
@@ -442,7 +408,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
params.addProperty("name", name); params.addProperty("name", name);
JsonObject json = sendMessage("getAttribute", params).getAsJsonObject(); JsonObject json = sendMessage("getAttribute", params, timeout(options.timeout)).getAsJsonObject();
if (json.has("value")) { if (json.has("value")) {
return json.get("value").getAsString(); return json.get("value").getAsString();
} }
@@ -451,7 +417,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override @Override
public ResponseImpl navigate(String url, NavigateOptions options) { public ResponseImpl navigate(String url, NavigateOptions options) {
return withLogging("Page.navigate", () -> navigateImpl(url, options)); return navigateImpl(url, options);
} }
ResponseImpl navigateImpl(String url, NavigateOptions options) { ResponseImpl navigateImpl(String url, NavigateOptions options) {
@@ -460,7 +426,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("url", url); params.addProperty("url", url);
JsonElement result = sendMessage("goto", params); JsonElement result = sendMessage("goto", params, navigationTimeout(options.timeout));
JsonObject jsonResponse = result.getAsJsonObject().getAsJsonObject("response"); JsonObject jsonResponse = result.getAsJsonObject().getAsJsonObject("response");
if (jsonResponse == null) { if (jsonResponse == null) {
return null; return null;
@@ -470,7 +436,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override @Override
public void hover(String selector, HoverOptions options) { public void hover(String selector, HoverOptions options) {
withLogging("Frame.hover", () -> hoverImpl(selector, options)); hoverImpl(selector, options);
} }
void hoverImpl(String selector, HoverOptions options) { void hoverImpl(String selector, HoverOptions options) {
@@ -479,27 +445,30 @@ public class FrameImpl extends ChannelOwner implements Frame {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
sendMessage("hover", params); sendMessage("hover", params, timeout(options.timeout));
} }
@Override @Override
public void dragAndDrop(String source, String target, DragAndDropOptions options) { public void dragAndDrop(String source, String target, DragAndDropOptions options) {
withLogging("Frame.dragAndDrop", () -> dragAndDropImpl(source, target, options)); dragAndDropImpl(source, target, options, null);
} }
void dragAndDropImpl(String source, String target, DragAndDropOptions options) { void dragAndDropImpl(String source, String target, DragAndDropOptions options, Integer steps) {
if (options == null) { if (options == null) {
options = new DragAndDropOptions(); options = new DragAndDropOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("source", source); params.addProperty("source", source);
params.addProperty("target", target); params.addProperty("target", target);
sendMessage("dragAndDrop", params); if (steps != null) {
params.addProperty("steps", steps);
}
sendMessage("dragAndDrop", params, timeout(options.timeout));
} }
@Override @Override
public String innerHTML(String selector, InnerHTMLOptions options) { public String innerHTML(String selector, InnerHTMLOptions options) {
return withLogging("Frame.innerHTML", () -> innerHTMLImpl(selector, options)); return innerHTMLImpl(selector, options);
} }
String innerHTMLImpl(String selector, InnerHTMLOptions options) { String innerHTMLImpl(String selector, InnerHTMLOptions options) {
@@ -508,13 +477,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
JsonObject json = sendMessage("innerHTML", params).getAsJsonObject(); JsonObject json = sendMessage("innerHTML", params, timeout(options.timeout)).getAsJsonObject();
return json.get("value").getAsString(); return json.get("value").getAsString();
} }
@Override @Override
public String innerText(String selector, InnerTextOptions options) { public String innerText(String selector, InnerTextOptions options) {
return withLogging("Frame.innerText", () -> innerTextImpl(selector, options)); return innerTextImpl(selector, options);
} }
String innerTextImpl(String selector, InnerTextOptions options) { String innerTextImpl(String selector, InnerTextOptions options) {
@@ -523,13 +492,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
JsonObject json = sendMessage("innerText", params).getAsJsonObject(); JsonObject json = sendMessage("innerText", params, timeout(options.timeout)).getAsJsonObject();
return json.get("value").getAsString(); return json.get("value").getAsString();
} }
@Override @Override
public String inputValue(String selector, InputValueOptions options) { public String inputValue(String selector, InputValueOptions options) {
return withLogging("Frame.inputValue", () -> inputValueImpl(selector, options)); return inputValueImpl(selector, options);
} }
String inputValueImpl(String selector, InputValueOptions options) { String inputValueImpl(String selector, InputValueOptions options) {
@@ -538,13 +507,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
JsonObject json = sendMessage("inputValue", params).getAsJsonObject(); JsonObject json = sendMessage("inputValue", params, timeout(options.timeout)).getAsJsonObject();
return json.get("value").getAsString(); return json.get("value").getAsString();
} }
@Override @Override
public boolean isChecked(String selector, IsCheckedOptions options) { public boolean isChecked(String selector, IsCheckedOptions options) {
return withLogging("Page.isChecked", () -> isCheckedImpl(selector, options)); return isCheckedImpl(selector, options);
} }
boolean isCheckedImpl(String selector, IsCheckedOptions options) { boolean isCheckedImpl(String selector, IsCheckedOptions options) {
@@ -553,7 +522,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
JsonObject json = sendMessage("isChecked", params).getAsJsonObject(); JsonObject json = sendMessage("isChecked", params, timeout(options.timeout)).getAsJsonObject();
return json.get("value").getAsBoolean(); return json.get("value").getAsBoolean();
} }
@@ -564,7 +533,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override @Override
public boolean isDisabled(String selector, IsDisabledOptions options) { public boolean isDisabled(String selector, IsDisabledOptions options) {
return withLogging("Page.isDisabled", () -> isDisabledImpl(selector, options)); return isDisabledImpl(selector, options);
} }
boolean isDisabledImpl(String selector, IsDisabledOptions options) { boolean isDisabledImpl(String selector, IsDisabledOptions options) {
@@ -573,13 +542,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
JsonObject json = sendMessage("isDisabled", params).getAsJsonObject(); JsonObject json = sendMessage("isDisabled", params, timeout(options.timeout)).getAsJsonObject();
return json.get("value").getAsBoolean(); return json.get("value").getAsBoolean();
} }
@Override @Override
public boolean isEditable(String selector, IsEditableOptions options) { public boolean isEditable(String selector, IsEditableOptions options) {
return withLogging("Page.isEditable", () -> isEditableImpl(selector, options)); return isEditableImpl(selector, options);
} }
boolean isEditableImpl(String selector, IsEditableOptions options) { boolean isEditableImpl(String selector, IsEditableOptions options) {
@@ -588,13 +557,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
JsonObject json = sendMessage("isEditable", params).getAsJsonObject(); JsonObject json = sendMessage("isEditable", params, timeout(options.timeout)).getAsJsonObject();
return json.get("value").getAsBoolean(); return json.get("value").getAsBoolean();
} }
@Override @Override
public boolean isEnabled(String selector, IsEnabledOptions options) { public boolean isEnabled(String selector, IsEnabledOptions options) {
return withLogging("Page.isEnabled", () -> isEnabledImpl(selector, options)); return isEnabledImpl(selector, options);
} }
boolean isEnabledImpl(String selector, IsEnabledOptions options) { boolean isEnabledImpl(String selector, IsEnabledOptions options) {
@@ -603,13 +572,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
JsonObject json = sendMessage("isEnabled", params).getAsJsonObject(); JsonObject json = sendMessage("isEnabled", params, timeout(options.timeout)).getAsJsonObject();
return json.get("value").getAsBoolean(); return json.get("value").getAsBoolean();
} }
@Override @Override
public boolean isHidden(String selector, IsHiddenOptions options) { public boolean isHidden(String selector, IsHiddenOptions options) {
return withLogging("Page.isHidden", () -> isHiddenImpl(selector, options)); return isHiddenImpl(selector, options);
} }
boolean isHiddenImpl(String selector, IsHiddenOptions options) { boolean isHiddenImpl(String selector, IsHiddenOptions options) {
@@ -618,13 +587,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
JsonObject json = sendMessage("isHidden", params).getAsJsonObject(); JsonObject json = sendMessage("isHidden", params, timeout(options.timeout)).getAsJsonObject();
return json.get("value").getAsBoolean(); return json.get("value").getAsBoolean();
} }
@Override @Override
public boolean isVisible(String selector, IsVisibleOptions options) { public boolean isVisible(String selector, IsVisibleOptions options) {
return withLogging("Page.isVisible", () -> isVisibleImpl(selector, options)); return isVisibleImpl(selector, options);
} }
@Override @Override
@@ -638,7 +607,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
JsonObject json = sendMessage("isVisible", params).getAsJsonObject(); JsonObject json = sendMessage("isVisible", params, timeout(options.timeout)).getAsJsonObject();
return json.get("value").getAsBoolean(); return json.get("value").getAsBoolean();
} }
@@ -659,7 +628,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override @Override
public void press(String selector, String key, PressOptions options) { public void press(String selector, String key, PressOptions options) {
withLogging("Frame.press", () -> pressImpl(selector, key, options)); pressImpl(selector, key, options);
} }
void pressImpl(String selector, String key, PressOptions options) { void pressImpl(String selector, String key, PressOptions options) {
@@ -669,12 +638,12 @@ public class FrameImpl extends ChannelOwner implements Frame {
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
params.addProperty("key", key); params.addProperty("key", key);
sendMessage("press", params); sendMessage("press", params, timeout(options.timeout));
} }
@Override @Override
public List<String> selectOption(String selector, SelectOption[] values, SelectOptionOptions options) { public List<String> selectOption(String selector, SelectOption[] values, SelectOptionOptions options) {
return withLogging("Frame.selectOption", () -> selectOptionImpl(selector, values, options)); return selectOptionImpl(selector, values, options);
} }
List<String> selectOptionImpl(String selector, SelectOption[] values, SelectOptionOptions options) { List<String> selectOptionImpl(String selector, SelectOption[] values, SelectOptionOptions options) {
@@ -686,7 +655,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
if (values != null) { if (values != null) {
params.add("options", gson().toJsonTree(values)); params.add("options", gson().toJsonTree(values));
} }
return selectOption(params); return selectOption(params, options.timeout);
} }
List<String> selectOptionImpl(String selector, String[] values, SelectOptionOptions options) { List<String> selectOptionImpl(String selector, String[] values, SelectOptionOptions options) {
@@ -698,12 +667,12 @@ public class FrameImpl extends ChannelOwner implements Frame {
if (values != null) { if (values != null) {
params.add("options", toSelectValueOrLabel(values)); params.add("options", toSelectValueOrLabel(values));
} }
return selectOption(params); return selectOption(params, options.timeout);
} }
@Override @Override
public List<String> selectOption(String selector, ElementHandle[] values, SelectOptionOptions options) { public List<String> selectOption(String selector, ElementHandle[] values, SelectOptionOptions options) {
return withLogging("Frame.selectOption", () -> selectOptionImpl(selector, values, options)); return selectOptionImpl(selector, values, options);
} }
List<String> selectOptionImpl(String selector, ElementHandle[] values, SelectOptionOptions options) { List<String> selectOptionImpl(String selector, ElementHandle[] values, SelectOptionOptions options) {
@@ -715,30 +684,35 @@ public class FrameImpl extends ChannelOwner implements Frame {
if (values != null) { if (values != null) {
params.add("elements", Serialization.toProtocol(values)); params.add("elements", Serialization.toProtocol(values));
} }
return selectOption(params); return selectOption(params, options.timeout);
} }
private List<String> selectOption(JsonObject params) { private List<String> selectOption(JsonObject params, Double timeout) {
JsonObject json = sendMessage("selectOption", params).getAsJsonObject(); JsonObject json = sendMessage("selectOption", params, timeout(timeout)).getAsJsonObject();
return parseStringList(json.getAsJsonArray("values")); return parseStringList(json.getAsJsonArray("values"));
} }
@Override @Override
public void setChecked(String selector, boolean checked, SetCheckedOptions options) { public void setChecked(String selector, boolean checked, SetCheckedOptions options) {
withLogging("Frame.setChecked", () -> setCheckedImpl(selector, checked, options)); setCheckedImpl(selector, checked, options);
} }
void setCheckedImpl(String selector, boolean checked, SetCheckedOptions options) { void setCheckedImpl(String selector, boolean checked, SetCheckedOptions options) {
if (checked) { if (checked) {
checkImpl(selector, convertType(options, CheckOptions.class)); check(selector, convertType(options, CheckOptions.class));
} else { } else {
uncheckImpl(selector, convertType(options, UncheckOptions.class)); uncheck(selector, convertType(options, UncheckOptions.class));
} }
} }
@Override @Override
public void setContent(String html, SetContentOptions options) { public void setContent(String html, SetContentOptions options) {
withLogging("Frame.setContent", () -> setContentImpl(html, options)); if (options == null) {
options = new SetContentOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("html", html);
sendMessage("setContent", params, navigationTimeout(options.timeout));
} }
@Override @Override
@@ -746,18 +720,9 @@ public class FrameImpl extends ChannelOwner implements Frame {
setInputFiles(selector, new Path[] {files}, options); setInputFiles(selector, new Path[] {files}, options);
} }
void setContentImpl(String html, SetContentOptions options) {
if (options == null) {
options = new SetContentOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("html", html);
sendMessage("setContent", params);
}
@Override @Override
public void setInputFiles(String selector, Path[] files, SetInputFilesOptions options) { public void setInputFiles(String selector, Path[] files, SetInputFilesOptions options) {
withLogging("Frame.setInputFiles", () -> setInputFilesImpl(selector, files, options)); setInputFilesImpl(selector, files, options);
} }
void setInputFilesImpl(String selector, Path[] files, SetInputFilesOptions options) { void setInputFilesImpl(String selector, Path[] files, SetInputFilesOptions options) {
@@ -767,7 +732,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
addFilePathUploadParams(files, params, page.context()); addFilePathUploadParams(files, params, page.context());
params.addProperty("selector", selector); params.addProperty("selector", selector);
sendMessage("setInputFiles", params); sendMessage("setInputFiles", params, timeout(options.timeout));
} }
@Override @Override
@@ -777,7 +742,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override @Override
public void setInputFiles(String selector, FilePayload[] files, SetInputFilesOptions options) { public void setInputFiles(String selector, FilePayload[] files, SetInputFilesOptions options) {
withLogging("Frame.setInputFiles", () -> setInputFilesImpl(selector, files, options)); setInputFilesImpl(selector, files, options);
} }
void setInputFilesImpl(String selector, FilePayload[] files, SetInputFilesOptions options) { void setInputFilesImpl(String selector, FilePayload[] files, SetInputFilesOptions options) {
@@ -788,73 +753,54 @@ public class FrameImpl extends ChannelOwner implements Frame {
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
params.add("payloads", toJsonArray(files)); params.add("payloads", toJsonArray(files));
sendMessage("setInputFiles", params); sendMessage("setInputFiles", params, timeout(options.timeout));
} }
@Override @Override
public void tap(String selector, TapOptions options) { public void tap(String selector, TapOptions options) {
withLogging("Frame.tap", () -> tapImpl(selector, options));
}
void tapImpl(String selector, TapOptions options) {
if (options == null) { if (options == null) {
options = new TapOptions(); options = new TapOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
sendMessage("tap", params); sendMessage("tap", params, timeout(options.timeout));
} }
@Override @Override
public String textContent(String selector, TextContentOptions options) { public String textContent(String selector, TextContentOptions options) {
return withLogging("Frame.textContent", () -> textContentImpl(selector, options));
}
String textContentImpl(String selector, TextContentOptions options) {
if (options == null) { if (options == null) {
options = new TextContentOptions(); options = new TextContentOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
return sendMessage("textContent", params).getAsJsonObject().get("value").getAsString(); return sendMessage("textContent", params, timeout(options.timeout)).getAsJsonObject().get("value").getAsString();
} }
@Override @Override
public String title() { public String title() {
return withLogging("Frame.title", () -> titleImpl());
}
String titleImpl() {
JsonElement json = sendMessage("title"); JsonElement json = sendMessage("title");
return json.getAsJsonObject().get("value").getAsString(); return json.getAsJsonObject().get("value").getAsString();
} }
@Override @Override
public void type(String selector, String text, TypeOptions options) { public void type(String selector, String text, TypeOptions options) {
withLogging("Frame.type", () -> typeImpl(selector, text, options));
}
void typeImpl(String selector, String text, TypeOptions options) {
if (options == null) { if (options == null) {
options = new TypeOptions(); options = new TypeOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
params.addProperty("text", text); params.addProperty("text", text);
sendMessage("type", params); sendMessage("type", params, timeout(options.timeout));
} }
@Override @Override
public void uncheck(String selector, UncheckOptions options) { public void uncheck(String selector, UncheckOptions options) {
withLogging("Frame.uncheck", () -> uncheckImpl(selector, options));
}
void uncheckImpl(String selector, UncheckOptions options) {
if (options == null) { if (options == null) {
options = new UncheckOptions(); options = new UncheckOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
sendMessage("uncheck", params); sendMessage("uncheck", params, timeout(options.timeout));
} }
@Override @Override
@@ -864,17 +810,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override @Override
public JSHandle waitForFunction(String pageFunction, Object arg, WaitForFunctionOptions options) { public JSHandle waitForFunction(String pageFunction, Object arg, WaitForFunctionOptions options) {
return withLogging("Frame.waitForFunction", () -> waitForFunctionImpl(pageFunction, arg, options));
}
JSHandle waitForFunctionImpl(String pageFunction, Object arg, WaitForFunctionOptions options) {
if (options == null) { if (options == null) {
options = new WaitForFunctionOptions(); options = new WaitForFunctionOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("expression", pageFunction); params.addProperty("expression", pageFunction);
params.add("arg", gson().toJsonTree(serializeArgument(arg))); params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("waitForFunction", params); JsonElement json = sendMessage("waitForFunction", params, timeout(options.timeout));
JsonObject element = json.getAsJsonObject().getAsJsonObject("handle"); JsonObject element = json.getAsJsonObject().getAsJsonObject("handle");
return connection.getExistingObject(element.get("guid").getAsString()); return connection.getExistingObject(element.get("guid").getAsString());
} }
@@ -1031,7 +973,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
List<Waitable<Response>> waitables = new ArrayList<>(); List<Waitable<Response>> waitables = new ArrayList<>();
if (matcher == null) { if (matcher == null) {
matcher = UrlMatcher.forOneOf(page.context().baseUrl, options.url); matcher = UrlMatcher.forOneOf(page.context().baseUrl(), options.url, this.connection.localUtils, false);
} }
logger.log("waiting for navigation " + matcher); logger.log("waiting for navigation " + matcher);
waitables.add(new WaitForNavigationHelper(matcher, options.waitUntil, logger)); waitables.add(new WaitForNavigationHelper(matcher, options.waitUntil, logger));
@@ -1043,10 +985,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override @Override
public ElementHandle waitForSelector(String selector, WaitForSelectorOptions options) { public ElementHandle waitForSelector(String selector, WaitForSelectorOptions options) {
return withLogging("Frame.waitForSelector", () -> waitForSelectorImpl(selector, options));
}
ElementHandle waitForSelectorImpl(String selector, WaitForSelectorOptions options) {
return waitForSelectorImpl(selector, options, false); return waitForSelectorImpl(selector, options, false);
} }
@@ -1057,7 +995,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
params.addProperty("omitReturnValue", omitReturnValue); params.addProperty("omitReturnValue", omitReturnValue);
JsonElement json = sendMessage("waitForSelector", params); JsonElement json = sendMessage("waitForSelector", params, timeout(options.timeout));
JsonObject element = json.getAsJsonObject().getAsJsonObject("element"); JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
if (element == null) { if (element == null) {
return null; return null;
@@ -1067,18 +1005,14 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override @Override
public void waitForTimeout(double timeout) { public void waitForTimeout(double timeout) {
withLogging("Frame.waitForTimeout", () -> waitForTimeoutImpl(timeout));
}
void waitForTimeoutImpl(double timeout) {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("timeout", timeout); params.addProperty("waitTimeout", timeout);
sendMessage("waitForTimeout", params); sendMessage("waitForTimeout", params, NO_TIMEOUT);
} }
@Override @Override
public void waitForURL(String url, WaitForURLOptions options) { public void waitForURL(String url, WaitForURLOptions options) {
waitForURL(new UrlMatcher(page.context().baseUrl, url), options); waitForURL(UrlMatcher.forGlob(page.context().baseUrl(), url, this.connection.localUtils, false), options);
} }
@Override @Override
@@ -1113,14 +1047,60 @@ public class FrameImpl extends ChannelOwner implements Frame {
int queryCount(String selector) { int queryCount(String selector) {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
JsonObject result = sendMessage("queryCount", params).getAsJsonObject(); JsonObject result = sendMessage("queryCount", params, NO_TIMEOUT).getAsJsonObject();
return result.get("value").getAsInt(); return result.get("value").getAsInt();
} }
void highlightImpl(String selector) { void dropImpl(String selector, DropPayload payload, com.microsoft.playwright.Locator.DropOptions options) {
if (options == null) {
options = new com.microsoft.playwright.Locator.DropOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
params.addProperty("strict", true);
if (payload != null) {
if (payload.files != null) {
if (payload.files instanceof Path) {
addFilePathUploadParams(new Path[] { (Path) payload.files }, params, page.context());
} else if (payload.files instanceof Path[]) {
addFilePathUploadParams((Path[]) payload.files, params, page.context());
} else if (payload.files instanceof com.microsoft.playwright.options.FilePayload) {
checkFilePayloadSize(new com.microsoft.playwright.options.FilePayload[] { (com.microsoft.playwright.options.FilePayload) payload.files });
params.add("payloads", toJsonArray(new com.microsoft.playwright.options.FilePayload[] { (com.microsoft.playwright.options.FilePayload) payload.files }));
} else if (payload.files instanceof com.microsoft.playwright.options.FilePayload[]) {
checkFilePayloadSize((com.microsoft.playwright.options.FilePayload[]) payload.files);
params.add("payloads", toJsonArray((com.microsoft.playwright.options.FilePayload[]) payload.files));
} else {
throw new com.microsoft.playwright.PlaywrightException("Unsupported files type: " + payload.files.getClass());
}
}
if (payload.data != null) {
com.google.gson.JsonArray dataArray = new com.google.gson.JsonArray();
for (java.util.Map.Entry<String, String> entry : payload.data.entrySet()) {
JsonObject e = new JsonObject();
e.addProperty("mimeType", entry.getKey());
e.addProperty("value", entry.getValue());
dataArray.add(e);
}
params.add("data", dataArray);
}
}
sendMessage("drop", params, timeout(options.timeout));
}
void highlightImpl(String selector, String style) {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
sendMessage("highlight", params); if (style != null) {
params.addProperty("style", style);
}
sendMessage("highlight", params, NO_TIMEOUT);
}
void hideHighlightImpl(String selector) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
sendMessage("hideHighlight", params, NO_TIMEOUT);
} }
protected void handleEvent(String event, JsonObject params) { protected void handleEvent(String event, JsonObject params) {
@@ -1132,6 +1112,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
if (parentFrame == null && page != null) { if (parentFrame == null && page != null) {
if (state == LOAD) { if (state == LOAD) {
page.listeners.notify(PageImpl.EventType.LOAD, page); page.listeners.notify(PageImpl.EventType.LOAD, page);
page.browserContext.notifyPageLoad(page);
} else if (state == DOMCONTENTLOADED) { } else if (state == DOMCONTENTLOADED) {
page.listeners.notify(PageImpl.EventType.DOMCONTENTLOADED, page); page.listeners.notify(PageImpl.EventType.DOMCONTENTLOADED, page);
} }
@@ -1151,4 +1132,39 @@ public class FrameImpl extends ChannelOwner implements Frame {
internalListeners.notify(InternalEventType.NAVIGATED, params); internalListeners.notify(InternalEventType.NAVIGATED, params);
} }
} }
protected double timeout(Double timeout) {
if (page != null) {
return page.timeoutSettings.timeout(timeout);
}
return new TimeoutSettings().timeout(timeout);
}
protected double navigationTimeout(Double timeout) {
if (page != null) {
return page.timeoutSettings.navigationTimeout(timeout);
}
return new TimeoutSettings().navigationTimeout(timeout);
}
FrameExpectResult expect(String expression, FrameExpectOptions options, String title) {
return withTitle(title, () -> expect(expression, options));
}
FrameExpectResult expect(String expression, FrameExpectOptions options) {
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("expression", expression);
FrameExpectResult result = new FrameExpectResult();
try {
sendMessage("expect", params, options.timeout);
result.matches = !options.isNot;
} catch (ServerErrorWithDetails e) {
FrameExpectErrorDetails details = gson().fromJson(e.errorDetails(), FrameExpectErrorDetails.class);
result.matches = options.isNot;
result.received = details.received;
result.errorMessage = details.customErrorMessage == null ? null : "Error: " + details.customErrorMessage;
result.log = e.log();
}
return result;
}
} }
@@ -136,6 +136,6 @@ class FrameLocatorImpl implements FrameLocator {
@Override @Override
public Locator owner() { public Locator owner() {
return new LocatorImpl(frame, frameSelector); return new LocatorImpl(frame, frameSelector, null);
} }
} }
@@ -16,6 +16,8 @@
package com.microsoft.playwright.impl; package com.microsoft.playwright.impl;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Request; import com.microsoft.playwright.Request;
@@ -26,6 +28,7 @@ import java.nio.file.Path;
import java.util.Base64; import java.util.Base64;
import java.util.Map; import java.util.Map;
import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT;
import static com.microsoft.playwright.impl.LoggingSupport.*; import static com.microsoft.playwright.impl.LoggingSupport.*;
import static com.microsoft.playwright.impl.Serialization.fromNameValues; import static com.microsoft.playwright.impl.Serialization.fromNameValues;
import static com.microsoft.playwright.impl.Serialization.gson; import static com.microsoft.playwright.impl.Serialization.gson;
@@ -41,7 +44,7 @@ public class HARRouter {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("file", harFile.toString()); params.addProperty("file", harFile.toString());
JsonObject json = localUtils.sendMessage("harOpen", params).getAsJsonObject(); JsonObject json = localUtils.sendMessage("harOpen", params, NO_TIMEOUT).getAsJsonObject();
if (json.has("error")) { if (json.has("error")) {
throw new PlaywrightException(json.get("error").getAsString()); throw new PlaywrightException(json.get("error").getAsString());
} }
@@ -61,7 +64,7 @@ public class HARRouter {
params.addProperty("postData", base64); params.addProperty("postData", base64);
} }
params.addProperty("isNavigationRequest", request.isNavigationRequest()); params.addProperty("isNavigationRequest", request.isNavigationRequest());
JsonObject response = localUtils.sendMessage("harLookup", params).getAsJsonObject(); JsonObject response = localUtils.sendMessage("harLookup", params, NO_TIMEOUT).getAsJsonObject();
String action = response.get("action").getAsString(); String action = response.get("action").getAsString();
if ("redirect".equals(action)) { if ("redirect".equals(action)) {
@@ -81,7 +84,7 @@ public class HARRouter {
if (status == -1) { if (status == -1) {
return; return;
} }
Map<String, String> headers = fromNameValues(response.getAsJsonArray("headers")); Map<String, String> headers = mergeSetCookieHeaders(response.getAsJsonArray("headers"));
byte[] buffer = Base64.getDecoder().decode(response.get("body").getAsString()); byte[] buffer = Base64.getDecoder().decode(response.get("body").getAsString());
route.fulfill(new Route.FulfillOptions() route.fulfill(new Route.FulfillOptions()
.setStatus(status) .setStatus(status)
@@ -104,6 +107,25 @@ public class HARRouter {
route.abort(); route.abort();
} }
private static Map<String, String> mergeSetCookieHeaders(JsonArray headersArray) {
Map<String, String> result = new java.util.LinkedHashMap<>();
for (JsonElement element : headersArray) {
JsonObject pair = element.getAsJsonObject();
String name = pair.get("name").getAsString();
String value = pair.get("value").getAsString();
if ("set-cookie".equalsIgnoreCase(name)) {
if (!result.containsKey("set-cookie")) {
result.put("set-cookie", value);
} else {
result.put("set-cookie", result.get("set-cookie") + "\n" + value);
}
} else {
result.put(name, value);
}
}
return result;
}
void dispose() { void dispose() {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("harId", harId); params.addProperty("harId", harId);
@@ -41,70 +41,58 @@ public class JSHandleImpl extends ChannelOwner implements JSHandle {
@Override @Override
public void dispose() { public void dispose() {
withLogging("JSHandle.dispose", () -> { try {
try { sendMessage("dispose");
sendMessage("dispose"); } catch (TargetClosedError e) {
} catch (TargetClosedError e) { }
}
});
} }
@Override @Override
public Object evaluate(String pageFunction, Object arg) { public Object evaluate(String pageFunction, Object arg) {
return withLogging("JSHandle.evaluate", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("expression", pageFunction);
params.addProperty("expression", pageFunction); params.addProperty("world", "main");
params.addProperty("world", "main"); params.add("arg", gson().toJsonTree(serializeArgument(arg)));
params.add("arg", gson().toJsonTree(serializeArgument(arg))); JsonElement json = sendMessage("evaluateExpression", params, NO_TIMEOUT);
JsonElement json = sendMessage("evaluateExpression", params); SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class); return deserialize(value);
return deserialize(value);
});
} }
@Override @Override
public JSHandle evaluateHandle(String pageFunction, Object arg) { public JSHandle evaluateHandle(String pageFunction, Object arg) {
return withLogging("JSHandle.evaluateHandle", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("expression", pageFunction);
params.addProperty("expression", pageFunction); params.addProperty("world", "main");
params.addProperty("world", "main"); params.add("arg", gson().toJsonTree(serializeArgument(arg)));
params.add("arg", gson().toJsonTree(serializeArgument(arg))); JsonElement json = sendMessage("evaluateExpressionHandle", params, NO_TIMEOUT);
JsonElement json = sendMessage("evaluateExpressionHandle", params); return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("handle").get("guid").getAsString());
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("handle").get("guid").getAsString());
});
} }
@Override @Override
public Map<String, JSHandle> getProperties() { public Map<String, JSHandle> getProperties() {
return withLogging("JSHandle.getProperties", () -> { JsonObject json = sendMessage("getPropertyList").getAsJsonObject();
JsonObject json = sendMessage("getPropertyList").getAsJsonObject(); Map<String, JSHandle> result = new HashMap<>();
Map<String, JSHandle> result = new HashMap<>(); for (JsonElement e : json.getAsJsonArray("properties")) {
for (JsonElement e : json.getAsJsonArray("properties")) { JsonObject item = e.getAsJsonObject();
JsonObject item = e.getAsJsonObject(); JSHandle value = connection.getExistingObject(item.getAsJsonObject("value").get("guid").getAsString());
JSHandle value = connection.getExistingObject(item.getAsJsonObject("value").get("guid").getAsString()); result.put(item.get("name").getAsString(), value);
result.put(item.get("name").getAsString(), value); }
} return result;
return result;
});
} }
@Override @Override
public JSHandle getProperty(String propertyName) { public JSHandle getProperty(String propertyName) {
return withLogging("JSHandle.getProperty", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("name", propertyName);
params.addProperty("name", propertyName); JsonObject json = sendMessage("getProperty", params, NO_TIMEOUT).getAsJsonObject();
JsonObject json = sendMessage("getProperty", params).getAsJsonObject(); return connection.getExistingObject(json.getAsJsonObject("handle").get("guid").getAsString());
return connection.getExistingObject(json.getAsJsonObject("handle").get("guid").getAsString());
});
} }
@Override @Override
public Object jsonValue() { public Object jsonValue() {
return withLogging("JSHandle.jsonValue", () -> { JsonObject json = sendMessage("jsonValue").getAsJsonObject();
JsonObject json = sendMessage("jsonValue").getAsJsonObject(); SerializedValue value = gson().fromJson(json.get("value"), SerializedValue.class);
SerializedValue value = gson().fromJson(json.get("value"), SerializedValue.class); return deserialize(value);
return deserialize(value);
});
} }
@Override @Override
@@ -33,6 +33,7 @@ class JsonPipe extends ChannelOwner implements Transport {
private ListenerCollection<EventType> listeners = new ListenerCollection<>(); private ListenerCollection<EventType> listeners = new ListenerCollection<>();
private enum EventType { CLOSE } private enum EventType { CLOSE }
private boolean isClosed; private boolean isClosed;
private String closeReason = "Browser has been closed";
JsonPipe(ChannelOwner parent, String type, String guid, JsonObject initializer) { JsonPipe(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
@@ -43,7 +44,7 @@ class JsonPipe extends ChannelOwner implements Transport {
checkIfClosed(); checkIfClosed();
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.add("message", message); params.add("message", message);
sendMessage("send", params); sendMessage("send", params, NO_TIMEOUT);
} }
@Override @Override
@@ -97,13 +98,19 @@ class JsonPipe extends ChannelOwner implements Transport {
incoming.add(params.get("message").getAsJsonObject()); incoming.add(params.get("message").getAsJsonObject());
} else if ("closed".equals(event)) { } else if ("closed".equals(event)) {
isClosed = true; isClosed = true;
if (params.has("reason")) {
String reason = params.get("reason").getAsString();
if (reason.trim().length() > 0) {
closeReason = reason;
}
}
listeners.notify(EventType.CLOSE, this); listeners.notify(EventType.CLOSE, this);
} }
} }
private void checkIfClosed() { private void checkIfClosed() {
if (isClosed) { if (isClosed) {
throw new PlaywrightException("Browser has been closed"); throw new PlaywrightException(closeReason);
} }
} }
} }
@@ -19,6 +19,7 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.microsoft.playwright.Keyboard; import com.microsoft.playwright.Keyboard;
import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT;
import static com.microsoft.playwright.impl.Serialization.gson; import static com.microsoft.playwright.impl.Serialization.gson;
class KeyboardImpl implements Keyboard { class KeyboardImpl implements Keyboard {
@@ -30,25 +31,21 @@ class KeyboardImpl implements Keyboard {
@Override @Override
public void down(String key) { public void down(String key) {
page.withLogging("Keyboard.down", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("key", key);
params.addProperty("key", key); page.sendMessage("keyboardDown", params, NO_TIMEOUT);
page.sendMessage("keyboardDown", params);
});
} }
@Override @Override
public void insertText(String text) { public void insertText(String text) {
page.withLogging("Keyboard.insertText", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("text", text);
params.addProperty("text", text); page.sendMessage("keyboardInsertText", params, NO_TIMEOUT);
page.sendMessage("keyboardInsertText", params);
});
} }
@Override @Override
public void press(String key, PressOptions options) { public void press(String key, PressOptions options) {
page.withLogging("Keyboard.press", () -> pressImpl(key, options)); pressImpl(key, options);
} }
private void pressImpl(String key, PressOptions options) { private void pressImpl(String key, PressOptions options) {
@@ -57,12 +54,12 @@ class KeyboardImpl implements Keyboard {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("key", key); params.addProperty("key", key);
page.sendMessage("keyboardPress", params); page.sendMessage("keyboardPress", params, NO_TIMEOUT);
} }
@Override @Override
public void type(String text, TypeOptions options) { public void type(String text, TypeOptions options) {
page.withLogging("Keyboard.type", () -> typeImpl(text, options)); typeImpl(text, options);
} }
private void typeImpl(String text, TypeOptions options) { private void typeImpl(String text, TypeOptions options) {
@@ -71,15 +68,13 @@ class KeyboardImpl implements Keyboard {
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("text", text); params.addProperty("text", text);
page.sendMessage("keyboardType", params); page.sendMessage("keyboardType", params, NO_TIMEOUT);
} }
@Override @Override
public void up(String key) { public void up(String key) {
page.withLogging("Keyboard.up", () -> { JsonObject params = new JsonObject();
JsonObject params = new JsonObject(); params.addProperty("key", key);
params.addProperty("key", key); page.sendMessage("keyboardUp", params, NO_TIMEOUT);
page.sendMessage("keyboardUp", params);
});
} }
} }
@@ -17,6 +17,7 @@
package com.microsoft.playwright.impl; package com.microsoft.playwright.impl;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import java.util.*; import java.util.*;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -46,6 +47,9 @@ class ListenerCollection <EventType> {
} }
void add(EventType type, Consumer<?> listener) { void add(EventType type, Consumer<?> listener) {
if (listener == null) {
throw new PlaywrightException("Can't add a null listener");
}
List<Consumer<?>> list = listeners.get(type); List<Consumer<?>> list = listeners.get(type);
if (list == null) { if (list == null) {
list = new ArrayList<>(); list = new ArrayList<>();
@@ -21,10 +21,11 @@ import com.google.gson.JsonObject;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.gson; import static com.microsoft.playwright.impl.Serialization.gson;
class LocalUtils extends ChannelOwner { public class LocalUtils extends ChannelOwner {
LocalUtils(ChannelOwner parent, String type, String guid, JsonObject initializer) { LocalUtils(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
} }
@@ -33,20 +34,27 @@ class LocalUtils extends ChannelOwner {
return initializer.getAsJsonArray("deviceDescriptors"); return initializer.getAsJsonArray("deviceDescriptors");
} }
void zip(Path zipFile, JsonArray entries, String stacksId, boolean appendMode, boolean includeSources) { void zip(Path zipFile, JsonArray entries, String stacksId, boolean appendMode, boolean includeSources, List<String> additionalSources) {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("zipFile", zipFile.toString()); params.addProperty("zipFile", zipFile.toString());
params.add("entries", entries); params.add("entries", entries);
params.addProperty("mode", appendMode ? "append" : "write"); params.addProperty("mode", appendMode ? "append" : "write");
params.addProperty("stacksId", stacksId); params.addProperty("stacksId", stacksId);
params.addProperty("includeSources", includeSources); params.addProperty("includeSources", includeSources);
sendMessage("zip", params); if (!additionalSources.isEmpty()) {
JsonArray sourcesArray = new JsonArray();
for (String source : additionalSources) {
sourcesArray.add(source);
}
params.add("additionalSources", sourcesArray);
}
sendMessage("zip", params, NO_TIMEOUT);
} }
void traceDiscarded(String stacksId) { void traceDiscarded(String stacksId) {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("stacksId", stacksId); params.addProperty("stacksId", stacksId);
sendMessage("traceDiscarded", params); sendMessage("traceDiscarded", params, NO_TIMEOUT);
} }
String tracingStarted(String tracesDir, String traceName) { String tracingStarted(String tracesDir, String traceName) {
@@ -55,7 +63,19 @@ class LocalUtils extends ChannelOwner {
params.addProperty("tracesDir", ""); params.addProperty("tracesDir", "");
} }
params.addProperty("traceName", traceName); params.addProperty("traceName", traceName);
JsonObject json = connection.localUtils().sendMessage("tracingStarted", params).getAsJsonObject(); JsonObject json = connection.localUtils().sendMessage("tracingStarted", params, NO_TIMEOUT).getAsJsonObject();
return json.get("stacksId").getAsString(); return json.get("stacksId").getAsString();
} }
public Pattern globToRegex(String glob, String baseURL, boolean webSocketUrl) {
JsonObject params = new JsonObject();
params.addProperty("glob", glob);
if (baseURL != null) {
params.addProperty("baseURL", baseURL);
}
params.addProperty("webSocketUrl", webSocketUrl);
JsonObject json = connection.localUtils().sendMessage("globToRegex", params, NO_TIMEOUT).getAsJsonObject();
String regex = json.get("regex").getAsString();
return Pattern.compile(regex);
}
} }
@@ -18,22 +18,51 @@ package com.microsoft.playwright.impl;
import com.microsoft.playwright.Locator; import com.microsoft.playwright.Locator;
import com.microsoft.playwright.assertions.LocatorAssertions; import com.microsoft.playwright.assertions.LocatorAssertions;
import com.microsoft.playwright.options.AriaRole;
import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.serializeArgument; import static com.microsoft.playwright.impl.Serialization.serializeArgument;
import static com.microsoft.playwright.impl.Utils.convertType; import static com.microsoft.playwright.impl.Utils.convertType;
public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAssertions { public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAssertions {
LocatorImpl actualLocator;
public LocatorAssertionsImpl(Locator locator) { public LocatorAssertionsImpl(Locator locator) {
this(locator, false); this(locator, false);
} }
private LocatorAssertionsImpl(Locator locator, boolean isNot) { private LocatorAssertionsImpl(Locator locator, boolean isNot) {
super((LocatorImpl) locator, isNot); super(isNot);
this.actualLocator = (LocatorImpl) locator;
}
@Override
FrameExpectResult doExpect(String expression, FrameExpectOptions expectOptions, String title) {
return actualLocator.expect(expression, expectOptions, title);
}
@Override
public void containsClass(String classname, ContainsClassOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = classname;
expectImpl("to.contain.class", expected, classname, "Locator expected to contain class", convertType(options, FrameExpectOptions.class), "Assert \"containsClass\"");
}
@Override
public void containsClass(List<String> classnames, ContainsClassOptions options) {
List<ExpectedTextValue> list = new ArrayList<>();
for (String text : classnames) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
list.add(expected);
}
expectImpl("to.contain.class.array", list, classnames, "Locator expected to contain classes", convertType(options, FrameExpectOptions.class), "Assert \"containsClass\"");
} }
@Override @Override
@@ -43,7 +72,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expected.ignoreCase = shouldIgnoreCase(options); expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = true; expected.matchSubstring = true;
expected.normalizeWhiteSpace = true; expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, text, "Locator expected to contain text", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.text", expected, text, "Locator expected to contain text", convertType(options, FrameExpectOptions.class), "Assert \"containsText\"");
} }
@Override @Override
@@ -52,7 +81,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expected.ignoreCase = shouldIgnoreCase(options); expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = true; expected.matchSubstring = true;
expected.normalizeWhiteSpace = true; expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, pattern, "Locator expected to contain regex", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.text", expected, pattern, "Locator expected to contain regex", convertType(options, FrameExpectOptions.class), "Assert \"containsText\"");
} }
@Override @Override
@@ -66,7 +95,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expected.normalizeWhiteSpace = true; expected.normalizeWhiteSpace = true;
list.add(expected); list.add(expected);
} }
expectImpl("to.contain.text.array", list, strings, "Locator expected to contain text", convertType(options, FrameExpectOptions.class)); expectImpl("to.contain.text.array", list, strings, "Locator expected to contain text", convertType(options, FrameExpectOptions.class), "Assert \"containsText\"");
} }
@Override @Override
@@ -79,7 +108,58 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expected.normalizeWhiteSpace = true; expected.normalizeWhiteSpace = true;
list.add(expected); list.add(expected);
} }
expectImpl("to.contain.text.array", list, patterns, "Locator expected to contain text", convertType(options, FrameExpectOptions.class)); expectImpl("to.contain.text.array", list, patterns, "Locator expected to contain text", convertType(options, FrameExpectOptions.class), "Assert \"containsText\"");
}
@Override
public void hasAccessibleDescription(String description, HasAccessibleDescriptionOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = description;
expected.ignoreCase = shouldIgnoreCase(options);
expected.normalizeWhiteSpace = true;
expectImpl("to.have.accessible.description", expected, description, "Locator expected to have accessible description", convertType(options, FrameExpectOptions.class), "Assert \"hasAccessibleDescription\"");
}
@Override
public void hasAccessibleDescription(Pattern pattern, HasAccessibleDescriptionOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
expected.normalizeWhiteSpace = true;
expectImpl("to.have.accessible.description", expected, pattern, "Locator expected to have accessible description", convertType(options, FrameExpectOptions.class), "Assert \"hasAccessibleDescription\"");
}
@Override
public void hasAccessibleErrorMessage(String errorMessage, HasAccessibleErrorMessageOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = errorMessage;
expected.ignoreCase = shouldIgnoreCase(options);
expected.normalizeWhiteSpace = true;
expectImpl("to.have.accessible.error.message", expected, errorMessage, "Locator expected to have accessible error message", convertType(options, FrameExpectOptions.class), "Assert \"hasAccessibleErrorMessage\"");
}
@Override
public void hasAccessibleErrorMessage(Pattern pattern, HasAccessibleErrorMessageOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
expected.normalizeWhiteSpace = true;
expectImpl("to.have.accessible.error.message", expected, pattern, "Locator expected to have accessible error message", convertType(options, FrameExpectOptions.class), "Assert \"hasAccessibleErrorMessage\"");
}
@Override
public void hasAccessibleName(String name, HasAccessibleNameOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = name;
expected.ignoreCase = shouldIgnoreCase(options);
expected.normalizeWhiteSpace = true;
expectImpl("to.have.accessible.name", expected, name, "Locator expected to have accessible name", convertType(options, FrameExpectOptions.class), "Assert \"hasAccessibleName\"");
}
@Override
public void hasAccessibleName(Pattern pattern, HasAccessibleNameOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
expected.normalizeWhiteSpace = true;
expectImpl("to.have.accessible.name", expected, pattern, "Locator expected to have accessible name", convertType(options, FrameExpectOptions.class), "Assert \"hasAccessibleName\"");
} }
@Override @Override
@@ -107,20 +187,20 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
if (expectedValue instanceof Pattern) { if (expectedValue instanceof Pattern) {
message += " matching regex"; message += " matching regex";
} }
expectImpl("to.have.attribute.value", expectedText, expectedValue, message, commonOptions); expectImpl("to.have.attribute.value", expectedText, expectedValue, message, commonOptions, "Assert \"hasAttribute\"");
} }
@Override @Override
public void hasClass(String text, HasClassOptions options) { public void hasClass(String text, HasClassOptions options) {
ExpectedTextValue expected = new ExpectedTextValue(); ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text; expected.string = text;
expectImpl("to.have.class", expected, text, "Locator expected to have class", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.class", expected, text, "Locator expected to have class", convertType(options, FrameExpectOptions.class), "Assert \"hasClass\"");
} }
@Override @Override
public void hasClass(Pattern pattern, HasClassOptions options) { public void hasClass(Pattern pattern, HasClassOptions options) {
ExpectedTextValue expected = expectedRegex(pattern); ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.class", expected, pattern, "Locator expected to have class matching regex", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.class", expected, pattern, "Locator expected to have class matching regex", convertType(options, FrameExpectOptions.class), "Assert \"hasClass\"");
} }
@Override @Override
@@ -131,7 +211,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expected.string = text; expected.string = text;
list.add(expected); list.add(expected);
} }
expectImpl("to.have.class.array", list, strings, "Locator expected to have class", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.class.array", list, strings, "Locator expected to have class", convertType(options, FrameExpectOptions.class), "Assert \"hasClass\"");
} }
@Override @Override
@@ -141,7 +221,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
ExpectedTextValue expected = expectedRegex(pattern); ExpectedTextValue expected = expectedRegex(pattern);
list.add(expected); list.add(expected);
} }
expectImpl("to.have.class.array", list, patterns, "Locator expected to have class matching regex", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.class.array", list, patterns, "Locator expected to have class matching regex", convertType(options, FrameExpectOptions.class), "Assert \"hasClass\"");
} }
@Override @Override
@@ -152,7 +232,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
FrameExpectOptions commonOptions = convertType(options, FrameExpectOptions.class); FrameExpectOptions commonOptions = convertType(options, FrameExpectOptions.class);
commonOptions.expectedNumber = (double) count; commonOptions.expectedNumber = (double) count;
List<ExpectedTextValue> expectedText = null; List<ExpectedTextValue> expectedText = null;
expectImpl("to.have.count", expectedText, count, "Locator expected to have count", commonOptions); expectImpl("to.have.count", expectedText, count, "Locator expected to have count", commonOptions, "Assert \"hasCount\"");
} }
@Override @Override
@@ -178,20 +258,20 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
if (expectedValue instanceof Pattern) { if (expectedValue instanceof Pattern) {
message += " matching regex"; message += " matching regex";
} }
expectImpl("to.have.css", expectedText, expectedValue, message, commonOptions); expectImpl("to.have.css", expectedText, expectedValue, message, commonOptions, "Assert \"hasCSS\"");
} }
@Override @Override
public void hasId(String id, HasIdOptions options) { public void hasId(String id, HasIdOptions options) {
ExpectedTextValue expected = new ExpectedTextValue(); ExpectedTextValue expected = new ExpectedTextValue();
expected.string = id; expected.string = id;
expectImpl("to.have.id", expected, id, "Locator expected to have ID", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.id", expected, id, "Locator expected to have ID", convertType(options, FrameExpectOptions.class), "Assert \"hasId\"");
} }
@Override @Override
public void hasId(Pattern pattern, HasIdOptions options) { public void hasId(Pattern pattern, HasIdOptions options) {
ExpectedTextValue expected = expectedRegex(pattern); ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.id", expected, pattern, "Locator expected to have ID matching regex", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.id", expected, pattern, "Locator expected to have ID matching regex", convertType(options, FrameExpectOptions.class), "Assert \"hasId\"");
} }
@Override @Override
@@ -203,7 +283,14 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
commonOptions.expressionArg = name; commonOptions.expressionArg = name;
commonOptions.expectedValue = serializeArgument(value); commonOptions.expectedValue = serializeArgument(value);
List<ExpectedTextValue> list = null; List<ExpectedTextValue> list = null;
expectImpl("to.have.property", list, value, "Locator expected to have JavaScript property '" + name + "'", commonOptions); expectImpl("to.have.property", list, value, "Locator expected to have JavaScript property '" + name + "'", commonOptions, "Assert \"hasJSProperty\"");
}
@Override
public void hasRole(AriaRole role, HasRoleOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = role.toString().toLowerCase();
expectImpl("to.have.role", expected, expected.string, "Locator expected to have role", convertType(options, FrameExpectOptions.class), "Assert \"hasRole\"");
} }
@Override @Override
@@ -213,7 +300,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expected.ignoreCase = shouldIgnoreCase(options); expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = false; expected.matchSubstring = false;
expected.normalizeWhiteSpace = true; expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, text, "Locator expected to have text", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.text", expected, text, "Locator expected to have text", convertType(options, FrameExpectOptions.class), "Assert \"hasText\"");
} }
@Override @Override
@@ -223,7 +310,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
// Just match substring, same as containsText. // Just match substring, same as containsText.
expected.matchSubstring = true; expected.matchSubstring = true;
expected.normalizeWhiteSpace = true; expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, pattern, "Locator expected to have text matching regex", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.text", expected, pattern, "Locator expected to have text matching regex", convertType(options, FrameExpectOptions.class), "Assert \"hasText\"");
} }
@Override @Override
@@ -237,7 +324,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expected.normalizeWhiteSpace = true; expected.normalizeWhiteSpace = true;
list.add(expected); list.add(expected);
} }
expectImpl("to.have.text.array", list, strings, "Locator expected to have text", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.text.array", list, strings, "Locator expected to have text", convertType(options, FrameExpectOptions.class), "Assert \"hasText\"");
} }
@Override @Override
@@ -250,20 +337,20 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expected.normalizeWhiteSpace = true; expected.normalizeWhiteSpace = true;
list.add(expected); list.add(expected);
} }
expectImpl("to.have.text.array", list, patterns, "Locator expected to have text matching regex", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.text.array", list, patterns, "Locator expected to have text matching regex", convertType(options, FrameExpectOptions.class), "Assert \"hasText\"");
} }
@Override @Override
public void hasValue(String value, HasValueOptions options) { public void hasValue(String value, HasValueOptions options) {
ExpectedTextValue expected = new ExpectedTextValue(); ExpectedTextValue expected = new ExpectedTextValue();
expected.string = value; expected.string = value;
expectImpl("to.have.value", expected, value, "Locator expected to have value", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.value", expected, value, "Locator expected to have value", convertType(options, FrameExpectOptions.class), "Assert \"hasValue\"");
} }
@Override @Override
public void hasValue(Pattern pattern, HasValueOptions options) { public void hasValue(Pattern pattern, HasValueOptions options) {
ExpectedTextValue expected = expectedRegex(pattern); ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.value", expected, pattern, "Locator expected to have value matching regex", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.value", expected, pattern, "Locator expected to have value matching regex", convertType(options, FrameExpectOptions.class), "Assert \"hasValue\"");
} }
@Override @Override
@@ -274,7 +361,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expected.string = text; expected.string = text;
list.add(expected); list.add(expected);
} }
expectImpl("to.have.values", list, values, "Locator expected to have values", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.values", list, values, "Locator expected to have values", convertType(options, FrameExpectOptions.class), "Assert \"hasValues\"");
} }
@Override @Override
@@ -285,20 +372,50 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expected.matchSubstring = true; expected.matchSubstring = true;
list.add(expected); list.add(expected);
} }
expectImpl("to.have.values", list, patterns, "Locator expected to have values matching regex", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.values", list, patterns, "Locator expected to have values matching regex", convertType(options, FrameExpectOptions.class), "Assert \"hasValues\"");
}
@Override
public void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions snapshotOptions) {
if (snapshotOptions == null) {
snapshotOptions = new MatchesAriaSnapshotOptions();
}
FrameExpectOptions options = convertType(snapshotOptions, FrameExpectOptions.class);
options.expectedValue = serializeArgument(expected);
expectImpl("to.match.aria", options, expected,"Locator expected to match Aria snapshot", "Assert \"matchesAriaSnapshot\"");
} }
@Override @Override
public void isChecked(IsCheckedOptions options) { public void isChecked(IsCheckedOptions options) {
boolean unchecked = options != null && options.checked != null && !options.checked; if (options == null) {
String expression = unchecked ? "to.be.unchecked" : "to.be.checked"; options = new IsCheckedOptions();
String message = "Locator expected to be " + (unchecked ? "un" : "") + "checked"; }
expectTrue(expression, message, convertType(options, FrameExpectOptions.class));
Map<String, Boolean> expectedValue = new HashMap<>();
if (options.indeterminate != null) {
expectedValue.put("indeterminate", options.indeterminate);
}
if (options.checked != null) {
expectedValue.put("checked", options.checked);
}
String expected;
if (options.indeterminate != null && options.indeterminate) {
expected = "indeterminate";
} else {
boolean unchecked = options.checked != null && !options.checked;
expected = unchecked ? "unchecked" : "checked";
}
String message = "Locator expected to be";
FrameExpectOptions expectOptions = convertType(options, FrameExpectOptions.class);
expectOptions.expectedValue = serializeArgument(expectedValue);
expectImpl("to.be.checked", expectOptions, expected, message, "Assert \"isChecked\"");
} }
@Override @Override
public void isDisabled(IsDisabledOptions options) { public void isDisabled(IsDisabledOptions options) {
expectTrue("to.be.disabled", "Locator expected to be disabled", convertType(options, FrameExpectOptions.class)); expectTrue("to.be.disabled", "Locator expected to be disabled", convertType(options, FrameExpectOptions.class), "Assert \"isDisabled\"");
} }
@Override @Override
@@ -306,12 +423,12 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class); FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class);
boolean editable = options == null || options.editable == null || options.editable == true; boolean editable = options == null || options.editable == null || options.editable == true;
String message = "Locator expected to be " + (editable ? "editable" : "readonly"); String message = "Locator expected to be " + (editable ? "editable" : "readonly");
expectTrue(editable ? "to.be.editable" : "to.be.readonly", message, frameOptions); expectTrue(editable ? "to.be.editable" : "to.be.readonly", message, frameOptions, "Assert \"isEditable\"");
} }
@Override @Override
public void isEmpty(IsEmptyOptions options) { public void isEmpty(IsEmptyOptions options) {
expectTrue("to.be.empty", "Locator expected to be empty", convertType(options, FrameExpectOptions.class)); expectTrue("to.be.empty", "Locator expected to be empty", convertType(options, FrameExpectOptions.class), "Assert \"isEmpty\"");
} }
@Override @Override
@@ -319,17 +436,17 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class); FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class);
boolean enabled = options == null || options.enabled == null || options.enabled == true; boolean enabled = options == null || options.enabled == null || options.enabled == true;
String message = "Locator expected to be " + (enabled ? "enabled" : "disabled"); String message = "Locator expected to be " + (enabled ? "enabled" : "disabled");
expectTrue(enabled ? "to.be.enabled" : "to.be.disabled", message, frameOptions); expectTrue(enabled ? "to.be.enabled" : "to.be.disabled", message, frameOptions, "Assert \"isEnabled\"");
} }
@Override @Override
public void isFocused(IsFocusedOptions options) { public void isFocused(IsFocusedOptions options) {
expectTrue("to.be.focused", "Locator expected to be focused", convertType(options, FrameExpectOptions.class)); expectTrue("to.be.focused", "Locator expected to be focused", convertType(options, FrameExpectOptions.class), "Assert \"isFocused\"");
} }
@Override @Override
public void isHidden(IsHiddenOptions options) { public void isHidden(IsHiddenOptions options) {
expectTrue("to.be.hidden", "Locator expected to be hidden", convertType(options, FrameExpectOptions.class)); expectTrue("to.be.hidden", "Locator expected to be hidden", convertType(options, FrameExpectOptions.class), "Assert \"isHidden\"");
} }
@Override @Override
@@ -338,7 +455,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
if (options != null && options.ratio != null) { if (options != null && options.ratio != null) {
expectOptions.expectedNumber = options.ratio; expectOptions.expectedNumber = options.ratio;
} }
expectTrue("to.be.in.viewport", "Locator expected to be in viewport", expectOptions); expectTrue("to.be.in.viewport", "Locator expected to be in viewport", expectOptions, "Assert \"isInViewport\"");
} }
@Override @Override
@@ -346,12 +463,12 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class); FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class);
boolean visible = options == null || options.visible == null || options.visible == true; boolean visible = options == null || options.visible == null || options.visible == true;
String message = "Locator expected to be " + (visible ? "visible" : "hidden"); String message = "Locator expected to be " + (visible ? "visible" : "hidden");
expectTrue(visible ? "to.be.visible" : "to.be.hidden", message, frameOptions); expectTrue(visible ? "to.be.visible" : "to.be.hidden", message, frameOptions, "Assert \"isVisible\"");
} }
private void expectTrue(String expression, String message, FrameExpectOptions options) { private void expectTrue(String expression, String message, FrameExpectOptions options, String title) {
List<ExpectedTextValue> expectedText = null; List<ExpectedTextValue> expectedText = null;
expectImpl(expression, expectedText, null, message, options); expectImpl(expression, expectedText, null, message, options, title);
} }
@Override @Override
@@ -364,19 +481,6 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class); FrameExpectOptions frameOptions = convertType(options, FrameExpectOptions.class);
boolean attached = options == null || options.attached == null || options.attached == true; boolean attached = options == null || options.attached == null || options.attached == true;
String message = "Locator expected to be " + (attached ? "attached" : "detached"); String message = "Locator expected to be " + (attached ? "attached" : "detached");
expectTrue(attached ? "to.be.attached" : "to.be.detached", message, frameOptions); expectTrue(attached ? "to.be.attached" : "to.be.detached", message, frameOptions, "Assert \"isAttached\"");
}
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;
}
} }
} }
@@ -16,34 +16,30 @@
package com.microsoft.playwright.impl; package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.microsoft.playwright.*; import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*; import com.microsoft.playwright.options.*;
import java.lang.reflect.Field;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.LocatorUtils.*; import static com.microsoft.playwright.impl.LocatorUtils.*;
import static com.microsoft.playwright.impl.Serialization.gson; import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.convertType; import static com.microsoft.playwright.impl.Utils.convertType;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
class LocatorImpl implements Locator { class LocatorImpl implements Locator {
final FrameImpl frame; final FrameImpl frame;
final String selector; final String selector;
LocatorImpl(FrameImpl frame, String frameSelector) { LocatorImpl(FrameImpl frame, String selector, LocatorOptions options) {
this(frame, frameSelector, null); this(frame, selector, options, null);
} }
public LocatorImpl(FrameImpl frame, String selector, LocatorOptions options) { private LocatorImpl(FrameImpl frame, String selector, LocatorOptions options, Boolean visible) {
this.frame = frame; this.frame = frame;
if (options != null) { if (options != null) {
if (options.hasText != null) { if (options.hasText != null) {
@@ -65,30 +61,31 @@ class LocatorImpl implements Locator {
selector += " >> internal:has-not=" + gson().toJson(locator.selector); selector += " >> internal:has-not=" + gson().toJson(locator.selector);
} }
} }
if (visible != null) {
selector += " >> visible=" + visible;
}
this.selector = selector; this.selector = selector;
} }
private static String escapeWithQuotes(String text) { private <R, O> R withElement(BiFunction<ElementHandle, O, R> callback, O options, String title) {
return gson().toJson(text); return frame.withTitle(title, () -> {
} ElementHandleOptions handleOptions = convertType(options, ElementHandleOptions.class);
// TODO: support deadline based timeout
private <R, O> R withElement(BiFunction<ElementHandle, O, R> callback, O options) { // Double timeout = null;
ElementHandleOptions handleOptions = convertType(options, ElementHandleOptions.class); // if (handleOptions != null) {
// TODO: support deadline based timeout // timeout = handleOptions.timeout;
// Double timeout = null; // }
// if (handleOptions != null) { // timeout = frame.page.timeoutSettings.timeout(timeout);
// timeout = handleOptions.timeout; // long deadline = System.nanoTime() + (long) timeout.doubleValue() * 1_000_000;
// } ElementHandle handle = elementHandle(handleOptions);
// timeout = frame.page.timeoutSettings.timeout(timeout); try {
// long deadline = System.nanoTime() + (long) timeout.doubleValue() * 1_000_000; return callback.apply(handle, options);
ElementHandle handle = elementHandle(handleOptions); } finally {
try { if (handle != null) {
return callback.apply(handle, options); handle.dispose();
} finally { }
if (handle != null) {
handle.dispose();
} }
} });
} }
@Override @Override
@@ -111,6 +108,14 @@ class LocatorImpl implements Locator {
return (List<String>) frame.evalOnSelectorAll(selector, "ee => ee.map(e => e.textContent || '')"); return (List<String>) frame.evalOnSelectorAll(selector, "ee => ee.map(e => e.textContent || '')");
} }
@Override
public Locator normalize() {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
JsonObject result = frame.sendMessage("resolveSelector", params, ChannelOwner.NO_TIMEOUT).getAsJsonObject();
return new LocatorImpl(frame, result.get("resolvedSelector").getAsString(), null);
}
@Override @Override
public Locator and(Locator locator) { public Locator and(Locator locator) {
LocatorImpl other = (LocatorImpl) locator; LocatorImpl other = (LocatorImpl) locator;
@@ -120,23 +125,30 @@ class LocatorImpl implements Locator {
} }
@Override @Override
public void blur(BlurOptions options) { public String ariaSnapshot(AriaSnapshotOptions options) {
frame.withLogging("Locator.blur", () -> blurImpl(options)); if (options == null) {
options = new AriaSnapshotOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
JsonObject result = frame.sendMessage("ariaSnapshot", params, frame.timeout(options.timeout)).getAsJsonObject();
return result.get("snapshot").getAsString();
} }
private void blurImpl(BlurOptions options) { @Override
public void blur(BlurOptions options) {
if (options == null) { if (options == null) {
options = new BlurOptions(); options = new BlurOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
params.addProperty("strict", true); params.addProperty("strict", true);
frame.sendMessage("blur", params); frame.sendMessage("blur", params, frame.timeout(options.timeout));
} }
@Override @Override
public BoundingBox boundingBox(BoundingBoxOptions options) { public BoundingBox boundingBox(BoundingBoxOptions options) {
return withElement((h, o) -> h.boundingBox(), options); return withElement((h, o) -> h.boundingBox(), options, "Bounding Box");
} }
@Override @Override
@@ -149,7 +161,7 @@ class LocatorImpl implements Locator {
@Override @Override
public void clear(ClearOptions options) { public void clear(ClearOptions options) {
fill("", convertType(options, FillOptions.class)); frame.withTitle("Clear", () -> fill("", convertType(options, FillOptions.class)));
} }
@Override @Override
@@ -157,7 +169,7 @@ class LocatorImpl implements Locator {
if (options == null) { if (options == null) {
options = new ClickOptions(); options = new ClickOptions();
} }
frame.click(selector, convertType(options, Frame.ClickOptions.class).setStrict(true)); frame.clickImpl(selector, convertType(options, Frame.ClickOptions.class).setStrict(true), options.steps);
} }
@Override @Override
@@ -165,12 +177,38 @@ class LocatorImpl implements Locator {
return frame.queryCount(selector); return frame.queryCount(selector);
} }
@Override
public Locator describe(String description) {
return locator(describeSelector(description));
}
@Override
public String description() {
// Match internal:describe= at the end of the selector with a JSON string
// Pattern matches: >> internal:describe="..." where ... is a JSON-encoded string
Pattern pattern = Pattern.compile(" >> internal:describe=(\"(?:[^\"\\\\]|\\\\.)*\")$");
Matcher matcher = pattern.matcher(selector);
if (matcher.find()) {
String jsonString = matcher.group(1);
try {
// Deserialize the JSON string
return gson().fromJson(jsonString, String.class);
} catch (Exception e) {
// If we can't parse it, return null
return null;
}
}
return null;
}
@Override @Override
public void dblclick(DblclickOptions options) { public void dblclick(DblclickOptions options) {
if (options == null) { if (options == null) {
options = new DblclickOptions(); options = new DblclickOptions();
} }
frame.dblclick(selector, convertType(options, Frame.DblclickOptions.class).setStrict(true)); frame.dblclickImpl(selector, convertType(options, Frame.DblclickOptions.class).setStrict(true), options.steps);
} }
@Override @Override
@@ -188,7 +226,7 @@ class LocatorImpl implements Locator {
} }
Frame.DragAndDropOptions frameOptions = convertType(options, Frame.DragAndDropOptions.class); Frame.DragAndDropOptions frameOptions = convertType(options, Frame.DragAndDropOptions.class);
frameOptions.setStrict(true); frameOptions.setStrict(true);
frame.dragAndDrop(selector, ((LocatorImpl) target).selector, frameOptions); frame.dragAndDropImpl(selector, ((LocatorImpl) target).selector, frameOptions, options.steps);
} }
@Override @Override
@@ -214,7 +252,7 @@ class LocatorImpl implements Locator {
@Override @Override
public Object evaluate(String expression, Object arg, EvaluateOptions options) { public Object evaluate(String expression, Object arg, EvaluateOptions options) {
return withElement((h, o) -> h.evaluate(expression, arg), options); return withElement((h, o) -> h.evaluate(expression, arg), options, "Evaluate");
} }
@Override @Override
@@ -224,7 +262,7 @@ class LocatorImpl implements Locator {
@Override @Override
public JSHandle evaluateHandle(String expression, Object arg, EvaluateHandleOptions options) { public JSHandle evaluateHandle(String expression, Object arg, EvaluateHandleOptions options) {
return withElement((h, o) -> h.evaluateHandle(expression, arg), options); return withElement((h, o) -> h.evaluateHandle(expression, arg), options, "Evaluate");
} }
@Override @Override
@@ -237,7 +275,8 @@ class LocatorImpl implements Locator {
@Override @Override
public Locator filter(FilterOptions options) { public Locator filter(FilterOptions options) {
return new LocatorImpl(frame, selector, convertType(options,LocatorOptions.class)); Boolean visible = (options == null) ? null : options.visible;
return new LocatorImpl(frame, selector, convertType(options, LocatorOptions.class), visible);
} }
@Override @Override
@@ -332,8 +371,20 @@ class LocatorImpl implements Locator {
} }
@Override @Override
public void highlight() { public void drop(DropPayload payload, DropOptions options) {
frame.highlightImpl(selector); frame.dropImpl(selector, payload, options);
}
@Override
public AutoCloseable highlight(HighlightOptions options) {
String style = options == null ? null : options.style;
frame.highlightImpl(selector, style);
return new DisposableStub(this::hideHighlight);
}
@Override
public void hideHighlight() {
frame.hideHighlightImpl(selector);
} }
@Override @Override
@@ -468,7 +519,7 @@ class LocatorImpl implements Locator {
@Override @Override
public byte[] screenshot(ScreenshotOptions options) { public byte[] screenshot(ScreenshotOptions options) {
return withElement((h, o) -> h.screenshot(o), convertType(options, ElementHandle.ScreenshotOptions.class)); return withElement((h, o) -> h.screenshot(o), convertType(options, ElementHandle.ScreenshotOptions.class), "Screenshot");
} }
@Override @Override
@@ -476,7 +527,7 @@ class LocatorImpl implements Locator {
withElement((h, o) -> { withElement((h, o) -> {
h.scrollIntoViewIfNeeded(o); h.scrollIntoViewIfNeeded(o);
return null; return null;
}, convertType(options, ElementHandle.ScrollIntoViewIfNeededOptions.class)); }, convertType(options, ElementHandle.ScrollIntoViewIfNeededOptions.class), "Scroll into view");
} }
@Override @Override
@@ -532,7 +583,7 @@ class LocatorImpl implements Locator {
withElement((h, o) -> { withElement((h, o) -> {
h.selectText(o); h.selectText(o);
return null; return null;
}, convertType(options, ElementHandle.SelectTextOptions.class)); }, convertType(options, ElementHandle.SelectTextOptions.class), "Select text");
} }
@Override @Override
@@ -612,20 +663,35 @@ class LocatorImpl implements Locator {
if (options == null) { if (options == null) {
options = new WaitForOptions(); options = new WaitForOptions();
} }
waitForImpl(options); frame.waitForSelectorImpl(selector, convertType(options, Frame.WaitForSelectorOptions.class).setStrict(true), true);
}
private void waitForImpl(WaitForOptions options) {
frame.withLogging("Locator.waitFor", () -> frame.waitForSelectorImpl(selector, convertType(options, Frame.WaitForSelectorOptions.class).setStrict(true), true));
} }
@Override @Override
public String toString() { public String toString() {
String description = description();
if (description != null) {
return description;
}
return "Locator@" + selector; return "Locator@" + selector;
} }
FrameExpectResult expect(String expression, FrameExpectOptions options) { @Override
return frame.withLogging("Locator.expect", () -> expectImpl(expression, options)); public boolean equals(Object obj) {
if (!(obj instanceof LocatorImpl)) {
return false;
}
LocatorImpl locator = (LocatorImpl) obj;
return frame.equals(locator.frame) && selector.equals(locator.selector);
}
@Override
public int hashCode() {
return frame.hashCode() ^ selector.hashCode();
}
FrameExpectResult expect(String expression, FrameExpectOptions options, String title) {
options.selector = selector;
return frame.expect(expression, options, title);
} }
JsonObject toProtocol() { JsonObject toProtocol() {
@@ -634,16 +700,4 @@ class LocatorImpl implements Locator {
result.addProperty("selector", selector); result.addProperty("selector", selector);
return result; return result;
} }
private FrameExpectResult expectImpl(String expression, FrameExpectOptions options) {
if (options == null) {
options = new FrameExpectOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
params.addProperty("expression", expression);
JsonElement json = frame.sendMessage("expect", params);
FrameExpectResult result = gson().fromJson(json, FrameExpectResult.class);
return result;
}
} }
@@ -39,9 +39,18 @@ public class LocatorUtils {
return "internal:attr=[" + attrName + "=" + escapeForAttributeSelector(value, exact) + "]"; return "internal:attr=[" + attrName + "=" + escapeForAttributeSelector(value, exact) + "]";
} }
static String describeSelector(String description) {
return "internal:describe=" + gson().toJson(description);
}
// Multiple test id attribute names can be joined with a comma. Attribute names cannot contain commas.
private static String encodeTestIdAttributeName(String testIdAttributeName) {
return testIdAttributeName.contains(",") ? gson().toJson(testIdAttributeName) : testIdAttributeName;
}
static String getByTestIdSelector(Object testId, PlaywrightImpl playwright) { static String getByTestIdSelector(Object testId, PlaywrightImpl playwright) {
String testIdAttributeName = ((SharedSelectors) playwright.selectors()).testIdAttributeName; String attributeName = encodeTestIdAttributeName(playwright.selectors.testIdAttributeName);
return getByAttributeTextSelector(testIdAttributeName, testId, true); return "internal:testid=[" + attributeName + "=" + escapeForAttributeSelector(testId, true) + "]";
} }
static String getByAltTextSelector(Object text, Locator.GetByAltTextOptions options) { static String getByAltTextSelector(Object text, Locator.GetByAltTextOptions options) {
@@ -83,6 +92,10 @@ public class LocatorUtils {
String name = escapeForAttributeSelector(options.name, options.exact != null && options.exact); String name = escapeForAttributeSelector(options.name, options.exact != null && options.exact);
addAttr(result, "name", name); addAttr(result, "name", name);
} }
if (options.description != null) {
String description = escapeForAttributeSelector(options.description, options.exact != null && options.exact);
addAttr(result, "description", description);
}
if (options.pressed != null) if (options.pressed != null)
addAttr(result, "pressed", options.pressed.toString()); addAttr(result, "pressed", options.pressed.toString());
} }
@@ -19,7 +19,6 @@ package com.microsoft.playwright.impl;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.function.Supplier;
class LoggingSupport { class LoggingSupport {
private static final boolean isEnabled; private static final boolean isEnabled;
@@ -31,29 +30,6 @@ class LoggingSupport {
private static final DateTimeFormatter timestampFormat = DateTimeFormatter.ofPattern( private static final DateTimeFormatter timestampFormat = DateTimeFormatter.ofPattern(
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX").withZone(ZoneId.of("UTC")); "yyyy-MM-dd'T'HH:mm:ss.SSSXXX").withZone(ZoneId.of("UTC"));
void withLogging(String apiName, Runnable code) {
withLogging(apiName, () -> {
code.run();
return null;
});
}
<T> T withLogging(String apiName, Supplier<T> code) {
if (isEnabled) {
logApi("=> " + apiName + " started");
}
boolean success = false;
try {
T result = code.get();
success = true;
return result;
} finally {
if (isEnabled) {
logApi("<= " + apiName + (success ? " succeeded" : " failed"));
}
}
}
static void logWithTimestamp(String message) { static void logWithTimestamp(String message) {
// This matches log format produced by the server. // This matches log format produced by the server.
String timestamp = ZonedDateTime.now().format(timestampFormat); String timestamp = ZonedDateTime.now().format(timestampFormat);
@@ -19,6 +19,7 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.microsoft.playwright.Mouse; import com.microsoft.playwright.Mouse;
import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT;
import static com.microsoft.playwright.impl.Serialization.gson; import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.convertType; import static com.microsoft.playwright.impl.Utils.convertType;
@@ -31,22 +32,18 @@ class MouseImpl implements Mouse {
@Override @Override
public void click(double x, double y, ClickOptions options) { public void click(double x, double y, ClickOptions options) {
page.withLogging("Mouse.click", () -> clickImpl(x, y, options));
}
private void clickImpl(double x, double y, ClickOptions options) {
if (options == null) { if (options == null) {
options = new ClickOptions(); options = new ClickOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("x", x); params.addProperty("x", x);
params.addProperty("y", y); params.addProperty("y", y);
page.sendMessage("mouseClick", params); page.sendMessage("mouseClick", params, NO_TIMEOUT);
} }
@Override @Override
public void dblclick(double x, double y, DblclickOptions options) { public void dblclick(double x, double y, DblclickOptions options) {
page.withLogging("Mouse.dblclick", () -> dblclickImpl(x, y, options)); page.withTitle("Double click", () -> dblclickImpl(x, y, options));
} }
private void dblclickImpl(double x, double y, DblclickOptions options) { private void dblclickImpl(double x, double y, DblclickOptions options) {
@@ -62,52 +59,38 @@ class MouseImpl implements Mouse {
@Override @Override
public void down(DownOptions options) { public void down(DownOptions options) {
page.withLogging("Mouse.down", () -> downImpl(options));
}
private void downImpl(DownOptions options) {
if (options == null) { if (options == null) {
options = new DownOptions(); options = new DownOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
page.sendMessage("mouseDown", params); page.sendMessage("mouseDown", params, NO_TIMEOUT);
} }
@Override @Override
public void move(double x, double y, MoveOptions options) { public void move(double x, double y, MoveOptions options) {
page.withLogging("Mouse.move", () -> moveImpl(x, y, options));
}
private void moveImpl(double x, double y, MoveOptions options) {
if (options == null) { if (options == null) {
options = new MoveOptions(); options = new MoveOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("x", x); params.addProperty("x", x);
params.addProperty("y", y); params.addProperty("y", y);
page.sendMessage("mouseMove", params); page.sendMessage("mouseMove", params, NO_TIMEOUT);
} }
@Override @Override
public void up(UpOptions options) { public void up(UpOptions options) {
page.withLogging("Mouse.up", () -> upImpl(options));
}
@Override
public void wheel(double deltaX, double deltaY) {
page.withLogging("Mouse.wheel", () -> {
JsonObject params = new JsonObject();
params.addProperty("deltaX", deltaX);
params.addProperty("deltaY", deltaY);
page.sendMessage("mouseWheel", params);
});
}
private void upImpl(UpOptions options) {
if (options == null) { if (options == null) {
options = new UpOptions(); options = new UpOptions();
} }
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
page.sendMessage("mouseUp", params); page.sendMessage("mouseUp", params, NO_TIMEOUT);
}
@Override
public void wheel(double deltaX, double deltaY) {
JsonObject params = new JsonObject();
params.addProperty("deltaX", deltaX);
params.addProperty("deltaY", deltaY);
page.sendMessage("mouseWheel", params, NO_TIMEOUT);
} }
} }
@@ -32,38 +32,55 @@ public class PageAssertionsImpl extends AssertionsBase implements PageAssertions
} }
private PageAssertionsImpl(Page page, boolean isNot) { private PageAssertionsImpl(Page page, boolean isNot) {
super((LocatorImpl) page.locator(":root"), isNot); super(isNot);
this.actualPage = (PageImpl) page; this.actualPage = (PageImpl) page;
} }
@Override
FrameExpectResult doExpect(String expression, FrameExpectOptions expectOptions, String title) {
FrameImpl frame = (FrameImpl) actualPage.mainFrame();
return frame.expect(expression, expectOptions, title);
}
@Override @Override
public void hasTitle(String title, HasTitleOptions options) { public void hasTitle(String title, HasTitleOptions options) {
ExpectedTextValue expected = new ExpectedTextValue(); ExpectedTextValue expected = new ExpectedTextValue();
expected.string = title; expected.string = title;
expected.normalizeWhiteSpace = true; expected.normalizeWhiteSpace = true;
expectImpl("to.have.title", expected, title, "Page title expected to be", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.title", expected, title, "Page title expected to be", convertType(options, FrameExpectOptions.class), "Assert \"hasTitle\"");
} }
@Override @Override
public void hasTitle(Pattern pattern, HasTitleOptions options) { public void hasTitle(Pattern pattern, HasTitleOptions options) {
ExpectedTextValue expected = expectedRegex(pattern); ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.title", expected, pattern, "Page title expected to match regex", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.title", expected, pattern, "Page title expected to match regex", convertType(options, FrameExpectOptions.class), "Assert \"hasTitle\"");
} }
@Override @Override
public void hasURL(String url, HasURLOptions options) { public void hasURL(String url, HasURLOptions options) {
ExpectedTextValue expected = new ExpectedTextValue(); ExpectedTextValue expected = new ExpectedTextValue();
if (actualPage.context().baseUrl != null) { if (actualPage.context().baseUrl() != null) {
url = resolveUrl(actualPage.context().baseUrl, url); url = resolveUrl(actualPage.context().baseUrl(), url);
} }
expected.string = url; expected.string = url;
expectImpl("to.have.url", expected, url, "Page URL expected to be", convertType(options, FrameExpectOptions.class)); expected.ignoreCase = shouldIgnoreCase(options);
expectImpl("to.have.url", expected, url, "Page URL expected to be", convertType(options, FrameExpectOptions.class), "Assert \"hasURL\"");
} }
@Override @Override
public void hasURL(Pattern pattern, HasURLOptions options) { public void hasURL(Pattern pattern, HasURLOptions options) {
ExpectedTextValue expected = expectedRegex(pattern); ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.url", expected, pattern, "Page URL expected to match regex", convertType(options, FrameExpectOptions.class)); expectImpl("to.have.url", expected, pattern, "Page URL expected to match regex", convertType(options, FrameExpectOptions.class), "Assert \"hasURL\"");
}
@Override
public void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions snapshotOptions) {
if (snapshotOptions == null) {
snapshotOptions = new MatchesAriaSnapshotOptions();
}
FrameExpectOptions options = convertType(snapshotOptions, FrameExpectOptions.class);
options.expectedValue = Serialization.serializeArgument(expected);
expectImpl("to.match.aria", options, expected, "Page expected to match Aria snapshot", "Assert \"matchesAriaSnapshot\"");
} }
@Override @Override
File diff suppressed because it is too large Load Diff
@@ -52,7 +52,6 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
Connection connection = new Connection(new PipeTransport(p.getInputStream(), p.getOutputStream()), env); Connection connection = new Connection(new PipeTransport(p.getInputStream(), p.getOutputStream()), env);
PlaywrightImpl result = connection.initializePlaywright(); PlaywrightImpl result = connection.initializePlaywright();
result.driverProcess = p; result.driverProcess = p;
result.initSharedSelectors(null);
return result; return result;
} catch (IOException e) { } catch (IOException e) {
throw new PlaywrightException("Failed to launch driver", e); throw new PlaywrightException("Failed to launch driver", e);
@@ -62,9 +61,8 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
private final BrowserTypeImpl chromium; private final BrowserTypeImpl chromium;
private final BrowserTypeImpl firefox; private final BrowserTypeImpl firefox;
private final BrowserTypeImpl webkit; private final BrowserTypeImpl webkit;
private final SelectorsImpl selectors;
private final APIRequestImpl apiRequest; private final APIRequestImpl apiRequest;
private SharedSelectors sharedSelectors; protected SelectorsImpl selectors;
PlaywrightImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { PlaywrightImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
@@ -72,26 +70,20 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
firefox = parent.connection.getExistingObject(initializer.getAsJsonObject("firefox").get("guid").getAsString()); firefox = parent.connection.getExistingObject(initializer.getAsJsonObject("firefox").get("guid").getAsString());
webkit = parent.connection.getExistingObject(initializer.getAsJsonObject("webkit").get("guid").getAsString()); webkit = parent.connection.getExistingObject(initializer.getAsJsonObject("webkit").get("guid").getAsString());
selectors = connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString()); chromium.playwright = this;
firefox.playwright = this;
webkit.playwright = this;
selectors = new SelectorsImpl();
apiRequest = new APIRequestImpl(this); apiRequest = new APIRequestImpl(this);
} }
void initSharedSelectors(PlaywrightImpl parent) { public LocalUtils localUtils() {
assert sharedSelectors == null; return connection.localUtils;
if (parent == null) {
sharedSelectors = new SharedSelectors();
} else {
sharedSelectors = parent.sharedSelectors;
}
sharedSelectors.addChannel(selectors);
}
void unregisterSelectors() {
sharedSelectors.removeChannel(selectors);
} }
public JsonArray deviceDescriptors() { public JsonArray deviceDescriptors() {
return connection.localUtils.deviceDescriptors(); return localUtils().deviceDescriptors();
} }
@Override @Override
@@ -116,7 +108,7 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
@Override @Override
public Selectors selectors() { public Selectors selectors() {
return sharedSelectors; return selectors;
} }
@Override @Override

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