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

Compare commits

...

120 Commits

Author SHA1 Message Date
Yury Semikhatsky 10da11cd7c chore: update javadocs to reference set* methods (#328) 2021-03-05 15:17:28 -08:00
Max Schmitt 71b5865d5e docs(readme): use aka.ms Playwright Slack invite link (#327) 2021-03-05 14:38:40 -08:00
Yury Semikhatsky ce54e385c5 docs: add more badges (#326) 2021-03-05 14:32:34 -08:00
Yury Semikhatsky 7d333f994e docs: update examples to use set* instead of with* (#325) 2021-03-05 14:29:41 -08:00
Yury Semikhatsky 617822fa18 fix: rename builder methods from with* to set* (#324) 2021-03-05 13:38:26 -08:00
Yury Semikhatsky 86c06c4fd3 fix(api): throw TimeoutError on timeout (#323) 2021-03-05 13:18:24 -08:00
Yury Semikhatsky d57c449aef test: add basic cli test (#321) 2021-03-05 10:12:45 -08:00
Yury Semikhatsky 9fe7496261 fix: set default codegen language to java (#320) 2021-03-04 22:33:49 -08:00
Yury Semikhatsky 9b568ab4ae feat: add class for launching cli using mvn (#319) 2021-03-04 21:52:12 -08:00
Yury Semikhatsky e9d5d6c5cd fix: correctly terminate connection on remote browser close (#318) 2021-03-04 14:26:22 -08:00
Yury Semikhatsky 2094d40ecb fix: delete deprecated accessibility API, add onceDialog (#317) 2021-03-03 21:15:34 -08:00
Yury Semikhatsky 315bda9b48 feat: provide stacktraces for inspector (#316) 2021-03-03 14:45:35 -08:00
Yury Semikhatsky db5bb8b9c2 fix: use default file system for relative path resolution (#315) 2021-03-03 11:42:17 -08:00
Yury Semikhatsky 6d982db6b9 fix: implement BrowserType.connect (#314) 2021-03-02 18:13:27 -08:00
Yury Semikhatsky 65fdde3e36 docs: use double quotes, linkify Promise (#312) 2021-03-02 12:27:48 -08:00
Yury Semikhatsky 33cbcd6fc7 fix: roll 1.9.1-1614654987000, rename onConsole->onConsoleMessage (#311) 2021-03-01 21:29:12 -08:00
Yury Semikhatsky 5ce2bb6527 docs: generate even documentation (#310) 2021-03-01 17:36:52 -08:00
Yury Semikhatsky 7e3cec5aac docs: patch javadoc links to API members and external sources (#309) 2021-03-01 16:38:29 -08:00
Yury Semikhatsky 606d9947d4 docs: add code snippets from upstream (#308) 2021-03-01 15:56:57 -08:00
Yury Semikhatsky aea9218162 chore: roll driver to 1.9.1-1614633211000 (#307) 2021-03-01 15:04:54 -08:00
Yury Semikhatsky 9885f72a66 docs: add examples from intro (#304) 2021-02-23 13:18:15 -08:00
Yury Semikhatsky d63a8e31c3 fix(logging): use thread-safe time formatter (#303) 2021-02-23 12:52:02 -08:00
Yury Semikhatsky 32afbc065b docs: fix examples to work with the latest api (#302) 2021-02-23 12:30:03 -08:00
Yury Semikhatsky 08d84cf629 chore: harden custom type mapping (#299) 2021-02-19 18:54:06 -08:00
Yury Semikhatsky 85553f691f chore: remove custom generator for cookie methods (#298) 2021-02-19 18:22:23 -08:00
Yury Semikhatsky 2c6aa8f8b3 chore: remove Types.java (#297) 2021-02-19 16:45:57 -08:00
Yury Semikhatsky 80c11f76aa fix: change postData field type to Object (#296) 2021-02-19 16:28:26 -08:00
Yury Semikhatsky aa853e8386 chore: generate selectOption from api.json (#295) 2021-02-19 15:44:10 -08:00
Yury Semikhatsky 99890b57bc fix: generate helper builder methods for nested types (#294) 2021-02-19 14:34:01 -08:00
Yury Semikhatsky ab5cd1c511 fix: generate overloaded builders for options (#293) 2021-02-19 13:46:08 -08:00
Yury Semikhatsky b7a822ee41 chore: always use number for polling interval option (#292) 2021-02-19 12:39:23 -08:00
Yury Semikhatsky c9787e2102 chore: user array instead of list only for overloaded params (#291) 2021-02-19 11:44:58 -08:00
Yury Semikhatsky 8bfb69b93d fix: run tests on release-* branches too (#289) 2021-02-19 09:35:30 -08:00
Yury Semikhatsky 8e62a47f6d fix: generate FilePayload and setInputFiles from api.json (#286) 2021-02-12 18:14:09 -08:00
Yury Semikhatsky b8a774eecc fix: generate frameByUrl from api.json (#285) 2021-02-12 16:23:04 -08:00
Yury Semikhatsky 6fb0b01a17 fix: generate javadocs for all overloads (#284) 2021-02-12 15:15:41 -08:00
Yury Semikhatsky 5fa5a513d2 fix: start generating overloaded method for union params from api.json (#283) 2021-02-12 14:53:47 -08:00
Yury Semikhatsky 5f122784f8 chore: roll cli to 1.9.0-next-1613157126000 (#282) 2021-02-12 12:21:45 -08:00
Yury Semikhatsky f3003bad20 chore: remove custom signatures for non-java methods (#281) 2021-02-12 10:08:11 -08:00
Yury Semikhatsky 0afb42bb0f fix: remove devices from the API (#280) 2021-02-11 12:53:18 -08:00
Yury Semikhatsky 8db59ba9a1 chore: update cli to match current api (#279) 2021-02-11 08:29:53 -08:00
Yury Semikhatsky 5bf883a456 fix: move exposed binding and funcion callbacks to top level (#278) 2021-02-10 18:14:24 -08:00
Yury Semikhatsky d4ef6d6431 chore: simplify generator (#277) 2021-02-10 16:20:33 -08:00
Yury Semikhatsky ea34deeb2b fix: generate return and param types from api.json (#276) 2021-02-10 15:42:03 -08:00
Yury Semikhatsky 442a577506 fix: drop RecordHar and RecordVideo clasess, use flat list of their options (#275) 2021-02-10 12:00:43 -08:00
Yury Semikhatsky fcc1d8672a fix: take required options as constructor params (#274) 2021-02-09 17:58:55 -08:00
Yury Semikhatsky 429f2969aa fix: extract some option classes to top level (#273) 2021-02-09 17:15:09 -08:00
Yury Semikhatsky 7642097291 chore: remove special case for WebSocketFrame (#272) 2021-02-09 16:50:01 -08:00
Yury Semikhatsky bd14c560d2 fix: import options package in the example (#271) 2021-02-09 10:59:19 -08:00
Yury Semikhatsky 18ca3e6ace chore: remove custom mapping for pdf options (#268) 2021-02-08 18:35:27 -08:00
Yury Semikhatsky b77b9b0d20 chore: move enums and second level nested classes into options package (#267) 2021-02-08 18:33:38 -08:00
Yury Semikhatsky c2690f925c fix: allow building persistent context options from device (#266) 2021-02-08 15:52:32 -08:00
Yury Semikhatsky f409965c8e chore: remove unused code from generator (#265) 2021-02-08 15:52:02 -08:00
Yury Semikhatsky 566e0f90e2 fix(api): generate enums from api.json, put them into separate files (#264) 2021-02-08 15:36:01 -08:00
Yury Semikhatsky b98e920efb docs: fix network interception example to work with 0.190.0 (#262) 2021-02-08 09:20:16 -08:00
Yury Semikhatsky 90940b23f3 fix: generate waitFor* methods from upstream docs (#260) 2021-02-04 22:29:55 -08:00
Yury Semikhatsky 2c9fa04a43 tests: stop using waitFor* methods which will be removed (#259) 2021-02-04 20:36:29 -08:00
Yury Semikhatsky eebb223bc3 chore(generator): use case-sensitive event names from upstream (#257) 2021-02-04 11:55:36 -08:00
Yury Semikhatsky f99d6643cd feat: implement Page.pause (#256) 2021-02-04 09:39:44 -08:00
ephung01 37e5de729f test: add new test 'Test Browser Context Viewport' (#255) 2021-02-04 09:23:34 -08:00
Yury Semikhatsky 811ca89fd4 chore: remove custom generator for bodyBytes (#254) 2021-02-03 18:59:42 -08:00
Yury Semikhatsky 6dbebaed5a fix: rename Route.continue_ to Route.resume (#253) 2021-02-03 14:14:35 -08:00
Yury Semikhatsky ee4f8698da fix: auto dismiss dialogs if there are no listeners (#252) 2021-02-03 13:18:01 -08:00
Yury Semikhatsky e31ea3cbe9 fix: change Accessibility.snapshot type to String (#251) 2021-02-03 12:58:38 -08:00
Yury Semikhatsky b60bbc8b88 fix: change storage state type to String (#250) 2021-02-02 18:05:57 -08:00
Yury Semikhatsky a577e62ece chore: delete code for generating custom option names (#249) 2021-02-02 18:05:34 -08:00
Yury Semikhatsky d14d35610e chore: remove custom generator code for Response.finished (#248) 2021-02-02 12:49:20 -08:00
Yury Semikhatsky 301234aa4f chore: roll cli, remove custom ignoreDefaultArgs generator (#247) 2021-02-02 12:15:30 -08:00
Yury Semikhatsky 7a7edea189 fix: add snapshot repo to the examples project (#246) 2021-02-02 11:10:07 -08:00
JB Nizet a6f25595f3 feat: extend AutoCloseable in Playwright, BrowserContext, Browser and… (#238) 2021-02-01 17:31:51 -08:00
Yury Semikhatsky 5af091880f chore: remove more upstreamed stuff (#244) 2021-02-01 13:34:23 -08:00
Yury Semikhatsky 4c165c2069 feat: update driver to 1.9.0-next-1612209901000 (#243) 2021-02-01 12:51:25 -08:00
Yury Semikhatsky 65df7be823 fix: remove Exception from Playwright.close signature (#240) 2021-01-29 21:12:25 -08:00
JB Nizet 2c6e278167 chore: avoid creating a useless DataOutputStream when writing to a file (#236) 2021-01-29 11:14:19 -08:00
JB Nizet 68525d1426 fix: avoid NPE when saving storage state (#235) 2021-01-29 11:13:52 -08:00
Yury Semikhatsky 1da197fdf5 chore: delete Page.LoadState, use Frame.LoadState instead (#234) 2021-01-25 16:00:30 -08:00
Yury Semikhatsky e545829bf5 fix(api): make Dialog.type() return string (#233) 2021-01-25 15:11:05 -08:00
Yury Semikhatsky 982cc6a3bc chore: streamline enum serialization (#232) 2021-01-25 12:35:13 -08:00
Yury Semikhatsky dc139d25d1 chore: delete Event interface (#231) 2021-01-25 11:18:01 -08:00
ephung01 4f9d4f170f test: add new test 'Selectors CSS' (#229) 2021-01-25 11:17:08 -08:00
Yury Semikhatsky 55344516ea chore: move on to 0.190.0-SNAPSHOT (#228) 2021-01-22 18:19:31 -08:00
Yury Semikhatsky 454b155bbc chore: delete Listener interface from implementation (#226) 2021-01-22 13:35:11 -08:00
Yury Semikhatsky 808f666399 fix(api): make all event listeners accept one parameter (#225) 2021-01-22 12:49:27 -08:00
Yury Semikhatsky d1f1287e77 fix(api): update waitForRequest/Response predicate to accept Request/… (#224) 2021-01-21 18:50:04 -08:00
Yury Semikhatsky 0c30cc1bfc chore: simplify api generator (#223) 2021-01-21 18:22:02 -08:00
Yury Semikhatsky e80c23980a test(dom): convert remaining element handle convenience tests (#222) 2021-01-21 13:51:29 -08:00
Yury Semikhatsky 50a631304d feat(driver): roll driver to 1.8.0 (#221) 2021-01-21 13:35:39 -08:00
Yury Semikhatsky bfc7bcdc4b docs: add table of contents, highlight min Java version (#220) 2021-01-21 09:04:53 -08:00
Yury Semikhatsky 3376836752 fix(api): remove Listener interface from public API (#219) 2021-01-20 19:32:53 -08:00
ephung01 2ba2706104 test: add new test 'Page Keyboard' (#217) 2021-01-20 19:31:45 -08:00
Yury Semikhatsky 58e97f1b36 fix(api): delete Deferred interface, use waitFor* with callback (#218) 2021-01-20 16:33:26 -08:00
Yury Semikhatsky 6c71f15866 docs: add thread-safety notes 2021-01-19 11:38:10 -08:00
ephung01 3b495615c2 test: add new test 'Element Handle Type' and 'Element Handle Press' (#211) 2021-01-15 12:26:27 -08:00
Yury Semikhatsky ba5f8a4160 fix(logs): use same format for pw:api logs as on server (#205) 2021-01-11 22:22:58 -08:00
Yury Semikhatsky 88bd51ce74 docs: remove extra paragraphs from javadoc (#204) 2021-01-11 13:42:28 -08:00
codeboyzhou cc1057d910 fix(scripts): 'generate_api.sh' does not work on macOS (#201)
Co-authored-by: Yury Semikhatsky <yurys@chromium.org>
2021-01-11 11:31:53 -08:00
ephung01 a3e6fa320f test: add new test 'Element Handle Select Text', Fixed webkit windows check various SSL error messages base on platform and OS (#198) 2021-01-11 11:22:06 -08:00
Yury Semikhatsky 89492da7d6 fix: change some int types to double (#200) 2021-01-08 18:29:49 -08:00
Yury Semikhatsky 01d7eac7ad feat: roll driver, switch to new api.json format (#199) 2021-01-08 15:50:38 -08:00
Yury Semikhatsky 1777f1aac8 fix: do not dismiss dialogs automatically (#197) 2021-01-07 11:50:55 -08:00
ephung01 e17689e16e test: add new test 'Element Handle Query Selector' (#194) 2021-01-07 09:33:51 -08:00
codeboyzhou 75c47a88ff chore: add help section for the driver download script (#196) 2021-01-07 09:30:23 -08:00
Yury Semikhatsky c3190b152a feat(logging): support DEBUG=pw:api (#195) 2021-01-06 23:09:49 -08:00
Yury Semikhatsky fc5bd76334 fix(video): make Video.path work (#193) 2021-01-06 15:12:01 -08:00
Yury Semikhatsky 13d7ee6b44 fix: remove Logger from API (#192) 2021-01-06 14:38:46 -08:00
codeboyzhou 1ab6e6b78c fix(scripts): Dockerfile is outdated (#191) 2021-01-06 10:23:53 -08:00
Yury Semikhatsky 6a67124e1c fix: limit number of tracked unused Deferred objects to 10 (#189) 2021-01-05 15:05:12 -08:00
Yury Semikhatsky b2c27143a8 fix(api): redo waitFor* methods (#188) 2021-01-04 18:50:08 -08:00
Yury Semikhatsky 206a224bf6 fix(pdf): change type of pdf scale option to double (#187) 2021-01-04 17:56:13 -08:00
Yury Semikhatsky a318e3f261 test: add missing .get() call on the result of page.waitForSelector (#185) 2021-01-04 15:19:04 -08:00
Yury Semikhatsky eeee9c6bb1 fix: ignoreDefaultArgs and ignoreAllDefaultArgs (#184) 2021-01-04 14:54:23 -08:00
codeboyzhou e51bb075e7 fix: java main process will be suspended when browsers installation timed out (#180) 2021-01-04 10:56:57 -08:00
codeboyzhou 2ce8321178 chore: give more friendly exception info while create driver (#181) 2021-01-04 10:56:03 -08:00
Yury Semikhatsky 68a0b74215 feat: throw in Playwright.close() if .get() was not called on Deferred (#177) 2020-12-28 17:09:56 -08:00
Yury Semikhatsky aca2d94625 fix: allow configuring environment variables (#176) 2020-12-28 12:29:25 -08:00
Yury Semikhatsky cad90a3af6 fix: interrupt listener on context close, more tests (#174) 2020-12-24 11:05:03 -08:00
Yury Semikhatsky 5b90067ffc chore: roll to playwright-cli@0.180.0-next.1608746109749-cbc13bd (#172) 2020-12-23 11:23:27 -08:00
Yury Semikhatsky 8eb1c034b1 test: unflake TestGeolocation.shouldUseContextOptionsForPopup (#171) 2020-12-23 11:10:43 -08:00
Yury Semikhatsky 66334131ca chore: bump version to 0.180.0-SNAPSHOT (#170) 2020-12-23 10:59:56 -08:00
Yury Semikhatsky 0bdaa52533 chore: bump version to 0.171.1-SNAPSHOT (#168) 2020-12-22 15:50:40 -08:00
190 changed files with 17395 additions and 9391 deletions
+6 -8
View File
@@ -1,9 +1,13 @@
name: Test
on:
push:
branches: [ master ]
branches:
- master
- release-*
pull_request:
branches: [ master ]
branches:
- master
- release-*
jobs:
build:
timeout-minutes: 30
@@ -26,12 +30,6 @@ jobs:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Cache Downloaded Drivers
uses: actions/cache@v2
with:
path: driver-bundle/src/main/resources/driver
key: ${{ runner.os }}-drivers-${{ hashFiles('scripts/*') }}
restore-keys: ${{ runner.os }}-drivers
- name: Download drivers
shell: bash
run: scripts/download_driver_for_all_platforms.sh
+30
View File
@@ -0,0 +1,30 @@
name: Test CLI
on:
push:
branches:
- master
- release-*
pull_request:
branches:
- master
- release-*
jobs:
verify:
timeout-minutes: 30
strategy:
fail-fast: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Download drivers
run: scripts/download_driver_for_all_platforms.sh
- name: Intall Playwright
run: mvn install -D skipTests --no-transfer-progress
- name: Test CLI
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -f playwright/pom.xml -D exec.args=-V
+6 -2
View File
@@ -1,12 +1,16 @@
name: Verify API
on:
push:
branches: [ master ]
branches:
- master
- release-*
paths:
- 'scripts/*'
- 'api-generator/*'
pull_request:
branches: [ master ]
branches:
- master
- release-*
paths:
- 'scripts/**'
- 'api-generator/**'
+2 -1
View File
@@ -66,5 +66,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN mkdir /tmp/pw-java
COPY . /tmp/pw-java
RUN cd /tmp/pw-java && ./scripts/download_driver.sh && mvn install -D skipTests --no-transfer-progress && \
RUN cd /tmp/pw-java && ./scripts/download_driver_for_all_platforms.sh && \
mvn install -D skipTests --no-transfer-progress && \
rm -rf /tmp/pw-java
+77 -59
View File
@@ -1,6 +1,9 @@
# 🎭 [Playwright](https://playwright.dev) for Java
[![maven version](https://img.shields.io/maven-central/v/com.microsoft.playwright/playwright)](https://search.maven.org/search?q=com.microsoft.playwright) [![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://join.slack.com/t/playwright/shared_invite/enQtOTEyMTUxMzgxMjIwLThjMDUxZmIyNTRiMTJjNjIyMzdmZDA3MTQxZWUwZTFjZjQwNGYxZGM5MzRmNzZlMWI5ZWUyOTkzMjE5Njg1NDg)
[![javadoc](https://javadoc.io/badge2/com.microsoft.playwright/playwright/javadoc.svg)](https://javadoc.io/doc/com.microsoft.playwright/playwright)
[![maven version](https://img.shields.io/maven-central/v/com.microsoft.playwright/playwright)](https://search.maven.org/search?q=com.microsoft.playwright)
[![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/https/oss.sonatype.org/com.microsoft.playwright/playwright.svg)](https://oss.sonatype.org/content/repositories/snapshots/com/microsoft/playwright/playwright/)
[![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://aka.ms/playwright-slack)
#### [Website](https://playwright.dev/) | [API reference](https://www.javadoc.io/doc/com.microsoft.playwright/playwright/latest/index.html)
@@ -8,15 +11,27 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->89.0.4344.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->90.0.4392.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->14.1<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->84.0b9<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->85.0b5<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all the browsers on all platforms. Check out [system requirements](https://playwright.dev/#?path=docs/intro.md&q=system-requirements) for details.
* [Usage](#usage)
- [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
Playwright supports Java 8 and above.
Playwright requires **Java 8** or newer.
#### Add Maven dependency
@@ -28,10 +43,14 @@ To run Playwright simply add following dependency to your Maven project:
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>0.170.3</version>
<version>0.180.0</version>
</dependency>
```
#### Is Playwright thread-safe?
No, Playwright is not thread safe, i.e. all its methods as well as methods on all objects created by it (such as BrowserContext, Browser, Page etc.) are expected to be called on the same thread where Playwright object was created or proper synchronization should be implemented to ensure only one thread calls Playwright methods at any given time. Having said that it's okay to create new many Playwright instances each on its own thread.
## Examples
You can find Maven project with the examples [here](./examples).
@@ -48,23 +67,22 @@ import java.util.Arrays;
import java.util.List;
public class PageScreenshot {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
List<BrowserType> browserTypes = Arrays.asList(
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
List<BrowserType> browserTypes = Arrays.asList(
playwright.chromium(),
playwright.webkit(),
playwright.firefox()
);
for (BrowserType browserType : browserTypes) {
Browser browser = browserType.launch();
BrowserContext context = browser.newContext(
new Browser.NewContextOptions().withViewport(800, 600));
Page page = context.newPage();
page.navigate("http://whatsmyuseragent.org/");
page.screenshot(new Page.ScreenshotOptions().withPath(Paths.get("screenshot-" + browserType.name() + ".png")));
browser.close();
);
for (BrowserType browserType : browserTypes) {
try (Browser browser = browserType.launch()) {
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("http://whatsmyuseragent.org/");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("screenshot-" + browserType.name() + ".png")));
}
}
}
playwright.close();
}
}
```
@@ -74,32 +92,36 @@ public class PageScreenshot {
This snippet emulates Mobile Chromium on a device at a given geolocation, navigates to openstreetmap.org, performs action and takes a screenshot.
```java
import com.microsoft.playwright.options.*;
import com.microsoft.playwright.*;
import java.nio.file.Paths;
import static java.util.Arrays.asList;
public class MobileAndGeolocation {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
BrowserType browserType = playwright.chromium();
Browser browser = browserType.launch(new BrowserType.LaunchOptions().withHeadless(false));
DeviceDescriptor pixel2 = playwright.devices().get("Pixel 2");
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.withDevice(pixel2)
.withLocale("en-US")
.withGeolocation(new Geolocation(41.889938, 12.492507))
.withPermissions(asList("geolocation")));
Page page = context.newPage();
page.navigate("https://www.openstreetmap.org/");
page.click("a[data-original-title=\"Show My Location\"]");
page.screenshot(new Page.ScreenshotOptions().withPath(Paths.get("colosseum-pixel2.png")));
browser.close();
playwright.close();
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setUserAgent("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36")
.setViewportSize(411, 731)
.setDeviceScaleFactor(2.625)
.setIsMobile(true)
.setHasTouch(true)
.setLocale("en-US")
.setGeolocation(41.889938, 12.492507)
.setPermissions(asList("geolocation")));
Page page = context.newPage();
page.navigate("https://www.openstreetmap.org/");
page.click("a[data-original-title=\"Show My Location\"]");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("colosseum-pixel2.png")));
}
}
}
```
#### Evaluate in browser context
#### Evaluate JavaScript in browser
This code snippet navigates to example.com in Firefox, and executes a script in the page context.
@@ -107,23 +129,21 @@ This code snippet navigates to example.com in Firefox, and executes a script in
import com.microsoft.playwright.*;
public class EvaluateInBrowserContext {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
BrowserType browserType = playwright.firefox();
Browser browser = browserType.launch(new BrowserType.LaunchOptions().withHeadless(false));
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://www.example.com/");
Object dimensions = page.evaluate("() => {\n" +
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.firefox().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://www.example.com/");
Object dimensions = page.evaluate("() => {\n" +
" return {\n" +
" width: document.documentElement.clientWidth,\n" +
" height: document.documentElement.clientHeight,\n" +
" deviceScaleFactor: window.devicePixelRatio\n" +
" }\n" +
"}");
System.out.println(dimensions);
browser.close();
playwright.close();
System.out.println(dimensions);
}
}
}
```
@@ -136,19 +156,17 @@ This code snippet sets up request routing for a WebKit page to log all network r
import com.microsoft.playwright.*;
public class InterceptNetworkRequests {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
BrowserType browserType = playwright.webkit();
Browser browser = browserType.launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.route("**", route -> {
System.out.println(route.request().url());
route.continue_();
});
page.navigate("http://todomvc.com");
browser.close();
playwright.close();
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.webkit().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.route("**", route -> {
System.out.println(route.request().url());
route.resume();
});
page.navigate("http://todomvc.com");
}
}
}
```
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>0.171.0</version>
<version>0.190.0-SNAPSHOT</version>
</parent>
<artifactId>driver-bundle</artifactId>
@@ -34,25 +34,51 @@ public class DriverJar extends Driver {
}
private void installBrowsers() throws IOException, InterruptedException {
Path driver = driverTempDir.resolve("playwright-cli");
String cliFileName = super.cliFileName();
Path driver = driverTempDir.resolve(cliFileName);
if (!Files.exists(driver)) {
throw new RuntimeException("Failed to find " + cliFileName + " at " + driver);
}
ProcessBuilder pb = new ProcessBuilder(driver.toString(), "install");
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
Process p = pb.start();
boolean result = p.waitFor(10, TimeUnit.MINUTES);
if (!result) {
p.destroy();
throw new RuntimeException("Timed out waiting for browsers to install");
}
}
private static boolean isExecutable(Path filePath) {
String name = filePath.getFileName().toString();
return name.endsWith(".sh") || name.endsWith(".exe") || !name.contains(".");
}
private void extractDriverToTempDir() throws URISyntaxException, IOException {
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
URI uri = classloader.getResource("driver/" + platformDir()).toURI();
// Create zip filesystem if loading from jar.
try (FileSystem fileSystem = "jar".equals(uri.getScheme()) ? FileSystems.newFileSystem(uri, Collections.emptyMap()) : null) {
Files.list(Paths.get(uri)).forEach(filePath -> {
Path srcRoot = Paths.get(uri);
// jar file system's .relativize gives wrong results when used with
// spring-boot-maven-plugin, convert to the default filesystem to
// have predictable results.
// See https://github.com/microsoft/playwright-java/issues/306
Path srcRootDefaultFs = Paths.get(srcRoot.toString());
Files.walk(srcRoot).forEach(fromPath -> {
Path relative = srcRootDefaultFs.relativize(Paths.get(fromPath.toString()));
Path toPath = driverTempDir.resolve(relative.toString());
try {
extractResource(filePath, driverTempDir);
if (Files.isDirectory(fromPath)) {
Files.createDirectories(toPath);
} else {
Files.copy(fromPath, toPath);
if (isExecutable(toPath)) {
toPath.toFile().setExecutable(true, true);
}
}
toPath.toFile().deleteOnExit();
} catch (IOException e) {
throw new RuntimeException("Failed to extract driver from " + uri, e);
}
@@ -74,14 +100,6 @@ public class DriverJar extends Driver {
throw new RuntimeException("Unexpected os.name value: " + name);
}
private static Path extractResource(Path from, Path toDir) throws IOException {
Path path = toDir.resolve(from.getFileName().toString());
Files.copy(from, path);
path.toFile().setExecutable(true);
path.toFile().deleteOnExit();
return path;
}
@Override
Path driverDir() {
return driverTempDir;
@@ -23,6 +23,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestInstall {
@@ -30,14 +31,19 @@ public class TestInstall {
void playwrightCliInstalled() throws Exception {
// Clear system property to ensure that the driver is loaded from jar.
System.clearProperty("playwright.cli.dir");
Path cli = Driver.ensureDriverInstalled();
assertTrue(Files.exists(cli));
try {
Path cli = Driver.ensureDriverInstalled();
assertTrue(Files.exists(cli));
ProcessBuilder pb = new ProcessBuilder(cli.toString(), "install");
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
Process p = pb.start();
boolean result = p.waitFor(1, TimeUnit.MINUTES);
assertTrue(result, "Timed out waiting for browsers to install");
ProcessBuilder pb = new ProcessBuilder(cli.toString(), "install");
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
Process p = pb.start();
boolean result = p.waitFor(1, TimeUnit.MINUTES);
assertTrue(result, "Timed out waiting for browsers to install");
} catch (Exception e) {
e.printStackTrace();
assertNull(e);
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>0.171.0</version>
<version>0.190.0-SNAPSHOT</version>
</parent>
<artifactId>driver</artifactId>
@@ -43,14 +43,18 @@ public abstract class Driver {
try {
instance = createDriver();
} catch (Exception exception) {
throw new RuntimeException("Failed to find playwright-cli", exception);
throw new RuntimeException("Failed to create driver", exception);
}
}
String name = System.getProperty("os.name").toLowerCase().contains("windows") ?
"playwright-cli.exe" : "playwright-cli";
String name = instance.cliFileName();
return instance.driverDir().resolve(name);
}
protected String cliFileName() {
return System.getProperty("os.name").toLowerCase().contains("windows") ?
"playwright.cmd" : "playwright.sh";
}
private static Driver createDriver() throws Exception {
String pathFromProperty = System.getProperty("playwright.cli.dir");
if (pathFromProperty != null) {
+9 -1
View File
@@ -15,7 +15,7 @@
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>0.170.3</version>
<version>0.190.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
@@ -31,4 +31,12 @@
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>snapshots-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases><enabled>false</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
</project>
@@ -19,22 +19,20 @@ package org.example;
import com.microsoft.playwright.*;
public class EvaluateInBrowserContext {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
BrowserType browserType = playwright.firefox();
Browser browser = browserType.launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://www.example.com/");
Object dimensions = page.evaluate("() => {\n" +
" return {\n" +
" width: document.documentElement.clientWidth,\n" +
" height: document.documentElement.clientHeight,\n" +
" deviceScaleFactor: window.devicePixelRatio\n" +
" }\n" +
"}");
System.out.println(dimensions);
browser.close();
playwright.close();
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.firefox().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://www.example.com/");
Object dimensions = page.evaluate("() => {\n" +
" return {\n" +
" width: document.documentElement.clientWidth,\n" +
" height: document.documentElement.clientHeight,\n" +
" deviceScaleFactor: window.devicePixelRatio\n" +
" }\n" +
"}");
System.out.println(dimensions);
}
}
}
@@ -19,18 +19,16 @@ package org.example;
import com.microsoft.playwright.*;
public class InterceptNetworkRequests {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
BrowserType browserType = playwright.webkit();
Browser browser = browserType.launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.route("**", route -> {
System.out.println(route.request().url());
route.continue_();
});
page.navigate("http://todomvc.com");
browser.close();
playwright.close();
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.webkit().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.route("**", route -> {
System.out.println(route.request().url());
route.resume();
});
page.navigate("http://todomvc.com");
}
}
}
@@ -16,6 +16,7 @@
package org.example;
import com.microsoft.playwright.options.*;
import com.microsoft.playwright.*;
import java.nio.file.Paths;
@@ -23,21 +24,22 @@ import java.nio.file.Paths;
import static java.util.Arrays.asList;
public class MobileAndGeolocation {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
BrowserType browserType = playwright.chromium();
Browser browser = browserType.launch(new BrowserType.LaunchOptions().withHeadless(false));
DeviceDescriptor pixel2 = playwright.devices().get("Pixel 2");
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.withDevice(pixel2)
.withLocale("en-US")
.withGeolocation(new Geolocation(41.889938, 12.492507))
.withPermissions(asList("geolocation")));
Page page = context.newPage();
page.navigate("https://www.openstreetmap.org/");
page.click("a[data-original-title=\"Show My Location\"]");
page.screenshot(new Page.ScreenshotOptions().withPath(Paths.get("colosseum-pixel2.png")));
browser.close();
playwright.close();
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setUserAgent("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36")
.setViewportSize(411, 731)
.setDeviceScaleFactor(2.625)
.setIsMobile(true)
.setHasTouch(true)
.setLocale("en-US")
.setGeolocation(41.889938, 12.492507)
.setPermissions(asList("geolocation")));
Page page = context.newPage();
page.navigate("https://www.openstreetmap.org/");
page.click("a[data-original-title=\"Show My Location\"]");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("colosseum-pixel2.png")));
}
}
}
@@ -23,22 +23,21 @@ import java.util.Arrays;
import java.util.List;
public class PageScreenshot {
public static void main(String[] args) throws Exception {
Playwright playwright = Playwright.create();
List<BrowserType> browserTypes = Arrays.asList(
playwright.chromium(),
playwright.webkit(),
playwright.firefox()
);
for (BrowserType browserType : browserTypes) {
Browser browser = browserType.launch();
BrowserContext context = browser.newContext(
new Browser.NewContextOptions().withViewport(800, 600));
Page page = context.newPage();
page.navigate("http://whatsmyuseragent.org/");
page.screenshot(new Page.ScreenshotOptions().withPath(Paths.get("screenshot-" + browserType.name() + ".png")));
browser.close();
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
List<BrowserType> browserTypes = Arrays.asList(
playwright.chromium(),
playwright.webkit(),
playwright.firefox()
);
for (BrowserType browserType : browserTypes) {
try (Browser browser = browserType.launch()) {
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("http://whatsmyuseragent.org/");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("screenshot-" + browserType.name() + ".png")));
}
}
}
playwright.close();
}
}
@@ -0,0 +1,30 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.example;
import com.microsoft.playwright.*;
public class PrintTitle {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
Page page = browser.newPage();
page.navigate("http://playwright.dev");
System.out.println(page.title());
}
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.example;
import com.microsoft.playwright.*;
import java.nio.file.Paths;
public class WebKitScreenshot {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.webkit().launch();
Page page = browser.newPage();
page.navigate("http://whatsmyuseragent.org/");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("example.png")));
}
}
}
+5 -6
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>0.171.0</version>
<version>0.190.0-SNAPSHOT</version>
</parent>
<artifactId>playwright</artifactId>
@@ -61,6 +61,10 @@
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
@@ -73,10 +77,5 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>driver-bundle</artifactId>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -1,77 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import java.util.*;
/**
* The Accessibility class provides methods for inspecting Chromium's accessibility tree. The accessibility tree is used by
* <p>
* assistive technology such as screen readers or
* <p>
* switches.
* <p>
* Accessibility is a very platform-specific thing. On different platforms, there are different screen readers that might
* <p>
* have wildly different output.
* <p>
* Blink - Chromium's rendering engine - has a concept of "accessibility tree", which is then translated into different
* <p>
* platform-specific APIs. Accessibility namespace gives users access to the Blink Accessibility Tree.
* <p>
* Most of the accessibility tree gets filtered out when converting from Blink AX Tree to Platform-specific AX-Tree or by
* <p>
* assistive technologies themselves. By default, Playwright tries to approximate this filtering, exposing only the
* <p>
* "interesting" nodes of the tree.
*/
public interface Accessibility {
class SnapshotOptions {
/**
* Prune uninteresting nodes from the tree. Defaults to {@code true}.
*/
public Boolean interestingOnly;
/**
* The root DOM element for the snapshot. Defaults to the whole page.
*/
public ElementHandle root;
public SnapshotOptions withInterestingOnly(Boolean interestingOnly) {
this.interestingOnly = interestingOnly;
return this;
}
public SnapshotOptions withRoot(ElementHandle root) {
this.root = root;
return this;
}
}
default AccessibilityNode snapshot() {
return snapshot(null);
}
/**
* Captures the current state of the accessibility tree. The returned object represents the root accessible node of the
* <p>
* page.
* <p>
* <strong>NOTE</strong> The Chromium accessibility tree contains nodes that go unused on most platforms and by most screen readers.
* <p>
* Playwright will discard them as well for an easier to process tree, unless {@code interestingOnly} is set to {@code false}.
* <p>
*/
AccessibilityNode snapshot(SnapshotOptions options);
}
@@ -1,51 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import java.util.List;
public interface AccessibilityNode {
String role();
String name();
String valueString();
Double valueNumber();
String description();
String keyshortcuts();
String roledescription();
String valuetext();
Boolean disabled();
Boolean expanded();
Boolean focused();
Boolean modal();
Boolean multiline();
Boolean multiselectable();
Boolean readonly();
Boolean required();
Boolean selected();
enum CheckedState { CHECKED, UNCHECKED, MIXED }
CheckedState checked();
enum PressedState { PRESSED, RELEASED, MIXED }
PressedState pressed();
Integer level();
Double valuemin();
Double valuemax();
String autocomplete();
String haspopup();
String invalid();
String orientation();
List<AccessibilityNode> children();
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,41 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.impl.Driver;
import java.io.IOException;
import java.nio.file.Path;
import static java.util.Arrays.asList;
/**
* Use this class to launch playwright cli.
*/
public class CLI {
public static void main(String[] args) throws IOException, InterruptedException {
Path driver = Driver.ensureDriverInstalled();
ProcessBuilder pb = new ProcessBuilder(driver.toString());
pb.command().addAll(asList(args));
if (!pb.environment().containsKey("PW_CLI_TARGET_LANG")) {
pb.environment().put("PW_CLI_TARGET_LANG", "java");
}
pb.inheritIO();
Process process = pb.start();
System.exit(process.waitFor());
}
}
@@ -1,3 +0,0 @@
package com.microsoft.playwright;
public enum ColorScheme { DARK, LIGHT, NO_PREFERENCE }
@@ -19,42 +19,19 @@ package com.microsoft.playwright;
import java.util.*;
/**
* ConsoleMessage objects are dispatched by page via the page.on('console') event.
* {@code ConsoleMessage} objects are dispatched by page via the {@link Page#onConsole Page.onConsole()} event.
*/
public interface ConsoleMessage {
class Location {
/**
* URL of the resource if available, otherwise empty string.
*/
private String url;
/**
* 0-based line number in the resource.
*/
private int lineNumber;
/**
* 0-based column number in the resource.
*/
private int columnNumber;
public String url() {
return this.url;
}
public int lineNumber() {
return this.lineNumber;
}
public int columnNumber() {
return this.columnNumber;
}
}
List<JSHandle> args();
Location location();
/**
* URL of the resource followed by 0-based line and column numbers in the resource formatted as {@code URL:line:column}.
*/
String location();
String text();
/**
* One of the following values: {@code 'log'}, {@code 'debug'}, {@code 'info'}, {@code 'error'}, {@code 'warning'}, {@code 'dir'}, {@code 'dirxml'}, {@code 'table'},
* <p>
* {@code 'trace'}, {@code 'clear'}, {@code 'startGroup'}, {@code 'startGroupCollapsed'}, {@code 'endGroup'}, {@code 'assert'}, {@code 'profile'}, {@code 'profileEnd'},
* <p>
* {@code 'count'}, {@code 'timeEnd'}.
* One of the following values: {@code "log"}, {@code "debug"}, {@code "info"}, {@code "error"}, {@code "warning"}, {@code "dir"}, {@code "dirxml"}, {@code "table"},
* {@code "trace"}, {@code "clear"}, {@code "startGroup"}, {@code "startGroupCollapsed"}, {@code "endGroup"}, {@code "assert"}, {@code "profile"}, {@code "profileEnd"},
* {@code "count"}, {@code "timeEnd"}.
*/
String type();
}
@@ -19,16 +19,45 @@ package com.microsoft.playwright;
import java.util.*;
/**
* Dialog objects are dispatched by page via the page.on('dialog') event.
* {@code Dialog} objects are dispatched by page via the {@link Page#onDialog Page.onDialog()} event.
*
* <p> An example of using {@code Dialog} class:
* <pre>{@code
* import com.microsoft.playwright.*;
*
* public class Example {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType chromium = playwright.chromium();
* Browser browser = chromium.launch();
* Page page = browser.newPage();
* page.onDialog(dialog -> {
* System.out.println(dialog.message());
* dialog.dismiss();
* });
* page.evaluate("alert('1')");
* browser.close();
* }
* }
* }
* }</pre>
*
* <p> <strong>NOTE:</strong> Dialogs are dismissed automatically, unless there is a {@link Page#onDialog Page.onDialog()} listener. When listener is
* present, it **must** either {@link Dialog#accept Dialog.accept()} or {@link Dialog#dismiss Dialog.dismiss()} the dialog
* - otherwise the page will <a
* href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking">freeze</a> waiting for the
* dialog, and actions like click will never finish.
*/
public interface Dialog {
enum Type { ALERT, BEFOREUNLOAD, CONFIRM, PROMPT }
/**
* Returns when the dialog has been accepted.
*/
default void accept() {
accept(null);
}
/**
* Returns when the dialog has been accepted.
*
* @param promptText A text to enter in prompt. Does not cause any effects if the dialog's {@code type} is not prompt. Optional.
*/
void accept(String promptText);
@@ -47,6 +76,6 @@ public interface Dialog {
/**
* Returns dialog's type, can be one of {@code alert}, {@code beforeunload}, {@code confirm} or {@code prompt}.
*/
Type type();
String type();
}
@@ -21,21 +21,30 @@ import java.nio.file.Path;
import java.util.*;
/**
* Download objects are dispatched by page via the page.on('download') event.
* <p>
* All the downloaded files belonging to the browser context are deleted when the browser context is closed. All downloaded
* <p>
* {@code Download} objects are dispatched by page via the {@link Page#onDownload Page.onDownload()} event.
*
* <p> All the downloaded files belonging to the browser context are deleted when the browser context is closed. All downloaded
* files are deleted when the browser closes.
* <p>
* Download event is emitted once the download starts. Download path becomes available once download completes:
* <p>
*
* <p>
* <strong>NOTE</strong> Browser context **must** be created with the {@code acceptDownloads} set to {@code true} when user needs access to the
* <p>
* downloaded content. If {@code acceptDownloads} is not set or set to {@code false}, download events are emitted, but the actual
* <p>
* download is not performed and user has no access to the downloaded files.
*
* <p> Download event is emitted once the download starts. Download path becomes available once download completes:
* <pre>{@code
* // wait for download to start
* Download download = page.waitForDownload(() -> page.click("a"));
* // wait for download to complete
* Path path = download.path();
* }</pre>
* <pre>{@code
* // wait for download to start
* Download download = page.waitForDownload(() -> {
* page.click("a");
* });
* // wait for download to complete
* Path path = download.path();
* }</pre>
*
* <p> <strong>NOTE:</strong> Browser context **must** be created with the {@code acceptDownloads} set to {@code true} when user needs access to the downloaded
* content. If {@code acceptDownloads} is not set, download events are emitted, but the actual download is not performed and user
* has no access to the downloaded files.
*/
public interface Download {
/**
@@ -56,17 +65,16 @@ public interface Download {
Path path();
/**
* Saves the download to a user-specified path.
*
* @param path Path where the download should be saved.
*/
void saveAs(Path path);
/**
* Returns suggested filename for this download. It is typically computed by the browser from the
* <p>
* {@code Content-Disposition} response header
* <p>
* or the {@code download} attribute. See the spec on whatwg. Different
* <p>
* browsers can use different logic for computing it.
* Returns suggested filename for this download. It is typically computed by the browser from the <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition">{@code Content-Disposition}</a> response
* header or the {@code download} attribute. See the spec on <a
* href="https://html.spec.whatwg.org/#downloading-resources">whatwg</a>. Different browsers can use different logic for
* computing it.
*/
String suggestedFilename();
/**
File diff suppressed because it is too large Load Diff
@@ -16,41 +16,37 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.nio.file.Path;
import java.util.*;
/**
* FileChooser objects are dispatched by the page in the page.on('filechooser') event.
* <p>
* {@code FileChooser} objects are dispatched by the page in the {@link Page#onFileChooser Page.onFileChooser()} event.
* <pre>{@code
* FileChooser fileChooser = page.waitForFileChooser(() -> page.click("upload"));
* fileChooser.setFiles(Paths.get("myfile.pdf"));
* }</pre>
*/
public interface FileChooser {
class FilePayload {
public final String name;
public final String mimeType;
public final byte[] buffer;
public FilePayload(String name, String mimeType, byte[] buffer) {
this.name = name;
this.mimeType = mimeType;
this.buffer = buffer;
}
}
class SetFilesOptions {
/**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can 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}.
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can
* 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;
/**
* Maximum time in milliseconds, defaults to 30 seconds, pass {@code 0} to disable timeout. The default value can be changed by using the browserContext.setDefaultTimeout(timeout) or page.setDefaultTimeout(timeout) methods.
* Maximum time in milliseconds, defaults to 30 seconds, pass {@code 0} to disable timeout. The default value can be changed by
* using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()} or {@link Page#setDefaultTimeout
* Page.setDefaultTimeout()} methods.
*/
public Integer timeout;
public Double timeout;
public SetFilesOptions withNoWaitAfter(Boolean noWaitAfter) {
public SetFilesOptions setNoWaitAfter(boolean noWaitAfter) {
this.noWaitAfter = noWaitAfter;
return this;
}
public SetFilesOptions withTimeout(Integer timeout) {
public SetFilesOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
@@ -67,20 +63,53 @@ public interface FileChooser {
* Returns page this file chooser belongs to.
*/
Page page();
default void setFiles(Path file) { setFiles(file, null); }
default void setFiles(Path file, SetFilesOptions options) { setFiles(new Path[]{ file }, options); }
default void setFiles(Path[] files) { setFiles(files, null); }
void setFiles(Path[] files, SetFilesOptions options);
default void setFiles(FileChooser.FilePayload file) { setFiles(file, null); }
default void setFiles(FileChooser.FilePayload file, SetFilesOptions options) { setFiles(new FileChooser.FilePayload[]{ file }, options); }
default void setFiles(FileChooser.FilePayload[] files) { setFiles(files, null); }
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* <p>
* they are resolved relative to the current working directory.
* <p>
* For empty array, clears the selected files.
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
void setFiles(FileChooser.FilePayload[] files, SetFilesOptions options);
default void setFiles(Path files) {
setFiles(files, null);
}
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
void setFiles(Path files, SetFilesOptions options);
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
default void setFiles(Path[] files) {
setFiles(files, null);
}
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
void setFiles(Path[] files, SetFilesOptions options);
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
default void setFiles(FilePayload files) {
setFiles(files, null);
}
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
void setFiles(FilePayload files, SetFilesOptions options);
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
default void setFiles(FilePayload[] files) {
setFiles(files, null);
}
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
*/
void setFiles(FilePayload[] files, SetFilesOptions options);
}
File diff suppressed because it is too large Load Diff
@@ -19,87 +19,131 @@ package com.microsoft.playwright;
import java.util.*;
/**
* JSHandle represents an in-page JavaScript object. JSHandles can be created with the page.evaluateHandle(pageFunction[, arg]) method.
* <p>
* JSHandle prevents the referenced JavaScript object being garbage collected unless the handle is exposed with
* <p>
* jsHandle.dispose(). JSHandles are auto-disposed when their origin frame gets navigated or the parent context gets
* <p>
* destroyed.
* <p>
* JSHandle instances can be used as an argument in page.$eval(selector, pageFunction[, arg]), page.evaluate(pageFunction[, arg]) and page.evaluateHandle(pageFunction[, arg])
* <p>
* methods.
* JSHandle represents an in-page JavaScript object. JSHandles can be created with the {@link Page#evaluateHandle
* Page.evaluateHandle()} method.
* <pre>{@code
* JSHandle windowHandle = page.evaluateHandle("() => window");
* // ...
* }</pre>
*
* <p> JSHandle prevents the referenced JavaScript object being garbage collected unless the handle is exposed with {@link
* JSHandle#dispose JSHandle.dispose()}. JSHandles are auto-disposed when their origin frame gets navigated or the parent
* context gets destroyed.
*
* <p> JSHandle instances can be used as an argument in {@link Page#evalOnSelector Page.evalOnSelector()}, {@link Page#evaluate
* Page.evaluate()} and {@link Page#evaluateHandle Page.evaluateHandle()} methods.
*/
public interface JSHandle {
/**
* Returns either {@code null} or the object handle itself, if the object handle is an instance of ElementHandle.
* Returns either {@code null} or the object handle itself, if the object handle is an instance of {@code ElementHandle}.
*/
ElementHandle asElement();
/**
* The {@code jsHandle.dispose} method stops referencing the element handle.
*/
void dispose();
default Object evaluate(String pageFunction) {
return evaluate(pageFunction, null);
/**
* Returns the return value of {@code expression}.
*
* <p> This method passes this handle as the first argument to {@code expression}.
*
* <p> If {@code expression} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then
* {@code handle.evaluate} would wait for the promise to resolve and return its value.
*
* <p> Examples:
* <pre>{@code
* ElementHandle tweetHandle = page.querySelector(".tweet .retweets");
* assertEquals("10 retweets", tweetHandle.evaluate("node => node.innerText"));
* }</pre>
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
*/
default Object evaluate(String expression) {
return evaluate(expression, null);
}
/**
* Returns the return value of {@code pageFunction}
* <p>
* This method passes this handle as the first argument to {@code pageFunction}.
* <p>
* If {@code pageFunction} returns a Promise, then {@code handle.evaluate} would wait for the promise to resolve and return its
* <p>
* value.
* <p>
* Examples:
* <p>
*
* @param pageFunction Function to be evaluated in browser context
* @param arg Optional argument to pass to {@code pageFunction}
* Returns the return value of {@code expression}.
*
* <p> This method passes this handle as the first argument to {@code expression}.
*
* <p> If {@code expression} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then
* {@code handle.evaluate} would wait for the promise to resolve and return its value.
*
* <p> Examples:
* <pre>{@code
* ElementHandle tweetHandle = page.querySelector(".tweet .retweets");
* assertEquals("10 retweets", tweetHandle.evaluate("node => node.innerText"));
* }</pre>
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param arg Optional argument to pass to {@code expression}.
*/
Object evaluate(String pageFunction, Object arg);
default JSHandle evaluateHandle(String pageFunction) {
return evaluateHandle(pageFunction, null);
Object evaluate(String expression, Object arg);
/**
* Returns the return value of {@code expression} as a {@code JSHandle}.
*
* <p> This method passes this handle as the first argument to {@code expression}.
*
* <p> The only difference between {@code jsHandle.evaluate} and {@code jsHandle.evaluateHandle} is that {@code jsHandle.evaluateHandle} returns
* {@code JSHandle}.
*
* <p> If the function passed to the {@code jsHandle.evaluateHandle} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then
* {@code jsHandle.evaluateHandle} would wait for the promise to resolve and return its value.
*
* <p> See {@link Page#evaluateHandle Page.evaluateHandle()} for more details.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
*/
default JSHandle evaluateHandle(String expression) {
return evaluateHandle(expression, null);
}
/**
* Returns the return value of {@code pageFunction} as in-page object (JSHandle).
* <p>
* This method passes this handle as the first argument to {@code pageFunction}.
* <p>
* The only difference between {@code jsHandle.evaluate} and {@code jsHandle.evaluateHandle} is that {@code jsHandle.evaluateHandle} returns
* <p>
* in-page object (JSHandle).
* <p>
* If the function passed to the {@code jsHandle.evaluateHandle} returns a Promise, then {@code jsHandle.evaluateHandle} would wait
* <p>
* for the promise to resolve and return its value.
* <p>
* See page.evaluateHandle(pageFunction[, arg]) for more details.
* @param pageFunction Function to be evaluated
* @param arg Optional argument to pass to {@code pageFunction}
* Returns the return value of {@code expression} as a {@code JSHandle}.
*
* <p> This method passes this handle as the first argument to {@code expression}.
*
* <p> The only difference between {@code jsHandle.evaluate} and {@code jsHandle.evaluateHandle} is that {@code jsHandle.evaluateHandle} returns
* {@code JSHandle}.
*
* <p> If the function passed to the {@code jsHandle.evaluateHandle} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then
* {@code jsHandle.evaluateHandle} would wait for the promise to resolve and return its value.
*
* <p> See {@link Page#evaluateHandle Page.evaluateHandle()} for more details.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param arg Optional argument to pass to {@code expression}.
*/
JSHandle evaluateHandle(String pageFunction, Object arg);
JSHandle evaluateHandle(String expression, Object arg);
/**
* The method returns a map with **own property names** as keys and JSHandle instances for the property values.
* <p>
* <pre>{@code
* JSHandle handle = page.evaluateHandle("() => ({window, document}"););
* Map<String, JSHandle> properties = handle.getProperties();
* JSHandle windowHandle = properties.get("window");
* JSHandle documentHandle = properties.get("document");
* handle.dispose();
* }</pre>
*/
Map<String, JSHandle> getProperties();
/**
* Fetches a single property from the referenced object.
*
* @param propertyName property to get
*/
JSHandle getProperty(String propertyName);
/**
* Returns a JSON representation of the object. If the object has a
* <p>
* {@code toJSON}
* <p>
* function, it **will not be called**.
* <p>
* <strong>NOTE</strong> The method will return an empty JSON object if the referenced object is not stringifiable. It will throw an
* <p>
* error if the object has circular references.
* Returns a JSON representation of the object. If the object has a {@code toJSON} function, it **will not be called**.
*
* <p> <strong>NOTE:</strong> The method will return an empty JSON object if the referenced object is not stringifiable. It will throw an error if the
* object has circular references.
*/
Object jsonValue();
}
@@ -16,113 +16,216 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* Keyboard provides an api for managing a virtual keyboard. The high level api is keyboard.type(text[, options]), which takes raw
* <p>
* characters and generates proper keydown, keypress/input, and keyup events on your page.
* <p>
* For finer control, you can use keyboard.down(key), keyboard.up(key), and keyboard.insertText(text) to manually fire
* <p>
* events as if they were generated from a real keyboard.
* <p>
* An example to trigger select-all with the keyboard
* <p>
* Keyboard provides an api for managing a virtual keyboard. The high level api is {@link Keyboard#type Keyboard.type()},
* which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page.
*
* <p> For finer control, you can use {@link Keyboard#down Keyboard.down()}, {@link Keyboard#up Keyboard.up()}, and {@link
* Keyboard#insertText Keyboard.insertText()} to manually fire events as if they were generated from a real keyboard.
*
* <p> An example of holding down {@code Shift} in order to select and delete some text:
* <pre>{@code
* page.keyboard().type("Hello World!");
* page.keyboard().press("ArrowLeft");
* page.keyboard().down("Shift");
* for (int i = 0; i < " World".length(); i++)
* page.keyboard().press("ArrowLeft");
* page.keyboard().up("Shift");
* page.keyboard().press("Backspace");
* // Result text will end up saying "Hello!"
* }</pre>
*
* <p> An example of pressing uppercase {@code A}
* <pre>{@code
* page.keyboard().press("Shift+KeyA");
* // or
* page.keyboard().press("Shift+A");
* }</pre>
*
* <p> An example to trigger select-all with the keyboard
* <pre>{@code
* // on Windows and Linux
* page.keyboard().press("Control+A");
* // on macOS
* page.keyboard().press("Meta+A");
* }</pre>
*/
public interface Keyboard {
enum Modifier { ALT, CONTROL, META, SHIFT }
class PressOptions {
/**
* Time to wait between {@code keydown} and {@code keyup} in milliseconds. Defaults to 0.
*/
public Double delay;
public PressOptions setDelay(double delay) {
this.delay = delay;
return this;
}
}
class TypeOptions {
/**
* Time to wait between key presses in milliseconds. Defaults to 0.
*/
public Double delay;
public TypeOptions setDelay(double delay) {
this.delay = delay;
return this;
}
}
/**
* Dispatches a {@code keydown} event.
* <p>
* {@code key} can specify the intended keyboardEvent.key
* <p>
* value or a single character to generate the text for. A superset of the {@code key} values can be found
* <p>
* here. Examples of the keys are:
* <p>
* {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus}, {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab},
* <p>
*
* <p> {@code key} can specify the intended <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key">keyboardEvent.key</a> value or a single
* character to generate the text for. A superset of the {@code key} values can be found <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a>. Examples of the keys are:
*
* <p> {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus}, {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab},
* {@code Delete}, {@code Escape}, {@code ArrowDown}, {@code End}, {@code Enter}, {@code Home}, {@code Insert}, {@code PageDown}, {@code PageUp}, {@code ArrowRight}, {@code ArrowUp}, etc.
* <p>
* Following modification shortcuts are also suported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code ShiftLeft}.
* <p>
* Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
* <p>
* If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate different respective
* <p>
*
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code ShiftLeft}.
*
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
*
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate different respective
* texts.
* <p>
* If {@code key} is a modifier key, {@code Shift}, {@code Meta}, {@code Control}, or {@code Alt}, subsequent key presses will be sent with that modifier
* <p>
* active. To release the modifier key, use keyboard.up(key).
* <p>
* After the key is pressed once, subsequent calls to keyboard.down(key) will have
* <p>
* repeat set to true. To release the key, use
* <p>
* keyboard.up(key).
* <p>
* <strong>NOTE</strong> Modifier keys DO influence {@code keyboard.down}. Holding down {@code Shift} will type the text in upper case.
*
* <p> If {@code key} is a modifier key, {@code Shift}, {@code Meta}, {@code Control}, or {@code Alt}, subsequent key presses will be sent with that modifier
* active. To release the modifier key, use {@link Keyboard#up Keyboard.up()}.
*
* <p> After the key is pressed once, subsequent calls to {@link Keyboard#down Keyboard.down()} will have <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat">repeat</a> set to true. To release the key,
* use {@link Keyboard#up Keyboard.up()}.
*
* <p> <strong>NOTE:</strong> Modifier keys DO influence {@code keyboard.down}. Holding down {@code Shift} will type the text in upper case.
*
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
*/
void down(String key);
/**
* Dispatches only {@code input} event, does not emit the {@code keydown}, {@code keyup} or {@code keypress} events.
* <p>
*
* <p>
* <strong>NOTE</strong> Modifier keys DO NOT effect {@code keyboard.insertText}. Holding down {@code Shift} will not type the text in upper case.
* <pre>{@code
* page.keyboard().insertText("嗨");
* }</pre>
*
* <p> <strong>NOTE:</strong> Modifier keys DO NOT effect {@code keyboard.insertText}. Holding down {@code Shift} will not type the text in upper case.
*
* @param text Sets input to the specified text value.
*/
void insertText(String text);
default void press(String key) {
press(key, 0);
}
/**
* {@code key} can specify the intended keyboardEvent.key
* <p>
* value or a single character to generate the text for. A superset of the {@code key} values can be found
* <p>
* here. Examples of the keys are:
* <p>
* {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus}, {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab},
* <p>
* {@code key} can specify the intended <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key">keyboardEvent.key</a> value or a single
* character to generate the text for. A superset of the {@code key} values can be found <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a>. Examples of the keys are:
*
* <p> {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus}, {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab},
* {@code Delete}, {@code Escape}, {@code ArrowDown}, {@code End}, {@code Enter}, {@code Home}, {@code Insert}, {@code PageDown}, {@code PageUp}, {@code ArrowRight}, {@code ArrowUp}, etc.
* <p>
* Following modification shortcuts are also suported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code ShiftLeft}.
* <p>
* Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
* <p>
* If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate different respective
* <p>
*
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code ShiftLeft}.
*
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
*
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate different respective
* texts.
* <p>
* Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When speficied with the
* <p>
*
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When speficied with the
* modifier, modifier is pressed and being held while the subsequent key is being pressed.
* <p>
* Shortcut for keyboard.down(key) and keyboard.up(key).
* <pre>{@code
* Page page = browser.newPage();
* page.navigate("https://keycode.info");
* page.keyboard().press("A");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png"));
* page.keyboard().press("ArrowLeft");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("ArrowLeft.png")));
* page.keyboard().press("Shift+O");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("O.png")));
* browser.close();
* }</pre>
*
* <p> Shortcut for {@link Keyboard#down Keyboard.down()} and {@link Keyboard#up Keyboard.up()}.
*
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
*/
void press(String key, int delay);
default void press(String key) {
press(key, null);
}
/**
* {@code key} can specify the intended <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key">keyboardEvent.key</a> value or a single
* character to generate the text for. A superset of the {@code key} values can be found <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a>. Examples of the keys are:
*
* <p> {@code F1} - {@code F12}, {@code Digit0}- {@code Digit9}, {@code KeyA}- {@code KeyZ}, {@code Backquote}, {@code Minus}, {@code Equal}, {@code Backslash}, {@code Backspace}, {@code Tab},
* {@code Delete}, {@code Escape}, {@code ArrowDown}, {@code End}, {@code Enter}, {@code Home}, {@code Insert}, {@code PageDown}, {@code PageUp}, {@code ArrowRight}, {@code ArrowUp}, etc.
*
* <p> Following modification shortcuts are also supported: {@code Shift}, {@code Control}, {@code Alt}, {@code Meta}, {@code ShiftLeft}.
*
* <p> Holding down {@code Shift} will type the text that corresponds to the {@code key} in the upper case.
*
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate different respective
* texts.
*
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When speficied with the
* modifier, modifier is pressed and being held while the subsequent key is being pressed.
* <pre>{@code
* Page page = browser.newPage();
* page.navigate("https://keycode.info");
* page.keyboard().press("A");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png"));
* page.keyboard().press("ArrowLeft");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("ArrowLeft.png")));
* page.keyboard().press("Shift+O");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("O.png")));
* browser.close();
* }</pre>
*
* <p> Shortcut for {@link Keyboard#down Keyboard.down()} and {@link Keyboard#up Keyboard.up()}.
*
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
*/
void press(String key, PressOptions options);
/**
* Sends a {@code keydown}, {@code keypress}/{@code input}, and {@code keyup} event for each character in the text.
*
* <p> To press a special key, like {@code Control} or {@code ArrowDown}, use {@link Keyboard#press Keyboard.press()}.
* <pre>{@code
* // Types instantly
* page.keyboard().type("Hello");
* // Types slower, like a user
* page.keyboard().type("World", new Keyboard.TypeOptions().setDelay(100));
* }</pre>
*
* <p> <strong>NOTE:</strong> Modifier keys DO NOT effect {@code keyboard.type}. Holding down {@code Shift} will not type the text in upper case.
*
* @param text A text to type into a focused element.
*/
default void type(String text) {
type(text, 0);
type(text, null);
}
/**
* Sends a {@code keydown}, {@code keypress}/{@code input}, and {@code keyup} event for each character in the text.
* <p>
* To press a special key, like {@code Control} or {@code ArrowDown}, use keyboard.press(key[, options]).
* <p>
*
* <p>
* <strong>NOTE</strong> Modifier keys DO NOT effect {@code keyboard.type}. Holding down {@code Shift} will not type the text in upper case.
*
* <p> To press a special key, like {@code Control} or {@code ArrowDown}, use {@link Keyboard#press Keyboard.press()}.
* <pre>{@code
* // Types instantly
* page.keyboard().type("Hello");
* // Types slower, like a user
* page.keyboard().type("World", new Keyboard.TypeOptions().setDelay(100));
* }</pre>
*
* <p> <strong>NOTE:</strong> Modifier keys DO NOT effect {@code keyboard.type}. Holding down {@code Shift} will not type the text in upper case.
*
* @param text A text to type into a focused element.
*/
void type(String text, int delay);
void type(String text, TypeOptions options);
/**
* Dispatches a {@code keyup} event.
*
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
*/
void up(String key);
@@ -1,52 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import java.util.*;
/**
* Playwright generates a lot of logs and they are accessible via the pluggable logger sink.
* <p>
*/
public interface Logger {
enum Severity { ERROR, INFO, VERBOSE, WARNING }
class LogHints {
/**
* preferred logger color
*/
public String color;
public LogHints withColor(String color) {
this.color = color;
return this;
}
}
/**
* Determines whether sink is interested in the logger with the given name and severity.
* @param name logger name
*/
boolean isEnabled(String name, Severity severity);
/**
*
* @param name logger name
* @param message log message format
* @param args message arguments
* @param hints optional formatting hints
*/
void log(String name, Severity severity, String message, List<Object> args, LogHints hints);
}
@@ -16,40 +16,48 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* The Mouse class operates in main-frame CSS pixels relative to the top-left corner of the viewport.
* <p>
* Every {@code page} object has its own Mouse, accessible with page.mouse.
* <p>
*
* <p> Every {@code page} object has its own Mouse, accessible with {@link Page#mouse Page.mouse()}.
* <pre>{@code
* // Using page.mouse to trace a 100x100 square.
* page.mouse().move(0, 0);
* page.mouse().down();
* page.mouse().move(0, 100);
* page.mouse().move(100, 100);
* page.mouse().move(100, 0);
* page.mouse().move(0, 0);
* page.mouse().up();
* }</pre>
*/
public interface Mouse {
enum Button { LEFT, MIDDLE, RIGHT }
class ClickOptions {
/**
* Defaults to {@code left}.
*/
public Button button;
public MouseButton button;
/**
* defaults to 1. See UIEvent.detail.
* defaults to 1. See [UIEvent.detail].
*/
public Integer clickCount;
/**
* Time to wait between {@code mousedown} and {@code mouseup} in milliseconds. Defaults to 0.
*/
public Integer delay;
public Double delay;
public ClickOptions withButton(Button button) {
public ClickOptions setButton(MouseButton button) {
this.button = button;
return this;
}
public ClickOptions withClickCount(Integer clickCount) {
public ClickOptions setClickCount(int clickCount) {
this.clickCount = clickCount;
return this;
}
public ClickOptions withDelay(Integer delay) {
public ClickOptions setDelay(double delay) {
this.delay = delay;
return this;
}
@@ -58,17 +66,17 @@ public interface Mouse {
/**
* Defaults to {@code left}.
*/
public Button button;
public MouseButton button;
/**
* Time to wait between {@code mousedown} and {@code mouseup} in milliseconds. Defaults to 0.
*/
public Integer delay;
public Double delay;
public DblclickOptions withButton(Button button) {
public DblclickOptions setButton(MouseButton button) {
this.button = button;
return this;
}
public DblclickOptions withDelay(Integer delay) {
public DblclickOptions setDelay(double delay) {
this.delay = delay;
return this;
}
@@ -77,17 +85,17 @@ public interface Mouse {
/**
* Defaults to {@code left}.
*/
public Button button;
public MouseButton button;
/**
* defaults to 1. See UIEvent.detail.
* defaults to 1. See [UIEvent.detail].
*/
public Integer clickCount;
public DownOptions withButton(Button button) {
public DownOptions setButton(MouseButton button) {
this.button = button;
return this;
}
public DownOptions withClickCount(Integer clickCount) {
public DownOptions setClickCount(int clickCount) {
this.clickCount = clickCount;
return this;
}
@@ -98,7 +106,7 @@ public interface Mouse {
*/
public Integer steps;
public MoveOptions withSteps(Integer steps) {
public MoveOptions setSteps(int steps) {
this.steps = steps;
return this;
}
@@ -107,35 +115,46 @@ public interface Mouse {
/**
* Defaults to {@code left}.
*/
public Button button;
public MouseButton button;
/**
* defaults to 1. See UIEvent.detail.
* defaults to 1. See [UIEvent.detail].
*/
public Integer clickCount;
public UpOptions withButton(Button button) {
public UpOptions setButton(MouseButton button) {
this.button = button;
return this;
}
public UpOptions withClickCount(Integer clickCount) {
public UpOptions setClickCount(int clickCount) {
this.clickCount = clickCount;
return this;
}
}
default void click(int x, int y) {
/**
* Shortcut for {@link Mouse#move Mouse.move()}, {@link Mouse#down Mouse.down()}, {@link Mouse#up Mouse.up()}.
*/
default void click(double x, double y) {
click(x, y, null);
}
/**
* Shortcut for mouse.move(x, y[, options]), mouse.down([options]), mouse.up([options]).
* Shortcut for {@link Mouse#move Mouse.move()}, {@link Mouse#down Mouse.down()}, {@link Mouse#up Mouse.up()}.
*/
void click(int x, int y, ClickOptions options);
default void dblclick(int x, int y) {
void click(double x, double y, ClickOptions options);
/**
* Shortcut for {@link Mouse#move Mouse.move()}, {@link Mouse#down Mouse.down()}, {@link Mouse#up Mouse.up()}, {@link
* Mouse#down Mouse.down()} and {@link Mouse#up Mouse.up()}.
*/
default void dblclick(double x, double y) {
dblclick(x, y, null);
}
/**
* Shortcut for mouse.move(x, y[, options]), mouse.down([options]), mouse.up([options]), mouse.down([options]) and mouse.up([options]).
* Shortcut for {@link Mouse#move Mouse.move()}, {@link Mouse#down Mouse.down()}, {@link Mouse#up Mouse.up()}, {@link
* Mouse#down Mouse.down()} and {@link Mouse#up Mouse.up()}.
*/
void dblclick(double x, double y, DblclickOptions options);
/**
* Dispatches a {@code mousedown} event.
*/
void dblclick(int x, int y, DblclickOptions options);
default void down() {
down(null);
}
@@ -143,13 +162,19 @@ public interface Mouse {
* Dispatches a {@code mousedown} event.
*/
void down(DownOptions options);
default void move(int x, int y) {
/**
* Dispatches a {@code mousemove} event.
*/
default void move(double x, double y) {
move(x, y, null);
}
/**
* Dispatches a {@code mousemove} event.
*/
void move(int x, int y, MoveOptions options);
void move(double x, double y, MoveOptions options);
/**
* Dispatches a {@code mouseup} event.
*/
default void up() {
up(null);
}
File diff suppressed because it is too large Load Diff
@@ -17,22 +17,63 @@
package com.microsoft.playwright;
import com.microsoft.playwright.impl.PlaywrightImpl;
import java.util.*;
import java.util.Map;
/**
* Playwright module provides a method to launch a browser instance. The following is a typical example of using Playwright
* to drive automation:
* <pre>{@code
* import com.microsoft.playwright.*;
*
* public class Example {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType chromium = playwright.chromium();
* Browser browser = chromium.launch();
* Page page = browser.newPage();
* page.navigate("http://example.com");
* // other actions...
* browser.close();
* }
* }
* }
* }</pre>
*/
public interface Playwright extends AutoCloseable {
/**
* This object can be used to launch or connect to Chromium, returning instances of {@code ChromiumBrowser}.
*/
BrowserType chromium();
/**
* This object can be used to launch or connect to Firefox, returning instances of {@code FirefoxBrowser}.
*/
BrowserType firefox();
/**
* Selectors can be used to install custom selector engines. See <a
* href="https://playwright.dev/java/docs/selectors/">Working with selectors</a> for more information.
*/
Selectors selectors();
/**
* This object can be used to launch or connect to WebKit, returning instances of {@code WebKitBrowser}.
*/
BrowserType webkit();
/**
* Terminates this instance of Playwright, will also close all created browsers if they are still running.
*/
void close();
/**
* Launches new Playwright driver process and connects to it. {@link Playwright#close Playwright.close()} should be called
* when the instance is no longer needed.
* <pre>{@code
* Playwright playwright = Playwright.create()) {
* Browser browser = playwright.webkit().launch();
* Page page = browser.newPage();
* page.navigate("https://www.w3.org/");
* playwright.close();
* }</pre>
*/
static Playwright create() {
return PlaywrightImpl.create();
}
BrowserType chromium();
BrowserType firefox();
BrowserType webkit();
Map<String, DeviceDescriptor> devices();
Selectors selectors();
@Override
void close() throws Exception;
}
@@ -16,118 +16,41 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* Whenever the page sends a request for a network resource the following sequence of events are emitted by Page:
* <p>
* page.on('request') emitted when the request is issued by the page.
* <p>
* page.on('response') emitted when/if the response status and headers are received for the request.
* <p>
* page.on('requestfinished') emitted when the response body is downloaded and the request is complete.
* <p>
* If request fails at some point, then instead of {@code 'requestfinished'} event (and possibly instead of 'response' event),
* <p>
* the page.on('requestfailed') event is emitted.
* <p>
* <strong>NOTE</strong> HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request
* <p>
* will complete with {@code 'requestfinished'} event.
* <p>
* If request gets a 'redirect' response, the request is successfully finished with the 'requestfinished' event, and a new
* <p>
* Whenever the page sends a request for a network resource the following sequence of events are emitted by {@code Page}:
* <ul>
* <li> {@link Page#onRequest Page.onRequest()} emitted when the request is issued by the page.</li>
* <li> {@link Page#onResponse Page.onResponse()} emitted when/if the response status and headers are received for the request.</li>
* <li> {@link Page#onRequestFinished Page.onRequestFinished()} emitted when the response body is downloaded and the request is
* complete.</li>
* </ul>
*
* <p> If request fails at some point, then instead of {@code "requestfinished"} event (and possibly instead of 'response' event),
* the {@link Page#onRequestFailed Page.onRequestFailed()} event is emitted.
*
* <p> <strong>NOTE:</strong> HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request will complete
* with {@code "requestfinished"} event.
*
* <p> If request gets a 'redirect' response, the request is successfully finished with the 'requestfinished' event, and a new
* request is issued to a redirected url.
*/
public interface Request {
class RequestFailure {
/**
* Human-readable error message, e.g. {@code 'net::ERR_FAILED'}.
*/
private String errorText;
public RequestFailure(String errorText) {
this.errorText = errorText;
}
public String errorText() {
return this.errorText;
}
}
class RequestTiming {
/**
* Request start time in milliseconds elapsed since January 1, 1970 00:00:00 UTC
*/
private int startTime;
/**
* Time immediately before the browser starts the domain name lookup for the resource. The value is given in milliseconds relative to {@code startTime}, -1 if not available.
*/
private int domainLookupStart;
/**
* Time immediately after the browser starts the domain name lookup for the resource. The value is given in milliseconds relative to {@code startTime}, -1 if not available.
*/
private int domainLookupEnd;
/**
* Time immediately before the user agent starts establishing the connection to the server to retrieve the resource. The value is given in milliseconds relative to {@code startTime}, -1 if not available.
*/
private int connectStart;
/**
* Time immediately before the browser starts the handshake process to secure the current connection. The value is given in milliseconds relative to {@code startTime}, -1 if not available.
*/
private int secureConnectionStart;
/**
* Time immediately before the user agent starts establishing the connection to the server to retrieve the resource. The value is given in milliseconds relative to {@code startTime}, -1 if not available.
*/
private int connectEnd;
/**
* Time immediately before the browser starts requesting the resource from the server, cache, or local resource. The value is given in milliseconds relative to {@code startTime}, -1 if not available.
*/
private int requestStart;
/**
* Time immediately after the browser starts requesting the resource from the server, cache, or local resource. The value is given in milliseconds relative to {@code startTime}, -1 if not available.
*/
private int responseStart;
/**
* Time immediately after the browser receives the last byte of the resource or immediately before the transport connection is closed, whichever comes first. The value is given in milliseconds relative to {@code startTime}, -1 if not available.
*/
private int responseEnd;
public int startTime() {
return this.startTime;
}
public int domainLookupStart() {
return this.domainLookupStart;
}
public int domainLookupEnd() {
return this.domainLookupEnd;
}
public int connectStart() {
return this.connectStart;
}
public int secureConnectionStart() {
return this.secureConnectionStart;
}
public int connectEnd() {
return this.connectEnd;
}
public int requestStart() {
return this.requestStart;
}
public int responseStart() {
return this.responseStart;
}
public int responseEnd() {
return this.responseEnd;
}
}
/**
* The method returns {@code null} unless this request has failed, as reported by {@code requestfailed} event.
* <p>
* Example of logging of all the failed requests:
* <p>
*
* <p> Example of logging of all the failed requests:
* <pre>{@code
* page.onRequestFailed(request -> {
* System.out.println(request.url() + " " + request.failure());
* });
* }</pre>
*/
RequestFailure failure();
String failure();
/**
* Returns the Frame that initiated this request.
* Returns the {@code Frame} that initiated this request.
*/
Frame frame();
/**
@@ -152,47 +75,56 @@ public interface Request {
byte[] postDataBuffer();
/**
* Request that was redirected by the server to this one, if any.
* <p>
* When the server responds with a redirect, Playwright creates a new Request object. The two requests are connected by
* <p>
*
* <p> When the server responds with a redirect, Playwright creates a new {@code Request} object. The two requests are connected by
* {@code redirectedFrom()} and {@code redirectedTo()} methods. When multiple server redirects has happened, it is possible to
* <p>
* construct the whole redirect chain by repeatedly calling {@code redirectedFrom()}.
* <p>
* For example, if the website {@code http://example.com} redirects to {@code https://example.com}:
* <p>
* If the website {@code https://google.com} has no redirects:
* <p>
*
* <p> For example, if the website {@code http://example.com} redirects to {@code https://example.com}:
* <pre>{@code
* Response response = page.navigate("http://example.com");
* System.out.println(response.request().redirectedFrom().url()); // "http://example.com"
* }</pre>
*
* <p> If the website {@code https://google.com} has no redirects:
* <pre>{@code
* Response response = page.navigate("https://google.com");
* System.out.println(response.request().redirectedFrom()); // null
* }</pre>
*/
Request redirectedFrom();
/**
* New request issued by the browser if the server responded with redirect.
* <p>
* This method is the opposite of request.redirectedFrom():
* <p>
*
* <p> This method is the opposite of {@link Request#redirectedFrom Request.redirectedFrom()}:
* <pre>{@code
* System.out.println(request.redirectedFrom().redirectedTo() == request); // true
* }</pre>
*/
Request redirectedTo();
/**
* Contains the request's resource type as it was perceived by the rendering engine. ResourceType will be one of the
* <p>
* following: {@code document}, {@code stylesheet}, {@code image}, {@code media}, {@code font}, {@code script}, {@code texttrack}, {@code xhr}, {@code fetch}, {@code eventsource},
* <p>
* {@code websocket}, {@code manifest}, {@code other}.
*/
String resourceType();
/**
* Returns the matching Response object, or {@code null} if the response was not received due to error.
* Returns the matching {@code Response} object, or {@code null} if the response was not received due to error.
*/
Response response();
/**
* Returns resource timing information for given request. Most of the timing values become available upon the response,
* <p>
* {@code responseEnd} becomes available when request finishes. Find more information at Resource Timing
* <p>
* API.
* <p>
* {@code responseEnd} becomes available when request finishes. Find more information at <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming">Resource Timing API</a>.
* <pre>{@code
* page.onRequestFinished(request -> {
* Timing timing = request.timing();
* System.out.println(timing.responseEnd - timing.startTime);
* });
* page.navigate("http://example.com");
* }</pre>
*/
RequestTiming timing();
Timing timing();
/**
* URL of the request.
*/
@@ -19,7 +19,7 @@ package com.microsoft.playwright;
import java.util.*;
/**
* Response class represents responses which are received by page.
* {@code Response} class represents responses which are received by page.
*/
public interface Response {
/**
@@ -31,7 +31,7 @@ public interface Response {
*/
String finished();
/**
* Returns the Frame that initiated this response.
* Returns the {@code Frame} that initiated this response.
*/
Frame frame();
/**
@@ -43,7 +43,7 @@ public interface Response {
*/
boolean ok();
/**
* Returns the matching Request object.
* Returns the matching {@code Request} object.
*/
Request request();
/**
@@ -21,16 +21,15 @@ import java.nio.file.Path;
import java.util.*;
/**
* Whenever a network route is set up with page.route(url, handler) or browserContext.route(url, handler), the {@code Route} object allows to
* <p>
* handle the route.
* Whenever a network route is set up with {@link Page#route Page.route()} or {@link BrowserContext#route
* BrowserContext.route()}, the {@code Route} object allows to handle the route.
*/
public interface Route {
class ContinueOverrides {
class ResumeOptions {
/**
* If set changes the request URL. New URL must have same protocol as original one.
* If set changes the request HTTP headers. Header values will be converted to a string.
*/
public String url;
public Map<String, String> headers;
/**
* If set changes the request method (e.g. GET or POST)
*/
@@ -38,118 +37,185 @@ public interface Route {
/**
* If set changes the post data of request
*/
public byte[] postData;
public Object postData;
/**
* If set changes the request HTTP headers. Header values will be converted to a string.
* If set changes the request URL. New URL must have same protocol as original one.
*/
public Map<String, String> headers;
public String url;
public ContinueOverrides withUrl(String url) {
this.url = url;
return this;
}
public ContinueOverrides withMethod(String method) {
this.method = method;
return this;
}
public ContinueOverrides withPostData(String postData) {
this.postData = postData.getBytes(StandardCharsets.UTF_8);
return this;
}
public ContinueOverrides withPostData(byte[] postData) {
this.postData = postData;
return this;
}
public ContinueOverrides withHeaders(Map<String, String> headers) {
public ResumeOptions setHeaders(Map<String, String> headers) {
this.headers = headers;
return this;
}
public ResumeOptions setMethod(String method) {
this.method = method;
return this;
}
public ResumeOptions setPostData(String postData) {
this.postData = postData;
return this;
}
public ResumeOptions setPostData(byte[] postData) {
this.postData = postData;
return this;
}
public ResumeOptions setUrl(String url) {
this.url = url;
return this;
}
}
class FulfillResponse {
class FulfillOptions {
/**
* Response status code, defaults to {@code 200}.
* Optional response body as text.
*/
public int status;
public String body;
/**
* Optional response headers. Header values will be converted to a string.
* Optional response body as raw bytes.
*/
public Map<String, String> headers;
public byte[] bodyBytes;
/**
* If set, equals to setting {@code Content-Type} response header.
*/
public String contentType;
/**
* Optional response body.
* Response headers. Header values will be converted to a string.
*/
public String body;
public byte[] bodyBytes;
public Map<String, String> headers;
/**
* Optional file path to respond with. The content type will be inferred from file extension. If {@code path} is a relative path, then it is resolved relative to current working directory.
* File path to respond with. The content type will be inferred from file extension. If {@code path} is a relative path, then it
* is resolved relative to the current working directory.
*/
public Path path;
/**
* Response status code, defaults to {@code 200}.
*/
public Integer status;
public FulfillResponse withStatus(int status) {
this.status = status;
return this;
}
public FulfillResponse withHeaders(Map<String, String> headers) {
this.headers = headers;
return this;
}
public FulfillResponse withContentType(String contentType) {
this.contentType = contentType;
return this;
}
public FulfillResponse withBody(byte[] body) {
this.bodyBytes = body;
return this;
}
public FulfillResponse withBody(String body) {
public FulfillOptions setBody(String body) {
this.body = body;
return this;
}
public FulfillResponse withPath(Path path) {
public FulfillOptions setBodyBytes(byte[] bodyBytes) {
this.bodyBytes = bodyBytes;
return this;
}
public FulfillOptions setContentType(String contentType) {
this.contentType = contentType;
return this;
}
public FulfillOptions setHeaders(Map<String, String> headers) {
this.headers = headers;
return this;
}
public FulfillOptions setPath(Path path) {
this.path = path;
return this;
}
public FulfillOptions setStatus(int status) {
this.status = status;
return this;
}
}
/**
* Aborts the route's request.
*/
default void abort() {
abort(null);
}
/**
* Aborts the route's request.
*
* @param errorCode Optional error code. Defaults to {@code failed}, could be one of the following:
* - {@code 'aborted'} - An operation was aborted (due to user action)
* - {@code 'accessdenied'} - Permission to access a resource, other than the network, was denied
* - {@code 'addressunreachable'} - The IP address is unreachable. This usually means that there is no route to the specified host or network.
* - {@code 'blockedbyclient'} - The client chose to block the request.
* - {@code 'blockedbyresponse'} - The request failed because the response was delivered along with requirements which are not met ('X-Frame-Options' and 'Content-Security-Policy' ancestor checks, for instance).
* - {@code 'connectionaborted'} - A connection timed out as a result of not receiving an ACK for data sent.
* - {@code 'connectionclosed'} - A connection was closed (corresponding to a TCP FIN).
* - {@code 'connectionfailed'} - A connection attempt failed.
* - {@code 'connectionrefused'} - A connection attempt was refused.
* - {@code 'connectionreset'} - A connection was reset (corresponding to a TCP RST).
* - {@code 'internetdisconnected'} - The Internet connection has been lost.
* - {@code 'namenotresolved'} - The host name could not be resolved.
* - {@code 'timedout'} - An operation timed out.
* - {@code 'failed'} - A generic failure occurred.
* <ul>
* <li> {@code "aborted"} - An operation was aborted (due to user action)</li>
* <li> {@code "accessdenied"} - Permission to access a resource, other than the network, was denied</li>
* <li> {@code "addressunreachable"} - The IP address is unreachable. This usually means that there is no route to the specified host
* or network.</li>
* <li> {@code "blockedbyclient"} - The client chose to block the request.</li>
* <li> {@code "blockedbyresponse"} - The request failed because the response was delivered along with requirements which are not met
* ('X-Frame-Options' and 'Content-Security-Policy' ancestor checks, for instance).</li>
* <li> {@code "connectionaborted"} - A connection timed out as a result of not receiving an ACK for data sent.</li>
* <li> {@code "connectionclosed"} - A connection was closed (corresponding to a TCP FIN).</li>
* <li> {@code "connectionfailed"} - A connection attempt failed.</li>
* <li> {@code "connectionrefused"} - A connection attempt was refused.</li>
* <li> {@code "connectionreset"} - A connection was reset (corresponding to a TCP RST).</li>
* <li> {@code "internetdisconnected"} - The Internet connection has been lost.</li>
* <li> {@code "namenotresolved"} - The host name could not be resolved.</li>
* <li> {@code "timedout"} - An operation timed out.</li>
* <li> {@code "failed"} - A generic failure occurred.</li>
* </ul>
*/
void abort(String errorCode);
default void continue_() {
continue_(null);
/**
* Continues route's request with optional overrides.
* <pre>{@code
* page.route("**\/*", route -> {
* // Override headers
* Map<String, String> headers = new HashMap<>(route.request().headers());
* headers.put("foo", "bar"); // set "foo" header
* headers.remove("origin"); // remove "origin" header
* route.resume(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
*/
default void resume() {
resume(null);
}
/**
* Continues route's request with optional overrides.
* <p>
*
* @param overrides Optional request overrides, can override following properties:
* <pre>{@code
* page.route("**\/*", route -> {
* // Override headers
* Map<String, String> headers = new HashMap<>(route.request().headers());
* headers.put("foo", "bar"); // set "foo" header
* headers.remove("origin"); // remove "origin" header
* route.resume(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
*/
void continue_(ContinueOverrides overrides);
void resume(ResumeOptions options);
/**
* Fulfills route's request with given response.
* @param response Response that will fulfill this route's request.
*
* <p> An example of fulfilling all requests with 404 responses:
* <pre>{@code
* page.route("**\/*", route -> {
* route.fulfill(new Route.FulfillOptions()
* .setStatus(404)
* .setContentType("text/plain")
* .setBody("Not Found!"));
* });
* }</pre>
*
* <p> An example of serving static file:
* <pre>{@code
* page.route("**\/xhr_endpoint", route -> route.fulfill(
* new Route.FulfillOptions().setPath(Paths.get("mock_data.json")));
* }</pre>
*/
void fulfill(FulfillResponse response);
default void fulfill() {
fulfill(null);
}
/**
* Fulfills route's request with given response.
*
* <p> An example of fulfilling all requests with 404 responses:
* <pre>{@code
* page.route("**\/*", route -> {
* route.fulfill(new Route.FulfillOptions()
* .setStatus(404)
* .setContentType("text/plain")
* .setBody("Not Found!"));
* });
* }</pre>
*
* <p> An example of serving static file:
* <pre>{@code
* page.route("**\/xhr_endpoint", route -> route.fulfill(
* new Route.FulfillOptions().setPath(Paths.get("mock_data.json")));
* }</pre>
*/
void fulfill(FulfillOptions options);
/**
* A request to be routed.
*/
@@ -20,32 +20,158 @@ import java.nio.file.Path;
import java.util.*;
/**
* Selectors can be used to install custom selector engines. See Working with selectors for more
* <p>
* information.
* Selectors can be used to install custom selector engines. See <a
* href="https://playwright.dev/java/docs/selectors/">Working with selectors</a> for more information.
*/
public interface Selectors {
class RegisterOptions {
/**
* Whether to run this selector engine in isolated JavaScript environment. This environment has access to the same DOM, but not any JavaScript objects from the frame's scripts. Defaults to {@code false}. Note that running as a content script is not guaranteed when this engine is used together with other registered engines.
* Whether to run this selector engine in isolated JavaScript environment. This environment has access to the same DOM, but
* not any JavaScript objects from the frame's scripts. Defaults to {@code false}. Note that running as a content script is not
* guaranteed when this engine is used together with other registered engines.
*/
public Boolean contentScript;
public RegisterOptions withContentScript(Boolean contentScript) {
public RegisterOptions setContentScript(boolean contentScript) {
this.contentScript = contentScript;
return this;
}
}
default void register(String name, String script) { register(name, script, null); }
void register(String name, String script, RegisterOptions options);
default void register(String name, Path path) { register(name, path, null); }
/**
* An example of registering selector engine that queries elements based on a tag name:
* <p>
*
* @param name Name that is used in selectors as a prefix, e.g. {@code {name: 'foo'}} enables {@code foo=myselectorbody} selectors. May only contain {@code [a-zA-Z0-9_]} characters.
* <pre>{@code
* // Script that evaluates to a selector engine instance.
* String createTagNameEngine = "{\n" +
* " // Returns the first element matching given selector in the root's subtree.\n" +
* " query(root, selector) {\n" +
* " return root.querySelector(selector);\n" +
* " },\n" +
* " // Returns all elements matching given selector in the root's subtree.\n" +
* " queryAll(root, selector) {\n" +
* " return Array.from(root.querySelectorAll(selector));\n" +
* " }\n" +
* "}";
* // Register the engine. Selectors will be prefixed with "tag=".
* playwright.selectors().register("tag", createTagNameEngine);
* Browser browser = playwright.firefox().launch();
* Page page = browser.newPage();
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* ElementHandle button = page.querySelector("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* browser.close();
* }</pre>
*
* @param name Name that is used in selectors as a prefix, e.g. {@code {name: 'foo'}} enables {@code foo=myselectorbody} selectors. May only
* contain {@code [a-zA-Z0-9_]} characters.
* @param script Script that evaluates to a selector engine instance.
*/
void register(String name, Path path, RegisterOptions options);
default void register(String name, String script) {
register(name, script, null);
}
/**
* An example of registering selector engine that queries elements based on a tag name:
* <pre>{@code
* // Script that evaluates to a selector engine instance.
* String createTagNameEngine = "{\n" +
* " // Returns the first element matching given selector in the root's subtree.\n" +
* " query(root, selector) {\n" +
* " return root.querySelector(selector);\n" +
* " },\n" +
* " // Returns all elements matching given selector in the root's subtree.\n" +
* " queryAll(root, selector) {\n" +
* " return Array.from(root.querySelectorAll(selector));\n" +
* " }\n" +
* "}";
* // Register the engine. Selectors will be prefixed with "tag=".
* playwright.selectors().register("tag", createTagNameEngine);
* Browser browser = playwright.firefox().launch();
* Page page = browser.newPage();
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* ElementHandle button = page.querySelector("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* browser.close();
* }</pre>
*
* @param name Name that is used in selectors as a prefix, e.g. {@code {name: 'foo'}} enables {@code foo=myselectorbody} selectors. May only
* contain {@code [a-zA-Z0-9_]} characters.
* @param script Script that evaluates to a selector engine instance.
*/
void register(String name, String script, RegisterOptions options);
/**
* An example of registering selector engine that queries elements based on a tag name:
* <pre>{@code
* // Script that evaluates to a selector engine instance.
* String createTagNameEngine = "{\n" +
* " // Returns the first element matching given selector in the root's subtree.\n" +
* " query(root, selector) {\n" +
* " return root.querySelector(selector);\n" +
* " },\n" +
* " // Returns all elements matching given selector in the root's subtree.\n" +
* " queryAll(root, selector) {\n" +
* " return Array.from(root.querySelectorAll(selector));\n" +
* " }\n" +
* "}";
* // Register the engine. Selectors will be prefixed with "tag=".
* playwright.selectors().register("tag", createTagNameEngine);
* Browser browser = playwright.firefox().launch();
* Page page = browser.newPage();
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* ElementHandle button = page.querySelector("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* browser.close();
* }</pre>
*
* @param name Name that is used in selectors as a prefix, e.g. {@code {name: 'foo'}} enables {@code foo=myselectorbody} selectors. May only
* contain {@code [a-zA-Z0-9_]} characters.
* @param script Script that evaluates to a selector engine instance.
*/
default void register(String name, Path script) {
register(name, script, null);
}
/**
* An example of registering selector engine that queries elements based on a tag name:
* <pre>{@code
* // Script that evaluates to a selector engine instance.
* String createTagNameEngine = "{\n" +
* " // Returns the first element matching given selector in the root's subtree.\n" +
* " query(root, selector) {\n" +
* " return root.querySelector(selector);\n" +
* " },\n" +
* " // Returns all elements matching given selector in the root's subtree.\n" +
* " queryAll(root, selector) {\n" +
* " return Array.from(root.querySelectorAll(selector));\n" +
* " }\n" +
* "}";
* // Register the engine. Selectors will be prefixed with "tag=".
* playwright.selectors().register("tag", createTagNameEngine);
* Browser browser = playwright.firefox().launch();
* Page page = browser.newPage();
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* ElementHandle button = page.querySelector("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* browser.close();
* }</pre>
*
* @param name Name that is used in selectors as a prefix, e.g. {@code {name: 'foo'}} enables {@code foo=myselectorbody} selectors. May only
* contain {@code [a-zA-Z0-9_]} characters.
* @param script Script that evaluates to a selector engine instance.
*/
void register(String name, Path script, RegisterOptions options);
}
@@ -16,13 +16,17 @@
package com.microsoft.playwright;
import java.util.*;
/**
* TimeoutError is emitted whenever certain operations are terminated due to timeout, e.g. page.waitForSelector(selector[, options]) or
* <p>
* browserType.launch([options]).
* TimeoutError is emitted whenever certain operations are terminated due to timeout, e.g. {@link Page#waitForSelector
* Page.waitForSelector()} or {@link BrowserType#launch BrowserType.launch()}.
*/
public interface TimeoutError {
public class TimeoutError extends PlaywrightException {
public TimeoutError(String message) {
super(message);
}
public TimeoutError(String message, Throwable exception) {
super(message, exception);
}
}
@@ -20,13 +20,12 @@ import java.util.*;
/**
* The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the
* <p>
* touchscreen can only be used in browser contexts that have been intialized with {@code hasTouch} set to true.
*/
public interface Touchscreen {
/**
* Dispatches a {@code touchstart} and {@code touchend} event with a single touch at the position ({@code x},{@code y}).
*/
void tap(int x, int y);
void tap(double x, double y);
}
@@ -16,18 +16,20 @@
package com.microsoft.playwright;
import java.nio.file.Path;
import java.util.*;
/**
* When browser context is created with the {@code videosPath} option, each page has a video object associated with it.
* <p>
* <pre>{@code
* System.out.println(page.video().path());
* }</pre>
*/
public interface Video {
/**
* Returns the file system path this video will be recorded to. The video is guaranteed to be written to the filesystem
* <p>
* upon closing the browser context.
*/
String path();
Path path();
}
@@ -17,39 +17,90 @@
package com.microsoft.playwright;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* The WebSocket class represents websocket connections in the page.
* The {@code WebSocket} class represents websocket connections in the page.
*/
public interface WebSocket {
interface FrameData {
byte[] body();
String text();
}
class WaitForEventOptions {
public Integer timeout;
public Predicate<Event<EventType>> predicate;
public WaitForEventOptions withTimeout(int millis) {
timeout = millis;
return this;
}
public WaitForEventOptions withPredicate(Predicate<Event<EventType>> predicate) {
/**
* Fired when the websocket closes.
*/
void onClose(Consumer<WebSocket> handler);
/**
* Removes handler that was previously added with {@link #onClose onClose(handler)}.
*/
void offClose(Consumer<WebSocket> handler);
/**
* Fired when the websocket recieves a frame.
*/
void onFrameReceived(Consumer<WebSocketFrame> handler);
/**
* Removes handler that was previously added with {@link #onFrameReceived onFrameReceived(handler)}.
*/
void offFrameReceived(Consumer<WebSocketFrame> handler);
/**
* Fired when the websocket sends a frame.
*/
void onFrameSent(Consumer<WebSocketFrame> handler);
/**
* Removes handler that was previously added with {@link #onFrameSent onFrameSent(handler)}.
*/
void offFrameSent(Consumer<WebSocketFrame> handler);
/**
* Fired when the websocket has an error.
*/
void onSocketError(Consumer<String> handler);
/**
* Removes handler that was previously added with {@link #onSocketError onSocketError(handler)}.
*/
void offSocketError(Consumer<String> handler);
class WaitForFrameReceivedOptions {
/**
* Receives the {@code WebSocketFrame} object and resolves to truthy value when the waiting should resolve.
*/
public Predicate<WebSocketFrame> predicate;
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
public WaitForFrameReceivedOptions setPredicate(Predicate<WebSocketFrame> predicate) {
this.predicate = predicate;
return this;
}
public WaitForFrameReceivedOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class WaitForFrameSentOptions {
/**
* Receives the {@code WebSocketFrame} object and resolves to truthy value when the waiting should resolve.
*/
public Predicate<WebSocketFrame> predicate;
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
enum EventType {
CLOSE,
FRAMERECEIVED,
FRAMESENT,
SOCKETERROR,
public WaitForFrameSentOptions setPredicate(Predicate<WebSocketFrame> predicate) {
this.predicate = predicate;
return this;
}
public WaitForFrameSentOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
void addListener(EventType type, Listener<EventType> listener);
void removeListener(EventType type, Listener<EventType> listener);
/**
* Indicates that the web socket has been closed.
*/
@@ -58,22 +109,41 @@ public interface WebSocket {
* Contains the URL of the WebSocket.
*/
String url();
default Deferred<Event<EventType>> waitForEvent(EventType event) {
return waitForEvent(event, (WaitForEventOptions) null);
}
default Deferred<Event<EventType>> waitForEvent(EventType event, Predicate<Event<EventType>> predicate) {
WaitForEventOptions options = new WaitForEventOptions();
options.predicate = predicate;
return waitForEvent(event, options);
/**
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into the
* {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an error if the
* WebSocket or Page is closed before the frame is received.
*
* @param callback Callback that performs the action triggering the event.
*/
default WebSocketFrame waitForFrameReceived(Runnable callback) {
return waitForFrameReceived(null, callback);
}
/**
* Returns the event data value.
* <p>
* Waits for event to fire and passes its value into the predicate function. Resolves when the predicate returns truthy
* <p>
* value. Will throw an error if the webSocket is closed before the event is fired.
* @param event Event name, same one would pass into {@code webSocket.on(event)}.
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into the
* {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an error if the
* WebSocket or Page is closed before the frame is received.
*
* @param callback Callback that performs the action triggering the event.
*/
Deferred<Event<EventType>> waitForEvent(EventType event, WaitForEventOptions options);
WebSocketFrame waitForFrameReceived(WaitForFrameReceivedOptions options, Runnable callback);
/**
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into the
* {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an error if the
* WebSocket or Page is closed before the frame is sent.
*
* @param callback Callback that performs the action triggering the event.
*/
default WebSocketFrame waitForFrameSent(Runnable callback) {
return waitForFrameSent(null, callback);
}
/**
* Performs action and waits for a frame to be sent. If predicate is provided, it passes {@code WebSocketFrame} value into the
* {@code predicate} function and waits for {@code predicate(webSocketFrame)} to return a truthy value. Will throw an error if the
* WebSocket or Page is closed before the frame is sent.
*
* @param callback Callback that performs the action triggering the event.
*/
WebSocketFrame waitForFrameSent(WaitForFrameSentOptions options, Runnable callback);
}
@@ -0,0 +1,36 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import java.util.*;
/**
* The {@code WebSocketFrame} class represents frames sent over {@code WebSocket} connections in the page. Frame payload is returned by
* either {@link WebSocketFrame#text WebSocketFrame.text()} or {@link WebSocketFrame#binary WebSocketFrame.binary()} method
* depending on the its type.
*/
public interface WebSocketFrame {
/**
* Returns binary payload.
*/
byte[] binary();
/**
* Returns text payload.
*/
String text();
}
@@ -17,59 +17,124 @@
package com.microsoft.playwright;
import java.util.*;
import java.util.function.Consumer;
/**
* The Worker class represents a WebWorker. {@code worker}
* <p>
* event is emitted on the page object to signal a worker creation. {@code close} event is emitted on the worker object when the
* <p>
* worker is gone.
* <p>
* The Worker class represents a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">WebWorker</a>.
* {@code worker} event is emitted on the page object to signal a worker creation. {@code close} event is emitted on the worker object
* when the worker is gone.
* <pre>{@code
* page.onWorker(worker -> {
* System.out.println("Worker created: " + worker.url());
* worker.onClose(worker1 -> System.out.println("Worker destroyed: " + worker1.url()));
* });
* System.out.println("Current workers:");
* for (Worker worker : page.workers())
* System.out.println(" " + worker.url());
* }</pre>
*/
public interface Worker {
enum EventType {
CLOSE,
}
void addListener(EventType type, Listener<EventType> listener);
void removeListener(EventType type, Listener<EventType> listener);
default Object evaluate(String pageFunction) {
return evaluate(pageFunction, null);
/**
* Emitted when this dedicated <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">WebWorker</a> is
* terminated.
*/
void onClose(Consumer<Worker> handler);
/**
* Removes handler that was previously added with {@link #onClose onClose(handler)}.
*/
void offClose(Consumer<Worker> handler);
class WaitForCloseOptions {
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
public WaitForCloseOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
/**
* Returns the return value of {@code pageFunction}
* <p>
* If the function passed to the {@code worker.evaluate} returns a Promise, then {@code worker.evaluate} would wait for the promise
* <p>
* to resolve and return its value.
* <p>
* If the function passed to the {@code worker.evaluate} returns a non-Serializable value, then {@code worker.evaluate} resolves to
* <p>
* {@code undefined}. DevTools Protocol also supports transferring some additional values that are not serializable by {@code JSON}:
* <p>
* {@code -0}, {@code NaN}, {@code Infinity}, {@code -Infinity}, and bigint literals.
* @param pageFunction Function to be evaluated in the worker context
* @param arg Optional argument to pass to {@code pageFunction}
* Returns the return value of {@code expression}.
*
* <p> If the function passed to the {@link Worker#evaluate Worker.evaluate()} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@link
* Worker#evaluate Worker.evaluate()} would wait for the promise to resolve and return its value.
*
* <p> If the function passed to the {@link Worker#evaluate Worker.evaluate()} returns a non-[Serializable] value, then {@link
* Worker#evaluate Worker.evaluate()} returns {@code undefined}. Playwright also supports transferring some additional values
* that are not serializable by {@code JSON}: {@code -0}, {@code NaN}, {@code Infinity}, {@code -Infinity}.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
*/
Object evaluate(String pageFunction, Object arg);
default JSHandle evaluateHandle(String pageFunction) {
return evaluateHandle(pageFunction, null);
default Object evaluate(String expression) {
return evaluate(expression, null);
}
/**
* Returns the return value of {@code pageFunction} as in-page object (JSHandle).
* <p>
* The only difference between {@code worker.evaluate} and {@code worker.evaluateHandle} is that {@code worker.evaluateHandle} returns
* <p>
* in-page object (JSHandle).
* <p>
* If the function passed to the {@code worker.evaluateHandle} returns a Promise, then {@code worker.evaluateHandle} would wait for
* <p>
* the promise to resolve and return its value.
* @param pageFunction Function to be evaluated in the page context
* @param arg Optional argument to pass to {@code pageFunction}
* Returns the return value of {@code expression}.
*
* <p> If the function passed to the {@link Worker#evaluate Worker.evaluate()} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@link
* Worker#evaluate Worker.evaluate()} would wait for the promise to resolve and return its value.
*
* <p> If the function passed to the {@link Worker#evaluate Worker.evaluate()} returns a non-[Serializable] value, then {@link
* Worker#evaluate Worker.evaluate()} returns {@code undefined}. Playwright also supports transferring some additional values
* that are not serializable by {@code JSON}: {@code -0}, {@code NaN}, {@code Infinity}, {@code -Infinity}.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param arg Optional argument to pass to {@code expression}.
*/
JSHandle evaluateHandle(String pageFunction, Object arg);
Object evaluate(String expression, Object arg);
/**
* Returns the return value of {@code expression} as a {@code JSHandle}.
*
* <p> The only difference between {@link Worker#evaluate Worker.evaluate()} and {@link Worker#evaluateHandle
* Worker.evaluateHandle()} is that {@link Worker#evaluateHandle Worker.evaluateHandle()} returns {@code JSHandle}.
*
* <p> If the function passed to the {@link Worker#evaluateHandle Worker.evaluateHandle()} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@link
* Worker#evaluateHandle Worker.evaluateHandle()} would wait for the promise to resolve and return its value.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
*/
default JSHandle evaluateHandle(String expression) {
return evaluateHandle(expression, null);
}
/**
* Returns the return value of {@code expression} as a {@code JSHandle}.
*
* <p> The only difference between {@link Worker#evaluate Worker.evaluate()} and {@link Worker#evaluateHandle
* Worker.evaluateHandle()} is that {@link Worker#evaluateHandle Worker.evaluateHandle()} returns {@code JSHandle}.
*
* <p> If the function passed to the {@link Worker#evaluateHandle Worker.evaluateHandle()} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, then {@link
* Worker#evaluateHandle Worker.evaluateHandle()} would wait for the promise to resolve and return its value.
*
* @param expression JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted
* as a function. Otherwise, evaluated as an expression.
* @param arg Optional argument to pass to {@code expression}.
*/
JSHandle evaluateHandle(String expression, Object arg);
String url();
Deferred<Event<EventType>> waitForEvent(EventType event);
/**
* Performs action and waits for the Worker to close.
*
* @param callback Callback that performs the action triggering the event.
*/
default Worker waitForClose(Runnable callback) {
return waitForClose(null, callback);
}
/**
* Performs action and waits for the Worker to close.
*
* @param callback Callback that performs the action triggering the event.
*/
Worker waitForClose(WaitForCloseOptions options, Runnable callback);
}
@@ -1,44 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Accessibility;
import com.microsoft.playwright.AccessibilityNode;
import static com.microsoft.playwright.impl.Serialization.gson;
class AccessibilityImpl implements Accessibility {
private final PageImpl page;
AccessibilityImpl(PageImpl page) {
this.page = page;
}
@Override
public AccessibilityNode snapshot(SnapshotOptions options) {
if (options == null) {
options = new SnapshotOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
JsonObject json = page.sendMessage("accessibilitySnapshot", params).getAsJsonObject();
if (!json.has("rootAXNode")) {
return null;
}
return new AccessibilityNodeImpl(json.getAsJsonObject("rootAXNode"));
}
}
@@ -1,258 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.AccessibilityNode;
import java.util.ArrayList;
import java.util.List;
class AccessibilityNodeImpl implements AccessibilityNode {
private final JsonObject json;
AccessibilityNodeImpl(JsonObject json) {
this.json = json;
}
@Override
public String role() {
return json.get("role").getAsString();
}
@Override
public String name() {
return json.get("name").getAsString();
}
@Override
public String valueString() {
if (!json.has("valueString")) {
return null;
}
return json.get("valueString").getAsString();
}
@Override
public Double valueNumber() {
if (!json.has("valueNumber")) {
return null;
}
return json.get("valueNumber").getAsDouble();
}
@Override
public String description() {
if (!json.has("description")) {
return null;
}
return json.get("description").getAsString();
}
@Override
public String keyshortcuts() {
if (!json.has("keyshortcuts")) {
return null;
}
return json.get("keyshortcuts").getAsString();
}
@Override
public String roledescription() {
if (!json.has("roledescription")) {
return null;
}
return json.get("roledescription").getAsString();
}
@Override
public String valuetext() {
if (!json.has("valuetext")) {
return null;
}
return json.get("valuetext").getAsString();
}
@Override
public Boolean disabled() {
if (!json.has("disabled")) {
return null;
}
return json.get("disabled").getAsBoolean();
}
@Override
public Boolean expanded() {
if (!json.has("expanded")) {
return null;
}
return json.get("expanded").getAsBoolean();
}
@Override
public Boolean focused() {
if (!json.has("focused")) {
return null;
}
return json.get("focused").getAsBoolean();
}
@Override
public Boolean modal() {
if (!json.has("modal")) {
return null;
}
return json.get("modal").getAsBoolean();
}
@Override
public Boolean multiline() {
if (!json.has("multiline")) {
return null;
}
return json.get("multiline").getAsBoolean();
}
@Override
public Boolean multiselectable() {
if (!json.has("multiselectable")) {
return null;
}
return json.get("multiselectable").getAsBoolean();
}
@Override
public Boolean readonly() {
if (!json.has("readonly")) {
return null;
}
return json.get("readonly").getAsBoolean();
}
@Override
public Boolean required() {
if (!json.has("required")) {
return null;
}
return json.get("required").getAsBoolean();
}
@Override
public Boolean selected() {
if (!json.has("selected")) {
return null;
}
return json.get("selected").getAsBoolean();
}
@Override
public CheckedState checked() {
if (!json.has("checked")) {
return null;
}
String value = json.get("checked").getAsString();
switch (value) {
case "checked": return CheckedState.CHECKED;
case "unchecked": return CheckedState.UNCHECKED;
case "mixed": return CheckedState.MIXED;
default: throw new IllegalStateException("Unexpected value: " + value);
}
}
@Override
public PressedState pressed() {
if (!json.has("pressed")) {
return null;
}
String value = json.get("pressed").getAsString();
switch (value) {
case "pressed": return PressedState.PRESSED;
case "released": return PressedState.RELEASED;
case "mixed": return PressedState.MIXED;
default: throw new IllegalStateException("Unexpected value: " + value);
}
}
@Override
public Integer level() {
if (!json.has("level")) {
return null;
}
return json.get("level").getAsInt();
}
@Override
public Double valuemin() {
if (!json.has("valuemin")) {
return null;
}
return json.get("valuemin").getAsDouble();
}
@Override
public Double valuemax() {
if (!json.has("valuemax")) {
return null;
}
return json.get("valuemax").getAsDouble();
}
@Override
public String autocomplete() {
if (!json.has("autocomplete")) {
return null;
}
return json.get("autocomplete").getAsString();
}
@Override
public String haspopup() {
if (!json.has("haspopup")) {
return null;
}
return json.get("haspopup").getAsString();
}
@Override
public String invalid() {
if (!json.has("invalid")) {
return null;
}
return json.get("invalid").getAsString();
}
@Override
public String orientation() {
if (!json.has("orientation")) {
return null;
}
return json.get("orientation").getAsString();
}
@Override
public List<AccessibilityNode> children() {
if (!json.has("children")) {
return null;
}
List<AccessibilityNode> result = new ArrayList<>();
for (JsonElement e : json.getAsJsonArray("children")) {
result.add(new AccessibilityNodeImpl(e.getAsJsonObject()));
}
return result;
}
}
@@ -22,6 +22,7 @@ import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.JSHandle;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.BindingCallback;
import java.util.ArrayList;
import java.util.List;
@@ -29,7 +30,7 @@ import java.util.List;
import static com.microsoft.playwright.impl.Serialization.*;
class BindingCall extends ChannelOwner {
private static class SourceImpl implements Page.Binding.Source {
private static class SourceImpl implements BindingCallback.Source {
private final Frame frame;
public SourceImpl(Frame frame) {
@@ -60,10 +61,10 @@ class BindingCall extends ChannelOwner {
return initializer.get("name").getAsString();
}
void call(Page.Binding binding) {
void call(BindingCallback binding) {
try {
Frame frame = connection.getExistingObject(initializer.getAsJsonObject("frame").get("guid").getAsString());
Page.Binding.Source source = new SourceImpl(frame);
BindingCallback.Source source = new SourceImpl(frame);
List<Object> args = new ArrayList<>();
if (initializer.has("handle")) {
JSHandle handle = connection.getExistingObject(initializer.getAsJsonObject("handle").get("guid").getAsString());
@@ -20,31 +20,40 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.isFunctionBody;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
class BrowserContextImpl extends ChannelOwner implements BrowserContext {
private final BrowserImpl browser;
final List<PageImpl> pages = new ArrayList<>();
final Router routes = new Router();
private boolean isClosedOrClosing;
final Map<String, Page.Binding> bindings = new HashMap<>();
final Map<String, BindingCallback> bindings = new HashMap<>();
PageImpl ownerPage;
private final ListenerCollection<EventType> listeners = new ListenerCollection<>();
final TimeoutSettings timeoutSettings = new TimeoutSettings();
Path videosDir;
protected BrowserContextImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
enum EventType {
CLOSE,
PAGE,
}
BrowserContextImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
if (parent instanceof BrowserImpl) {
browser = (BrowserImpl) parent;
@@ -54,17 +63,52 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public void addListener(EventType type, Listener<EventType> listener) {
listeners.add(type, listener);
public void onClose(Consumer<BrowserContext> handler) {
listeners.add(EventType.CLOSE, handler);
}
@Override
public void removeListener(EventType type, Listener<EventType> listener) {
listeners.remove(type, listener);
public void offClose(Consumer<BrowserContext> handler) {
listeners.remove(EventType.CLOSE, handler);
}
@Override
public void onPage(Consumer<Page> handler) {
listeners.add(EventType.PAGE, handler);
}
@Override
public void offPage(Consumer<Page> handler) {
listeners.remove(EventType.PAGE, handler);
}
private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Double timeout) {
List<Waitable<T>> waitables = new ArrayList<>();
waitables.add(new WaitableEvent<>(listeners, eventType));
waitables.add(new WaitableContextClose<>());
waitables.add(timeoutSettings.createWaitable(timeout));
return runUntil(code, new WaitableRace<>(waitables));
}
@Override
public Page waitForPage(WaitForPageOptions options, Runnable code) {
if (options == null) {
options = new WaitForPageOptions();
}
return waitForEventWithTimeout(EventType.PAGE, code, options.timeout);
}
@Override
public void close() {
withLogging("BrowserContext.close", () -> closeImpl());
}
@Override
public List<Cookie> cookies(String url) {
return cookies(url == null ? emptyList() : asList(url));
}
private void closeImpl() {
if (isClosedOrClosing) {
return;
}
@@ -79,19 +123,33 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public void addCookies(List<AddCookie> cookies) {
JsonObject params = new JsonObject();
params.add("cookies", gson().toJsonTree(cookies));
sendMessage("addCookies", params);
public void addCookies(List<Cookie> cookies) {
withLogging("BrowserContext.addCookies", () -> {
JsonObject params = new JsonObject();
params.add("cookies", gson().toJsonTree(cookies));
sendMessage("addCookies", params);
});
}
@Override
public void addInitScript(String script, Object arg) {
// TODO: serialize arg
public void addInitScript(String script) {
withLogging("BrowserContext.addInitScript", () -> addInitScriptImpl(script));
}
@Override
public void addInitScript(Path path) {
withLogging("BrowserContext.addInitScript", () -> {
try {
byte[] bytes = readAllBytes(path);
addInitScriptImpl(new String(bytes, UTF_8));
} catch (IOException e) {
throw new PlaywrightException("Failed to read script from file", e);
}
});
}
private void addInitScriptImpl(String script) {
JsonObject params = new JsonObject();
if (isFunctionBody(script)) {
script = "(" + script + ")()";
}
params.addProperty("source", script);
sendMessage("addInitScript", params);
}
@@ -103,28 +161,36 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public void clearCookies() {
sendMessage("clearCookies");
withLogging("BrowserContext.clearCookies", () -> sendMessage("clearCookies"));
}
@Override
public void clearPermissions() {
sendMessage("clearPermissions");
withLogging("BrowserContext.clearPermissions", () -> sendMessage("clearPermissions"));
}
@Override
public List<Cookie> cookies(List<String> urls) {
return withLogging("BrowserContext.cookies", () -> cookiesImpl(urls));
}
private List<Cookie> cookiesImpl(List<String> urls) {
JsonObject params = new JsonObject();
if (urls == null) {
urls = Collections.emptyList();
urls = emptyList();
}
params.add("urls", gson().toJsonTree(urls));
JsonObject json = sendMessage("cookies", params).getAsJsonObject();
Cookie[] cookies = gson().fromJson(json.getAsJsonArray("cookies"), Cookie[].class);
return Arrays.asList(cookies);
return asList(cookies);
}
@Override
public void exposeBinding(String name, Page.Binding playwrightBinding, ExposeBindingOptions options) {
public void exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
withLogging("BrowserContext.exposeBinding", () -> exposeBindingImpl(name, playwrightBinding, options));
}
private void exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
if (bindings.containsKey(name)) {
throw new PlaywrightException("Function \"" + name + "\" has been already registered");
}
@@ -144,17 +210,22 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public void exposeFunction(String name, Page.Function playwrightFunction) {
exposeBinding(name, (Page.Binding.Source source, Object... args) -> playwrightFunction.call(args));
public void exposeFunction(String name, FunctionCallback playwrightFunction) {
withLogging("BrowserContext.exposeFunction",
() -> exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args), null));
}
@Override
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) {
options = new GrantPermissionsOptions();
}
if (permissions == null) {
permissions = Collections.emptyList();
permissions = emptyList();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.add("permissions", gson().toJsonTree(permissions));
@@ -163,6 +234,10 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public PageImpl newPage() {
return withLogging("BrowserContext.newPage", () -> newPageImpl());
}
private PageImpl newPageImpl() {
if (ownerPage != null) {
throw new PlaywrightException("Please use browser.newContext()");
}
@@ -191,75 +266,82 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
private void route(UrlMatcher matcher, Consumer<Route> handler) {
routes.add(matcher, handler);
if (routes.size() == 1) {
withLogging("BrowserContext.route", () -> {
routes.add(matcher, handler);
if (routes.size() == 1) {
JsonObject params = new JsonObject();
params.addProperty("enabled", true);
sendMessage("setNetworkInterceptionEnabled", params);
}
});
}
@Override
public void setDefaultNavigationTimeout(double timeout) {
withLogging("BrowserContext.setDefaultNavigationTimeout", () -> {
timeoutSettings.setDefaultNavigationTimeout(timeout);
JsonObject params = new JsonObject();
params.addProperty("enabled", true);
sendMessage("setNetworkInterceptionEnabled", params);
}
params.addProperty("timeout", timeout);
sendMessage("setDefaultNavigationTimeoutNoReply", params);
});
}
@Override
public void setDefaultNavigationTimeout(int timeout) {
timeoutSettings.setDefaultNavigationTimeout(timeout);
JsonObject params = new JsonObject();
params.addProperty("timeout", timeout);
sendMessage("setDefaultNavigationTimeoutNoReply", params);
}
@Override
public void setDefaultTimeout(int timeout) {
timeoutSettings.setDefaultTimeout(timeout);
JsonObject params = new JsonObject();
params.addProperty("timeout", timeout);
sendMessage("setDefaultTimeoutNoReply", params);
public void setDefaultTimeout(double timeout) {
withLogging("BrowserContext.setDefaultTimeout", () -> {
timeoutSettings.setDefaultTimeout(timeout);
JsonObject params = new JsonObject();
params.addProperty("timeout", timeout);
sendMessage("setDefaultTimeoutNoReply", params);
});
}
@Override
public void setExtraHTTPHeaders(Map<String, String> headers) {
JsonObject params = new JsonObject();
JsonArray jsonHeaders = new JsonArray();
for (Map.Entry<String, String> e : headers.entrySet()) {
JsonObject header = new JsonObject();
header.addProperty("name", e.getKey());
header.addProperty("value", e.getValue());
jsonHeaders.add(header);
}
params.add("headers", jsonHeaders);
sendMessage("setExtraHTTPHeaders", params);
withLogging("BrowserContext.setExtraHTTPHeaders", () -> {
JsonObject params = new JsonObject();
JsonArray jsonHeaders = new JsonArray();
for (Map.Entry<String, String> e : headers.entrySet()) {
JsonObject header = new JsonObject();
header.addProperty("name", e.getKey());
header.addProperty("value", e.getValue());
jsonHeaders.add(header);
}
params.add("headers", jsonHeaders);
sendMessage("setExtraHTTPHeaders", params);
});
}
@Override
public void setGeolocation(Geolocation geolocation) {
JsonObject params = new JsonObject();
if (geolocation != null) {
params.add("geolocation", gson().toJsonTree(geolocation));
}
sendMessage("setGeolocation", params);
withLogging("BrowserContext.setGeolocation", () -> {
JsonObject params = new JsonObject();
if (geolocation != null) {
params.add("geolocation", gson().toJsonTree(geolocation));
}
sendMessage("setGeolocation", params);
});
}
@Override
public void setOffline(boolean offline) {
JsonObject params = new JsonObject();
params.addProperty("offline", offline);
sendMessage("setOffline", params);
withLogging("BrowserContext.setOffline", () -> {
JsonObject params = new JsonObject();
params.addProperty("offline", offline);
sendMessage("setOffline", params);
});
}
@Override
public StorageState storageState(StorageStateOptions options) {
JsonElement json = sendMessage("storageState");
StorageState storageState = gson().fromJson(json, StorageState.class);
if (options != null && options.path != null) {
try {
Files.createDirectories(options.path.getParent());
try (FileWriter writer = new FileWriter(options.path.toFile())) {
writer.write(json.toString());
}
} catch (IOException e) {
throw new PlaywrightException("Failed to write storage state to file", e);
public String storageState(StorageStateOptions options) {
return withLogging("BrowserContext.storageState", () -> {
JsonElement json = sendMessage("storageState");
String storageState = json.toString();
if (options != null && options.path != null) {
Utils.writeToFile(storageState.getBytes(StandardCharsets.UTF_8), options.path);
}
}
return storageState;
return storageState;
});
}
@Override
@@ -277,24 +359,30 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
unroute(new UrlMatcher(url), handler);
}
@Override
public Deferred<Event<EventType>> waitForEvent(EventType event, WaitForEventOptions options) {
if (options == null) {
options = new WaitForEventOptions();
private class WaitableContextClose<R> extends WaitableEvent<EventType, R> {
WaitableContextClose() {
super(BrowserContextImpl.this.listeners, EventType.CLOSE);
}
@Override
public R get() {
throw new PlaywrightException("Context closed");
}
List<Waitable<Event<EventType>>> waitables = new ArrayList<>();
waitables.add(new WaitableEvent<>(listeners, event, options.predicate));
waitables.add(timeoutSettings.createWaitable(options.timeout));
return toDeferred(new WaitableRace<>(waitables));
}
private void unroute(UrlMatcher matcher, Consumer<Route> handler) {
routes.remove(matcher, handler);
if (routes.size() == 0) {
JsonObject params = new JsonObject();
params.addProperty("enabled", false);
sendMessage("setNetworkInterceptionEnabled", params);
}
withLogging("BrowserContext.unroute", () -> {
routes.remove(matcher, handler);
if (routes.size() == 0) {
JsonObject params = new JsonObject();
params.addProperty("enabled", false);
sendMessage("setNetworkInterceptionEnabled", params);
}
});
}
void pause() {
sendMessage("pause");
}
@Override
@@ -303,7 +391,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
Route route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
boolean handled = routes.handle(route);
if (!handled) {
route.continue_();
route.resume();
}
} else if ("page".equals(event)) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
@@ -311,16 +399,20 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
pages.add(page);
} else if ("bindingCall".equals(event)) {
BindingCall bindingCall = connection.getExistingObject(params.getAsJsonObject("binding").get("guid").getAsString());
Page.Binding binding = bindings.get(bindingCall.name());
BindingCallback binding = bindings.get(bindingCall.name());
if (binding != null) {
bindingCall.call(binding);
}
} else if ("close".equals(event)) {
isClosedOrClosing = true;
if (browser != null) {
browser.contexts.remove(this);
}
listeners.notify(EventType.CLOSE, null);
didClose();
}
}
void didClose() {
isClosedOrClosing = true;
if (browser != null) {
browser.contexts.remove(this);
}
listeners.notify(EventType.CLOSE, this);
}
}
@@ -16,43 +16,66 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.PlaywrightException;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
class BrowserImpl extends ChannelOwner implements Browser {
final Set<BrowserContext> contexts = new HashSet<>();
final Set<BrowserContextImpl> contexts = new HashSet<>();
private final ListenerCollection<EventType> listeners = new ListenerCollection<>();
public boolean isRemote;
private boolean isConnected = true;
enum EventType {
DISCONNECTED,
}
BrowserImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@Override
public void addListener(EventType type, Listener<EventType> listener) {
listeners.add(type, listener);
public void onDisconnected(Consumer<Browser> handler) {
listeners.add(EventType.DISCONNECTED, handler);
}
@Override
public void removeListener(EventType type, Listener<EventType> listener) {
listeners.remove(type, listener);
public void offDisconnected(Consumer<Browser> handler) {
listeners.remove(EventType.DISCONNECTED, handler);
}
@Override
public void close() {
withLogging("Browser.close", () -> closeImpl());
}
private void closeImpl() {
if (isRemote) {
try {
connection.close();
} catch (IOException e) {
throw new PlaywrightException("Failed to close browser connection", e);
}
notifyRemoteClosed();
return;
}
try {
sendMessage("close");
} catch (PlaywrightException e) {
@@ -62,6 +85,17 @@ class BrowserImpl extends ChannelOwner implements Browser {
}
}
void notifyRemoteClosed() {
// Emulate all pages, contexts and the browser closing upon disconnect.
for (BrowserContextImpl context : new ArrayList<>(contexts)) {
for (PageImpl page : new ArrayList<>(context.pages)) {
page.didClose();
}
context.didClose();
}
didClose();
}
@Override
public List<BrowserContext> contexts() {
return new ArrayList<>(contexts);
@@ -74,30 +108,81 @@ class BrowserImpl extends ChannelOwner implements Browser {
@Override
public BrowserContextImpl newContext(NewContextOptions options) {
return withLogging("Browser.newContext", () -> newContextImpl(options));
}
private BrowserContextImpl newContextImpl(NewContextOptions options) {
if (options == null) {
options = new NewContextOptions();
}
if (options.storageStatePath != null) {
try (FileReader reader = new FileReader(options.storageStatePath.toFile())) {
options.storageState = gson().fromJson(reader, BrowserContext.StorageState.class);
try {
byte[] bytes = Files.readAllBytes(options.storageStatePath);
options.storageState = new String(bytes, StandardCharsets.UTF_8);
options.storageStatePath = null;
} catch (IOException e) {
throw new PlaywrightException("Failed to read storage state from file", e);
}
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
if (options.extraHTTPHeaders != null) {
params.remove("extraHTTPHeaders");
params.add("extraHTTPHeaders", Serialization.toProtocol(options.extraHTTPHeaders));
JsonObject storageState = null;
if (options.storageState != null) {
storageState = new Gson().fromJson(options.storageState, JsonObject.class);
options.storageState = null;
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
if (storageState != null) {
params.add("storageState", storageState);
}
if (options.recordHarPath != null) {
JsonObject recordHar = new JsonObject();
recordHar.addProperty("path", options.recordHarPath.toString());
if (options.recordHarOmitContent != null) {
recordHar.addProperty("omitContent", true);
}
params.remove("recordHarPath");
params.remove("recordHarOmitContent");
params.add("recordHar", recordHar);
} else if (options.recordHarOmitContent != null) {
throw new PlaywrightException("recordHarOmitContent is set but recordHarPath is null");
}
if (options.recordVideoDir != null) {
JsonObject recordVideo = new JsonObject();
recordVideo.addProperty("dir", options.recordVideoDir.toString());
if (options.recordVideoSize != null) {
recordVideo.add("size", gson().toJsonTree(options.recordVideoSize));
}
params.remove("recordVideoDir");
params.remove("recordVideoSize");
params.add("recordVideo", recordVideo);
} else if (options.recordVideoSize != null) {
throw new PlaywrightException("recordVideoSize is set but recordVideoDir is null");
}
if (options.viewportSize != null) {
if (options.viewportSize.isPresent()) {
JsonElement size = params.get("viewportSize");
params.remove("viewportSize");
params.add("viewport", size);
} else {
params.remove("viewportSize");
params.addProperty("noDefaultViewport", true);
}
}
params.addProperty("sdkLanguage", "java");
JsonElement result = sendMessage("newContext", params);
BrowserContextImpl context = connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("context").get("guid").getAsString());
if (options.recordVideoDir != null) {
context.videosDir = options.recordVideoDir;
}
contexts.add(context);
return context;
}
@Override
public Page newPage(NewPageOptions options) {
return withLogging("Browser.newPage", () -> newPageImpl(options));
}
private Page newPageImpl(NewPageOptions options) {
BrowserContextImpl context = newContext(convertViaJson(options, NewContextOptions.class));
PageImpl page = context.newPage();
page.ownedContext = context;
@@ -121,8 +206,12 @@ class BrowserImpl extends ChannelOwner implements Browser {
@Override
void handleEvent(String event, JsonObject parameters) {
if ("close".equals(event)) {
isConnected = false;
listeners.notify(EventType.DISCONNECTED, null);
didClose();
}
}
private void didClose() {
isConnected = false;
listeners.notify(EventType.DISCONNECTED, this);
}
}
@@ -18,10 +18,16 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.PlaywrightException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.function.Consumer;
import static com.microsoft.playwright.impl.Serialization.gson;
@@ -32,6 +38,10 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
@Override
public BrowserImpl launch(LaunchOptions options) {
return withLogging("BrowserType.launch", () -> launchImpl(options));
}
private BrowserImpl launchImpl(LaunchOptions options) {
if (options == null) {
options = new LaunchOptions();
}
@@ -40,24 +50,104 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
return connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("browser").get("guid").getAsString());
}
@Override
public Browser connect(String wsEndpoint, ConnectOptions options) {
return withLogging("BrowserType.connect", () -> connectImpl(wsEndpoint, options));
}
private Browser connectImpl(String wsEndpoint, ConnectOptions options) {
try {
Duration timeout = Duration.ofDays(1);
if (options != null && options.timeout != null) {
timeout = Duration.ofMillis(Math.round(options.timeout));
}
WebSocketTransport transport = new WebSocketTransport(new URI(wsEndpoint), timeout);
Connection connection = new Connection(transport);
RemoteBrowser remoteBrowser = (RemoteBrowser) connection.waitForObjectWithKnownName("remoteBrowser");
PlaywrightImpl playwright = this.connection.getExistingObject("Playwright");
SelectorsImpl selectors = remoteBrowser.selectors();
playwright.sharedSelectors.addChannel(selectors);
BrowserImpl browser = remoteBrowser.browser();
browser.isRemote = true;
Consumer<WebSocketTransport> connectionCloseListener = new Consumer<WebSocketTransport>() {
@Override
public void accept(WebSocketTransport t) {
browser.notifyRemoteClosed();
}
};
transport.onClose(connectionCloseListener);
browser.onDisconnected(b -> {
playwright.sharedSelectors.removeChannel(selectors);
transport.offClose(connectionCloseListener);
try {
connection.close();
} catch (IOException e) {
e.printStackTrace(System.err);
}
});
return browser;
} catch (URISyntaxException e) {
throw new PlaywrightException("Failed to connect", e);
}
}
public String executablePath() {
return initializer.get("executablePath").getAsString();
}
@Override
public BrowserContext 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) {
options = new LaunchPersistentContextOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
if (options.extraHTTPHeaders != null) {
params.remove("extraHTTPHeaders");
params.add("extraHTTPHeaders", Serialization.toProtocol(options.extraHTTPHeaders));
}
params.addProperty("userDataDir", userDataDir.toString());
if (options.recordHarPath != null) {
JsonObject recordHar = new JsonObject();
recordHar.addProperty("path", options.recordHarPath.toString());
if (options.recordHarOmitContent != null) {
recordHar.addProperty("omitContent", true);
}
params.remove("recordHarPath");
params.remove("recordHarOmitContent");
params.add("recordHar", recordHar);
} else if (options.recordHarOmitContent != null) {
throw new PlaywrightException("recordHarOmitContent is set but recordHarPath is null");
}
if (options.recordVideoDir != null) {
JsonObject recordVideo = new JsonObject();
recordVideo.addProperty("dir", options.recordVideoDir.toString());
if (options.recordVideoSize != null) {
recordVideo.add("size", gson().toJsonTree(options.recordVideoSize));
}
params.remove("recordVideoDir");
params.remove("recordVideoSize");
params.add("recordVideo", recordVideo);
} else if (options.recordVideoSize != null) {
throw new PlaywrightException("recordVideoSize is set but recordVideoDir is null");
}
if (options.viewportSize != null) {
if (options.viewportSize.isPresent()) {
JsonElement size = params.get("viewportSize");
params.remove("viewportSize");
params.add("viewport", size);
} else {
params.remove("viewportSize");
params.addProperty("noDefaultViewport", true);
}
}
params.addProperty("sdkLanguage", "java");
JsonObject json = sendMessage("launchPersistentContext", params).getAsJsonObject();
return connection.getExistingObject(json.getAsJsonObject("context").get("guid").getAsString());
BrowserContextImpl context = connection.getExistingObject(json.getAsJsonObject("context").get("guid").getAsString());
if (options.recordVideoDir != null) {
context.videosDir = options.recordVideoDir;
}
return context;
}
public String name() {
@@ -18,13 +18,12 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Deferred;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
class ChannelOwner {
class ChannelOwner extends LoggingSupport {
final Connection connection;
private final ChannelOwner parent;
private final Map<String, ChannelOwner> objects = new HashMap<>();
@@ -80,22 +79,16 @@ class ChannelOwner {
return connection.sendMessage(guid, method, params);
}
void sendMessageNoWait(String method) {
sendMessageNoWait(method, new JsonObject());
}
void sendMessageNoWait(String method, JsonObject params) {
connection.sendMessageNoWait(guid, method, params);
}
@SuppressWarnings("unchecked")
<T> Deferred<T> toDeferred(Waitable waitable) {
return () -> {
<T> T runUntil(Runnable code, Waitable<T> waitable) {
try {
code.run();
while (!waitable.isDone()) {
connection.processOneMessage();
}
return (T) waitable.get();
};
return waitable.get();
} finally {
waitable.dispose();
}
}
void handleEvent(String event, JsonObject parameters) {
@@ -16,13 +16,22 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.TimeoutError;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@@ -56,6 +65,7 @@ public class Connection {
private final Map<String, ChannelOwner> objects = new HashMap<>();
private final Root root;
private int lastId = 0;
private final Path srcDir;
private final Map<Integer, WaitableResult<JsonElement>> callbacks = new HashMap<>();
class Root extends ChannelOwner {
@@ -64,9 +74,18 @@ public class Connection {
}
}
public Connection(InputStream in, OutputStream out) {
transport = new Transport(in, out);
Connection(Transport transport) {
this.transport = transport;
root = new Root(this);
String srcRoot = System.getenv("PLAYWRIGHT_JAVA_SRC");
if (srcRoot == null) {
srcDir = null;
} else {
srcDir = Paths.get(srcRoot);
if (!Files.exists(srcDir)) {
throw new PlaywrightException("PLAYWRIGHT_JAVA_SRC environment variable points to non-existing location: '" + srcRoot + "'");
}
}
}
void close() throws IOException {
@@ -74,15 +93,50 @@ public class Connection {
}
public JsonElement sendMessage(String guid, String method, JsonObject params) {
return (JsonElement) root.toDeferred(sendMessageAsync(guid, method, params)).get();
return root.runUntil(() -> {}, sendMessageAsync(guid, method, params));
}
public WaitableResult<JsonElement> sendMessageAsync(String guid, String method, JsonObject params) {
return internalSendMessage(guid, method, params);
}
public void sendMessageNoWait(String guid, String method, JsonObject params) {
internalSendMessage(guid, method, params);
private String sourceFile(StackTraceElement frame) {
String pkg = frame.getClassName();
int lastDot = pkg.lastIndexOf('.');
if (lastDot == -1) {
pkg = "";
} else {
pkg = frame.getClassName().substring(0, lastDot + 1);
}
pkg = pkg.replace('.', File.separatorChar);
return srcDir.resolve(pkg).resolve(frame.getFileName()).toString();
}
private JsonArray currentStackTrace() {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
int index = 0;
while (index < stack.length && !stack[index].getClassName().equals(getClass().getName())) {
index++;
};
// Find Playwright API call
while (index < stack.length && stack[index].getClassName().startsWith("com.microsoft.playwright.")) {
// hack for tests
if (stack[index].getClassName().startsWith("com.microsoft.playwright.Test")) {
break;
}
index++;
}
JsonArray jsonStack = new JsonArray();
for (; index < stack.length; index++) {
StackTraceElement frame = stack[index];
JsonObject jsonFrame = new JsonObject();
jsonFrame.addProperty("file", sourceFile(frame));
jsonFrame.addProperty("line", frame.getLineNumber());
jsonFrame.addProperty("function", frame.getClassName() + "." + frame.getMethodName());
jsonStack.add(jsonFrame);
}
return jsonStack;
}
private WaitableResult<JsonElement> internalSendMessage(String guid, String method, JsonObject params) {
@@ -94,6 +148,11 @@ public class Connection {
message.addProperty("guid", guid);
message.addProperty("method", method);
message.add("params", params);
if (srcDir != null) {
JsonObject metadata = new JsonObject();
metadata.add("stack", currentStackTrace());
message.add("metadata", metadata);
}
transport.send(gson().toJson(message));
return result;
}
@@ -142,10 +201,12 @@ public class Connection {
if (message.error == null) {
callback.complete(message.result);
} else {
if (message.error.error != null) {
callback.completeExceptionally(new ServerException(message.error.error));
} else {
if (message.error.error == null) {
callback.completeExceptionally(new PlaywrightException(message.error.toString()));
} else if ("TimeoutError".equals(message.error.error.name)) {
callback.completeExceptionally(new TimeoutError(message.error.error.toString()));
} else {
callback.completeExceptionally(new DriverException(message.error.error));
}
}
return;
@@ -236,6 +297,9 @@ public class Connection {
case "Request":
result = new RequestImpl(parent, type, guid, initializer);
break;
case "RemoteBrowser":
result = new RemoteBrowser(parent, type, guid, initializer);
break;
case "Response":
result = new ResponseImpl(parent, type, guid, initializer);
break;
@@ -48,7 +48,11 @@ public class ConsoleMessageImpl extends ChannelOwner implements ConsoleMessage {
return result;
}
public Location location() {
return gson().fromJson(initializer.get("location"), Location.class);
@Override
public String location() {
JsonObject location = initializer.getAsJsonObject("location");
return location.get("url").getAsString() + ":" +
location.get("lineNumber").getAsNumber() + ":" +
location.get("columnNumber").getAsNumber();
}
}
@@ -1,80 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.DeviceDescriptor;
import com.microsoft.playwright.PlaywrightException;
class DeviceDescriptorImpl implements DeviceDescriptor {
PlaywrightImpl playwright;
private static class ViewportImpl implements Viewport{
private int width;
private int height;
@Override
public int width() {
return width;
}
@Override
public int height() {
return height;
}
}
private ViewportImpl viewport;
private String userAgent;
private int deviceScaleFactor;
private boolean isMobile;
private boolean hasTouch;
private String defaultBrowserType;
@Override
public Viewport viewport() {
return viewport;
}
@Override
public String userAgent() {
return userAgent;
}
@Override
public int deviceScaleFactor() {
return deviceScaleFactor;
}
@Override
public boolean isMobile() {
return isMobile;
}
@Override
public boolean hasTouch() {
return hasTouch;
}
@Override
public BrowserType defaultBrowserType() {
switch (defaultBrowserType) {
case "chromium": return playwright.chromium();
case "firefox": return playwright.firefox();
case "webkit": return playwright.webkit();
default: throw new PlaywrightException("Unknown type: " + defaultBrowserType);
}
}
}
@@ -18,27 +18,26 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Dialog;
import com.microsoft.playwright.PlaywrightException;
public class DialogImpl extends ChannelOwner implements Dialog {
private boolean handled;
class DialogImpl extends ChannelOwner implements Dialog {
DialogImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@Override
public void accept(String promptText) {
handled = true;
JsonObject params = new JsonObject();
if (promptText != null)
params.addProperty("promptText", promptText);
sendMessageNoWait("accept", params);
withLogging("Dialog.accept", () -> {
JsonObject params = new JsonObject();
if (promptText != null) {
params.addProperty("promptText", promptText);
}
sendMessage("accept", params);
});
}
@Override
public void dismiss() {
handled = true;
sendMessageNoWait("dismiss");
withLogging("Dialog.dismiss", () -> sendMessage("dismiss"));
}
@Override
@@ -52,17 +51,7 @@ public class DialogImpl extends ChannelOwner implements Dialog {
}
@Override
public Type type() {
switch (initializer.get("type").getAsString()) {
case "alert": return Type.ALERT;
case "beforeunload": return Type.BEFOREUNLOAD;
case "confirm": return Type.CONFIRM;
case "prompt": return Type.PROMPT;
default: throw new PlaywrightException("Unexpected dialog type: " + initializer.get("type").getAsString());
}
}
boolean isHandled() {
return handled;
public String type() {
return initializer.get("type").getAsString();
}
}
@@ -16,16 +16,27 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Download;
import com.microsoft.playwright.PlaywrightException;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import static com.microsoft.playwright.impl.Utils.writeToFile;
public class DownloadImpl extends ChannelOwner implements Download {
private final BrowserImpl browser;
public DownloadImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
browser = ((BrowserContextImpl) parent).browser();
}
@Override
@@ -40,38 +51,58 @@ public class DownloadImpl extends ChannelOwner implements Download {
@Override
public InputStream createReadStream() {
JsonObject result = sendMessage("stream").getAsJsonObject();
if (!result.has("stream")) {
return null;
}
Stream stream = connection.getExistingObject(result.getAsJsonObject("stream").get("guid").getAsString());
return stream.stream();
return withLogging("Download.createReadStream", () -> {
JsonObject result = sendMessage("stream").getAsJsonObject();
if (!result.has("stream")) {
return null;
}
Stream stream = connection.getExistingObject(result.getAsJsonObject("stream").get("guid").getAsString());
return stream.stream();
});
}
@Override
public void delete() {
sendMessage("delete");
withLogging("Download.delete", () -> {
sendMessage("delete");
});
}
@Override
public String failure() {
JsonObject result = sendMessage("failure").getAsJsonObject();
if (result.has("error")) {
return result.get("error").getAsString();
}
return null;
return withLogging("Download.failure", () -> {
JsonObject result = sendMessage("failure").getAsJsonObject();
if (result.has("error")) {
return result.get("error").getAsString();
}
return null;
});
}
@Override
public Path path() {
JsonObject json = sendMessage("path").getAsJsonObject();
return FileSystems.getDefault().getPath(json.get("value").getAsString());
return withLogging("Download.path", () -> {
if (browser != null && browser.isRemote) {
throw new PlaywrightException("Path is not available when using browserType.connect(). Use download.saveAs() to save a local copy.");
}
JsonObject json = sendMessage("path").getAsJsonObject();
return FileSystems.getDefault().getPath(json.get("value").getAsString());
});
}
@Override
public void saveAs(Path path) {
JsonObject params = new JsonObject();
params.addProperty("path", path.toString());
sendMessage("saveAs", params);
withLogging("Download.saveAs", () -> {
if (browser != null && browser.isRemote) {
JsonObject jsonObject = sendMessage("saveAsStream").getAsJsonObject();
Stream stream = connection.getExistingObject(jsonObject.getAsJsonObject("stream").get("guid").getAsString());
writeToFile(stream.stream(), path);
return;
}
JsonObject params = new JsonObject();
params.addProperty("path", path.toString());
sendMessage("saveAs", params);
});
}
}
@@ -1,12 +1,12 @@
/*
* Copyright (c) Microsoft Corporation.
* <p>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
*
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -14,17 +14,15 @@
* limitations under the License.
*/
package com.microsoft.playwright;
package com.microsoft.playwright.impl;
public interface DeviceDescriptor {
interface Viewport {
int width();
int height();
import com.microsoft.playwright.PlaywrightException;
import java.io.PrintStream;
import java.io.PrintWriter;
class DriverException extends PlaywrightException {
DriverException(SerializedError.Error error) {
super(error.toString());
}
Viewport viewport();
String userAgent();
int deviceScaleFactor();
boolean isMobile();
boolean hasTouch();
BrowserType defaultBrowserType();
}
@@ -16,23 +16,26 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Deferred;
import com.microsoft.playwright.ElementHandle;
import com.microsoft.playwright.FileChooser;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.options.BoundingBox;
import com.microsoft.playwright.options.ElementState;
import com.microsoft.playwright.options.FilePayload;
import com.microsoft.playwright.options.SelectOption;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.isFunctionBody;
import static com.microsoft.playwright.options.ScreenshotType.JPEG;
import static com.microsoft.playwright.options.ScreenshotType.PNG;
public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
ElementHandleImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -46,67 +49,79 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override
public ElementHandle querySelector(String selector) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
JsonElement json = sendMessage("querySelector", params);
JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
if (element == null) {
return null;
}
return connection.getExistingObject(element.get("guid").getAsString());
return withLogging("ElementHandle.querySelector", () -> {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
JsonElement json = sendMessage("querySelector", params);
JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
if (element == null) {
return null;
}
return connection.getExistingObject(element.get("guid").getAsString());
});
}
@Override
public List<ElementHandle> querySelectorAll(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;
return withLogging("ElementHandle.<", () -> {
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
public Object evalOnSelector(String selector, String pageFunction, Object arg) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evalOnSelector", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
return deserialize(value);
return withLogging("ElementHandle.evalOnSelector", () -> {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
params.addProperty("expression", pageFunction);
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evalOnSelector", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
return deserialize(value);
});
}
@Override
public Object evalOnSelectorAll(String selector, String pageFunction, Object arg) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evalOnSelectorAll", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
return deserialize(value);
return withLogging("ElementHandle.evalOnSelectorAll", () -> {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
params.addProperty("expression", pageFunction);
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evalOnSelectorAll", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
return deserialize(value);
});
}
@Override
public BoundingBox boundingBox() {
JsonObject json = sendMessage("boundingBox").getAsJsonObject();
if (!json.has("value")) {
return null;
}
return gson().fromJson(json.get("value"), BoundingBox.class);
return withLogging("ElementHandle.boundingBox", () -> {
JsonObject json = sendMessage("boundingBox").getAsJsonObject();
if (!json.has("value")) {
return null;
}
return gson().fromJson(json.get("value"), BoundingBox.class);
});
}
@Override
public void check(CheckOptions options) {
withLogging("ElementHandle.check", () -> checkImpl(options));
}
private void checkImpl(CheckOptions options) {
if (options == null) {
options = new CheckOptions();
}
@@ -116,25 +131,23 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override
public void click(ClickOptions options) {
withLogging("ElementHandle.click", () -> clickImpl(options));
}
private void clickImpl(ClickOptions options) {
if (options == null) {
options = new ClickOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("button");
if (options.button != null) {
params.addProperty("button", Serialization.toProtocol(options.button));
}
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
sendMessage("click", params);
}
@Override
public Frame contentFrame() {
return withLogging("ElementHandle.contentFrame", () -> contentFrameImpl());
}
private Frame contentFrameImpl() {
JsonObject json = sendMessage("contentFrame").getAsJsonObject();
if (!json.has("frame")) {
return null;
@@ -144,33 +157,33 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override
public void dblclick(DblclickOptions options) {
withLogging("ElementHandle.dblclick", () -> dblclickImpl(options));
}
private void dblclickImpl(DblclickOptions options) {
if (options == null) {
options = new DblclickOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("button");
if (options.button != null) {
params.addProperty("button", Serialization.toProtocol(options.button));
}
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
sendMessage("dblclick", params);
}
@Override
public void dispatchEvent(String type, Object eventInit) {
JsonObject params = new JsonObject();
params.addProperty("type", type);
params.add("eventInit", gson().toJsonTree(serializeArgument(eventInit)));
sendMessage("dispatchEvent", params);
withLogging("ElementHandle.dispatchEvent", () -> {
JsonObject params = new JsonObject();
params.addProperty("type", type);
params.add("eventInit", gson().toJsonTree(serializeArgument(eventInit)));
sendMessage("dispatchEvent", params);
});
}
@Override
public void fill(String value, FillOptions options) {
withLogging("ElementHandle.fill", () -> fillImpl(value, options));
}
private void fillImpl(String value, FillOptions options) {
if (options == null) {
options = new FillOptions();
}
@@ -181,50 +194,107 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override
public void focus() {
sendMessage("focus");
withLogging("ElementHandle.focus", () -> sendMessage("focus"));
}
@Override
public String getAttribute(String name) {
JsonObject params = new JsonObject();
params.addProperty("name", name);
JsonObject json = sendMessage("getAttribute", params).getAsJsonObject();
return json.has("value") ? json.get("value").getAsString() : null;
return withLogging("ElementHandle.getAttribute", () -> {
JsonObject params = new JsonObject();
params.addProperty("name", name);
JsonObject json = sendMessage("getAttribute", params).getAsJsonObject();
return json.has("value") ? json.get("value").getAsString() : null;
});
}
@Override
public void hover(HoverOptions options) {
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
sendMessage("hover", params);
withLogging("ElementHandle.hover", () -> {
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("hover", params);
});
}
@Override
public String innerHTML() {
JsonObject json = sendMessage("innerHTML").getAsJsonObject();
return json.get("value").getAsString();
return withLogging("ElementHandle.innerHTML", () -> {
JsonObject json = sendMessage("innerHTML").getAsJsonObject();
return json.get("value").getAsString();
});
}
@Override
public String innerText() {
JsonObject json = sendMessage("innerText").getAsJsonObject();
return json.get("value").getAsString();
return withLogging("ElementHandle.innerText", () -> {
JsonObject json = sendMessage("innerText").getAsJsonObject();
return json.get("value").getAsString();
});
}
@Override
public boolean isChecked() {
return withLogging("ElementHandle.isChecked", () -> {
JsonObject json = sendMessage("isChecked").getAsJsonObject();
return json.get("value").getAsBoolean();
});
}
@Override
public boolean isDisabled() {
return withLogging("ElementHandle.isDisabled", () -> {
JsonObject json = sendMessage("isDisabled").getAsJsonObject();
return json.get("value").getAsBoolean();
});
}
@Override
public boolean isEditable() {
return withLogging("ElementHandle.isEditable", () -> {
JsonObject json = sendMessage("isEditable").getAsJsonObject();
return json.get("value").getAsBoolean();
});
}
@Override
public boolean isEnabled() {
return withLogging("ElementHandle.isEnabled", () -> {
JsonObject json = sendMessage("isEnabled").getAsJsonObject();
return json.get("value").getAsBoolean();
});
}
@Override
public boolean isHidden() {
return withLogging("ElementHandle.isHidden", () -> {
JsonObject json = sendMessage("isHidden").getAsJsonObject();
return json.get("value").getAsBoolean();
});
}
@Override
public boolean isVisible() {
return withLogging("ElementHandle.isVisible", () -> {
JsonObject json = sendMessage("isVisible").getAsJsonObject();
return json.get("value").getAsBoolean();
});
}
@Override
public Frame ownerFrame() {
JsonObject json = sendMessage("ownerFrame").getAsJsonObject();
if (!json.has("frame")) {
return null;
}
return connection.getExistingObject(json.getAsJsonObject("frame").get("guid").getAsString());
return withLogging("ElementHandle.ownerFrame", () -> {
JsonObject json = sendMessage("ownerFrame").getAsJsonObject();
if (!json.has("frame")) {
return null;
}
return connection.getExistingObject(json.getAsJsonObject("frame").get("guid").getAsString());
});
}
@Override
public void press(String key, PressOptions options) {
withLogging("ElementHandle.press", () -> pressImpl(key, options));
}
private void pressImpl(String key, PressOptions options) {
if (options == null) {
options = new PressOptions();
}
@@ -233,31 +303,29 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
sendMessage("press", params);
}
private static String toProtocol(ScreenshotOptions.Type type) {
return type.toString().toLowerCase();
}
@Override
public byte[] screenshot(ScreenshotOptions options) {
return withLogging("ElementHandle.screenshot", () -> screenshotImpl(options));
}
private byte[] screenshotImpl(ScreenshotOptions options) {
if (options == null) {
options = new ScreenshotOptions();
}
if (options.type == null) {
options.type = ScreenshotOptions.Type.PNG;
options.type = PNG;
if (options.path != null) {
String fileName = options.path.getFileName().toString();
int extStart = fileName.lastIndexOf('.');
if (extStart != -1) {
String extension = fileName.substring(extStart).toLowerCase();
if (".jpeg".equals(extension) || ".jpg".equals(extension)) {
options.type = ScreenshotOptions.Type.JPEG;
options.type = JPEG;
}
}
}
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("type");
params.addProperty("type", toProtocol(options.type));
params.remove("path");
JsonObject json = sendMessage("screenshot", params).getAsJsonObject();
@@ -270,6 +338,37 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override
public void scrollIntoViewIfNeeded(ScrollIntoViewIfNeededOptions options) {
withLogging("ElementHandle.scrollIntoViewIfNeeded", () -> scrollIntoViewIfNeededImpl(options));
}
@Override
public List<String> selectOption(String value, SelectOptionOptions options) {
String[] values = value == null ? null : new String[]{ value };
return selectOption(values, options);
}
@Override
public List<String> selectOption(ElementHandle value, SelectOptionOptions options) {
ElementHandle[] values = value == null ? null : new ElementHandle[]{ value };
return selectOption(values, options);
}
@Override
public List<String> selectOption(String[] values, SelectOptionOptions options) {
if (values == null) {
return selectOption(new SelectOption[0], options);
}
return selectOption(Arrays.asList(values).stream().map(
v -> new SelectOption().setValue(v)).toArray(SelectOption[]::new), options);
}
@Override
public List<String> selectOption(SelectOption value, SelectOptionOptions options) {
SelectOption[] values = value == null ? null : new SelectOption[]{ value };
return selectOption(values, options);
}
private void scrollIntoViewIfNeededImpl(ScrollIntoViewIfNeededOptions options) {
if (options == null) {
options = new ScrollIntoViewIfNeededOptions();
}
@@ -302,12 +401,23 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
}
private List<String> selectOption(JsonObject params) {
JsonObject json = sendMessage("selectOption", params).getAsJsonObject();
return parseStringList(json.getAsJsonArray("values"));
return withLogging("SelectOption", () -> {
JsonObject json = sendMessage("selectOption", params).getAsJsonObject();
return parseStringList(json.getAsJsonArray("values"));
});
}
@Override
public void selectText(SelectTextOptions options) {
withLogging("ElementHandle.selectText", () -> selectTextImpl(options));
}
@Override
public void setInputFiles(Path files, SetInputFilesOptions options) {
setInputFiles(new Path[]{files}, options);
}
private void selectTextImpl(SelectTextOptions options) {
if (options == null) {
options = new SelectTextOptions();
}
@@ -321,7 +431,16 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
}
@Override
public void setInputFiles(FileChooser.FilePayload[] files, SetInputFilesOptions options) {
public void setInputFiles(FilePayload files, SetInputFilesOptions options) {
setInputFiles(new FilePayload[]{files}, options);
}
@Override
public void setInputFiles(FilePayload[] files, SetInputFilesOptions options) {
withLogging("ElementHandle.setInputFiles", () -> setInputFilesImpl(files, options));
}
void setInputFilesImpl(FilePayload[] files, SetInputFilesOptions options) {
if (options == null) {
options = new SetInputFilesOptions();
}
@@ -332,17 +451,35 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override
public void tap(TapOptions options) {
withLogging("ElementHandle.tap", () -> tapImpl(options));
}
private void tapImpl(TapOptions options) {
if (options == null) {
options = new TapOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("tap", params);
}
@Override
public String textContent() {
JsonObject json = sendMessage("textContent").getAsJsonObject();
return json.has("value") ? json.get("value").getAsString() : null;
return withLogging("ElementHandle.textContent", () -> textContentImpl());
}
private String textContentImpl() {
return withLogging("ElementHandle.textContent", () -> {
JsonObject json = sendMessage("textContent").getAsJsonObject();
return json.has("value") ? json.get("value").getAsString() : null;
});
}
@Override
public void type(String text, TypeOptions options) {
withLogging("ElementHandle.type", () -> typeImpl(text, options));
}
private void typeImpl(String text, TypeOptions options) {
if (options == null) {
options = new TypeOptions();
}
@@ -353,6 +490,10 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override
public void uncheck(UncheckOptions options) {
withLogging("ElementHandle.uncheck", () -> uncheckImpl(options));
}
private void uncheckImpl(UncheckOptions options) {
if (options == null) {
options = new UncheckOptions();
}
@@ -361,48 +502,42 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
}
@Override
public Deferred<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) {
options = new WaitForElementStateOptions();
}
if (state == null) {
throw new IllegalArgumentException("State cannot be null");
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("state", toProtocol(state));
return toDeferred(sendMessageAsync("waitForElementState", params).apply(json -> null));
sendMessage("waitForElementState", params);
}
private static String toProtocol(ElementState state) {
if (state == null) {
throw new IllegalArgumentException("State cannot by null");
}
return state.toString().toLowerCase();
}
@Override
public Deferred<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) {
options = new WaitForSelectorOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("state");
params.addProperty("state", toProtocol(options.state));
params.addProperty("selector", selector);
return toDeferred(sendMessageAsync("waitForElementState", params).apply(json -> null));
}
public String createSelectorForTest(String name) {
JsonObject params = new JsonObject();
params.addProperty("name", name);
JsonObject json = sendMessage("createSelectorForTest", params).getAsJsonObject();
if (json.has("value")) {
return json.get("value").getAsString();
JsonElement json = sendMessage("waitForSelector", params);
JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
if (element == null) {
return null;
}
return null;
}
private static String toProtocol(WaitForSelectorOptions.State state) {
if (state == null) {
state = WaitForSelectorOptions.State.VISIBLE;
}
return state.toString().toLowerCase();
return connection.getExistingObject(element.get("guid").getAsString());
}
}
@@ -16,23 +16,21 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.microsoft.playwright.ElementHandle;
import com.microsoft.playwright.FileChooser;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.FilePayload;
import java.io.File;
import java.nio.file.Path;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
class FileChooserImpl implements FileChooser {
private final PageImpl page;
private final ElementHandle element;
private final ElementHandleImpl element;
private final boolean isMultiple;
FileChooserImpl(PageImpl page, ElementHandle element, boolean isMultiple) {
FileChooserImpl(PageImpl page, ElementHandleImpl element, boolean isMultiple) {
this.page = page;
this.element = element;
this.isMultiple = isMultiple;
@@ -53,13 +51,24 @@ class FileChooserImpl implements FileChooser {
return page;
}
@Override
public void setFiles(Path files, SetFilesOptions options) {
setFiles(new Path[]{files}, options);
}
@Override
public void setFiles(Path[] files, SetFilesOptions options) {
setFiles(Utils.toFilePayloads(files), options);
}
@Override
public void setFiles(FilePayload files, SetFilesOptions options) {
setFiles(new FilePayload[]{files}, options);
}
@Override
public void setFiles(FilePayload[] files, SetFilesOptions options) {
element.setInputFiles(files, convertViaJson(options, ElementHandle.SetInputFilesOptions.class));
page.withLogging("FileChooser.setInputFiles",
() -> element.setInputFilesImpl(files, convertViaJson(options, ElementHandle.SetInputFilesOptions.class)));
}
}
@@ -16,22 +16,22 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import static com.microsoft.playwright.Frame.LoadState.*;
import static com.microsoft.playwright.options.LoadState.*;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.isFunctionBody;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
public class FrameImpl extends ChannelOwner implements Frame {
private String name;
@@ -69,6 +69,10 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public ElementHandle querySelector(String selector) {
return withLogging("Frame.querySelector", () -> querySelectorImpl(selector));
}
ElementHandleImpl querySelectorImpl(String selector) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
JsonElement json = sendMessage("querySelector", params);
@@ -81,6 +85,37 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public List<ElementHandle> querySelectorAll(String selector) {
return withLogging("Frame.querySelectorAll", () -> querySelectorAllImpl(selector));
}
@Override
public List<String> selectOption(String selector, String value, SelectOptionOptions options) {
String[] values = value == null ? null : new String[]{ value };
return selectOption(selector, values, options);
}
@Override
public List<String> selectOption(String selector, ElementHandle value, SelectOptionOptions options) {
ElementHandle[] values = value == null ? null : new ElementHandle[]{value};
return selectOption(selector, values, options);
}
@Override
public List<String> selectOption(String selector, String[] values, SelectOptionOptions options) {
if (values == null) {
return selectOption(selector, new SelectOption[0], options);
}
return selectOption(selector, Arrays.asList(values).stream().map(
v -> new SelectOption().setValue(v)).toArray(SelectOption[]::new), options);
}
@Override
public List<String> selectOption(String selector, SelectOption value, SelectOptionOptions options) {
SelectOption[] values = value == null ? null : new SelectOption[]{value};
return selectOption(selector, values, options);
}
List<ElementHandle> querySelectorAllImpl(String selector) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
JsonElement json = sendMessage("querySelectorAll", params);
@@ -97,10 +132,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public Object evalOnSelector(String selector, String pageFunction, Object arg) {
return withLogging("Frame.evalOnSelector", () -> evalOnSelectorImpl(selector, pageFunction, arg));
}
Object evalOnSelectorImpl(String selector, String pageFunction, Object arg) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evalOnSelector", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
@@ -109,10 +147,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public Object evalOnSelectorAll(String selector, String pageFunction, Object arg) {
return withLogging("Frame.evalOnSelectorAll", () -> evalOnSelectorAllImpl(selector, pageFunction, arg));
}
Object evalOnSelectorAllImpl(String selector, String pageFunction, Object arg) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evalOnSelectorAll", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
@@ -120,13 +161,17 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public ElementHandle addScriptTag(AddScriptTagScript options) {
public ElementHandle addScriptTag(AddScriptTagOptions options){
return withLogging("Frame.addScriptTag", () -> addScriptTagImpl(options));
}
ElementHandle addScriptTagImpl(AddScriptTagOptions options) {
if (options == null) {
options = new AddScriptTagScript();
options = new AddScriptTagOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
JsonObject jsonOptions = gson().toJsonTree(options).getAsJsonObject();
if (options.path != null) {
params.remove("path");
jsonOptions.remove("path");
byte[] encoded;
try {
encoded = Files.readAllBytes(options.path);
@@ -135,20 +180,24 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
String content = new String(encoded, StandardCharsets.UTF_8);
content += "//# sourceURL=" + options.path.toString().replace("\n", "");
params.addProperty("content", content);
jsonOptions.addProperty("content", content);
}
JsonElement json = sendMessage("addScriptTag", params);
JsonElement json = sendMessage("addScriptTag", jsonOptions);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("element").get("guid").getAsString());
}
@Override
public ElementHandle addStyleTag(AddStyleTagStyle options) {
public ElementHandle addStyleTag(AddStyleTagOptions options){
return withLogging("Frame.addStyleTag", () -> addStyleTagImpl(options));
}
ElementHandle addStyleTagImpl(AddStyleTagOptions options) {
if (options == null) {
options = new AddStyleTagStyle();
options = new AddStyleTagOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
JsonObject jsonOptions = gson().toJsonTree(options).getAsJsonObject();
if (options.path != null) {
params.remove("path");
jsonOptions.remove("path");
byte[] encoded;
try {
encoded = Files.readAllBytes(options.path);
@@ -157,14 +206,18 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
String content = new String(encoded, StandardCharsets.UTF_8);
content += "/*# sourceURL=" + options.path.toString().replace("\n", "") + "*/";
params.addProperty("content", content);
jsonOptions.addProperty("content", content);
}
JsonElement json = sendMessage("addStyleTag", params);
JsonElement json = sendMessage("addStyleTag", jsonOptions);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("element").get("guid").getAsString());
}
@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) {
options = new CheckOptions();
}
@@ -180,53 +233,47 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void click(String selector, ClickOptions options) {
withLogging("Frame.click", () -> clickImpl(selector, options));
}
void clickImpl(String selector, ClickOptions options) {
if (options == null) {
options = new ClickOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
params.remove("button");
if (options.button != null) {
params.addProperty("button", Serialization.toProtocol(options.button));
}
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
sendMessage("click", params);
}
@Override
public String content() {
return withLogging("Frame.content", () -> contentImpl());
}
String contentImpl() {
return sendMessage("content").getAsJsonObject().get("value").getAsString();
}
@Override
public void dblclick(String selector, DblclickOptions options) {
withLogging("Frame.dblclick", () -> dblclickImpl(selector, options));
}
void dblclickImpl(String selector, DblclickOptions options) {
if (options == null) {
options = new DblclickOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
params.remove("button");
if (options.button != null) {
params.addProperty("button", Serialization.toProtocol(options.button));
}
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
sendMessage("dblclick", params);
}
@Override
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) {
options = new DispatchEventOptions();
}
@@ -239,10 +286,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public Object evaluate(String expression, Object arg) {
return withLogging("Frame.evaluate", () -> evaluateImpl(expression, arg));
}
Object evaluateImpl(String expression, Object arg) {
JsonObject params = new JsonObject();
params.addProperty("expression", expression);
params.addProperty("world", "main");
params.addProperty("isFunction", isFunctionBody(expression));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpression", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
@@ -251,10 +301,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public JSHandle evaluateHandle(String pageFunction, Object arg) {
return withLogging("Frame.evaluateHandle", () -> evaluateHandleImpl(pageFunction, arg));
}
JSHandle evaluateHandleImpl(String pageFunction, Object arg) {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("world", "main");
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpressionHandle", params);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("handle").get("guid").getAsString());
@@ -262,6 +315,10 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
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) {
options = new FillOptions();
}
@@ -273,6 +330,10 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void focus(String selector, FocusOptions options) {
withLogging("Frame.focus", () -> focusImpl(selector, options));
}
void focusImpl(String selector, FocusOptions options) {
if (options == null) {
options = new FocusOptions();
}
@@ -283,12 +344,21 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public ElementHandle frameElement() {
return withLogging("Frame.frameElement", () -> frameElementImpl());
}
ElementHandle frameElementImpl() {
JsonObject json = sendMessage("frameElement").getAsJsonObject();
return connection.getExistingObject(json.getAsJsonObject("element").get("guid").getAsString());
}
@Override
public String getAttribute(String selector, String name, GetAttributeOptions options) {
return withLogging("Frame.getAttribute", () -> getAttributeImpl(selector, name, options));
}
String getAttributeImpl(String selector, String name, GetAttributeOptions options) {
if (options == null) {
options = new GetAttributeOptions();
}
@@ -304,15 +374,15 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public ResponseImpl navigate(String url, NavigateOptions options) {
return withLogging("Page.navigate", () -> navigateImpl(url, options));
}
ResponseImpl navigateImpl(String url, NavigateOptions options) {
if (options == null) {
options = new NavigateOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("url", url);
if (options.waitUntil != null) {
params.remove("waitUntil");
params.addProperty("waitUntil", toProtocol(options.waitUntil));
}
JsonElement result = sendMessage("goto", params);
JsonObject jsonResponse = result.getAsJsonObject().getAsJsonObject("response");
if (jsonResponse == null) {
@@ -323,6 +393,10 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void hover(String selector, HoverOptions options) {
withLogging("Frame.hover", () -> hoverImpl(selector, options));
}
void hoverImpl(String selector, HoverOptions options) {
if (options == null) {
options = new HoverOptions();
}
@@ -333,6 +407,10 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public String innerHTML(String selector, InnerHTMLOptions options) {
return withLogging("Frame.innerHTML", () -> innerHTMLImpl(selector, options));
}
String innerHTMLImpl(String selector, InnerHTMLOptions options) {
if (options == null) {
options = new InnerHTMLOptions();
}
@@ -344,6 +422,10 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public String innerText(String selector, InnerTextOptions options) {
return withLogging("Frame.innerText", () -> innerTextImpl(selector, options));
}
String innerTextImpl(String selector, InnerTextOptions options) {
if (options == null) {
options = new InnerTextOptions();
}
@@ -353,11 +435,101 @@ public class FrameImpl extends ChannelOwner implements Frame {
return json.get("value").getAsString();
}
@Override
public boolean isChecked(String selector, IsCheckedOptions options) {
return withLogging("Page.isChecked", () -> isCheckedImpl(selector, options));
}
boolean isCheckedImpl(String selector, IsCheckedOptions options) {
if (options == null) {
options = new IsCheckedOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
JsonObject json = sendMessage("isChecked", params).getAsJsonObject();
return json.get("value").getAsBoolean();
}
@Override
public boolean isDetached() {
return isDetached;
}
@Override
public boolean isDisabled(String selector, IsDisabledOptions options) {
return withLogging("Page.isDisabled", () -> isDisabledImpl(selector, options));
}
boolean isDisabledImpl(String selector, IsDisabledOptions options) {
if (options == null) {
options = new IsDisabledOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
JsonObject json = sendMessage("isDisabled", params).getAsJsonObject();
return json.get("value").getAsBoolean();
}
@Override
public boolean isEditable(String selector, IsEditableOptions options) {
return withLogging("Page.isEditable", () -> isEditableImpl(selector, options));
}
boolean isEditableImpl(String selector, IsEditableOptions options) {
if (options == null) {
options = new IsEditableOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
JsonObject json = sendMessage("isEditable", params).getAsJsonObject();
return json.get("value").getAsBoolean();
}
@Override
public boolean isEnabled(String selector, IsEnabledOptions options) {
return withLogging("Page.isEnabled", () -> isEnabledImpl(selector, options));
}
boolean isEnabledImpl(String selector, IsEnabledOptions options) {
if (options == null) {
options = new IsEnabledOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
JsonObject json = sendMessage("isEnabled", params).getAsJsonObject();
return json.get("value").getAsBoolean();
}
@Override
public boolean isHidden(String selector, IsHiddenOptions options) {
return withLogging("Page.isHidden", () -> isHiddenImpl(selector, options));
}
boolean isHiddenImpl(String selector, IsHiddenOptions options) {
if (options == null) {
options = new IsHiddenOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
JsonObject json = sendMessage("isHidden", params).getAsJsonObject();
return json.get("value").getAsBoolean();
}
@Override
public boolean isVisible(String selector, IsVisibleOptions options) {
return withLogging("Page.isVisible", () -> isVisibleImpl(selector, options));
}
boolean isVisibleImpl(String selector, IsVisibleOptions options) {
if (options == null) {
options = new IsVisibleOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
JsonObject json = sendMessage("isVisible", params).getAsJsonObject();
return json.get("value").getAsBoolean();
}
@Override
public String name() {
return name;
@@ -375,6 +547,10 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void press(String selector, String key, PressOptions options) {
withLogging("Frame.press", () -> pressImpl(selector, key, options));
}
void pressImpl(String selector, String key, PressOptions options) {
if (options == null) {
options = new PressOptions();
}
@@ -385,7 +561,11 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public List<String> selectOption(String selector, ElementHandle.SelectOption[] values, SelectOptionOptions options) {
public List<String> selectOption(String selector, SelectOption[] values, SelectOptionOptions options) {
return withLogging("Frame.selectOption", () -> selectOptionImpl(selector, values, options));
}
List<String> selectOptionImpl(String selector, SelectOption[] values, SelectOptionOptions options) {
if (options == null) {
options = new SelectOptionOptions();
}
@@ -399,6 +579,10 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public List<String> selectOption(String selector, ElementHandle[] values, SelectOptionOptions options) {
return withLogging("Frame.selectOption", () -> selectOptionImpl(selector, values, options));
}
List<String> selectOptionImpl(String selector, ElementHandle[] values, SelectOptionOptions options) {
if (options == null) {
options = new SelectOptionOptions();
}
@@ -415,37 +599,45 @@ public class FrameImpl extends ChannelOwner implements Frame {
return parseStringList(json.getAsJsonArray("values"));
}
static String toProtocol(LoadState waitUntil) {
if (waitUntil == null) {
waitUntil = LoadState.LOAD;
}
switch (waitUntil) {
case DOMCONTENTLOADED: return "domcontentloaded";
case LOAD: return "load";
case NETWORKIDLE: return "networkidle";
default: throw new PlaywrightException("Unexpected value: " + waitUntil);
}
@Override
public void setContent(String html, SetContentOptions options) {
withLogging("Frame.setContent", () -> setContentImpl(html, options));
}
@Override
public void setContent(String html, SetContentOptions options) {
public void setInputFiles(String selector, Path files, SetInputFilesOptions options) {
setInputFiles(selector, new Path[] {files}, options);
}
void setContentImpl(String html, SetContentOptions options) {
if (options == null) {
options = new SetContentOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("html", html);
params.remove("waitUntil");
params.addProperty("waitUntil", toProtocol(options.waitUntil));
sendMessage("setContent", params);
}
@Override
public void setInputFiles(String selector, Path[] files, SetInputFilesOptions options) {
withLogging("Frame.setInputFiles", () -> setInputFilesImpl(selector, files, options));
}
@Override
public void setInputFiles(String selector, FilePayload files, SetInputFilesOptions options) {
setInputFiles(selector, new FilePayload[]{files}, options);
}
void setInputFilesImpl(String selector, Path[] files, SetInputFilesOptions options) {
setInputFiles(selector, Utils.toFilePayloads(files), options);
}
@Override
public void setInputFiles(String selector, FileChooser.FilePayload[] files, SetInputFilesOptions options) {
public void setInputFiles(String selector, FilePayload[] files, SetInputFilesOptions options) {
withLogging("Frame.setInputFiles", () -> setInputFilesImpl(selector, files, options));
}
void setInputFilesImpl(String selector, FilePayload[] files, SetInputFilesOptions options) {
if (options == null) {
options = new SetInputFilesOptions();
}
@@ -457,20 +649,23 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void tap(String selector, TapOptions options) {
withLogging("Frame.tap", () -> tapImpl(selector, options));
}
void tapImpl(String selector, TapOptions options) {
if (options == null) {
options = new TapOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
params.addProperty("selector", selector);
sendMessage("tap", params);
}
@Override
public String textContent(String selector, TextContentOptions options) {
return withLogging("Frame.textContent", () -> textContentImpl(selector, options));
}
String textContentImpl(String selector, TextContentOptions options) {
if (options == null) {
options = new TextContentOptions();
}
@@ -481,12 +676,20 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public String title() {
return withLogging("Frame.title", () -> titleImpl());
}
String titleImpl() {
JsonElement json = sendMessage("title");
return json.getAsJsonObject().get("value").getAsString();
}
@Override
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) {
options = new TypeOptions();
}
@@ -498,6 +701,10 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void uncheck(String selector, UncheckOptions options) {
withLogging("Frame.uncheck", () -> uncheckImpl(selector, options));
}
void uncheckImpl(String selector, UncheckOptions options) {
if (options == null) {
options = new UncheckOptions();
}
@@ -512,23 +719,28 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public Deferred<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) {
options = new WaitForFunctionOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
Waitable<JSHandle> handle = sendMessageAsync("waitForFunction", params).apply(json -> {
JsonObject element = json.getAsJsonObject().getAsJsonObject("handle");
return connection.getExistingObject(element.get("guid").getAsString());
});
return toDeferred(handle);
JsonElement json = sendMessage("waitForFunction", params);
JsonObject element = json.getAsJsonObject().getAsJsonObject("handle");
return connection.getExistingObject(element.get("guid").getAsString());
}
@Override
public Deferred<Void> waitForLoadState(LoadState state, WaitForLoadStateOptions options) {
public void waitForLoadState(LoadState state, WaitForLoadStateOptions options) {
withLogging("Frame.waitForLoadState", () -> waitForLoadStateImpl(state, options));
}
void waitForLoadStateImpl(LoadState state, WaitForLoadStateOptions options) {
if (options == null) {
options = new WaitForLoadStateOptions();
}
@@ -540,10 +752,10 @@ public class FrameImpl extends ChannelOwner implements Frame {
waitables.add(new WaitForLoadStateHelper(state));
waitables.add(page.createWaitForCloseHelper());
waitables.add(page.createWaitableTimeout(options.timeout));
return toDeferred(new WaitableRace<>(waitables));
runUntil(() -> {}, new WaitableRace<>(waitables));
}
private class WaitForLoadStateHelper implements Waitable<Void>, Listener<InternalEventType> {
private class WaitForLoadStateHelper implements Waitable<Void>, Consumer<LoadState> {
private final LoadState expectedState;
private boolean isDone;
@@ -556,9 +768,8 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public void handle(Event<InternalEventType> event) {
assert event.type() == InternalEventType.LOADSTATE;
if (expectedState.equals(event.data())) {
public void accept(LoadState state) {
if (expectedState.equals(state)) {
isDone = true;
dispose();
}
@@ -578,7 +789,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
}
private class WaitForNavigationHelper implements Waitable<Response>, Listener<InternalEventType> {
private class WaitForNavigationHelper implements Waitable<Response>, Consumer<JsonObject> {
private final UrlMatcher matcher;
private final LoadState expectedLoadState;
private WaitForLoadStateHelper loadStateHelper;
@@ -593,9 +804,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public void handle(Event<InternalEventType> event) {
assert InternalEventType.NAVIGATED == event.type();
JsonObject params = (JsonObject) event.data();
public void accept(JsonObject params) {
if (!matcher.test(params.get("url").getAsString())) {
return;
}
@@ -646,51 +855,53 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public Deferred<Response> waitForNavigation(WaitForNavigationOptions options) {
public Response waitForNavigation(WaitForNavigationOptions options, Runnable code) {
return withLogging("Frame.waitForNavigation", () -> waitForNavigationImpl(code, options));
}
Response waitForNavigationImpl(Runnable code, WaitForNavigationOptions options) {
if (options == null) {
options = new WaitForNavigationOptions();
}
if (options.waitUntil == null) {
options.waitUntil = LOAD;
options.waitUntil = WaitUntilState.LOAD;
}
List<Waitable<Response>> waitables = new ArrayList<>();
UrlMatcher matcher = UrlMatcher.forOneOf(options.glob, options.pattern, options.predicate);
waitables.add(new WaitForNavigationHelper(matcher, options.waitUntil));
UrlMatcher matcher = UrlMatcher.forOneOf(options.url);
waitables.add(new WaitForNavigationHelper(matcher, convertViaJson(options.waitUntil, LoadState.class)));
waitables.add(page.createWaitForCloseHelper());
waitables.add(page.createWaitableFrameDetach(this));
waitables.add(page.createWaitableNavigationTimeout(options.timeout));
return toDeferred(new WaitableRace<>(waitables));
}
private static String toProtocol(WaitForSelectorOptions.State state) {
return state.toString().toLowerCase();
return runUntil(code, new WaitableRace<>(waitables));
}
@Override
public Deferred<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) {
if (options == null) {
options = new WaitForSelectorOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
if (options.state != null) {
params.remove("state");
params.addProperty("state", toProtocol(options.state));
JsonElement json = sendMessage("waitForSelector", params);
JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
if (element == null) {
return null;
}
Waitable<ElementHandle> handle = sendMessageAsync("waitForSelector", params).apply(json -> {
JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
if (element == null) {
return null;
}
return connection.getExistingObject(element.get("guid").getAsString());
});
return toDeferred(handle);
return connection.getExistingObject(element.get("guid").getAsString());
}
@Override
public Deferred<Void> waitForTimeout(int timeout) {
return toDeferred(new WaitableTimeout<Void>(timeout) {
public void waitForTimeout(double timeout) {
withLogging("Frame.waitForTimeout", () -> waitForTimeoutImpl(timeout));
}
void waitForTimeoutImpl(double timeout) {
runUntil(() -> {}, new WaitableTimeout<Void>(timeout) {
@Override
public Void get() {
// Override to not throw.
@@ -16,7 +16,6 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.ElementHandle;
@@ -26,11 +25,13 @@ import java.util.HashMap;
import java.util.Map;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.isFunctionBody;
public class JSHandleImpl extends ChannelOwner implements JSHandle {
private String preview;
public JSHandleImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
this.preview = initializer.get("preview").getAsString();
}
@Override
@@ -40,56 +41,77 @@ public class JSHandleImpl extends ChannelOwner implements JSHandle {
@Override
public void dispose() {
sendMessage("dispose");
withLogging("JSHandle.dispose", () -> sendMessage("dispose"));
}
@Override
public Object evaluate(String pageFunction, Object arg) {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("world", "main");
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpression", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
return deserialize(value);
return withLogging("JSHandle.evaluate", () -> {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("world", "main");
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpression", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
return deserialize(value);
});
}
@Override
public JSHandle evaluateHandle(String pageFunction, Object arg) {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("world", "main");
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpressionHandle", params);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("handle").get("guid").getAsString());
return withLogging("JSHandle.evaluateHandle", () -> {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("world", "main");
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpressionHandle", params);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("handle").get("guid").getAsString());
});
}
@Override
public Map<String, JSHandle> getProperties() {
JsonObject json = sendMessage("getPropertyList").getAsJsonObject();
Map<String, JSHandle> result = new HashMap<>();
for (JsonElement e : json.getAsJsonArray("properties")) {
JsonObject item = e.getAsJsonObject();
JSHandle value = connection.getExistingObject(item.getAsJsonObject("value").get("guid").getAsString());
result.put(item.get("name").getAsString(), value);
}
return result;
return withLogging("JSHandle.getProperties", () -> {
JsonObject json = sendMessage("getPropertyList").getAsJsonObject();
Map<String, JSHandle> result = new HashMap<>();
for (JsonElement e : json.getAsJsonArray("properties")) {
JsonObject item = e.getAsJsonObject();
JSHandle value = connection.getExistingObject(item.getAsJsonObject("value").get("guid").getAsString());
result.put(item.get("name").getAsString(), value);
}
return result;
});
}
@Override
public JSHandle getProperty(String propertyName) {
JsonObject params = new JsonObject();
params.addProperty("name", propertyName);
JsonObject json = sendMessage("getProperty", params).getAsJsonObject();
return connection.getExistingObject(json.getAsJsonObject("handle").get("guid").getAsString());
return withLogging("JSHandle.getProperty", () -> {
JsonObject params = new JsonObject();
params.addProperty("name", propertyName);
JsonObject json = sendMessage("getProperty", params).getAsJsonObject();
return connection.getExistingObject(json.getAsJsonObject("handle").get("guid").getAsString());
});
}
@Override
public Object jsonValue() {
JsonObject json = sendMessage("jsonValue").getAsJsonObject();
SerializedValue value = gson().fromJson(json.get("value"), SerializedValue.class);
return deserialize(value);
return withLogging("JSHandle.jsonValue", () -> {
JsonObject json = sendMessage("jsonValue").getAsJsonObject();
SerializedValue value = gson().fromJson(json.get("value"), SerializedValue.class);
return deserialize(value);
});
}
@Override
void handleEvent(String event, JsonObject parameters) {
if ("previewUpdated".equals(event)) {
preview = parameters.get("preview").getAsString();
}
super.handleEvent(event, parameters);
}
@Override
public String toString() {
return preview;
}
}
@@ -19,7 +19,9 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Keyboard;
class KeyboardImpl implements Keyboard {
import static com.microsoft.playwright.impl.Serialization.gson;
class KeyboardImpl extends LoggingSupport implements Keyboard {
private final ChannelOwner page;
KeyboardImpl(ChannelOwner page) {
@@ -28,42 +30,56 @@ class KeyboardImpl implements Keyboard {
@Override
public void down(String key) {
JsonObject params = new JsonObject();
params.addProperty("key", key);
page.sendMessage("keyboardDown", params);
withLogging("Keyboard.down", () -> {
JsonObject params = new JsonObject();
params.addProperty("key", key);
page.sendMessage("keyboardDown", params);
});
}
@Override
public void insertText(String text) {
JsonObject params = new JsonObject();
params.addProperty("text", text);
page.sendMessage("keyboardInsertText", params);
withLogging("Keyboard.insertText", () -> {
JsonObject params = new JsonObject();
params.addProperty("text", text);
page.sendMessage("keyboardInsertText", params);
});
}
@Override
public void press(String key, int delay) {
JsonObject params = new JsonObject();
params.addProperty("key", key);
if (delay != 0) {
params.addProperty("delay", delay);
public void press(String key, PressOptions options) {
withLogging("Keyboard.press", () -> pressImpl(key, options));
}
private void pressImpl(String key, PressOptions options) {
if (options == null) {
options = new PressOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("key", key);
page.sendMessage("keyboardPress", params);
}
@Override
public void type(String text, int delay) {
JsonObject params = new JsonObject();
params.addProperty("text", text);
if (delay != 0) {
params.addProperty("delay", delay);
@Override
public void type(String text, TypeOptions options) {
withLogging("Keyboard.type", () -> typeImpl(text, options));
}
private void typeImpl(String text, TypeOptions options) {
if (options == null) {
options = new TypeOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("text", text);
page.sendMessage("keyboardType", params);
}
@Override
public void up(String key) {
JsonObject params = new JsonObject();
params.addProperty("key", key);
page.sendMessage("keyboardUp", params);
withLogging("Keyboard.up", () -> {
JsonObject params = new JsonObject();
params.addProperty("key", key);
page.sendMessage("keyboardUp", params);
});
}
}
@@ -16,43 +16,28 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.Deferred;
import com.microsoft.playwright.Event;
import com.microsoft.playwright.Listener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
class ListenerCollection <EventType> {
private final HashMap<EventType, List<Listener<EventType>>> listeners = new HashMap<>();
private final HashMap<EventType, List<Consumer<?>>> listeners = new HashMap<>();
void notify(EventType eventType, Object param) {
List<Listener<EventType>> list = listeners.get(eventType);
<T> void notify(EventType eventType, T param) {
List<Consumer<?>> list = listeners.get(eventType);
if (list == null) {
return;
}
Event<EventType> event = new Event<EventType>() {
@Override
public EventType type() {
return eventType;
}
@Override
public Object data() {
return param;
}
};
for (Listener<EventType> listener: new ArrayList<>(list)) {
listener.handle(event);
for (Consumer<?> listener: new ArrayList<>(list)) {
((Consumer<T>) listener).accept(param);
}
}
void add(EventType type, Listener<EventType> listener) {
List<Listener<EventType>> list = listeners.get(type);
void add(EventType type, Consumer<?> listener) {
List<Consumer<?>> list = listeners.get(type);
if (list == null) {
list = new ArrayList<>();
listeners.put(type, list);
@@ -60,8 +45,8 @@ class ListenerCollection <EventType> {
list.add(listener);
}
void remove(EventType type, Listener<EventType> listener) {
List<Listener<EventType>> list = listeners.get(type);
void remove(EventType type, Consumer<?> listener) {
List<Consumer<?>> list = listeners.get(type);
if (list == null) {
return;
}
@@ -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 java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.function.Supplier;
class LoggingSupport {
private static final boolean isEnabled;
static {
String debug = System.getenv("DEBUG");
isEnabled = (debug != null) && debug.contains("pw:api");
}
private static final DateTimeFormatter timestampFormat = DateTimeFormatter.ofPattern(
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX").withZone(ZoneId.of("UTC"));
void withLogging(String apiName, Runnable code) {
withLogging(apiName, () -> {
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"));
}
}
}
private void logApi(String message) {
// This matches log format produced by the server.
String timestamp = ZonedDateTime.now().format(timestampFormat);
System.err.println(timestamp + " pw:api " + message);
}
}
@@ -16,12 +16,10 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Mouse;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Serialization.toProtocol;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
class MouseImpl implements Mouse {
@@ -32,22 +30,26 @@ class MouseImpl implements Mouse {
}
@Override
public void click(int x, int 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) {
options = new ClickOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("x", x);
params.addProperty("y", y);
if (options.button != null) {
params.remove("button");
params.addProperty("button", toProtocol(options.button));
}
page.sendMessage("mouseClick", params);
}
@Override
public void dblclick(int x, int y, DblclickOptions options) {
public void dblclick(double x, double y, DblclickOptions options) {
page.withLogging("Mouse.dblclick", () -> dblclickImpl(x, y, options));
}
private void dblclickImpl(double x, double y, DblclickOptions options) {
ClickOptions clickOptions;
if (options == null) {
clickOptions = new ClickOptions();
@@ -60,6 +62,10 @@ class MouseImpl implements Mouse {
@Override
public void down(DownOptions options) {
page.withLogging("Mouse.down", () -> downImpl(options));
}
private void downImpl(DownOptions options) {
if (options == null) {
options = new DownOptions();
}
@@ -68,7 +74,11 @@ class MouseImpl implements Mouse {
}
@Override
public void move(int x, int 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) {
options = new MoveOptions();
}
@@ -80,6 +90,10 @@ class MouseImpl implements Mouse {
@Override
public void up(UpOptions options) {
page.withLogging("Mouse.up", () -> upImpl(options));
}
private void upImpl(UpOptions options) {
if (options == null) {
options = new UpOptions();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,167 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class PipeTransport implements Transport {
private final BlockingQueue<String> incoming = new ArrayBlockingQueue<>(1000);
private final BlockingQueue<String> outgoing= new ArrayBlockingQueue<>(1000);
private final ReaderThread readerThread;
private final WriterThread writerThread;
private boolean isClosed;
PipeTransport(InputStream input, OutputStream output) {
DataInputStream in = new DataInputStream(new BufferedInputStream(input));
readerThread = new ReaderThread(in, incoming);
readerThread.start();
writerThread = new WriterThread(output, outgoing);
writerThread.start();
}
@Override
public void send(String message) {
if (isClosed) {
throw new PlaywrightException("Playwright connection closed");
}
try {
outgoing.put(message);
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to send message", e);
}
}
@Override
public String poll(Duration timeout) {
if (isClosed) {
throw new PlaywrightException("Playwright connection closed");
}
try {
return incoming.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to read message", e);
}
}
@Override
public void close() throws IOException {
if (isClosed) {
return;
}
isClosed = true;
// We interrupt only the outgoing pipe and keep reader thread running as
// otherwise child process may block on writing to its stdout and never
// exit (observed on Windows).
readerThread.isClosing = true;
writerThread.out.close();
writerThread.interrupt();
}
}
class ReaderThread extends Thread {
private final DataInputStream in;
private final BlockingQueue<String> queue;
volatile boolean isClosing;
private static int readIntLE(DataInputStream in) throws IOException {
int ch1 = in.read();
int ch2 = in.read();
int ch3 = in.read();
int ch4 = in.read();
if ((ch1 | ch2 | ch3 | ch4) < 0) {
throw new EOFException();
} else {
return (ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0);
}
}
ReaderThread(DataInputStream in, BlockingQueue<String> queue) {
this.in = in;
this.queue = queue;
}
@Override
public void run() {
while (!isInterrupted()) {
try {
queue.put(readMessage());
} catch (IOException e) {
if (!isInterrupted() && !isClosing) {
e.printStackTrace();
}
break;
} catch (InterruptedException e) {
break;
}
}
}
private String readMessage() throws IOException {
int len = readIntLE(in);
byte[] raw = new byte[len];
in.readFully(raw, 0, len);
return new String(raw, StandardCharsets.UTF_8);
}
}
class WriterThread extends Thread {
final OutputStream out;
private final BlockingQueue<String> queue;
private static void writeIntLE(OutputStream out, int v) throws IOException {
out.write(v >>> 0 & 255);
out.write(v >>> 8 & 255);
out.write(v >>> 16 & 255);
out.write(v >>> 24 & 255);
}
WriterThread(OutputStream out, BlockingQueue<String> queue) {
this.out = out;
this.queue = queue;
}
@Override
public void run() {
while (!isInterrupted()) {
try {
if (queue.isEmpty())
out.flush();
sendMessage(queue.take());
} catch (IOException e) {
if (!isInterrupted())
e.printStackTrace();
break;
} catch (InterruptedException e) {
break;
}
}
}
private void sendMessage(String message) throws IOException {
byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
writeIntLE(out, bytes.length);
out.write(bytes);
}
}
@@ -16,18 +16,13 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.DeviceDescriptor;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Selectors;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class PlaywrightImpl extends ChannelOwner implements Playwright {
@@ -40,7 +35,7 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
// pb.environment().put("DEBUG", "pw:pro*");
Process p = pb.start();
Connection connection = new Connection(p.getInputStream(), p.getOutputStream());
Connection connection = new Connection(new PipeTransport(p.getInputStream(), p.getOutputStream()));
PlaywrightImpl result = (PlaywrightImpl) connection.waitForObjectWithKnownName("Playwright");
result.driverProcess = p;
return result;
@@ -52,24 +47,15 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
private final BrowserTypeImpl chromium;
private final BrowserTypeImpl firefox;
private final BrowserTypeImpl webkit;
private final Selectors selectors;
private final Map<String, DeviceDescriptor> devices = new HashMap<>();
final SharedSelectors sharedSelectors = new SharedSelectors();;
PlaywrightImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
chromium = parent.connection.getExistingObject(initializer.getAsJsonObject("chromium").get("guid").getAsString());
firefox = parent.connection.getExistingObject(initializer.getAsJsonObject("firefox").get("guid").getAsString());
webkit = parent.connection.getExistingObject(initializer.getAsJsonObject("webkit").get("guid").getAsString());
selectors = parent.connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString());
Gson gson = Serialization.gson();
for (JsonElement item : initializer.getAsJsonArray("deviceDescriptors")) {
JsonObject o = item.getAsJsonObject();
String name = o.get("name").getAsString();
DeviceDescriptorImpl descriptor = gson.fromJson(o.get("descriptor"), DeviceDescriptorImpl.class);
descriptor.playwright = this;
devices.put(name, descriptor);
}
SelectorsImpl channel = parent.connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString());
sharedSelectors.addChannel(channel);
}
@Override
@@ -87,23 +73,25 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
return webkit;
}
@Override
public Map<String, DeviceDescriptor> devices() {
return devices;
}
@Override
public Selectors selectors() {
return selectors;
return sharedSelectors;
}
@Override
public void close() throws Exception {
connection.close();
// playwright-cli will exit when its stdin is closed, we wait for that.
boolean didClose = driverProcess.waitFor(30, TimeUnit.SECONDS);
if (!didClose) {
System.err.println("WARNING: Timed out while waiting for driver process to exit");
public void close() {
try {
connection.close();
// playwright-cli will exit when its stdin is closed, we wait for that.
boolean didClose = driverProcess.waitFor(30, TimeUnit.SECONDS);
if (!didClose) {
System.err.println("WARNING: Timed out while waiting for driver process to exit");
}
} catch (IOException e) {
throw new PlaywrightException("Failed to terminate", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new PlaywrightException("Operation interrupted", e);
}
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
public class RemoteBrowser extends ChannelOwner {
RemoteBrowser(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
BrowserImpl browser() {
return connection.getExistingObject(initializer.getAsJsonObject("browser").get("guid").getAsString());
}
SelectorsImpl selectors() {
return connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString());
}
}
@@ -21,6 +21,7 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.Request;
import com.microsoft.playwright.Response;
import com.microsoft.playwright.options.Timing;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@@ -32,7 +33,8 @@ public class RequestImpl extends ChannelOwner implements Request {
private RequestImpl redirectedFrom;
private RequestImpl redirectedTo;
final Map<String, String> headers = new HashMap<>();
RequestFailure failure;
String failure;
Timing timing;
RequestImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
@@ -53,7 +55,7 @@ public class RequestImpl extends ChannelOwner implements Request {
}
@Override
public RequestFailure failure() {
public String failure() {
return failure;
}
@@ -107,16 +109,18 @@ public class RequestImpl extends ChannelOwner implements Request {
@Override
public Response response() {
JsonObject result = sendMessage("response").getAsJsonObject();
if (!result.has("response")) {
return null;
}
return connection.getExistingObject(result.getAsJsonObject("response").get("guid").getAsString());
return withLogging("Request.response", () -> {
JsonObject result = sendMessage("response").getAsJsonObject();
if (!result.has("response")) {
return null;
}
return connection.getExistingObject(result.getAsJsonObject("response").get("guid").getAsString());
});
}
@Override
public RequestTiming timing() {
return null;
public Timing timing() {
return timing;
}
@Override
@@ -21,6 +21,7 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.Request;
import com.microsoft.playwright.Response;
import com.microsoft.playwright.options.Timing;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@@ -45,21 +46,26 @@ public class ResponseImpl extends ChannelOwner implements Response {
JsonObject item = e.getAsJsonObject();
request.headers.put(item.get("name").getAsString().toLowerCase(), item.get("value").getAsString());
}
request.timing = Serialization.gson().fromJson(initializer.get("timing"), Timing.class);
}
@Override
public byte[] body() {
JsonObject json = sendMessage("body").getAsJsonObject();
return Base64.getDecoder().decode(json.get("binary").getAsString());
return withLogging("Response.body", () -> {
JsonObject json = sendMessage("body").getAsJsonObject();
return Base64.getDecoder().decode(json.get("binary").getAsString());
});
}
@Override
public String finished() {
JsonObject json = sendMessage("finished").getAsJsonObject();
if (json.has("error")) {
return json.get("error").getAsString();
}
return null;
return withLogging("Response.finished", () -> {
JsonObject json = sendMessage("finished").getAsJsonObject();
if (json.has("error")) {
return json.get("error").getAsString();
}
return null;
});
}
@Override
@@ -16,16 +16,15 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Request;
import com.microsoft.playwright.Route;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -36,72 +35,90 @@ public class RouteImpl extends ChannelOwner implements Route {
@Override
public void abort(String errorCode) {
JsonObject params = new JsonObject();
params.addProperty("errorCode", errorCode);
sendMessage("abort", params);
withLogging("Route.abort", () -> {
JsonObject params = new JsonObject();
params.addProperty("errorCode", errorCode);
sendMessage("abort", params);
});
}
@Override
public void continue_(ContinueOverrides overrides) {
if (overrides == null) {
overrides = new ContinueOverrides();
public void resume(ResumeOptions options) {
withLogging("Route.resume", () -> resumeImpl(options));
}
private void resumeImpl(ResumeOptions options) {
if (options == null) {
options = new ResumeOptions();
}
JsonObject params = new JsonObject();
if (overrides.url != null) {
params.addProperty("url", overrides.url);
if (options.url != null) {
params.addProperty("url", options.url);
}
if (overrides.method != null) {
params.addProperty("method", overrides.method);
if (options.method != null) {
params.addProperty("method", options.method);
}
if (overrides.headers != null) {
params.add("headers", Serialization.toProtocol(overrides.headers));
if (options.headers != null) {
params.add("headers", Serialization.toProtocol(options.headers));
}
if (overrides.postData != null) {
String base64 = Base64.getEncoder().encodeToString(overrides.postData);
if (options.postData != null) {
byte[] bytes = null;
if (options.postData instanceof byte[]) {
bytes = (byte[]) options.postData;
} else if (options.postData instanceof String) {
bytes = ((String) options.postData).getBytes(StandardCharsets.UTF_8);
} else {
throw new PlaywrightException("postData must be either String or byte[], found: " + options.postData.getClass().getName());
}
String base64 = Base64.getEncoder().encodeToString(bytes);
params.addProperty("postData", base64);
}
sendMessage("continue", params);
}
@Override
public void fulfill(FulfillResponse response) {
if (response == null) {
response = new FulfillResponse();
public void fulfill(FulfillOptions options) {
withLogging("Route.fulfill", () -> fulfillImpl(options));
}
private void fulfillImpl(FulfillOptions options) {
if (options == null) {
options = new FulfillOptions();
}
int status = response.status == 0 ? 200 : response.status;
int status = options.status == null ? 200 : options.status;
String body = "";
boolean isBase64 = false;
int length = 0;
if (response.path != null) {
if (options.path != null) {
try {
byte[] buffer = Files.readAllBytes(response.path);
byte[] buffer = Files.readAllBytes(options.path);
body = Base64.getEncoder().encodeToString(buffer);
isBase64 = true;
length = buffer.length;
} catch (IOException e) {
throw new PlaywrightException("Failed to read from file: " + response.path, e);
throw new PlaywrightException("Failed to read from file: " + options.path, e);
}
} else if (response.body != null) {
body = response.body;
} else if (options.body != null) {
body = options.body;
isBase64 = false;
length = body.getBytes().length;
} else if (response.bodyBytes != null) {
body = Base64.getEncoder().encodeToString(response.bodyBytes);
} else if (options.bodyBytes != null) {
body = Base64.getEncoder().encodeToString(options.bodyBytes);
isBase64 = true;
length = response.bodyBytes.length;
length = options.bodyBytes.length;
}
Map<String, String> headers = new LinkedHashMap<>();
if (response.headers != null) {
for (Map.Entry<String, String> h : response.headers.entrySet()) {
if (options.headers != null) {
for (Map.Entry<String, String> h : options.headers.entrySet()) {
headers.put(h.getKey().toLowerCase(), h.getValue());
}
}
if (response.contentType != null) {
headers.put("content-type", response.contentType);
} else if (response.path != null) {
headers.put("content-type", Utils.mimeType(response.path));
if (options.contentType != null) {
headers.put("content-type", options.contentType);
} else if (options.path != null) {
headers.put("content-type", Utils.mimeType(options.path));
}
if (length != 0 && !headers.containsKey("content-length")) {
headers.put("content-length", Integer.toString(length));
@@ -16,12 +16,10 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.Request;
import com.microsoft.playwright.Route;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -27,30 +27,18 @@ import java.nio.file.Path;
import static com.microsoft.playwright.impl.Serialization.gson;
import static java.nio.charset.StandardCharsets.UTF_8;
class SelectorsImpl extends ChannelOwner implements Selectors {
class SelectorsImpl extends ChannelOwner {
SelectorsImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@Override
public void register(String name, String script, RegisterOptions options) {
void registerImpl(String name, String script, Selectors.RegisterOptions options) {
if (options == null) {
options = new RegisterOptions();
options = new Selectors.RegisterOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("name", name);
params.addProperty("source", script);
sendMessage("register", params);
}
@Override
public void register(String name, Path path, RegisterOptions options) {
byte[] buffer;
try {
buffer = Files.readAllBytes(path);
} catch (IOException e) {
throw new PlaywrightException("Failed to read selector from file: " + path, e);
}
register(name, new String(buffer, UTF_8), options);
}
}
@@ -20,6 +20,7 @@ import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.microsoft.playwright.options.*;
import com.microsoft.playwright.*;
import java.io.ByteArrayOutputStream;
@@ -36,11 +37,18 @@ class Serialization {
static Gson gson() {
if (gson == null) {
gson = new GsonBuilder()
.registerTypeAdapter(BrowserContext.SameSite.class, new SameSiteAdapter().nullSafe())
.registerTypeAdapter(SameSiteAttribute.class, new SameSiteAdapter().nullSafe())
.registerTypeAdapter(ColorScheme.class, new ColorSchemeAdapter().nullSafe())
.registerTypeAdapter(Page.EmulateMediaParams.Media.class, new MediaSerializer())
.registerTypeAdapter(Media.class, new MediaSerializer())
.registerTypeAdapter(ScreenshotType.class, new ToLowerCaseSerializer<ScreenshotType>())
.registerTypeAdapter(MouseButton.class, new ToLowerCaseSerializer<MouseButton>())
.registerTypeAdapter(LoadState.class, new ToLowerCaseSerializer<LoadState>())
.registerTypeAdapter(WaitUntilState.class, new ToLowerCaseSerializer<WaitUntilState>())
.registerTypeAdapter(WaitForSelectorState.class, new ToLowerCaseSerializer<WaitForSelectorState>())
.registerTypeAdapter((new TypeToken<List<KeyboardModifier>>(){}).getType(), new KeyboardModifiersSerializer())
.registerTypeAdapter(Optional.class, new OptionalSerializer())
.registerTypeHierarchyAdapter(JSHandleImpl.class, new HandleSerializer())
.registerTypeHierarchyAdapter(Map.class, new StringMapSerializer())
.registerTypeAdapter(Path.class, new PathSerializer()).create();
}
return gson;
@@ -179,39 +187,29 @@ class Serialization {
throw new PlaywrightException("Unexpected result: " + gson().toJson(value));
}
static String toProtocol(Mouse.Button button) {
switch (button) {
case LEFT:
return "left";
case RIGHT:
return "right";
case MIDDLE:
return "middle";
default:
throw new PlaywrightException("Unexpected value: " + button);
private static class KeyboardModifiersSerializer implements JsonSerializer<List<KeyboardModifier>> {
@Override
public JsonArray serialize(List<KeyboardModifier> modifiers, Type typeOfSrc, JsonSerializationContext context) {
JsonArray result = new JsonArray();
if (modifiers.contains(KeyboardModifier.ALT)) {
result.add("Alt");
}
if (modifiers.contains(KeyboardModifier.CONTROL)) {
result.add("Control");
}
if (modifiers.contains(KeyboardModifier.META)) {
result.add("Meta");
}
if (modifiers.contains(KeyboardModifier.SHIFT)) {
result.add("Shift");
}
return result;
}
}
static JsonArray toProtocol(Set<Keyboard.Modifier> modifiers) {
JsonArray result = new JsonArray();
if (modifiers.contains(Keyboard.Modifier.ALT)) {
result.add("Alt");
}
if (modifiers.contains(Keyboard.Modifier.CONTROL)) {
result.add("Control");
}
if (modifiers.contains(Keyboard.Modifier.META)) {
result.add("Meta");
}
if (modifiers.contains(Keyboard.Modifier.SHIFT)) {
result.add("Shift");
}
return result;
}
static JsonArray toJsonArray(FileChooser.FilePayload[] files) {
static JsonArray toJsonArray(FilePayload[] files) {
JsonArray jsonFiles = new JsonArray();
for (FileChooser.FilePayload p : files) {
for (FilePayload p : files) {
JsonObject jsonFile = new JsonObject();
jsonFile.addProperty("name", p.name);
jsonFile.addProperty("mimeType", p.mimeType);
@@ -252,13 +250,14 @@ class Serialization {
private static class OptionalSerializer implements JsonSerializer<Optional<?>> {
private static boolean isSupported(Type type) {
return new TypeToken<Optional<Page.EmulateMediaParams.Media>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
new TypeToken<Optional<ColorScheme>>() {}.getType().getTypeName().equals(type.getTypeName());
return new TypeToken<Optional<Media>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
new TypeToken<Optional<ColorScheme>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
new TypeToken<Optional<ViewportSize>>() {}.getType().getTypeName().equals(type.getTypeName());
}
@Override
public JsonElement serialize(Optional<?> src, Type typeOfSrc, JsonSerializationContext context) {
assert isSupported(typeOfSrc);
assert isSupported(typeOfSrc) : "Unexpected optional type: " + typeOfSrc.getTypeName();
if (!src.isPresent()) {
return new JsonPrimitive("null");
}
@@ -275,9 +274,19 @@ class Serialization {
}
}
private static class MediaSerializer implements JsonSerializer<Page.EmulateMediaParams.Media> {
private static class StringMapSerializer implements JsonSerializer<Map<String, String>> {
@Override
public JsonElement serialize(Page.EmulateMediaParams.Media src, Type typeOfSrc, JsonSerializationContext context) {
public JsonElement serialize(Map<String, String> src, Type typeOfSrc, JsonSerializationContext context) {
if (!"java.util.Map<java.lang.String, java.lang.String>".equals(typeOfSrc.getTypeName())) {
throw new PlaywrightException("Unexpected map type: " + typeOfSrc);
}
return toProtocol(src);
}
}
private static class MediaSerializer implements JsonSerializer<Media> {
@Override
public JsonElement serialize(Media src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString().toLowerCase());
}
}
@@ -289,9 +298,16 @@ class Serialization {
}
}
private static class SameSiteAdapter extends TypeAdapter<BrowserContext.SameSite> {
private static class ToLowerCaseSerializer<E extends Enum<E>> implements JsonSerializer<E> {
@Override
public void write(JsonWriter out, BrowserContext.SameSite value) throws IOException {
public JsonElement serialize(E src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString().toLowerCase());
}
}
private static class SameSiteAdapter extends TypeAdapter<SameSiteAttribute> {
@Override
public void write(JsonWriter out, SameSiteAttribute value) throws IOException {
String stringValue;
switch (value) {
case STRICT:
@@ -310,9 +326,9 @@ class Serialization {
}
@Override
public BrowserContext.SameSite read(JsonReader in) throws IOException {
public SameSiteAttribute read(JsonReader in) throws IOException {
String value = in.nextString();
return BrowserContext.SameSite.valueOf(value.toUpperCase());
return SameSiteAttribute.valueOf(value.toUpperCase());
}
}
@@ -1,47 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import java.io.PrintStream;
import java.io.PrintWriter;
class ServerException extends PlaywrightException {
private final SerializedError.Error error;
ServerException(SerializedError.Error error) {
super(error.name + ": " + error.message);
this.error = error;
}
@Override
public void printStackTrace(PrintWriter s) {
super.printStackTrace(s);
s.println("Caused by Playwright server error:");
s.println(getMessage());
s.println(error.stack);
}
@Override
public void printStackTrace(PrintStream s) {
super.printStackTrace(s);
s.println("Caused by Playwright server error:");
s.println(getMessage());
s.println(error.stack);
}
}
@@ -0,0 +1,77 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Selectors;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static java.nio.charset.StandardCharsets.UTF_8;
public class SharedSelectors extends LoggingSupport implements Selectors {
private final List<SelectorsImpl> channels = new ArrayList<>();
private final List<Registration> registrations = new ArrayList<>();
private static class Registration {
final String name;
final String script;
final RegisterOptions options;
Registration(String name, String script, RegisterOptions options) {
this.name = name;
this.script = script;
this.options = options;
}
}
@Override
public void register(String name, String script, RegisterOptions options) {
withLogging("Selectors.register", () -> registerImpl(name, script, options));
}
@Override
public void register(String name, Path path, RegisterOptions options) {
withLogging("Selectors.register", () -> {
byte[] buffer;
try {
buffer = Files.readAllBytes(path);
} catch (IOException e) {
throw new PlaywrightException("Failed to read selector from file: " + path, e);
}
registerImpl(name, new String(buffer, UTF_8), options);
});
}
void addChannel(SelectorsImpl channel) {
registrations.forEach(r -> channel.registerImpl(r.name, r.script, r.options));
channels.add(channel);
}
void removeChannel(SelectorsImpl channel) {
channels.remove(channel);
}
private void registerImpl(String name, String script, RegisterOptions options) {
channels.forEach(impl -> impl.registerImpl(name, script, options));
registrations.add(new Registration(name, script, options));
}
}
@@ -20,8 +20,8 @@ class TimeoutSettings {
private static final int DEFAULT_TIMEOUT_MS = 30_000;
private final TimeoutSettings parent;
private Integer defaultTimeout ;
private Integer defaultNavigationTimeout;
private Double defaultTimeout ;
private Double defaultNavigationTimeout;
TimeoutSettings() {
this(null);
@@ -30,15 +30,15 @@ class TimeoutSettings {
this.parent = parent;
}
void setDefaultTimeout(int timeout) {
void setDefaultTimeout(double timeout) {
defaultTimeout = timeout;
}
void setDefaultNavigationTimeout(int timeout) {
void setDefaultNavigationTimeout(double timeout) {
defaultNavigationTimeout = timeout;
}
int timeout(Integer timeout) {
double timeout(Double timeout) {
if (timeout != null) {
return timeout;
}
@@ -51,7 +51,7 @@ class TimeoutSettings {
return DEFAULT_TIMEOUT_MS;
}
int navigationTimeout(Integer timeout) {
double navigationTimeout(Double timeout) {
if (timeout != null) {
return timeout;
}
@@ -67,7 +67,7 @@ class TimeoutSettings {
return DEFAULT_TIMEOUT_MS;
}
<T> Waitable<T> createWaitable(Integer timeout) {
<T> Waitable<T> createWaitable(Double timeout) {
if (timeout != null && timeout == 0) {
return new WaitableNever<>();
}
@@ -27,10 +27,12 @@ class TouchscreenImpl implements Touchscreen {
}
@Override
public void tap(int x, int y) {
JsonObject params = new JsonObject();
params.addProperty("x", x);
params.addProperty("y", y);
page.sendMessage("touchscreenTap", params);
public void tap(double x, double y) {
page.withLogging("Touchscreen.tap", () -> {
JsonObject params = new JsonObject();
params.addProperty("x", x);
params.addProperty("y", y);
page.sendMessage("touchscreenTap", params);
});
}
}
@@ -13,152 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class Transport {
private final BlockingQueue<String> incoming = new ArrayBlockingQueue<>(1000);
private final BlockingQueue<String> outgoing= new ArrayBlockingQueue<>(1000);
private final ReaderThread readerThread;
private final WriterThread writerThread;
private boolean isClosed;
Transport(InputStream input, OutputStream output) {
DataInputStream in = new DataInputStream(new BufferedInputStream(input));
readerThread = new ReaderThread(in, incoming);
readerThread.start();
writerThread = new WriterThread(output, outgoing);
writerThread.start();
}
public void send(String message) {
if (isClosed) {
throw new PlaywrightException("Playwright connection closed");
}
try {
outgoing.put(message);
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to send message", e);
}
}
public String poll(Duration timeout) {
if (isClosed) {
throw new PlaywrightException("Playwright connection closed");
}
try {
return incoming.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to read message", e);
}
}
void close() throws IOException {
if (isClosed) {
return;
}
isClosed = true;
// We interrupt only the outgoing pipe and keep reader thread running as
// otherwise child process may block on writing to its stdout and never
// exit (observed on Windows).
readerThread.isClosing = true;
writerThread.out.close();
writerThread.interrupt();
}
}
class ReaderThread extends Thread {
private final DataInputStream in;
private final BlockingQueue<String> queue;
volatile boolean isClosing;
private static int readIntLE(DataInputStream in) throws IOException {
int ch1 = in.read();
int ch2 = in.read();
int ch3 = in.read();
int ch4 = in.read();
if ((ch1 | ch2 | ch3 | ch4) < 0) {
throw new EOFException();
} else {
return (ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0);
}
}
ReaderThread(DataInputStream in, BlockingQueue<String> queue) {
this.in = in;
this.queue = queue;
}
@Override
public void run() {
while (!isInterrupted()) {
try {
queue.put(readMessage());
} catch (IOException e) {
if (!isInterrupted() && !isClosing) {
e.printStackTrace();
}
break;
} catch (InterruptedException e) {
break;
}
}
}
private String readMessage() throws IOException {
int len = readIntLE(in);
byte[] raw = new byte[len];
in.readFully(raw, 0, len);
return new String(raw, StandardCharsets.UTF_8);
}
}
class WriterThread extends Thread {
final OutputStream out;
private final BlockingQueue<String> queue;
private static void writeIntLE(OutputStream out, int v) throws IOException {
out.write(v >>> 0 & 255);
out.write(v >>> 8 & 255);
out.write(v >>> 16 & 255);
out.write(v >>> 24 & 255);
}
WriterThread(OutputStream out, BlockingQueue<String> queue) {
this.out = out;
this.queue = queue;
}
@Override
public void run() {
while (!isInterrupted()) {
try {
if (queue.isEmpty())
out.flush();
sendMessage(queue.take());
} catch (IOException e) {
if (!isInterrupted())
e.printStackTrace();
break;
} catch (InterruptedException e) {
break;
}
}
}
private void sendMessage(String message) throws IOException {
byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
writeIntLE(out, bytes.length);
out.write(bytes);
}
public interface Transport {
void send(String message);
String poll(Duration timeout);
void close() throws IOException;
}
@@ -16,6 +16,8 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.regex.Pattern;
@@ -34,25 +36,20 @@ class UrlMatcher {
return new UrlMatcher(null, null);
}
static UrlMatcher forOneOf(String glob, Pattern pattern, Predicate<String> predicate) {
UrlMatcher result = UrlMatcher.any();
int conditionCount = 0;
if (glob != null) {
conditionCount += 1;
result = new UrlMatcher(glob);
static UrlMatcher forOneOf(Object object) {
if (object == null) {
return UrlMatcher.any();
}
if (pattern != null) {
conditionCount += 1;
result = new UrlMatcher(pattern);
if (object instanceof String) {
return new UrlMatcher((String) object);
}
if (predicate != null) {
conditionCount += 1;
result = new UrlMatcher(predicate);
if (object instanceof Pattern) {
return new UrlMatcher((Pattern) object);
}
if (conditionCount > 1) {
throw new IllegalArgumentException("Only one of glob, pattern and predicate can be specified");
if (object instanceof Predicate) {
return new UrlMatcher((Predicate<String>) object);
}
return result;
throw new PlaywrightException("Url must be String, Pattern or Predicate<String>, found: " + object.getClass().getTypeName());
}
UrlMatcher(String url) {
@@ -18,13 +18,13 @@ package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.microsoft.playwright.FileChooser;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.FilePayload;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -114,8 +114,8 @@ class Utils {
return mimeType;
}
static FileChooser.FilePayload[] toFilePayloads(Path[] files) {
List<FileChooser.FilePayload> payloads = new ArrayList<>();
static FilePayload[] toFilePayloads(Path[] files) {
List<FilePayload> payloads = new ArrayList<>();
for (Path file : files) {
byte[] buffer;
try {
@@ -123,13 +123,13 @@ class Utils {
} catch (IOException e) {
throw new PlaywrightException("Failed to read from file", e);
}
payloads.add(new FileChooser.FilePayload(file.getFileName().toString(), mimeType(file), buffer));
payloads.add(new FilePayload(file.getFileName().toString(), mimeType(file), buffer));
}
return payloads.toArray(new FileChooser.FilePayload[0]);
return payloads.toArray(new FilePayload[0]);
}
static void writeToFile(byte[] buffer, Path path) {
Path dir = path.getParent();
static void mkParentDirs(Path file) {
Path dir = file.getParent();
if (dir != null) {
if (!Files.exists(dir)) {
try {
@@ -139,13 +139,30 @@ class Utils {
}
}
}
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(path.toFile()));) {
}
static void writeToFile(byte[] buffer, Path path) {
mkParentDirs(path);
try (FileOutputStream out = new FileOutputStream(path.toFile())) {
out.write(buffer);
} catch (IOException e) {
throw new PlaywrightException("Failed to write to file", e);
}
}
static void writeToFile(InputStream inputStream, Path path) {
mkParentDirs(path);
try (FileOutputStream out = new FileOutputStream(path.toFile())) {
byte[] buf = new byte[8192];
int length;
while ((length = inputStream.read(buf)) > 0) {
out.write(buf, 0, length);
}
} catch (IOException e) {
throw new PlaywrightException("Failed to write to file", e);
}
}
static boolean isSafeCloseError(PlaywrightException exception) {
return isSafeCloseError(exception.getMessage());
}
@@ -0,0 +1,42 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.Video;
import java.nio.file.Path;
class VideoImpl implements Video {
private final PageImpl page;
private Path fullPath;
VideoImpl(PageImpl page) {
this.page = page;
}
void setRelativePath(String path) {
fullPath = page.context().videosDir.resolve(path);
}
@Override
public Path path() {
while (fullPath == null) {
page.connection.processOneMessage();
}
return fullPath;
}
}
@@ -16,23 +16,20 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.Event;
import com.microsoft.playwright.Listener;
import com.microsoft.playwright.Page;
import java.util.function.Consumer;
import java.util.function.Predicate;
class WaitableEvent<EventType> implements Waitable<Event<EventType>>, Listener<EventType> {
class WaitableEvent<EventType, T> implements Waitable<T>, Consumer<T> {
final ListenerCollection<EventType> listeners;
private final EventType type;
private final Predicate<Event<EventType>> predicate;
private Event<EventType> event;
private final Predicate<T> predicate;
private T eventArg;
WaitableEvent(ListenerCollection<EventType> listeners, EventType type) {
this(listeners, type, null);
}
WaitableEvent(ListenerCollection<EventType> listeners, EventType type, Predicate<Event<EventType>> predicate) {
WaitableEvent(ListenerCollection<EventType> listeners, EventType type, Predicate<T> predicate) {
this.listeners = listeners;
this.type = type;
this.predicate = predicate;
@@ -40,19 +37,18 @@ class WaitableEvent<EventType> implements Waitable<Event<EventType>>, Listener<E
}
@Override
public void handle(Event<EventType> event) {
assert type.equals(event.type());
if (predicate != null && !predicate.test(event)) {
public void accept(T eventArg) {
if (predicate != null && !predicate.test(eventArg)) {
return;
}
this.event = event;
this.eventArg = eventArg;
dispose();
}
@Override
public boolean isDone() {
return event != null;
return eventArg != null;
}
@Override
@@ -61,7 +57,7 @@ class WaitableEvent<EventType> implements Waitable<Event<EventType>>, Listener<E
}
@Override
public Event<EventType> get() {
return event;
public T get() {
return eventArg;
}
}
@@ -16,7 +16,6 @@
package com.microsoft.playwright.impl;
import java.util.Arrays;
import java.util.Collection;
class WaitableRace<T> implements Waitable<T> {
@@ -17,6 +17,7 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.TimeoutError;
class WaitableResult<T> implements Waitable<T> {
private T result;
@@ -47,6 +48,9 @@ class WaitableResult<T> implements Waitable<T> {
@Override
public T get() {
if (exception != null) {
if (exception instanceof TimeoutError) {
throw new TimeoutError(exception.getMessage(), exception);
}
throw new PlaywrightException(exception.getMessage(), exception);
}
return result;
@@ -16,13 +16,13 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.TimeoutError;
class WaitableTimeout<T> implements Waitable<T> {
private final long deadline;
private final int timeout;
private final double timeout;
WaitableTimeout(int millis) {
WaitableTimeout(double millis) {
timeout = millis;
deadline = System.nanoTime() + (long) millis * 1_000_000;
}
@@ -34,7 +34,11 @@ class WaitableTimeout<T> implements Waitable<T> {
@Override
public T get() {
throw new PlaywrightException("Timeout " + timeout + "ms exceeded");
String timeoutStr = Double.toString(timeout);
if (timeoutStr.endsWith(".0")) {
timeoutStr = timeoutStr.substring(0, timeoutStr.length() - 2);
}
throw new TimeoutError("Timeout " + timeoutStr + "ms exceeded");
}
@Override
@@ -17,32 +17,87 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.WebSocket;
import com.microsoft.playwright.WebSocketFrame;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.function.Consumer;
class WebSocketImpl extends ChannelOwner implements WebSocket {
private final ListenerCollection<EventType> listeners = new ListenerCollection<>();
private final PageImpl page;
private boolean isClosed;
public WebSocketImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
enum EventType {
CLOSE,
FRAMERECEIVED,
FRAMESENT,
SOCKETERROR,
}
WebSocketImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
page = (PageImpl) parent;
}
@Override
public void addListener(EventType type, Listener<EventType> listener) {
listeners.add(type, listener);
public void onClose(Consumer<WebSocket> handler) {
listeners.add(EventType.CLOSE, handler);
}
@Override
public void removeListener(EventType type, Listener<EventType> listener) {
listeners.remove(type, listener);
public void offClose(Consumer<WebSocket> handler) {
listeners.remove(EventType.CLOSE, handler);
}
@Override
public void onFrameReceived(Consumer<WebSocketFrame> handler) {
listeners.add(EventType.FRAMERECEIVED, handler);
}
@Override
public void offFrameReceived(Consumer<WebSocketFrame> handler) {
listeners.remove(EventType.FRAMERECEIVED, handler);
}
@Override
public void onFrameSent(Consumer<WebSocketFrame> handler) {
listeners.add(EventType.FRAMESENT, handler);
}
@Override
public void offFrameSent(Consumer<WebSocketFrame> handler) {
listeners.remove(EventType.FRAMESENT, handler);
}
@Override
public void onSocketError(Consumer<String> handler) {
listeners.add(EventType.SOCKETERROR, handler);
}
@Override
public void offSocketError(Consumer<String> handler) {
listeners.remove(EventType.SOCKETERROR, handler);
}
@Override
public WebSocketFrame waitForFrameReceived(WaitForFrameReceivedOptions options, Runnable code) {
if (options == null) {
options = new WaitForFrameReceivedOptions();
}
return waitForEventWithTimeout(EventType.FRAMERECEIVED, code, options.timeout);
}
@Override
public WebSocketFrame waitForFrameSent(WaitForFrameSentOptions options, Runnable code) {
if (options == null) {
options = new WaitForFrameSentOptions();
}
return waitForEventWithTimeout(EventType.FRAMESENT, code, options.timeout);
}
@Override
@@ -55,64 +110,42 @@ class WebSocketImpl extends ChannelOwner implements WebSocket {
return initializer.get("url").getAsString();
}
private class WaitableWebSocketError<R> implements Waitable<R>, Listener<EventType> {
private final List<EventType> subscribedEvents;
private String errorMessage;
WaitableWebSocketError() {
subscribedEvents = Arrays.asList(EventType.CLOSE, EventType.SOCKETERROR);
for (EventType e : subscribedEvents) {
addListener(e, this);
}
private class WaitableWebSocketClose<T> extends WaitableEvent<EventType, T> {
WaitableWebSocketClose() {
super(WebSocketImpl.this.listeners, EventType.CLOSE);
}
@Override
public void handle(Event<EventType> event) {
if (EventType.SOCKETERROR == event.type()) {
errorMessage = "Socket error";
} else if (EventType.CLOSE == event.type()) {
errorMessage = "Socket closed";
} else {
return;
}
dispose();
}
@Override
public boolean isDone() {
return errorMessage != null;
}
@Override
public R get() {
throw new PlaywrightException(errorMessage);
}
@Override
public void dispose() {
for (EventType e : subscribedEvents) {
removeListener(e, this);
}
public T get() {
throw new PlaywrightException("Socket closed");
}
}
@Override
public Deferred<Event<EventType>> waitForEvent(EventType event, WaitForEventOptions options) {
if (options == null) {
options = new WaitForEventOptions();
private class WaitableWebSocketError<T> extends WaitableEvent<EventType, T> {
WaitableWebSocketError() {
super(WebSocketImpl.this.listeners, EventType.SOCKETERROR);
}
List<Waitable<Event<EventType>>> waitables = new ArrayList<>();
waitables.add(new WaitableEvent<>(listeners, event, options.predicate));
@Override
public T get() {
throw new PlaywrightException("Socket error");
}
}
private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Double timeout) {
List<Waitable<T>> waitables = new ArrayList<>();
waitables.add(new WaitableEvent<>(listeners, eventType));
waitables.add(new WaitableWebSocketClose<>());
waitables.add(new WaitableWebSocketError<>());
waitables.add(page.createWaitForCloseHelper());
waitables.add(page.createWaitableTimeout(options.timeout));
return toDeferred(new WaitableRace<>(waitables));
waitables.add(page.createWaitableTimeout(timeout));
return runUntil(code, new WaitableRace<>(waitables));
}
private static class FrameDataImpl implements FrameData {
private static class WebSocketFrameImpl implements WebSocketFrame {
private final byte[] bytes;
FrameDataImpl(String payload, boolean isBase64) {
WebSocketFrameImpl(String payload, boolean isBase64) {
if (isBase64) {
bytes = Base64.getDecoder().decode(payload);
} else {
@@ -121,7 +154,7 @@ class WebSocketImpl extends ChannelOwner implements WebSocket {
}
@Override
public byte[] body() {
public byte[] binary() {
return bytes;
}
@@ -135,15 +168,15 @@ class WebSocketImpl extends ChannelOwner implements WebSocket {
void handleEvent(String event, JsonObject parameters) {
switch (event) {
case "frameSent": {
FrameDataImpl frameData = new FrameDataImpl(
WebSocketFrameImpl WebSocketFrame = new WebSocketFrameImpl(
parameters.get("data").getAsString(), parameters.get("opcode").getAsInt() == 2);
listeners.notify(EventType.FRAMESENT, frameData);
listeners.notify(EventType.FRAMESENT, WebSocketFrame);
break;
}
case "frameReceived": {
FrameDataImpl frameData = new FrameDataImpl(
WebSocketFrameImpl WebSocketFrame = new WebSocketFrameImpl(
parameters.get("data").getAsString(), parameters.get("opcode").getAsInt() == 2);
listeners.notify(EventType.FRAMERECEIVED, frameData);
listeners.notify(EventType.FRAMERECEIVED, WebSocketFrame);
break;
}
case "socketError": {
@@ -153,7 +186,7 @@ class WebSocketImpl extends ChannelOwner implements WebSocket {
}
case "close": {
isClosed = true;
listeners.notify(EventType.CLOSE, null);
listeners.notify(EventType.CLOSE, this);
break;
}
default: {
@@ -0,0 +1,119 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
class WebSocketTransport implements Transport {
private final BlockingQueue<String> incoming = new ArrayBlockingQueue<>(1000);
private final ClientConnection clientConnection;
private boolean isClosed;
private volatile Exception lastError;
ListenerCollection<EventType> listeners = new ListenerCollection<>();
private enum EventType { CLOSE }
private class ClientConnection extends WebSocketClient {
ClientConnection(URI serverUri) {
super(serverUri);
}
@Override
public void onOpen(ServerHandshake handshakedata) {
}
@Override
public void onMessage(String message) {
incoming.add(message);
}
@Override
public void onClose(int code, String reason, boolean remote) {
}
@Override
public void onError(Exception ex) {
lastError = ex;
}
}
WebSocketTransport(URI uri, Duration timeout) {
clientConnection = new ClientConnection(uri);
try {
if (!clientConnection.connectBlocking(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
throw new PlaywrightException("Failed to connect", lastError);
}
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to connect", e);
}
}
@Override
public void send(String message) {
checkIfClosed();
clientConnection.send(message);
}
@Override
public String poll(Duration timeout) {
checkIfClosed();
try {
return incoming.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to read message", e);
}
}
@Override
public void close() throws IOException {
if (isClosed) {
return;
}
isClosed = true;
clientConnection.close();
}
void onClose(Consumer<WebSocketTransport> handler) {
listeners.add(EventType.CLOSE, handler);
}
void offClose(Consumer<WebSocketTransport> handler) {
listeners.remove(EventType.CLOSE, handler);
}
private void checkIfClosed() {
if (isClosed) {
throw new PlaywrightException("Playwright connection closed");
}
if (clientConnection.isClosed()) {
isClosed = true;
listeners.notify(EventType.CLOSE, this);
throw new PlaywrightException("Playwright connection closed");
}
}
}
@@ -16,51 +16,76 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.JSHandle;
import com.microsoft.playwright.Worker;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.isFunctionBody;
class WorkerImpl extends ChannelOwner implements Worker {
private final ListenerCollection<EventType> listeners = new ListenerCollection<>();
PageImpl page;
enum EventType {
CLOSE,
}
WorkerImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@Override
public void addListener(EventType type, Listener<EventType> listener) {
listeners.add(type, listener);
public void onClose(Consumer<Worker> handler) {
listeners.add(EventType.CLOSE, handler);
}
@Override
public void removeListener(EventType type, Listener<EventType> listener) {
listeners.remove(type, listener);
public void offClose(Consumer<Worker> handler) {
listeners.remove(EventType.CLOSE, handler);
}
private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Double timeout) {
List<Waitable<T>> waitables = new ArrayList<>();
waitables.add(new WaitableEvent<>(listeners, eventType));
waitables.add(page.createWaitForCloseHelper());
waitables.add(page.createWaitableTimeout(timeout));
return runUntil(code, new WaitableRace<>(waitables));
}
@Override
public Worker waitForClose(WaitForCloseOptions options, Runnable code) {
if (options == null) {
options = new WaitForCloseOptions();
}
return waitForEventWithTimeout(EventType.CLOSE, code, options.timeout);
}
@Override
public Object evaluate(String pageFunction, Object arg) {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpression", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
return deserialize(value);
return withLogging("Worker.evaluate", () -> {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpression", params);
SerializedValue value = gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class);
return deserialize(value);
});
}
@Override
public JSHandle evaluateHandle(String pageFunction, Object arg) {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpressionHandle", params);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("handle").get("guid").getAsString());
return withLogging("Worker.evaluateHandle", () -> {
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.add("arg", gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpressionHandle", params);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("handle").get("guid").getAsString());
});
}
@Override
@@ -68,11 +93,6 @@ class WorkerImpl extends ChannelOwner implements Worker {
return initializer.get("url").getAsString();
}
@Override
public Deferred<Event<EventType>> waitForEvent(EventType event) {
return toDeferred(new WaitableEvent<>(listeners, event));
}
@Override
protected void handleEvent(String event, JsonObject params) {
if ("close".equals(event)) {
@@ -0,0 +1,31 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.Page;
public interface BindingCallback {
interface Source {
BrowserContext context();
Page page();
Frame frame();
}
Object call(Source source, Object... args);
}
@@ -0,0 +1,37 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class BoundingBox {
/**
* the x coordinate of the element in pixels.
*/
public double x;
/**
* the y coordinate of the element in pixels.
*/
public double y;
/**
* the width of the element in pixels.
*/
public double width;
/**
* the height of the element in pixels.
*/
public double height;
}
@@ -0,0 +1,43 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class Clip {
/**
* x-coordinate of top-left corner of clip area
*/
public double x;
/**
* y-coordinate of top-left corner of clip area
*/
public double y;
/**
* width of clipping area
*/
public double width;
/**
* height of clipping area
*/
public double height;
public Clip(double x, double y, double width, double height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
}
@@ -1,12 +1,12 @@
/*
* Copyright (c) Microsoft Corporation.
* <p>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
*
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -14,8 +14,10 @@
* limitations under the License.
*/
package com.microsoft.playwright;
package com.microsoft.playwright.options;
public interface Listener<EventType> {
void handle(Event<EventType> event);
}
public enum ColorScheme {
LIGHT,
DARK,
NO_PREFERENCE
}
@@ -0,0 +1,83 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class Cookie {
public String name;
public String value;
/**
* either url or domain / path are required. Optional.
*/
public String url;
/**
* either url or domain / path are required Optional.
*/
public String domain;
/**
* either url or domain / path are required Optional.
*/
public String path;
/**
* Unix time in seconds. Optional.
*/
public Double expires;
/**
* Optional.
*/
public Boolean httpOnly;
/**
* Optional.
*/
public Boolean secure;
/**
* Optional.
*/
public SameSiteAttribute sameSite;
public Cookie(String name, String value) {
this.name = name;
this.value = value;
}
public Cookie setUrl(String url) {
this.url = url;
return this;
}
public Cookie setDomain(String domain) {
this.domain = domain;
return this;
}
public Cookie setPath(String path) {
this.path = path;
return this;
}
public Cookie setExpires(double expires) {
this.expires = expires;
return this;
}
public Cookie setHttpOnly(boolean httpOnly) {
this.httpOnly = httpOnly;
return this;
}
public Cookie setSecure(boolean secure) {
this.secure = secure;
return this;
}
public Cookie setSameSite(SameSiteAttribute sameSite) {
this.sameSite = sameSite;
return this;
}
}
@@ -0,0 +1,26 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public enum ElementState {
VISIBLE,
HIDDEN,
STABLE,
ENABLED,
DISABLED,
EDITABLE
}
@@ -0,0 +1,38 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class FilePayload {
/**
* [File] name
*/
public String name;
/**
* [File] type
*/
public String mimeType;
/**
* File content
*/
public byte[] buffer;
public FilePayload(String name, String mimeType, byte[] buffer) {
this.name = name;
this.mimeType = mimeType;
this.buffer = buffer;
}
}

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