1
0
mirror of synced 2026-05-22 18:53:15 +00:00

Compare commits

...

36 Commits

Author SHA1 Message Date
Yury Semikhatsky c433c1d6e5 chore: mark 1.59.0 (#1909) 2026-04-09 14:14:15 -07:00
Yury Semikhatsky b01bf64e64 chore: resolve Object langAliases in function argument types (#1908) 2026-04-09 13:50:18 -07:00
Yury Semikhatsky 7dbd6cac3b chore: use langAliases from api.json in api generator (#1907) 2026-04-09 12:00:26 -07:00
Yury Semikhatsky c3e4b92982 chore: roll to 1.59.1-beta-1775752988000 (#1906) 2026-04-09 11:31:19 -07:00
dependabot[bot] 9e73db411c chore(deps): bump the actions group with 2 updates (#1904)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 11:18:56 -07:00
dependabot[bot] 10ed7e036d chore(deps-dev): bump org.apache.maven.plugins:maven-resources-plugin from 3.4.0 to 3.5.0 in the all group (#1903) 2026-04-09 11:17:56 -07:00
Yury Semikhatsky 5ccdd3e4b9 chore: roll to 1.59.0-alpha-1774622285000 (#1901) 2026-03-27 12:31:06 -07:00
Yury Semikhatsky afd80add27 chore: roll to 1.59.0-alpha (#1900) 2026-03-23 13:58:14 -07:00
dependabot[bot] 605e428fd7 chore(deps-dev): bump the all group with 2 updates (#1894) 2026-03-20 19:28:22 -07:00
nanne-rl 932669036b fix: handle null close code and reason in WebSocketRoute (#1886) 2026-01-29 12:55:15 -08:00
Copilot 480400793e Update Docker images to Java 25 and Maven 3.9.12 (#1888) 2026-01-28 17:35:11 -08:00
dependabot[bot] 647d8fc034 chore(deps): bump actions/cache from 4 to 5 in the actions group (#1878) 2026-01-28 14:59:28 -08:00
Simon Knott b5c2160d32 chore: roll to 1.58.0 (#1883) 2026-01-28 10:39:26 -08:00
Yury Semikhatsky 63bb008857 chore: roll driver to 1.57.0-beta-1764692940000 (#1870) 2025-12-02 13:59:36 -08:00
dependabot[bot] 9b3a788806 chore(deps): bump actions/checkout from 5 to 6 in the actions group (#1868) 2025-12-01 15:00:50 -08:00
dependabot[bot] 2f387edf0d chore(deps): bump the all group with 3 updates (#1867) 2025-12-01 14:59:52 -08:00
arukiidou 6eb30e275c chore(deps): bump junit.version from 5.13.4 to 5.14.1 (#1859) 2025-11-12 10:00:13 -08:00
arukiidou 0f14588df1 Migrate ExtensionContext.Store.CloseableResource to AutoCloseable (#1860) 2025-11-12 09:59:06 -08:00
arukiidou e417cad372 Fix document typo - setContextOptions (#1862) 2025-11-12 09:45:13 -08:00
Yury Semikhatsky 059667e311 chore: remove background pages implementation (#1861) 2025-10-31 10:25:35 -07:00
Yury Semikhatsky 98296d9cdf devops: update ado approver (#1857) 2025-10-24 13:38:05 -07:00
Yury Semikhatsky 1599e1c7bc chore: roll 1.56.1 (#1855) 2025-10-17 11:11:43 -07:00
dependabot[bot] a2555ddf9e chore(deps): bump the all group with 4 updates (#1846) 2025-10-03 15:46:53 -07:00
Yury Semikhatsky 0deadc2b90 chore: roll driver to 1.56.0-beta (#1849) 2025-10-03 15:45:53 -07:00
Simon Knott eb1fea9907 fix(trace): waitForLoadState title (#1840) 2025-09-08 09:49:00 +02:00
dependabot[bot] dd99ce8b34 chore(deps): bump the actions group with 2 updates (#1838) 2025-09-04 16:18:58 -07:00
dependabot[bot] ed8e9c434f chore(deps): bump the all group across 1 directory with 7 updates (#1839) 2025-09-04 16:18:15 -07:00
Yury Semikhatsky aee298b293 chore: rename headful -> headed (#1835) 2025-08-27 09:51:30 -07:00
Max Schmitt fd2ab4708a devops: enable retries in Docker tests (#1834) 2025-08-26 21:50:28 +02:00
Max Schmitt 2a6cdff664 chore: migrate Trace Viewer tests to use real Trace viewer (#1830) 2025-08-26 10:43:18 +02:00
Max Schmitt 44161e0558 chore: fix Maven test commands (#1832) 2025-08-26 00:35:06 +02:00
Max Schmitt 954b1c43ef refactor: remove unused ImplUtils class (#1833) 2025-08-25 15:29:53 -07:00
Simon Knott f4c7b9734f chore: roll 1.55.0 (#1827) 2025-08-21 17:09:15 +02:00
Yury Semikhatsky dd87b300fb fix: npe in page.pause() (#1828) 2025-08-18 15:51:17 -07:00
Janne Hyötylä f83c03af68 fix: Fix masking in single element screenshots. (#1825) 2025-08-11 11:40:00 -07:00
JONGSHIN d26dd0b112 fix: Replaced classLoader in DriverJar (#1811) 2025-07-31 11:03:19 -07:00
135 changed files with 4018 additions and 818 deletions
+1 -1
View File
@@ -90,7 +90,7 @@ extends:
folderlocation: '$(Build.ArtifactStagingDirectory)/esrp-build'
waitforreleasecompletion: true
owners: 'yurys@microsoft.com'
approvers: 'maxschmitt@microsoft.com'
approvers: 'yurys@microsoft.com'
serviceendpointurl: 'https://api.esrp.microsoft.com'
mainpublisher: 'Playwright'
domaintenantid: '975f013f-7f24-47e8-a7d3-abc4752bf346'
+168
View File
@@ -0,0 +1,168 @@
---
name: playwright-roll
description: Roll Playwright Java to a new version
---
Help the user roll to a new version of Playwright.
ROLLING.md contains general instructions and scripts.
Start with running ./scripts/roll_driver.sh to update the version and generate the API to see the state of things.
Afterwards, work through the list of changes that need to be backported.
You can find a list of pull requests that might need to be taking into account in the issue titled "Backport changes".
Work through them one-by-one and check off the items that you have handled.
Not all of them will be relevant, some might have partially been reverted, etc. - so feel free to check with the upstream release branch.
Rolling includes:
- updating client implementation to match changes in the upstream JS implementation (see ../playwright/packages/playwright-core/src/client)
- adding a couple of new tests to verify new/changed functionality
## Mimicking the JavaScript implementation
The Java client is a port of the JS client in `../playwright/packages/playwright-core/src/client/`. When implementing a new or changed method, always read the corresponding JS file first and mirror its logic:
```
../playwright/packages/playwright-core/src/client/browserContext.ts
../playwright/packages/playwright-core/src/client/page.ts
../playwright/packages/playwright-core/src/client/tracing.ts
../playwright/packages/playwright-core/src/client/video.ts
../playwright/packages/playwright-core/src/client/locator.ts
../playwright/packages/playwright-core/src/client/network.ts
...
```
Key translation rules:
**Protocol calls**`await this._channel.methodName(params)``sendMessage("methodName", params, NO_TIMEOUT)`
**Extracting a returned channel object from a result** — JS uses `SomeClass.from(result.foo)` which resolves the JS-side object for a channel reference. In Java, the object was already created when the server sent `__create__`, so extract it from the connection: `connection.getExistingObject(result.getAsJsonObject("foo").get("guid").getAsString())`
**Async/await** — all `await` calls become synchronous `sendMessage(...)` calls since the Java client is synchronous.
**`undefined` / optional params** — JS `options?.foo` checks translate to `if (options != null && options.foo != null)` null checks before adding to the params `JsonObject`.
**`_channel` fields** — the JS `this._channel.foo` maps to calling `sendMessage("foo", ...)` on `this` in the Impl class.
**Channel object references in params** — when a JS call passes a channel object as a param (e.g. `{ frame: frame._channel }`), in Java pass the guid: `params.addProperty("frame", ((FrameImpl) frame).guid)`.
## Fixing generator and compilation errors
After running `./scripts/roll_driver.sh`, the build often fails because the generated Java interfaces reference new types or methods that the generator doesn't know how to handle yet, and the `*Impl` classes don't implement new interface methods.
### ApiGenerator.java fixes (tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java)
The generator has hardcoded lists that control which imports are added to each generated file. When new classes appear in the API, add them to the relevant lists in `Interface.writeTo`:
- `options.*` import list — add new classes that use types from the options package
- `java.util.*` import list — add new classes that use `List`, `Map`, etc.
- `java.util.function.Consumer` list — add new classes with `Consumer`-typed event handlers
Type mapping: when JS-only types (like `Disposable`) are used as return types in Java-compatible methods, add a mapping in `convertBuiltinType`. For example, `Disposable``AutoCloseable`.
Event handler generation: events with `void` type generate invalid `Consumer<void>`. Handle this case in `Event.writeListenerMethods` by emitting `Runnable` instead.
After editing the generator, recompile and re-run it:
```
mvn -f tools/api-generator/pom.xml compile -q
mvn -f tools/api-generator/pom.xml exec:java -Dexec.mainClass=com.microsoft.playwright.tools.ApiGenerator
```
### Impl class fixes (playwright/src/main/java/com/microsoft/playwright/impl/)
After regenerating, compile `playwright/` to find what's missing:
```
mvn -f playwright/pom.xml compile 2>&1 | grep "ERROR"
```
Common patterns:
**Return type changed (e.g. `void` → `AutoCloseable`):** Update the method signature in the Impl class and return an appropriate `AutoCloseable`. Check the JS client to see what kind of disposable is used:
- If JS returns `DisposableObject.from(result.disposable)` — the server created a disposable channel object. Extract its guid from the protocol result and return `connection.getExistingObject(guid)` (a `DisposableObject`).
- If JS returns `new DisposableStub(() => this.someCleanup())` — it's a local callback. Return `new DisposableStub(this::someCleanup)` in Java.
- Examples: `addInitScript`/`exposeBinding`/`exposeFunction``DisposableObject`; `route(...)``DisposableStub(() -> unroute(...))`; `Tracing.group``DisposableStub(this::groupEnd)`; `Video.start``DisposableStub(this::stop)`.
**New method missing:** Add a stub implementation. Common patterns:
- Simple protocol message: `sendMessage("methodName", params, NO_TIMEOUT)`
- New property accessor (e.g. from initializer): `return initializer.get("fieldName").getAsString()`
- Delegation to mainFrame (for Page methods): `return mainFrame.locator(":root").method(...)`
**New interface entirely (e.g. `Debugger`):** Create a new `*Impl` class extending `ChannelOwner`, implement the interface, and register the type in `Connection.java`'s switch statement. Initialize the field from the parent's initializer in the parent's constructor (e.g. `connection.getExistingObject(initializer.getAsJsonObject("debugger").get("guid").getAsString())`).
**Field visibility:** If a field needs to be accessed from a sibling Impl class (e.g. setting `existingResponse` on `RequestImpl` from `BrowserContextImpl`), change it from `private` to package-private.
**`ListenerCollection` only supports `Consumer<T>`, not `Runnable`.** For void events that use `Runnable` handlers, maintain a plain `List<Runnable>` instead.
**Protocol changes that remove events** — when a method's response now returns an object directly instead of via a subsequent event, update the Impl to capture it from the `sendMessage` result and remove the old event handler. Example: `videoStart` used to fire a `"video"` page event to deliver the artifact; it now returns the artifact directly in the response. Check git history of the upstream JS client when tests hang unexpectedly.
**Protocol parameter renames** — protocol parameter names can change between versions (e.g. `wsEndpoint``endpoint` in `BrowserType.connect`). When a test fails with `expected string, got undefined` or similar validation errors from the driver, check `packages/protocol/src/protocol.yml` for the current parameter names and update the corresponding `params.addProperty(...)` call in the Impl class. Also check the JS client (`src/client/`) to see how it builds the params object.
## Rebuilding the driver-bundle after a roll
`./scripts/roll_driver.sh` does the whole roll pipeline end-to-end: bumps `DRIVER_VERSION`, downloads new driver files into `driver-bundle/src/main/resources/driver/<platform>/`, regenerates `api.json` and the Java interfaces, and updates the README. When all of that succeeds, the next `mvn` invocation that touches `driver-bundle` will pick up the new files and you don't need to think about it.
But if any step in the pipeline fails (the very common case is the API generator throwing on a new type — see *Fixing generator and compilation errors*), the run aborts before `driver-bundle/target/classes/` has been refreshed. From that point on, until you manually rebuild `driver-bundle`, the test JVM will load the **old** driver from the cached `target/classes`/installed jar even though the source resources have already been swapped to the new version.
Fix — rebuild `driver-bundle` once before re-running tests:
```
mvn -f driver-bundle/pom.xml install -DskipTests
```
## Porting and verifying tests
**Before porting an upstream test file, check the API exists in Java.** The upstream repo may have test files for brand-new APIs that haven't been added to the Java interface yet (e.g., `screencast.spec.ts` tests `page.screencast` which may not be in the generated `Page.java`). Check `git diff main --name-only` to see what interfaces were added this roll, and verify the method exists in the generated Java interface before porting.
**Java test file names don't always match upstream spec names.** `TestScreencast.java` tests `recordVideo` video-file recording (which corresponds to `video.spec.ts`), not the newer `page.screencast` streaming API (`screencast.spec.ts`). When comparing coverage, check test *content*, not just file names.
**Remove tests for behavior that was removed upstream.** When the JS client drops a client-side error check (e.g., "Page is not yet closed before saveAs", "Page did not produce any video frames"), delete the corresponding Java tests rather than trying to keep them passing. Check the upstream `tests/library/` spec to confirm the behavior is gone.
**Run the full suite to catch regressions, re-run flaky failures in isolation.** Some tests (e.g., `TestClientCertificates#shouldKeepSupportingHttp`) time out only under heavy parallel load. Run the failing test alone to confirm it's flaky before investigating further.
## Diagnosing hanging tests
When `mvn test` hangs and surefire eventually times the JVM out, it writes thread dumps to `playwright/target/surefire-reports/<timestamp>-jvmRun*.dump`. To find the stuck test:
```
grep "com.microsoft.playwright.Test" playwright/target/surefire-reports/*-jvmRun1.dump | sort -u
```
Each line is a stack frame inside a test method — typically you'll see one or two test methods blocked on a `Future.get()`, `waitForCondition`, or similar. That's the hanging test.
When you've identified a hanging test:
1. Run it in isolation: `mvn -f playwright/pom.xml test -Dtest='TestClass#testMethod'`. If it passes alone, it's a parallel-load flake — note it but move on.
2. If it still hangs in isolation, look for a recent fix in the upstream repo for the *same* test name. Use `git log --oneline tests/library/<spec>.spec.ts` in `~/playwright`. Upstream fixes for client-side hangs are often small and portable (e.g. `about:blank``server.EMPTY_PAGE` from microsoft/playwright#39840 fixed `route-web-socket.spec.ts` arraybuffer hangs — apparently some browser changed the WebSocket origin policy on `about:blank`).
3. When porting an upstream fix, mirror the helper signature change rather than hard-coding workarounds. E.g. if upstream added a `server` parameter to `setupWS`, do the same in Java by injecting `Server server` via the JUnit fixture (`@FixtureTest` already wires up `ServerLifecycle`, so adding `Server server` to the test method signature is enough — no class-level boilerplate). Watch for local-variable shadowing when you add a `Server server` parameter to a method that already has a `WebSocketRoute server` local; rename the local.
## Commit Convention
Semantic commit messages: `label(scope): description`
Labels: `fix`, `feat`, `chore`, `docs`, `test`, `devops`
```bash
git checkout -b fix-39562
# ... make changes ...
git add <changed-files>
git commit -m "$(cat <<'EOF'
fix(proxy): handle SOCKS proxy authentication
Fixes: https://github.com/microsoft/playwright-java/issues/39562
EOF
)"
git push origin fix-39562
gh pr create --repo microsoft/playwright-java --head username:fix-39562 \
--title "fix(proxy): handle SOCKS proxy authentication" \
--body "$(cat <<'EOF'
## Summary
- <describe the change very! briefly>
Fixes https://github.com/microsoft/playwright-java/issues/39562
EOF
)"
```
Never add Co-Authored-By agents in commit message.
Never add "Generated with" in commit message.
Branch naming for issue fixes: `fix-<issue-number>`
## Tips & Tricks
- Project checkouts are in the parent directory (`../`).
- When updating checkboxes, store the issue content into /tmp and edit it there, then update the issue based on the file
- use the "gh" cli to interact with GitHub
+4 -4
View File
@@ -13,9 +13,9 @@ jobs:
environment: Docker
if: github.repository == 'microsoft/playwright-java'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Azure login
uses: azure/login@v2
uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_DOCKER_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_DOCKER_TENANT_ID }}
@@ -23,8 +23,8 @@ jobs:
- name: Login to ACR via OIDC
run: az acr login --name playwright
- name: Set up Docker QEMU for arm64 docker builds
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: arm64
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- run: ./utils/docker/publish_docker.sh stable
+6 -6
View File
@@ -20,9 +20,9 @@ jobs:
browser: [chromium, firefox, webkit]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up JDK 1.8
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: zulu
java-version: 8
@@ -65,13 +65,13 @@ jobs:
browser-channel: msedge
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Media Pack
if: matrix.os == 'windows-latest'
shell: powershell
run: Install-WindowsFeature Server-Media-Foundation
- name: Set up JDK 1.8
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: zulu
java-version: 8
@@ -100,9 +100,9 @@ jobs:
browser: [chromium, firefox, webkit]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up JDK 21
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: adopt
java-version: 21
+2 -2
View File
@@ -13,9 +13,9 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Maven packages
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+18 -4
View File
@@ -21,18 +21,32 @@ jobs:
name: Test
timeout-minutes: 120
runs-on: ${{ matrix.runs-on }}
env:
PW_MAX_RETRIES: 3
strategy:
fail-fast: false
matrix:
flavor: [jammy, noble]
runs-on: [ubuntu-24.04, ubuntu-24.04-arm]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Build Docker image
run: |
ARCH="${{ matrix.runs-on == 'ubuntu-24.04-arm' && 'arm64' || 'amd64' }}"
bash utils/docker/build.sh --$ARCH ${{ matrix.flavor }} playwright-java:localbuild-${{ matrix.flavor }}
- name: Test
- name: Start container
run: |
CONTAINER_ID="$(docker run --rm -e CI --ipc=host -v $(pwd):/root/playwright --name playwright-docker-test -d -t playwright-java:localbuild-${{ matrix.flavor }} /bin/bash)"
docker exec "${CONTAINER_ID}" /root/playwright/tools/test-local-installation/create_project_and_run_tests.sh
CONTAINER_ID=$(docker run --rm -e CI -e PW_MAX_RETRIES --ipc=host -v "$(pwd)":/root/playwright --name playwright-docker-test -d -t playwright-java:localbuild-${{ matrix.flavor }} /bin/bash)
echo "CONTAINER_ID=$CONTAINER_ID" >> $GITHUB_ENV
- name: Run test in container
run: |
docker exec "$CONTAINER_ID" /root/playwright/tools/test-local-installation/create_project_and_run_tests.sh
- name: Test ClassLoader
run: |
docker exec "${CONTAINER_ID}" /root/playwright/tools/test-spring-boot-starter/package_and_run_async_test.sh
- name: Stop container
run: |
docker stop "$CONTAINER_ID"
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Download drivers
run: scripts/download_driver.sh
- name: Regenerate APIs
+2 -2
View File
@@ -32,9 +32,9 @@ scripts/download_driver.sh
mvn compile
mvn test
# Executing a single test
BROWSER=chromium mvn test --projects=playwright -Dtest=TestPageNetworkSizes#shouldHaveTheCorrectResponseBodySize
BROWSER=chromium mvn test -Dtest=TestPageNetworkSizes#shouldHaveTheCorrectResponseBodySize
# Executing a single test class
BROWSER=chromium mvn test --projects=playwright -Dtest=TestPageNetworkSizes
BROWSER=chromium mvn test -Dtest=TestPageNetworkSizes
```
### Generating API
+3 -3
View File
@@ -10,9 +10,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->139.0.7258.5<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->140.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->147.0.7727.15<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->148.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
## Documentation
+2 -14
View File
@@ -2,18 +2,6 @@
* make sure to have at least Java 8 and Maven 3.6.3
* clone playwright for java: http://github.com/microsoft/playwright-java
* `./scripts/roll_driver.sh 1.47.0-beta-1726138322000`
* roll the driver and update generated sources: `./scripts/roll_driver.sh next`
* fix any errors
* commit & send PR with the roll
## Finding driver version
For development versions of Playwright, you can find the latest version by looking at [publish_canary](https://github.com/microsoft/playwright/actions/workflows/publish_canary.yml) workflow -> `publish canary NPM & Publish canary Docker` -> `build & publish driver` step -> `PACKAGE_VERSION`
<img width="960" alt="image" src="https://github.com/microsoft/playwright-java/assets/9798949/4f33a7f1-b39a-4179-8ae7-fb1d84094c75">
# Updating Version
```bash
./scripts/set_maven_version.sh 1.15.0
```
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.59.0</version>
</parent>
<artifactId>driver-bundle</artifactId>
@@ -114,7 +114,7 @@ public class DriverJar extends Driver {
}
public static URI getDriverResourceURI() throws URISyntaxException {
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
ClassLoader classloader = DriverJar.class.getClassLoader();
return classloader.getResource("driver/" + platformDir()).toURI();
}
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.59.0</version>
</parent>
<artifactId>driver</artifactId>
+2 -2
View File
@@ -6,11 +6,11 @@
<groupId>org.example</groupId>
<artifactId>examples</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.59.0</version>
<name>Playwright Client Examples</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<playwright.version>1.54.0</playwright.version>
<playwright.version>1.59.0</playwright.version>
</properties>
<dependencies>
<dependency>
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.59.0</version>
</parent>
<artifactId>playwright</artifactId>
@@ -51,6 +51,10 @@ public interface APIRequest {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -134,6 +138,10 @@ public interface APIRequest {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -106,6 +106,10 @@ public interface Browser extends AutoCloseable {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -323,6 +327,10 @@ public interface Browser extends AutoCloseable {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -674,6 +682,10 @@ public interface Browser extends AutoCloseable {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -891,6 +903,10 @@ public interface Browser extends AutoCloseable {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -1206,6 +1222,44 @@ public interface Browser extends AutoCloseable {
return this;
}
}
class BindOptions {
/**
* Host to bind the web socket server to. When specified, a web socket server is created instead of a named pipe.
*/
public String host;
/**
* Port to bind the web socket server to. When specified, a web socket server is created instead of a named pipe. Use
* {@code 0} to let the OS pick an available port.
*/
public Integer port;
/**
* Working directory associated with this browser server.
*/
public String workspaceDir;
/**
* Host to bind the web socket server to. When specified, a web socket server is created instead of a named pipe.
*/
public BindOptions setHost(String host) {
this.host = host;
return this;
}
/**
* Port to bind the web socket server to. When specified, a web socket server is created instead of a named pipe. Use
* {@code 0} to let the OS pick an available port.
*/
public BindOptions setPort(int port) {
this.port = port;
return this;
}
/**
* Working directory associated with this browser server.
*/
public BindOptions setWorkspaceDir(String workspaceDir) {
this.workspaceDir = workspaceDir;
return this;
}
}
class StartTracingOptions {
/**
* specify custom categories to use instead of default.
@@ -1388,6 +1442,22 @@ public interface Browser extends AutoCloseable {
* @since v1.8
*/
Page newPage(NewPageOptions options);
/**
* Binds the browser to a named pipe or web socket, making it available for other clients to connect to.
*
* @param title Title of the browser server, used for identification.
* @since v1.59
*/
default BindResult bind(String title) {
return bind(title, null);
}
/**
* Binds the browser to a named pipe or web socket, making it available for other clients to connect to.
*
* @param title Title of the browser server, used for identification.
* @since v1.59
*/
BindResult bind(String title, BindOptions options);
/**
* <strong>NOTE:</strong> This API controls <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">Chromium Tracing</a>
* which is a low-level chromium-specific debugging tool. API to control <a
@@ -1468,6 +1538,12 @@ public interface Browser extends AutoCloseable {
* @since v1.11
*/
byte[] stopTracing();
/**
* Unbinds the browser server previously bound with {@link com.microsoft.playwright.Browser#bind Browser.bind()}.
*
* @since v1.59
*/
void unbind();
/**
* Returns the browser version.
*
@@ -46,14 +46,7 @@ import java.util.regex.Pattern;
public interface BrowserContext extends AutoCloseable {
/**
* <strong>NOTE:</strong> Only works with Chromium browser's persistent context.
*
* <p> Emitted when new background page is created in the context.
* <pre>{@code
* context.onBackgroundPage(backgroundPage -> {
* System.out.println(backgroundPage.url());
* });
* }</pre>
* @deprecated Background pages have been removed from Chromium together with Manifest V2 extensions.
*/
void onBackgroundPage(Consumer<Page> handler);
/**
@@ -521,6 +514,12 @@ public interface BrowserContext extends AutoCloseable {
* @since v1.45
*/
Clock clock();
/**
* Debugger allows to pause and resume the execution.
*
* @since v1.59
*/
Debugger debugger();
/**
* Adds cookies into this browser context. All pages within this context will have these cookies installed. Cookies can be
* obtained via {@link com.microsoft.playwright.BrowserContext#cookies BrowserContext.cookies()}.
@@ -559,7 +558,7 @@ public interface BrowserContext extends AutoCloseable {
* @param script Script to be evaluated in all pages in the browser context.
* @since v1.8
*/
void addInitScript(String script);
AutoCloseable addInitScript(String script);
/**
* Adds a script which would be evaluated in one of the following scenarios:
* <ul>
@@ -586,11 +585,9 @@ public interface BrowserContext extends AutoCloseable {
* @param script Script to be evaluated in all pages in the browser context.
* @since v1.8
*/
void addInitScript(Path script);
AutoCloseable addInitScript(Path script);
/**
* <strong>NOTE:</strong> Background pages are only supported on Chromium-based browsers.
*
* <p> All existing background pages in the context.
* @deprecated Background pages have been removed from Chromium together with Manifest V2 extensions.
*
* @since v1.11
*/
@@ -739,8 +736,8 @@ public interface BrowserContext extends AutoCloseable {
* @param callback Callback function that will be called in the Playwright's context.
* @since v1.8
*/
default void exposeBinding(String name, BindingCallback callback) {
exposeBinding(name, callback, null);
default AutoCloseable exposeBinding(String name, BindingCallback callback) {
return exposeBinding(name, callback, null);
}
/**
* The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context.
@@ -786,7 +783,7 @@ public interface BrowserContext extends AutoCloseable {
* @param callback Callback function that will be called in the Playwright's context.
* @since v1.8
*/
void exposeBinding(String name, BindingCallback callback, ExposeBindingOptions options);
AutoCloseable exposeBinding(String name, BindingCallback callback, ExposeBindingOptions options);
/**
* The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context.
* When called, the function executes {@code callback} and returns a <a
@@ -845,7 +842,7 @@ public interface BrowserContext extends AutoCloseable {
* @param callback Callback function that will be called in the Playwright's context.
* @since v1.8
*/
void exposeFunction(String name, FunctionCallback callback);
AutoCloseable exposeFunction(String name, FunctionCallback callback);
/**
* Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if
* specified.
@@ -865,6 +862,8 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "clipboard-write"}</li>
* <li> {@code "geolocation"}</li>
* <li> {@code "gyroscope"}</li>
* <li> {@code "local-fonts"}</li>
* <li> {@code "local-network-access"}</li>
* <li> {@code "magnetometer"}</li>
* <li> {@code "microphone"}</li>
* <li> {@code "midi-sysex"} (system-exclusive midi)</li>
@@ -872,7 +871,7 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "notifications"}</li>
* <li> {@code "payment-handler"}</li>
* <li> {@code "storage-access"}</li>
* <li> {@code "local-fonts"}</li>
* <li> {@code "screen-wake-lock"}</li>
* </ul>
* @since v1.8
*/
@@ -898,6 +897,8 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "clipboard-write"}</li>
* <li> {@code "geolocation"}</li>
* <li> {@code "gyroscope"}</li>
* <li> {@code "local-fonts"}</li>
* <li> {@code "local-network-access"}</li>
* <li> {@code "magnetometer"}</li>
* <li> {@code "microphone"}</li>
* <li> {@code "midi-sysex"} (system-exclusive midi)</li>
@@ -905,11 +906,17 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "notifications"}</li>
* <li> {@code "payment-handler"}</li>
* <li> {@code "storage-access"}</li>
* <li> {@code "local-fonts"}</li>
* <li> {@code "screen-wake-lock"}</li>
* </ul>
* @since v1.8
*/
void grantPermissions(List<String> permissions, GrantPermissionsOptions options);
/**
* Indicates that the browser context is in the process of closing or has already been closed.
*
* @since v1.59
*/
boolean isClosed();
/**
* <strong>NOTE:</strong> CDP sessions are only supported on Chromium-based browsers.
*
@@ -1001,8 +1008,8 @@ public interface BrowserContext extends AutoCloseable {
* @param handler handler function to route the request.
* @since v1.8
*/
default void route(String url, Consumer<Route> handler) {
route(url, handler, null);
default AutoCloseable route(String url, Consumer<Route> handler) {
return route(url, handler, null);
}
/**
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
@@ -1057,7 +1064,7 @@ public interface BrowserContext extends AutoCloseable {
* @param handler handler function to route the request.
* @since v1.8
*/
void route(String url, Consumer<Route> handler, RouteOptions options);
AutoCloseable route(String url, Consumer<Route> handler, RouteOptions options);
/**
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
@@ -1111,8 +1118,8 @@ public interface BrowserContext extends AutoCloseable {
* @param handler handler function to route the request.
* @since v1.8
*/
default void route(Pattern url, Consumer<Route> handler) {
route(url, handler, null);
default AutoCloseable route(Pattern url, Consumer<Route> handler) {
return route(url, handler, null);
}
/**
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
@@ -1167,7 +1174,7 @@ public interface BrowserContext extends AutoCloseable {
* @param handler handler function to route the request.
* @since v1.8
*/
void route(Pattern url, Consumer<Route> handler, RouteOptions options);
AutoCloseable route(Pattern url, Consumer<Route> handler, RouteOptions options);
/**
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
@@ -1221,8 +1228,8 @@ public interface BrowserContext extends AutoCloseable {
* @param handler handler function to route the request.
* @since v1.8
*/
default void route(Predicate<String> url, Consumer<Route> handler) {
route(url, handler, null);
default AutoCloseable route(Predicate<String> url, Consumer<Route> handler) {
return route(url, handler, null);
}
/**
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
@@ -1277,7 +1284,7 @@ public interface BrowserContext extends AutoCloseable {
* @param handler handler function to route the request.
* @since v1.8
*/
void route(Predicate<String> url, Consumer<Route> handler, RouteOptions options);
AutoCloseable route(Predicate<String> url, Consumer<Route> handler, RouteOptions options);
/**
* If specified the network requests that are made in the context will be served from the HAR file. Read more about <a
* href="https://playwright.dev/java/docs/mock#replaying-from-har">Replaying from HAR</a>.
@@ -1466,6 +1473,21 @@ public interface BrowserContext extends AutoCloseable {
* @since v1.8
*/
String storageState(StorageStateOptions options);
/**
* Clears the existing cookies, local storage and IndexedDB entries for all origins and sets the new storage state.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* // Load storage state from a file and apply it to the context.
* context.setStorageState(Paths.get("state.json"));
* }</pre>
*
* @param storageState Populates context with given storage state. This option can be used to initialize context with logged-in information
* obtained via {@link com.microsoft.playwright.BrowserContext#storageState BrowserContext.storageState()}. Path to the
* file with saved storage state.
* @since v1.59
*/
void setStorageState(Path storageState);
/**
*
*
@@ -1483,7 +1505,7 @@ public interface BrowserContext extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code
* handler} is not specified, removes all routes for the {@code url}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link
* @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}.
* @since v1.8
*/
@@ -1494,7 +1516,7 @@ public interface BrowserContext extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code
* handler} is not specified, removes all routes for the {@code url}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link
* @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}.
* @param handler Optional handler function used to register a routing with {@link com.microsoft.playwright.BrowserContext#route
* BrowserContext.route()}.
@@ -1505,7 +1527,7 @@ public interface BrowserContext extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code
* handler} is not specified, removes all routes for the {@code url}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link
* @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}.
* @since v1.8
*/
@@ -1516,7 +1538,7 @@ public interface BrowserContext extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code
* handler} is not specified, removes all routes for the {@code url}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link
* @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}.
* @param handler Optional handler function used to register a routing with {@link com.microsoft.playwright.BrowserContext#route
* BrowserContext.route()}.
@@ -1527,7 +1549,7 @@ public interface BrowserContext extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code
* handler} is not specified, removes all routes for the {@code url}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link
* @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}.
* @since v1.8
*/
@@ -1538,7 +1560,7 @@ public interface BrowserContext extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code
* handler} is not specified, removes all routes for the {@code url}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link
* @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}.
* @param handler Optional handler function used to register a routing with {@link com.microsoft.playwright.BrowserContext#route
* BrowserContext.route()}.
@@ -128,6 +128,11 @@ public interface BrowserType {
* Additional HTTP headers to be sent with connect request. Optional.
*/
public Map<String, String> headers;
/**
* Tells Playwright that it runs on the same host as the CDP server. It will enable certain optimizations that rely upon
* the file system being the same between Playwright and the Browser.
*/
public Boolean isLocal;
/**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0.
@@ -146,6 +151,14 @@ public interface BrowserType {
this.headers = headers;
return this;
}
/**
* Tells Playwright that it runs on the same host as the CDP server. It will enable certain optimizations that rely upon
* the file system being the same between Playwright and the Browser.
*/
public ConnectOverCDPOptions setIsLocal(boolean isLocal) {
this.isLocal = isLocal;
return this;
}
/**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0.
@@ -171,6 +184,12 @@ public interface BrowserType {
* href="https://peter.sh/experiments/chromium-command-line-switches/">here</a>.
*/
public List<String> args;
/**
* If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory is not
* cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the browser
* closes.
*/
public Path artifactsDir;
/**
* Browser distribution channel.
*
@@ -186,10 +205,6 @@ public interface BrowserType {
* Enable Chromium sandboxing. Defaults to {@code false}.
*/
public Boolean chromiumSandbox;
/**
* @deprecated Use <a href="https://playwright.dev/java/docs/debug">debugging tools</a> instead.
*/
public Boolean devtools;
/**
* If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is
* deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in
@@ -229,8 +244,7 @@ public interface BrowserType {
/**
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true} unless
* the {@code devtools} option is {@code true}.
* href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true}.
*/
public Boolean headless;
/**
@@ -271,6 +285,15 @@ public interface BrowserType {
this.args = args;
return this;
}
/**
* If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory is not
* cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the browser
* closes.
*/
public LaunchOptions setArtifactsDir(Path artifactsDir) {
this.artifactsDir = artifactsDir;
return this;
}
@Deprecated
/**
* Browser distribution channel.
@@ -307,13 +330,6 @@ public interface BrowserType {
this.chromiumSandbox = chromiumSandbox;
return this;
}
/**
* @deprecated Use <a href="https://playwright.dev/java/docs/debug">debugging tools</a> instead.
*/
public LaunchOptions setDevtools(boolean devtools) {
this.devtools = devtools;
return this;
}
/**
* If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is
* deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in
@@ -374,8 +390,7 @@ public interface BrowserType {
/**
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true} unless
* the {@code devtools} option is {@code true}.
* href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true}.
*/
public LaunchOptions setHeadless(boolean headless) {
this.headless = headless;
@@ -445,6 +460,12 @@ public interface BrowserType {
* href="https://peter.sh/experiments/chromium-command-line-switches/">here</a>.
*/
public List<String> args;
/**
* If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory is not
* cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the browser
* closes.
*/
public Path artifactsDir;
/**
* When using {@link com.microsoft.playwright.Page#navigate Page.navigate()}, {@link com.microsoft.playwright.Page#route
* Page.route()}, {@link com.microsoft.playwright.Page#waitForURL Page.waitForURL()}, {@link
@@ -491,6 +512,10 @@ public interface BrowserType {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -514,10 +539,6 @@ public interface BrowserType {
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
*/
public Double deviceScaleFactor;
/**
* @deprecated Use <a href="https://playwright.dev/java/docs/debug">debugging tools</a> instead.
*/
public Boolean devtools;
/**
* If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is
* deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in
@@ -573,8 +594,7 @@ public interface BrowserType {
/**
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true} unless
* the {@code devtools} option is {@code true}.
* href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true}.
*/
public Boolean headless;
/**
@@ -740,6 +760,15 @@ public interface BrowserType {
this.args = args;
return this;
}
/**
* If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory is not
* cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the browser
* closes.
*/
public LaunchPersistentContextOptions setArtifactsDir(Path artifactsDir) {
this.artifactsDir = artifactsDir;
return this;
}
/**
* When using {@link com.microsoft.playwright.Page#navigate Page.navigate()}, {@link com.microsoft.playwright.Page#route
* Page.route()}, {@link com.microsoft.playwright.Page#waitForURL Page.waitForURL()}, {@link
@@ -813,6 +842,10 @@ public interface BrowserType {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -848,13 +881,6 @@ public interface BrowserType {
this.deviceScaleFactor = deviceScaleFactor;
return this;
}
/**
* @deprecated Use <a href="https://playwright.dev/java/docs/debug">debugging tools</a> instead.
*/
public LaunchPersistentContextOptions setDevtools(boolean devtools) {
this.devtools = devtools;
return this;
}
/**
* If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is
* deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in
@@ -946,8 +972,7 @@ public interface BrowserType {
/**
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true} unless
* the {@code devtools} option is {@code true}.
* href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true}.
*/
public LaunchPersistentContextOptions setHeadless(boolean headless) {
this.headless = headless;
@@ -1229,11 +1254,11 @@ public interface BrowserType {
* <p> <strong>NOTE:</strong> The major and minor version of the Playwright instance that connects needs to match the version of Playwright that
* launches the browser (1.2.3 → is compatible with 1.2.x).
*
* @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}.
* @param endpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}.
* @since v1.8
*/
default Browser connect(String wsEndpoint) {
return connect(wsEndpoint, null);
default Browser connect(String endpoint) {
return connect(endpoint, null);
}
/**
* This method attaches Playwright to an existing browser instance created via {@code BrowserType.launchServer} in Node.js.
@@ -1241,10 +1266,10 @@ public interface BrowserType {
* <p> <strong>NOTE:</strong> The major and minor version of the Playwright instance that connects needs to match the version of Playwright that
* launches the browser (1.2.3 → is compatible with 1.2.x).
*
* @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}.
* @param endpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}.
* @since v1.8
*/
Browser connect(String wsEndpoint, ConnectOptions options);
Browser connect(String endpoint, ConnectOptions options);
/**
* This method attaches Playwright to an existing browser instance using the Chrome DevTools Protocol.
*
@@ -1386,6 +1411,11 @@ public interface BrowserType {
* the **parent** directory of the "Profile Path" seen at {@code chrome://version}.
*
* <p> Note that browsers do not allow launching multiple instances with the same User Data Directory.
*
* <p> <strong>NOTE:</strong> Chromium/Chrome: Due to recent Chrome policy changes, automating the default Chrome user profile is not supported.
* Pointing {@code userDataDir} to Chrome's main "User Data" directory (the profile used for your regular browsing) may
* result in pages not loading or the browser exiting. Create and use a separate directory (for example, an empty folder)
* as your automation profile instead. See https://developer.chrome.com/blog/remote-debugging-port for details.
* @since v1.8
*/
default BrowserContext launchPersistentContext(Path userDataDir) {
@@ -1406,6 +1436,11 @@ public interface BrowserType {
* the **parent** directory of the "Profile Path" seen at {@code chrome://version}.
*
* <p> Note that browsers do not allow launching multiple instances with the same User Data Directory.
*
* <p> <strong>NOTE:</strong> Chromium/Chrome: Due to recent Chrome policy changes, automating the default Chrome user profile is not supported.
* Pointing {@code userDataDir} to Chrome's main "User Data" directory (the profile used for your regular browsing) may
* result in pages not loading or the browser exiting. Create and use a separate directory (for example, an empty folder)
* as your automation profile instead. See https://developer.chrome.com/blog/remote-debugging-port for details.
* @since v1.8
*/
BrowserContext launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options);
@@ -48,6 +48,16 @@ import com.google.gson.JsonObject;
* }</pre>
*/
public interface CDPSession {
/**
* Emitted when the session is closed, either because the target was closed or {@code session.detach()} was called.
*/
void onClose(Consumer<CDPSession> handler);
/**
* Removes handler that was previously added with {@link #onClose onClose(handler)}.
*/
void offClose(Consumer<CDPSession> handler);
/**
* Detaches the CDPSession from the target. Once detached, the CDPSession object won't emit any events and can't be used to
* send messages.
@@ -69,6 +69,12 @@ public interface ConsoleMessage {
* @since v1.8
*/
String text();
/**
* The timestamp of the console message in milliseconds since the Unix epoch.
*
* @since v1.59
*/
double timestamp();
/**
* One of the following values: {@code "log"}, {@code "debug"}, {@code "info"}, {@code "error"}, {@code "warning"}, {@code
* "dir"}, {@code "dirxml"}, {@code "table"}, {@code "trace"}, {@code "clear"}, {@code "startGroup"}, {@code
@@ -78,5 +84,12 @@ public interface ConsoleMessage {
* @since v1.8
*/
String type();
/**
* The web worker or service worker that produced this console message, if any. Note that console messages from web workers
* also have non-null {@link com.microsoft.playwright.ConsoleMessage#page ConsoleMessage.page()}.
*
* @since v1.57
*/
Worker worker();
}
@@ -0,0 +1,78 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* API for controlling the Playwright debugger. The debugger allows pausing script execution and inspecting the page.
* Obtain the debugger instance via {@link com.microsoft.playwright.BrowserContext#debugger BrowserContext.debugger()}.
*/
public interface Debugger {
/**
* Emitted when the debugger pauses or resumes.
*/
void onPausedStateChanged(Runnable handler);
/**
* Removes handler that was previously added with {@link #onPausedStateChanged onPausedStateChanged(handler)}.
*/
void offPausedStateChanged(Runnable handler);
/**
* Returns details about the currently paused call. Returns {@code null} if the debugger is not paused.
*
* @since v1.59
*/
DebuggerPausedDetails pausedDetails();
/**
* Configures the debugger to pause before the next action is executed.
*
* <p> Throws if the debugger is already paused. Use {@link com.microsoft.playwright.Debugger#next Debugger.next()} or {@link
* com.microsoft.playwright.Debugger#runTo Debugger.runTo()} to step while paused.
*
* <p> Note that {@link com.microsoft.playwright.Page#pause Page.pause()} is equivalent to a "debugger" statement — it pauses
* execution at the call site immediately. On the contrary, {@link com.microsoft.playwright.Debugger#requestPause
* Debugger.requestPause()} is equivalent to "pause on next statement" — it configures the debugger to pause before the
* next action is executed.
*
* @since v1.59
*/
void requestPause();
/**
* Resumes script execution. Throws if the debugger is not paused.
*
* @since v1.59
*/
void resume();
/**
* Resumes script execution and pauses again before the next action. Throws if the debugger is not paused.
*
* @since v1.59
*/
void next();
/**
* Resumes script execution and pauses when an action originates from the given source location. Throws if the debugger is
* not paused.
*
* @param location The source location to pause at.
* @since v1.59
*/
void runTo(Location location);
}
@@ -48,6 +48,9 @@ public interface Download {
/**
* Returns a readable stream for a successful download, or throws for a failed/canceled download.
*
* <p> <strong>NOTE:</strong> If you don't need a readable stream, it's usually simpler to read the file from disk after the download completed. See
* {@link com.microsoft.playwright.Download#path Download.path()}.
*
* @since v1.8
*/
InputStream createReadStream();
@@ -170,6 +170,12 @@ public interface ElementHandle extends JSHandle {
* element.
*/
public Position position;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public Integer steps;
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -244,6 +250,15 @@ public interface ElementHandle extends JSHandle {
this.position = position;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public ClickOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -293,6 +308,12 @@ public interface ElementHandle extends JSHandle {
* element.
*/
public Position position;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public Integer steps;
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -360,6 +381,15 @@ public interface ElementHandle extends JSHandle {
this.position = position;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public DblclickOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -563,6 +563,11 @@ public interface Frame {
* specified, some visible point of the element is used.
*/
public Position sourcePosition;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public Integer steps;
/**
* When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
* element, the call throws an exception.
@@ -617,6 +622,14 @@ public interface Frame {
this.sourcePosition = sourcePosition;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public DragAndDropOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
* element, the call throws an exception.
@@ -2259,7 +2272,7 @@ public interface Frame {
*/
public Double timeout;
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
@@ -2289,7 +2302,7 @@ public interface Frame {
return this;
}
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
@@ -2298,7 +2311,7 @@ public interface Frame {
return this;
}
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
@@ -2307,7 +2320,7 @@ public interface Frame {
return this;
}
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
@@ -3367,7 +3380,7 @@ public interface Frame {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate each element by it's implicit role:
* <p> You can locate each element by its implicit role:
* <pre>{@code
* assertThat(page
* .getByRole(AriaRole.HEADING,
@@ -3410,7 +3423,7 @@ public interface Frame {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate each element by it's implicit role:
* <p> You can locate each element by its implicit role:
* <pre>{@code
* assertThat(page
* .getByRole(AriaRole.HEADING,
@@ -3449,7 +3462,7 @@ public interface Frame {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate the element by it's test id:
* <p> You can locate the element by its test id:
* <pre>{@code
* page.getByTestId("directions").click();
* }</pre>
@@ -3471,7 +3484,7 @@ public interface Frame {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate the element by it's test id:
* <p> You can locate the element by its test id:
* <pre>{@code
* page.getByTestId("directions").click();
* }</pre>
@@ -5126,7 +5139,7 @@ public interface Frame {
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
* @since v1.11
@@ -5143,7 +5156,7 @@ public interface Frame {
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
* @since v1.11
@@ -5158,7 +5171,7 @@ public interface Frame {
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
* @since v1.11
@@ -5175,7 +5188,7 @@ public interface Frame {
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
* @since v1.11
@@ -5190,7 +5203,7 @@ public interface Frame {
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
* @since v1.11
@@ -5207,7 +5220,7 @@ public interface Frame {
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
* @since v1.11
@@ -602,7 +602,7 @@ public interface FrameLocator {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate each element by it's implicit role:
* <p> You can locate each element by its implicit role:
* <pre>{@code
* assertThat(page
* .getByRole(AriaRole.HEADING,
@@ -645,7 +645,7 @@ public interface FrameLocator {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate each element by it's implicit role:
* <p> You can locate each element by its implicit role:
* <pre>{@code
* assertThat(page
* .getByRole(AriaRole.HEADING,
@@ -684,7 +684,7 @@ public interface FrameLocator {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate the element by it's test id:
* <p> You can locate the element by its test id:
* <pre>{@code
* page.getByTestId("directions").click();
* }</pre>
@@ -706,7 +706,7 @@ public interface FrameLocator {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate the element by it's test id:
* <p> You can locate the element by its test id:
* <pre>{@code
* page.getByTestId("directions").click();
* }</pre>
@@ -30,6 +30,15 @@ import java.util.regex.Pattern;
*/
public interface Locator {
class AriaSnapshotOptions {
/**
* When specified, limits the depth of the snapshot.
*/
public Integer depth;
/**
* When set to {@code "ai"}, returns a snapshot optimized for AI consumption. Defaults to {@code "default"}. See details
* for more information.
*/
public AriaSnapshotMode mode;
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -38,6 +47,21 @@ public interface Locator {
*/
public Double timeout;
/**
* When specified, limits the depth of the snapshot.
*/
public AriaSnapshotOptions setDepth(int depth) {
this.depth = depth;
return this;
}
/**
* When set to {@code "ai"}, returns a snapshot optimized for AI consumption. Defaults to {@code "default"}. See details
* for more information.
*/
public AriaSnapshotOptions setMode(AriaSnapshotMode mode) {
this.mode = mode;
return this;
}
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -245,6 +269,12 @@ public interface Locator {
* element.
*/
public Position position;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public Integer steps;
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -320,6 +350,15 @@ public interface Locator {
this.position = position;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public ClickOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -370,6 +409,12 @@ public interface Locator {
* element.
*/
public Position position;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public Integer steps;
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -438,6 +483,15 @@ public interface Locator {
this.position = position;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public DblclickOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -494,6 +548,11 @@ public interface Locator {
* specified, some visible point of the element is used.
*/
public Position sourcePosition;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public Integer steps;
/**
* Drops on the target element at this point relative to the top-left corner of the element's padding box. If not
* specified, some visible point of the element is used.
@@ -543,6 +602,14 @@ public interface Locator {
this.sourcePosition = sourcePosition;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public DragToOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* Drops on the target element at this point relative to the top-left corner of the element's padding box. If not
* specified, some visible point of the element is used.
@@ -2194,7 +2261,7 @@ public interface Locator {
*
* <p> <strong>Usage</strong>
* <pre>{@code
* String[] texts = page.getByRole(AriaRole.LINK).allInnerTexts();
* List<String> texts = page.getByRole(AriaRole.LINK).allInnerTexts();
* }</pre>
*
* @since v1.14
@@ -2209,7 +2276,7 @@ public interface Locator {
*
* <p> <strong>Usage</strong>
* <pre>{@code
* String[] texts = page.getByRole(AriaRole.LINK).allTextContents();
* List<String> texts = page.getByRole(AriaRole.LINK).allTextContents();
* }</pre>
*
* @since v1.14
@@ -2255,6 +2322,12 @@ public interface Locator {
*
* <p> Below is the HTML markup and the respective ARIA snapshot:
*
* <p> An AI-optimized snapshot, controlled by {@code mode}, is different from a default snapshot:
* <ol>
* <li> Includes element references {@code [ref=e2]}. 2. Does not wait for an element matching the locator, and throws when no
* elements match. 3. Includes snapshots of {@code <iframe>}s inside the target.</li>
* </ol>
*
* @since v1.49
*/
default String ariaSnapshot() {
@@ -2286,6 +2359,12 @@ public interface Locator {
*
* <p> Below is the HTML markup and the respective ARIA snapshot:
*
* <p> An AI-optimized snapshot, controlled by {@code mode}, is different from a default snapshot:
* <ol>
* <li> Includes element references {@code [ref=e2]}. 2. Does not wait for an element matching the locator, and throws when no
* elements match. 3. Includes snapshots of {@code <iframe>}s inside the target.</li>
* </ol>
*
* @since v1.49
*/
String ariaSnapshot(AriaSnapshotOptions options);
@@ -2616,6 +2695,22 @@ public interface Locator {
* @since v1.53
*/
Locator describe(String description);
/**
* Returns locator description previously set with {@link com.microsoft.playwright.Locator#describe Locator.describe()}.
* Returns {@code null} if no custom description has been set.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator button = page.getByRole(AriaRole.BUTTON).describe("Subscribe button");
* System.out.println(button.description()); // "Subscribe button"
*
* Locator input = page.getByRole(AriaRole.TEXTBOX);
* System.out.println(input.description()); // null
* }</pre>
*
* @since v1.57
*/
String description();
/**
* Programmatically dispatch an event on the matching element.
*
@@ -3433,7 +3528,7 @@ public interface Locator {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate each element by it's implicit role:
* <p> You can locate each element by its implicit role:
* <pre>{@code
* assertThat(page
* .getByRole(AriaRole.HEADING,
@@ -3476,7 +3571,7 @@ public interface Locator {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate each element by it's implicit role:
* <p> You can locate each element by its implicit role:
* <pre>{@code
* assertThat(page
* .getByRole(AriaRole.HEADING,
@@ -3515,7 +3610,7 @@ public interface Locator {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate the element by it's test id:
* <p> You can locate the element by its test id:
* <pre>{@code
* page.getByTestId("directions").click();
* }</pre>
@@ -3537,7 +3632,7 @@ public interface Locator {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate the element by it's test id:
* <p> You can locate the element by its test id:
* <pre>{@code
* page.getByTestId("directions").click();
* }</pre>
@@ -4186,6 +4281,14 @@ public interface Locator {
* @since v1.14
*/
Locator locator(Locator selectorOrLocator, LocatorOptions options);
/**
* Returns a new locator that uses best practices for referencing the matched element, prioritizing test ids, aria roles,
* and other user-facing attributes over CSS selectors. This is useful for converting implementation-detail selectors into
* more resilient, human-readable locators.
*
* @since v1.59
*/
Locator normalize();
/**
* Returns locator to the n-th matching element. It's zero based, {@code nth(0)} selects the first element.
*
@@ -127,12 +127,16 @@ public interface Mouse {
}
class MoveOptions {
/**
* Defaults to 1. Sends intermediate {@code mousemove} events.
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public Integer steps;
/**
* Defaults to 1. Sends intermediate {@code mousemove} events.
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public MoveOptions setSteps(int steps) {
this.steps = steps;
@@ -860,6 +860,11 @@ public interface Page extends AutoCloseable {
* specified, some visible point of the element is used.
*/
public Position sourcePosition;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public Integer steps;
/**
* When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
* element, the call throws an exception.
@@ -914,6 +919,14 @@ public interface Page extends AutoCloseable {
this.sourcePosition = sourcePosition;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public DragAndDropOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
* element, the call throws an exception.
@@ -1974,6 +1987,20 @@ public interface Page extends AutoCloseable {
return this;
}
}
class ConsoleMessagesOptions {
/**
* Controls which messages are returned:
*/
public ConsoleMessagesFilter filter;
/**
* Controls which messages are returned:
*/
public ConsoleMessagesOptions setFilter(ConsoleMessagesFilter filter) {
this.filter = filter;
return this;
}
}
class LocatorOptions {
/**
* Narrows down the results of the method to those which contain elements matching this relative locator. For example,
@@ -2953,6 +2980,50 @@ public interface Page extends AutoCloseable {
return this;
}
}
class AriaSnapshotOptions {
/**
* When specified, limits the depth of the snapshot.
*/
public Integer depth;
/**
* When set to {@code "ai"}, returns a snapshot optimized for AI consumption: including element references like {@code
* [ref=e2]} and snapshots of {@code <iframe>}s. Defaults to {@code "default"}.
*/
public AriaSnapshotMode mode;
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
* BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()}
* methods.
*/
public Double timeout;
/**
* When specified, limits the depth of the snapshot.
*/
public AriaSnapshotOptions setDepth(int depth) {
this.depth = depth;
return this;
}
/**
* When set to {@code "ai"}, returns a snapshot optimized for AI consumption: including element references like {@code
* [ref=e2]} and snapshots of {@code <iframe>}s. Defaults to {@code "default"}.
*/
public AriaSnapshotOptions setMode(AriaSnapshotMode mode) {
this.mode = mode;
return this;
}
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
* BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()}
* methods.
*/
public AriaSnapshotOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class TapOptions {
/**
* Whether to bypass the <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks. Defaults to
@@ -3415,7 +3486,7 @@ public interface Page extends AutoCloseable {
*/
public Double timeout;
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
@@ -3445,7 +3516,7 @@ public interface Page extends AutoCloseable {
return this;
}
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
@@ -3454,7 +3525,7 @@ public interface Page extends AutoCloseable {
return this;
}
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
@@ -3463,7 +3534,7 @@ public interface Page extends AutoCloseable {
return this;
}
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
@@ -3799,7 +3870,7 @@ public interface Page extends AutoCloseable {
* @param script Script to be evaluated in all pages in the browser context.
* @since v1.8
*/
void addInitScript(String script);
AutoCloseable addInitScript(String script);
/**
* Adds a script which would be evaluated in one of the following scenarios:
* <ul>
@@ -3826,7 +3897,7 @@ public interface Page extends AutoCloseable {
* @param script Script to be evaluated in all pages in the browser context.
* @since v1.8
*/
void addInitScript(Path script);
AutoCloseable addInitScript(Path script);
/**
* Adds a {@code <script>} tag into the page with the desired url or content. Returns the added tag when the script's
* onload fires or when the script content was injected into frame.
@@ -3867,6 +3938,13 @@ public interface Page extends AutoCloseable {
* @since v1.8
*/
void bringToFront();
/**
* Cancels an ongoing {@link com.microsoft.playwright.Page#pickLocator Page.pickLocator()} call by deactivating pick
* locator mode. If no pick locator mode is active, this method is a no-op.
*
* @since v1.59
*/
void cancelPickLocator();
/**
* This method checks an element matching {@code selector} by performing the following steps:
* <ol>
@@ -4598,8 +4676,8 @@ public interface Page extends AutoCloseable {
* @param callback Callback function that will be called in the Playwright's context.
* @since v1.8
*/
default void exposeBinding(String name, BindingCallback callback) {
exposeBinding(name, callback, null);
default AutoCloseable exposeBinding(String name, BindingCallback callback) {
return exposeBinding(name, callback, null);
}
/**
* The method adds a function called {@code name} on the {@code window} object of every frame in this page. When called,
@@ -4648,7 +4726,7 @@ public interface Page extends AutoCloseable {
* @param callback Callback function that will be called in the Playwright's context.
* @since v1.8
*/
void exposeBinding(String name, BindingCallback callback, ExposeBindingOptions options);
AutoCloseable exposeBinding(String name, BindingCallback callback, ExposeBindingOptions options);
/**
* The method adds a function called {@code name} on the {@code window} object of every frame in the page. When called, the
* function executes {@code callback} and returns a <a
@@ -4710,7 +4788,7 @@ public interface Page extends AutoCloseable {
* @param callback Callback function which will be called in Playwright's context.
* @since v1.8
*/
void exposeFunction(String name, FunctionCallback callback);
AutoCloseable exposeFunction(String name, FunctionCallback callback);
/**
* This method waits for an element matching {@code selector}, waits for <a
* href="https://playwright.dev/java/docs/actionability">actionability</a> checks, focuses the element, fills it and
@@ -5049,7 +5127,7 @@ public interface Page extends AutoCloseable {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate each element by it's implicit role:
* <p> You can locate each element by its implicit role:
* <pre>{@code
* assertThat(page
* .getByRole(AriaRole.HEADING,
@@ -5092,7 +5170,7 @@ public interface Page extends AutoCloseable {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate each element by it's implicit role:
* <p> You can locate each element by its implicit role:
* <pre>{@code
* assertThat(page
* .getByRole(AriaRole.HEADING,
@@ -5131,7 +5209,7 @@ public interface Page extends AutoCloseable {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate the element by it's test id:
* <p> You can locate the element by its test id:
* <pre>{@code
* page.getByTestId("directions").click();
* }</pre>
@@ -5153,7 +5231,7 @@ public interface Page extends AutoCloseable {
*
* <p> Consider the following DOM structure.
*
* <p> You can locate the element by it's test id:
* <p> You can locate the element by its test id:
* <pre>{@code
* page.getByTestId("directions").click();
* }</pre>
@@ -5729,6 +5807,43 @@ public interface Page extends AutoCloseable {
* @since v1.8
*/
Keyboard keyboard();
/**
* Clears all stored console messages from this page. Subsequent calls to {@link
* com.microsoft.playwright.Page#consoleMessages Page.consoleMessages()} will only return messages logged after the clear.
*
* @since v1.59
*/
void clearConsoleMessages();
/**
* Clears all stored page errors from this page. Subsequent calls to {@link com.microsoft.playwright.Page#pageErrors
* Page.pageErrors()} will only return errors thrown after the clear.
*
* @since v1.59
*/
void clearPageErrors();
/**
* Returns up to (currently) 200 last console messages from this page. See {@link
* com.microsoft.playwright.Page#onConsoleMessage Page.onConsoleMessage()} for more details.
*
* @since v1.56
*/
default List<ConsoleMessage> consoleMessages() {
return consoleMessages(null);
}
/**
* Returns up to (currently) 200 last console messages from this page. See {@link
* com.microsoft.playwright.Page#onConsoleMessage Page.onConsoleMessage()} for more details.
*
* @since v1.56
*/
List<ConsoleMessage> consoleMessages(ConsoleMessagesOptions options);
/**
* Returns up to (currently) 200 last page errors from this page. See {@link com.microsoft.playwright.Page#onPageError
* Page.onPageError()} for more details.
*
* @since v1.56
*/
List<String> pageErrors();
/**
* The method returns an element locator that can be used to perform actions on this page / frame. Locator is resolved to
* the element immediately before performing an action, so a series of actions on the same locator can in fact be performed
@@ -5812,8 +5927,8 @@ public interface Page extends AutoCloseable {
*/
Page opener();
/**
* Pauses script execution. Playwright will stop executing the script and wait for the user to either press 'Resume' button
* in the page overlay or to call {@code playwright.resume()} in the DevTools console.
* Pauses script execution. Playwright will stop executing the script and wait for the user to either press the 'Resume'
* button in the page overlay or to call {@code playwright.resume()} in the DevTools console.
*
* <p> User can inspect selectors or perform manual steps while paused. Resume will continue running the original script from
* the place it was paused.
@@ -5937,6 +6052,19 @@ public interface Page extends AutoCloseable {
* @since v1.8
*/
byte[] pdf(PdfOptions options);
/**
* Enters pick locator mode where hovering over page elements highlights them and shows the corresponding locator. Once the
* user clicks an element, the mode is deactivated and the {@code Locator} for the picked element is returned.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.pickLocator();
* System.out.println(locator);
* }</pre>
*
* @since v1.59
*/
Locator pickLocator();
/**
* Focuses the element, and then uses {@link com.microsoft.playwright.Keyboard#down Keyboard.down()} and {@link
* com.microsoft.playwright.Keyboard#up Keyboard.up()}.
@@ -6053,6 +6181,21 @@ public interface Page extends AutoCloseable {
* @since v1.9
*/
List<ElementHandle> querySelectorAll(String selector);
/**
* Returns up to (currently) 100 last network request from this page. See {@link com.microsoft.playwright.Page#onRequest
* Page.onRequest()} for more details.
*
* <p> Returned requests should be accessed immediately, otherwise they might be collected to prevent unbounded memory growth
* as new requests come in. Once collected, retrieving most information about the request is impossible.
*
* <p> Note that requests reported through the {@link com.microsoft.playwright.Page#onRequest Page.onRequest()} request are not
* collected, so there is a trade off between efficient memory usage with {@link com.microsoft.playwright.Page#requests
* Page.requests()} and the amount of available information reported through {@link com.microsoft.playwright.Page#onRequest
* Page.onRequest()}.
*
* @since v1.56
*/
List<Request> requests();
/**
* When testing a web page, sometimes unexpected overlays like a "Sign up" dialog appear and block actions you want to
* automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, making them
@@ -6310,6 +6453,8 @@ public interface Page extends AutoCloseable {
* });
* }</pre>
*
* <p> If a request matches multiple registered routes, the most recently registered route takes precedence.
*
* <p> Page routes take precedence over browser context routes (set up with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}) when request matches both handlers.
*
@@ -6323,8 +6468,8 @@ public interface Page extends AutoCloseable {
* @param handler handler function to route the request.
* @since v1.8
*/
default void route(String url, Consumer<Route> handler) {
route(url, handler, null);
default AutoCloseable route(String url, Consumer<Route> handler) {
return route(url, handler, null);
}
/**
* Routing provides the capability to modify network requests that are made by a page.
@@ -6369,6 +6514,8 @@ public interface Page extends AutoCloseable {
* });
* }</pre>
*
* <p> If a request matches multiple registered routes, the most recently registered route takes precedence.
*
* <p> Page routes take precedence over browser context routes (set up with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}) when request matches both handlers.
*
@@ -6382,7 +6529,7 @@ public interface Page extends AutoCloseable {
* @param handler handler function to route the request.
* @since v1.8
*/
void route(String url, Consumer<Route> handler, RouteOptions options);
AutoCloseable route(String url, Consumer<Route> handler, RouteOptions options);
/**
* Routing provides the capability to modify network requests that are made by a page.
*
@@ -6426,6 +6573,8 @@ public interface Page extends AutoCloseable {
* });
* }</pre>
*
* <p> If a request matches multiple registered routes, the most recently registered route takes precedence.
*
* <p> Page routes take precedence over browser context routes (set up with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}) when request matches both handlers.
*
@@ -6439,8 +6588,8 @@ public interface Page extends AutoCloseable {
* @param handler handler function to route the request.
* @since v1.8
*/
default void route(Pattern url, Consumer<Route> handler) {
route(url, handler, null);
default AutoCloseable route(Pattern url, Consumer<Route> handler) {
return route(url, handler, null);
}
/**
* Routing provides the capability to modify network requests that are made by a page.
@@ -6485,6 +6634,8 @@ public interface Page extends AutoCloseable {
* });
* }</pre>
*
* <p> If a request matches multiple registered routes, the most recently registered route takes precedence.
*
* <p> Page routes take precedence over browser context routes (set up with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}) when request matches both handlers.
*
@@ -6498,7 +6649,7 @@ public interface Page extends AutoCloseable {
* @param handler handler function to route the request.
* @since v1.8
*/
void route(Pattern url, Consumer<Route> handler, RouteOptions options);
AutoCloseable route(Pattern url, Consumer<Route> handler, RouteOptions options);
/**
* Routing provides the capability to modify network requests that are made by a page.
*
@@ -6542,6 +6693,8 @@ public interface Page extends AutoCloseable {
* });
* }</pre>
*
* <p> If a request matches multiple registered routes, the most recently registered route takes precedence.
*
* <p> Page routes take precedence over browser context routes (set up with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}) when request matches both handlers.
*
@@ -6555,8 +6708,8 @@ public interface Page extends AutoCloseable {
* @param handler handler function to route the request.
* @since v1.8
*/
default void route(Predicate<String> url, Consumer<Route> handler) {
route(url, handler, null);
default AutoCloseable route(Predicate<String> url, Consumer<Route> handler) {
return route(url, handler, null);
}
/**
* Routing provides the capability to modify network requests that are made by a page.
@@ -6601,6 +6754,8 @@ public interface Page extends AutoCloseable {
* });
* }</pre>
*
* <p> If a request matches multiple registered routes, the most recently registered route takes precedence.
*
* <p> Page routes take precedence over browser context routes (set up with {@link
* com.microsoft.playwright.BrowserContext#route BrowserContext.route()}) when request matches both handlers.
*
@@ -6614,7 +6769,7 @@ public interface Page extends AutoCloseable {
* @param handler handler function to route the request.
* @since v1.8
*/
void route(Predicate<String> url, Consumer<Route> handler, RouteOptions options);
AutoCloseable route(Predicate<String> url, Consumer<Route> handler, RouteOptions options);
/**
* If specified the network requests that are made in the page will be served from the HAR file. Read more about <a
* href="https://playwright.dev/java/docs/mock#replaying-from-har">Replaying from HAR</a>.
@@ -6718,6 +6873,14 @@ public interface Page extends AutoCloseable {
* @since v1.48
*/
void routeWebSocket(Predicate<String> url, Consumer<WebSocketRoute> handler);
/**
* {@code Screencast} object associated with this page.
*
* <p> <strong>Usage</strong>
*
* @since v1.59
*/
Screencast screencast();
/**
* Returns the buffer with the captured screenshot.
*
@@ -7372,6 +7535,22 @@ public interface Page extends AutoCloseable {
* @since v1.8
*/
void setViewportSize(int width, int height);
/**
* Captures the aria snapshot of the page. Read more about <a href="https://playwright.dev/java/docs/aria-snapshots">aria
* snapshots</a>.
*
* @since v1.59
*/
default String ariaSnapshot() {
return ariaSnapshot(null);
}
/**
* Captures the aria snapshot of the page. Read more about <a href="https://playwright.dev/java/docs/aria-snapshots">aria
* snapshots</a>.
*
* @since v1.59
*/
String ariaSnapshot(AriaSnapshotOptions options);
/**
* This method taps an element matching {@code selector} by performing the following steps:
* <ol>
@@ -7519,7 +7698,7 @@ public interface Page extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.Page#route Page.route()}. When {@code handler} is not
* specified, removes all routes for the {@code url}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing.
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while routing.
* @since v1.8
*/
default void unroute(String url) {
@@ -7529,7 +7708,7 @@ public interface Page extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.Page#route Page.route()}. When {@code handler} is not
* specified, removes all routes for the {@code url}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing.
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while routing.
* @param handler Optional handler function to route the request.
* @since v1.8
*/
@@ -7538,7 +7717,7 @@ public interface Page extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.Page#route Page.route()}. When {@code handler} is not
* specified, removes all routes for the {@code url}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing.
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while routing.
* @since v1.8
*/
default void unroute(Pattern url) {
@@ -7548,7 +7727,7 @@ public interface Page extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.Page#route Page.route()}. When {@code handler} is not
* specified, removes all routes for the {@code url}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing.
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while routing.
* @param handler Optional handler function to route the request.
* @since v1.8
*/
@@ -7557,7 +7736,7 @@ public interface Page extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.Page#route Page.route()}. When {@code handler} is not
* specified, removes all routes for the {@code url}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing.
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while routing.
* @since v1.8
*/
default void unroute(Predicate<String> url) {
@@ -7567,7 +7746,7 @@ public interface Page extends AutoCloseable {
* Removes a route created with {@link com.microsoft.playwright.Page#route Page.route()}. When {@code handler} is not
* specified, removes all routes for the {@code url}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing.
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while routing.
* @param handler Optional handler function to route the request.
* @since v1.8
*/
@@ -7579,7 +7758,8 @@ public interface Page extends AutoCloseable {
*/
String url();
/**
* Video object associated with this page.
* Video object associated with this page. Can be used to access the video file when using the {@code recordVideo} context
* option.
*
* @since v1.8
*/
@@ -8405,7 +8585,7 @@ public interface Page extends AutoCloseable {
* page.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
* @since v1.11
@@ -8422,7 +8602,7 @@ public interface Page extends AutoCloseable {
* page.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
* @since v1.11
@@ -8437,7 +8617,7 @@ public interface Page extends AutoCloseable {
* page.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
* @since v1.11
@@ -8454,7 +8634,7 @@ public interface Page extends AutoCloseable {
* page.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
* @since v1.11
@@ -8469,7 +8649,7 @@ public interface Page extends AutoCloseable {
* page.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
* @since v1.11
@@ -8486,7 +8666,7 @@ public interface Page extends AutoCloseable {
* page.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
* @since v1.11
@@ -183,6 +183,16 @@ public interface Request {
* @since v1.8
*/
Response response();
/**
* Returns the {@code Response} object if the response has already been received, {@code null} otherwise.
*
* <p> Unlike {@link com.microsoft.playwright.Request#response Request.response()}, this method does not wait for the response
* to arrive. It returns immediately with the response object if the response has been received, or {@code null} if the
* response has not been received yet.
*
* @since v1.59
*/
Response existingResponse();
/**
* Returns resource size information for given request.
*
@@ -86,6 +86,12 @@ public interface Response {
* @since v1.15
*/
List<String> headerValues(String name);
/**
* Returns the http version used by the response.
*
* @since v1.59
*/
String httpVersion();
/**
* Contains a boolean stating whether the response was successful (status in the range 200-299) or not.
*
@@ -370,9 +370,10 @@ public interface Route {
* matching handlers won't be invoked. Use {@link com.microsoft.playwright.Route#fallback Route.fallback()} If you want
* next matching handler in the chain to be invoked.
*
* <p> <strong>NOTE:</strong> The {@code Cookie} header cannot be overridden using this method. If a value is provided, it will be ignored, and the
* cookie will be loaded from the browser's cookie store. To set custom cookies, use {@link
* com.microsoft.playwright.BrowserContext#addCookies BrowserContext.addCookies()}.
* <p> <strong>NOTE:</strong> Some request headers are **forbidden** and cannot be overridden (for example, {@code Cookie}, {@code Host}, {@code
* Content-Length} and others, see <a
* href="https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header">this MDN page</a> for full list). If
* an override is provided for a forbidden header, it will be ignored and the original request header will be used.To set custom cookies, use {@link com.microsoft.playwright.BrowserContext#addCookies BrowserContext.addCookies()}.
*
* @since v1.8
*/
@@ -402,9 +403,10 @@ public interface Route {
* matching handlers won't be invoked. Use {@link com.microsoft.playwright.Route#fallback Route.fallback()} If you want
* next matching handler in the chain to be invoked.
*
* <p> <strong>NOTE:</strong> The {@code Cookie} header cannot be overridden using this method. If a value is provided, it will be ignored, and the
* cookie will be loaded from the browser's cookie store. To set custom cookies, use {@link
* com.microsoft.playwright.BrowserContext#addCookies BrowserContext.addCookies()}.
* <p> <strong>NOTE:</strong> Some request headers are **forbidden** and cannot be overridden (for example, {@code Cookie}, {@code Host}, {@code
* Content-Length} and others, see <a
* href="https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header">this MDN page</a> for full list). If
* an override is provided for a forbidden header, it will be ignored and the original request header will be used.To set custom cookies, use {@link com.microsoft.playwright.BrowserContext#addCookies BrowserContext.addCookies()}.
*
* @since v1.8
*/
@@ -0,0 +1,237 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
/**
* Interface for capturing screencast frames from a page.
*/
public interface Screencast {
class StartOptions {
/**
* Callback that receives JPEG-encoded frame data.
*/
public Consumer<ScreencastFrame> onFrame;
/**
* Path where the video should be saved when the screencast is stopped. When provided, video recording is started.
*/
public Path path;
/**
* The quality of the image, between 0-100.
*/
public Integer quality;
/**
* Callback that receives JPEG-encoded frame data.
*/
public StartOptions setOnFrame(Consumer<ScreencastFrame> onFrame) {
this.onFrame = onFrame;
return this;
}
/**
* Path where the video should be saved when the screencast is stopped. When provided, video recording is started.
*/
public StartOptions setPath(Path path) {
this.path = path;
return this;
}
/**
* The quality of the image, between 0-100.
*/
public StartOptions setQuality(int quality) {
this.quality = quality;
return this;
}
}
class ShowOverlayOptions {
/**
* Duration in milliseconds after which the overlay is automatically removed. Overlay stays until dismissed if not
* provided.
*/
public Double duration;
/**
* Duration in milliseconds after which the overlay is automatically removed. Overlay stays until dismissed if not
* provided.
*/
public ShowOverlayOptions setDuration(double duration) {
this.duration = duration;
return this;
}
}
class ShowChapterOptions {
/**
* Optional description text displayed below the title.
*/
public String description;
/**
* Duration in milliseconds after which the overlay is automatically removed. Defaults to {@code 2000}.
*/
public Double duration;
/**
* Optional description text displayed below the title.
*/
public ShowChapterOptions setDescription(String description) {
this.description = description;
return this;
}
/**
* Duration in milliseconds after which the overlay is automatically removed. Defaults to {@code 2000}.
*/
public ShowChapterOptions setDuration(double duration) {
this.duration = duration;
return this;
}
}
class ShowActionsOptions {
/**
* How long each annotation is displayed in milliseconds. Defaults to {@code 500}.
*/
public Double duration;
/**
* Font size of the action title in pixels. Defaults to {@code 24}.
*/
public Integer fontSize;
/**
* Position of the action title overlay. Defaults to {@code "top-right"}.
*/
public AnnotatePosition position;
/**
* How long each annotation is displayed in milliseconds. Defaults to {@code 500}.
*/
public ShowActionsOptions setDuration(double duration) {
this.duration = duration;
return this;
}
/**
* Font size of the action title in pixels. Defaults to {@code 24}.
*/
public ShowActionsOptions setFontSize(int fontSize) {
this.fontSize = fontSize;
return this;
}
/**
* Position of the action title overlay. Defaults to {@code "top-right"}.
*/
public ShowActionsOptions setPosition(AnnotatePosition position) {
this.position = position;
return this;
}
}
/**
* Starts the screencast. When {@code path} is provided, it saves video recording to the specified file. When {@code
* onFrame} is provided, delivers JPEG-encoded frames to the callback. Both can be used together.
*
* <p> <strong>Usage</strong>
*
* @since v1.59
*/
default AutoCloseable start() {
return start(null);
}
/**
* Starts the screencast. When {@code path} is provided, it saves video recording to the specified file. When {@code
* onFrame} is provided, delivers JPEG-encoded frames to the callback. Both can be used together.
*
* <p> <strong>Usage</strong>
*
* @since v1.59
*/
AutoCloseable start(StartOptions options);
/**
* Stops the screencast and video recording if active. If a video was being recorded, saves it to the path specified in
* {@link com.microsoft.playwright.Screencast#start Screencast.start()}.
*
* @since v1.59
*/
void stop();
/**
* Adds an overlay with the given HTML content. The overlay is displayed on top of the page until removed. Returns a
* disposable that removes the overlay when disposed.
*
* @param html HTML content for the overlay.
* @since v1.59
*/
default AutoCloseable showOverlay(String html) {
return showOverlay(html, null);
}
/**
* Adds an overlay with the given HTML content. The overlay is displayed on top of the page until removed. Returns a
* disposable that removes the overlay when disposed.
*
* @param html HTML content for the overlay.
* @since v1.59
*/
AutoCloseable showOverlay(String html, ShowOverlayOptions options);
/**
* Shows a chapter overlay with a title and optional description, centered on the page with a blurred backdrop. Useful for
* narrating video recordings. The overlay is removed after the specified duration, or 2000ms.
*
* @param title Title text displayed prominently in the overlay.
* @since v1.59
*/
default void showChapter(String title) {
showChapter(title, null);
}
/**
* Shows a chapter overlay with a title and optional description, centered on the page with a blurred backdrop. Useful for
* narrating video recordings. The overlay is removed after the specified duration, or 2000ms.
*
* @param title Title text displayed prominently in the overlay.
* @since v1.59
*/
void showChapter(String title, ShowChapterOptions options);
/**
* Enables visual annotations on interacted elements. Returns a disposable that stops showing actions when disposed.
*
* @since v1.59
*/
default AutoCloseable showActions() {
return showActions(null);
}
/**
* Enables visual annotations on interacted elements. Returns a disposable that stops showing actions when disposed.
*
* @since v1.59
*/
AutoCloseable showActions(ShowActionsOptions options);
/**
* Shows overlays.
*
* @since v1.59
*/
void showOverlays();
/**
* Removes action decorations.
*
* @since v1.59
*/
void hideActions();
/**
* Hides overlays without removing them.
*
* @since v1.59
*/
void hideOverlays();
}
@@ -44,6 +44,12 @@ import java.nio.file.Path;
*/
public interface Tracing {
class StartOptions {
/**
* When enabled, the trace is written to an unarchived file that is updated in real time as actions occur, instead of
* caching changes and archiving them into a zip file at the end. This is useful for live trace viewing during test
* execution.
*/
public Boolean live;
/**
* If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code
* tracesDir} directory specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify
@@ -74,6 +80,15 @@ public interface Tracing {
*/
public String title;
/**
* When enabled, the trace is written to an unarchived file that is updated in real time as actions occur, instead of
* caching changes and archiving them into a zip file at the end. This is useful for live trace viewing during test
* execution.
*/
public StartOptions setLive(boolean live) {
this.live = live;
return this;
}
/**
* If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code
* tracesDir} directory specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify
@@ -333,8 +348,8 @@ public interface Tracing {
* @param name Group name shown in the trace viewer.
* @since v1.49
*/
default void group(String name) {
group(name, null);
default AutoCloseable group(String name) {
return group(name, null);
}
/**
* <strong>NOTE:</strong> Use {@code test.step} instead when available.
@@ -356,7 +371,7 @@ public interface Tracing {
* @param name Group name shown in the trace viewer.
* @since v1.49
*/
void group(String name, GroupOptions options);
AutoCloseable group(String name, GroupOptions options);
/**
* Closes the last group created by {@link com.microsoft.playwright.Tracing#group Tracing.group()}.
*
@@ -16,6 +16,7 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.nio.file.Path;
/**
@@ -17,6 +17,7 @@
package com.microsoft.playwright;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* The Worker class represents a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">WebWorker</a>.
@@ -44,6 +45,16 @@ public interface Worker {
*/
void offClose(Consumer<Worker> handler);
/**
* Emitted when JavaScript within the worker calls one of console API methods, e.g. {@code console.log} or {@code
* console.dir}.
*/
void onConsole(Consumer<ConsoleMessage> handler);
/**
* Removes handler that was previously added with {@link #onConsole onConsole(handler)}.
*/
void offConsole(Consumer<ConsoleMessage> handler);
class WaitForCloseOptions {
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
@@ -62,6 +73,35 @@ public interface Worker {
return this;
}
}
class WaitForConsoleMessageOptions {
/**
* Receives the {@code ConsoleMessage} object and resolves to true when the waiting should resolve.
*/
public Predicate<ConsoleMessage> predicate;
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
* BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
/**
* Receives the {@code ConsoleMessage} object and resolves to true when the waiting should resolve.
*/
public WaitForConsoleMessageOptions setPredicate(Predicate<ConsoleMessage> predicate) {
this.predicate = predicate;
return this;
}
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
* BrowserContext.setDefaultTimeout()}.
*/
public WaitForConsoleMessageOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
/**
* Returns the return value of {@code expression}.
*
@@ -158,5 +198,21 @@ public interface Worker {
* @since v1.10
*/
Worker waitForClose(WaitForCloseOptions options, Runnable callback);
/**
* Performs action and waits for a console message.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.57
*/
default ConsoleMessage waitForConsoleMessage(Runnable callback) {
return waitForConsoleMessage(null, callback);
}
/**
* Performs action and waits for a console message.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.57
*/
ConsoleMessage waitForConsoleMessage(WaitForConsoleMessageOptions options, Runnable callback);
}
@@ -37,8 +37,11 @@ package com.microsoft.playwright.assertions;
*/
public interface APIResponseAssertions {
/**
* Makes the assertion check for the opposite condition. For example, this code tests that the response status is not
* successful:
* Makes the assertion check for the opposite condition.
*
* <p> <strong>Usage</strong>
*
* <p> For example, this code tests that the response status is not successful:
* <pre>{@code
* assertThat(response).not().isOK();
* }</pre>
@@ -563,8 +563,11 @@ public interface LocatorAssertions {
}
}
/**
* Makes the assertion check for the opposite condition. For example, this code tests that the Locator doesn't contain text
* {@code "error"}:
* Makes the assertion check for the opposite condition.
*
* <p> <strong>Usage</strong>
*
* <p> For example, this code tests that the Locator doesn't contain text {@code "error"}:
* <pre>{@code
* assertThat(locator).not().containsText("error");
* }</pre>
@@ -885,7 +888,7 @@ public interface LocatorAssertions {
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class lists. Each element's class attribute is matched against the corresponding class in the array:
* <pre>{@code
* assertThat(page.locator(".list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
* assertThat(page.locator(".list > .component")).containsClass(Arrays.asList("inactive", "active", "inactive"));
* }</pre>
*
* @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
@@ -909,7 +912,7 @@ public interface LocatorAssertions {
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class lists. Each element's class attribute is matched against the corresponding class in the array:
* <pre>{@code
* assertThat(page.locator(".list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
* assertThat(page.locator(".list > .component")).containsClass(Arrays.asList("inactive", "active", "inactive"));
* }</pre>
*
* @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
@@ -931,7 +934,7 @@ public interface LocatorAssertions {
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class lists. Each element's class attribute is matched against the corresponding class in the array:
* <pre>{@code
* assertThat(page.locator(".list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
* assertThat(page.locator(".list > .component")).containsClass(Arrays.asList("inactive", "active", "inactive"));
* }</pre>
*
* @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
@@ -955,7 +958,7 @@ public interface LocatorAssertions {
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class lists. Each element's class attribute is matched against the corresponding class in the array:
* <pre>{@code
* assertThat(page.locator(".list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
* assertThat(page.locator(".list > .component")).containsClass(Arrays.asList("inactive", "active", "inactive"));
* }</pre>
*
* @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
@@ -989,7 +992,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion:
* <pre>{@code
* // ✓ Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"});
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
*
* // ✖ Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -1034,7 +1037,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion:
* <pre>{@code
* // ✓ Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"});
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
*
* // ✖ Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -1077,7 +1080,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion:
* <pre>{@code
* // ✓ Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"});
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
*
* // ✖ Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -1122,7 +1125,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion:
* <pre>{@code
* // ✓ Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"});
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
*
* // ✖ Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -1165,7 +1168,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion:
* <pre>{@code
* // ✓ Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"});
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
*
* // ✖ Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -1210,7 +1213,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion:
* <pre>{@code
* // ✓ Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"});
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
*
* // ✖ Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -1253,7 +1256,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion:
* <pre>{@code
* // ✓ Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"});
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
*
* // ✖ Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -1298,7 +1301,7 @@ public interface LocatorAssertions {
* <p> Let's see how we can use the assertion:
* <pre>{@code
* // ✓ Contains the right items in the right order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"});
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"});
*
* // ✖ Wrong order
* assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"});
@@ -79,8 +79,11 @@ public interface PageAssertions {
}
}
/**
* Makes the assertion check for the opposite condition. For example, this code tests that the page URL doesn't contain
* {@code "error"}:
* Makes the assertion check for the opposite condition.
*
* <p> <strong>Usage</strong>
*
* <p> For example, this code tests that the page URL doesn't contain {@code "error"}:
* <pre>{@code
* assertThat(page).not().hasURL("error");
* }</pre>
@@ -62,12 +62,15 @@ abstract class AssertionsBase {
if (!log.isEmpty()) {
log = "\nCall log:\n" + log;
}
if (result.errorMessage != null) {
message += "\n" + result.errorMessage;
}
if (expected == null) {
throw new AssertionFailedError(message + log);
}
ValueWrapper expectedValue = formatValue(expected);
ValueWrapper actualValue = formatValue(actual);
message += ": " + expectedValue.getStringRepresentation() + "\nReceived: " + actualValue.getStringRepresentation() + "\n";
message += "\nExpected: " + expectedValue.getStringRepresentation() + "\nReceived: " + actualValue.getStringRepresentation() + "\n";
throw new AssertionFailedError(message + log, expectedValue, actualValue);
}
}
@@ -34,8 +34,7 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes;
@@ -44,10 +43,10 @@ import static java.util.Arrays.asList;
class BrowserContextImpl extends ChannelOwner implements BrowserContext {
protected BrowserImpl browser;
private final TracingImpl tracing;
private final DebuggerImpl debugger;
private final APIRequestContextImpl request;
private final ClockImpl clock;
final List<PageImpl> pages = new ArrayList<>();
final List<PageImpl> backgroundPages = new ArrayList<>();
final Router routes = new Router();
final WebSocketRouter webSocketRoutes = new WebSocketRouter();
@@ -82,7 +81,6 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
enum EventType {
BACKGROUNDPAGE,
CLOSE,
CONSOLE,
DIALOG,
@@ -97,6 +95,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
BrowserContextImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString());
debugger = connection.getExistingObject(initializer.getAsJsonObject("debugger").get("guid").getAsString());
request = connection.getExistingObject(initializer.getAsJsonObject("requestContext").get("guid").getAsString());
request.timeoutSettings = timeoutSettings;
clock = new ClockImpl(this);
@@ -134,12 +133,10 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public void onBackgroundPage(Consumer<Page> handler) {
listeners.add(EventType.BACKGROUNDPAGE, handler);
}
@Override
public void offBackgroundPage(Consumer<Page> handler) {
listeners.remove(EventType.BACKGROUNDPAGE, handler);
}
@Override
@@ -273,6 +270,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
}
@Override
public boolean isClosed() {
return closingOrClosed;
}
@Override
public void close(CloseOptions options) {
if (!closingOrClosed) {
@@ -323,17 +325,18 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public void addInitScript(String script) {
public AutoCloseable addInitScript(String script) {
JsonObject params = new JsonObject();
params.addProperty("source", script);
sendMessage("addInitScript", params, NO_TIMEOUT);
JsonObject result = sendMessage("addInitScript", params, NO_TIMEOUT).getAsJsonObject();
return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString());
}
@Override
public void addInitScript(Path path) {
public AutoCloseable addInitScript(Path path) {
try {
byte[] bytes = readAllBytes(path);
addInitScript(new String(bytes, UTF_8));
return addInitScript(new String(bytes, UTF_8));
} catch (IOException e) {
throw new PlaywrightException("Failed to read script from file", e);
}
@@ -341,7 +344,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public List<Page> backgroundPages() {
return new ArrayList<>(backgroundPages);
return Collections.emptyList();
}
@Override
@@ -389,11 +392,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public void exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
exposeBindingImpl(name, playwrightBinding, options);
public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
return exposeBindingImpl(name, playwrightBinding, options);
}
private void exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
if (bindings.containsKey(name)) {
throw new PlaywrightException("Function \"" + name + "\" has been already registered");
}
@@ -409,12 +412,13 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
if (options != null && options.handle != null && options.handle) {
params.addProperty("needsHandle", true);
}
sendMessage("exposeBinding", params, NO_TIMEOUT);
JsonObject result = sendMessage("exposeBinding", params, NO_TIMEOUT).getAsJsonObject();
return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString());
}
@Override
public void exposeFunction(String name, FunctionCallback playwrightFunction) {
exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args), null);
public AutoCloseable exposeFunction(String name, FunctionCallback playwrightFunction) {
return exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args), null);
}
@Override
@@ -450,18 +454,21 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public void route(String url, Consumer<Route> handler, RouteOptions options) {
public AutoCloseable route(String url, Consumer<Route> handler, RouteOptions options) {
route(UrlMatcher.forGlob(baseUrl(), url, this.connection.localUtils, false), handler, options);
return new DisposableStub(() -> unroute(url, handler));
}
@Override
public void route(Pattern url, Consumer<Route> handler, RouteOptions options) {
public AutoCloseable route(Pattern url, Consumer<Route> handler, RouteOptions options) {
route(new UrlMatcher(url), handler, options);
return new DisposableStub(() -> unroute(url, handler));
}
@Override
public void route(Predicate<String> url, Consumer<Route> handler, RouteOptions options) {
public AutoCloseable route(Predicate<String> url, Consumer<Route> handler, RouteOptions options) {
route(new UrlMatcher(url), handler, options);
return new DisposableStub(() -> unroute(url, handler));
}
@Override
@@ -568,6 +575,18 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
sendMessage("setOffline", params, NO_TIMEOUT);
}
@Override
public void setStorageState(Path storageState) {
try {
String state = new String(readAllBytes(storageState), UTF_8);
JsonObject params = new JsonObject();
params.addProperty("storageState", state);
sendMessage("setStorageState", params, NO_TIMEOUT);
} catch (IOException e) {
throw new PlaywrightException("Failed to read storage state from file", e);
}
}
@Override
public String storageState(StorageStateOptions options) {
if (options == null) {
@@ -584,6 +603,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
return storageState;
}
@Override
public DebuggerImpl debugger() {
return debugger;
}
@Override
public TracingImpl tracing() {
return tracing;
@@ -720,10 +744,6 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
if (page.opener() != null && !page.opener().isClosed()) {
page.opener().notifyPopup(page);
}
} else if ("backgroundPage".equals(event)) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
backgroundPages.add(page);
listeners.notify(EventType.BACKGROUNDPAGE, page);
} else if ("bindingCall".equals(event)) {
BindingCall bindingCall = connection.getExistingObject(params.getAsJsonObject("binding").get("guid").getAsString());
BindingCallback binding = bindings.get(bindingCall.name());
@@ -731,9 +751,19 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
bindingCall.call(binding);
}
} else if ("console".equals(event)) {
ConsoleMessageImpl message = new ConsoleMessageImpl(connection, params);
PageImpl page = null;
if (params.has("page")) {
page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
}
WorkerImpl worker = null;
if (params.has("worker")) {
worker = connection.getExistingObject(params.getAsJsonObject("worker").get("guid").getAsString());
}
ConsoleMessageImpl message = new ConsoleMessageImpl(connection, params, page, worker);
listeners.notify(BrowserContextImpl.EventType.CONSOLE, message);
PageImpl page = message.page();
if (worker != null) {
worker.listeners.notify(WorkerImpl.EventType.CONSOLE, message);
}
if (page != null) {
page.listeners.notify(PageImpl.EventType.CONSOLE, message);
}
@@ -774,21 +804,14 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
} else if ("response".equals(event)) {
String guid = params.getAsJsonObject("response").get("guid").getAsString();
Response response = connection.getExistingObject(guid);
ResponseImpl response = connection.getExistingObject(guid);
listeners.notify(EventType.RESPONSE, response);
if (params.has("page")) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
page.listeners.notify(PageImpl.EventType.RESPONSE, response);
}
} else if ("pageError".equals(event)) {
SerializedError error = gson().fromJson(params.getAsJsonObject("error"), SerializedError.class);
String errorStr = "";
if (error.error != null) {
errorStr = error.error.name + ": " + error.error.message;
if (error.error.stack != null && !error.error.stack.isEmpty()) {
errorStr += "\n" + error.error.stack;
}
}
String errorStr = parseError(params.getAsJsonObject("error"));
PageImpl page;
try {
page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
@@ -20,6 +20,7 @@ import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.BindResult;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -195,6 +196,32 @@ class BrowserImpl extends ChannelOwner implements Browser {
sendMessage("startTracing", params, NO_TIMEOUT);
}
@Override
public BindResult bind(String title, BindOptions options) {
JsonObject params = new JsonObject();
params.addProperty("title", title);
if (options != null) {
if (options.host != null) {
params.addProperty("host", options.host);
}
if (options.port != null) {
params.addProperty("port", options.port);
}
if (options.workspaceDir != null) {
params.addProperty("workspaceDir", options.workspaceDir);
}
}
JsonObject result = sendMessage("startServer", params, NO_TIMEOUT).getAsJsonObject();
BindResult bindResult = new BindResult();
bindResult.endpoint = result.get("endpoint").getAsString();
return bindResult;
}
@Override
public void unbind() {
sendMessage("stopServer", new JsonObject(), NO_TIMEOUT);
}
@Override
public byte[] stopTracing() {
JsonObject json = sendMessage("stopTracing").getAsJsonObject();
@@ -59,7 +59,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
}
// We don't use gson() here as the headers map should be serialized to a json object.
JsonObject params = new Gson().toJsonTree(options).getAsJsonObject();
params.addProperty("wsEndpoint", wsEndpoint);
params.addProperty("endpoint", wsEndpoint);
if (!params.has("headers")) {
params.add("headers", new JsonObject());
@@ -25,6 +25,11 @@ import java.util.function.Consumer;
public class CDPSessionImpl extends ChannelOwner implements CDPSession {
private final ListenerCollection<String> listeners = new ListenerCollection<>(new HashMap<>(), this);
private final ListenerCollection<EventType> typedListeners = new ListenerCollection<>(new HashMap<>(), this);
enum EventType {
CLOSE,
}
protected CDPSessionImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
@@ -40,9 +45,22 @@ public class CDPSessionImpl extends ChannelOwner implements CDPSession {
params = parameters.get("params").getAsJsonObject();
}
listeners.notify(method, params);
listeners.notify("event", parameters);
} else if ("close".equals(event)) {
typedListeners.notify(EventType.CLOSE, this);
}
}
@Override
public void onClose(Consumer<CDPSession> handler) {
typedListeners.add(EventType.CLOSE, handler);
}
@Override
public void offClose(Consumer<CDPSession> handler) {
typedListeners.remove(EventType.CLOSE, handler);
}
public JsonObject send(String method) {
return send(method, null);
}
@@ -333,6 +333,9 @@ public class Connection {
case "Dialog":
result = new DialogImpl(parent, type, guid, initializer);
break;
case "Disposable":
result = new DisposableObject(parent, type, guid, initializer);
break;
case "Electron":
// result = new Playwright(parent, type, guid, initializer);
break;
@@ -378,6 +381,9 @@ public class Connection {
break;
case "SocksSupport":
break;
case "Debugger":
result = new DebuggerImpl(parent, type, guid, initializer);
break;
case "Tracing":
result = new TracingImpl(parent, type, guid, initializer);
break;
@@ -20,30 +20,38 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.ConsoleMessage;
import com.microsoft.playwright.JSHandle;
import com.microsoft.playwright.Worker;
import java.util.ArrayList;
import java.util.List;
public class ConsoleMessageImpl implements ConsoleMessage {
private final Connection connection;
private PageImpl page;
private final PageImpl page;
private final WorkerImpl worker;
private final JsonObject initializer;
public ConsoleMessageImpl(Connection connection, JsonObject initializer) {
public ConsoleMessageImpl(Connection connection, JsonObject initializer, PageImpl page, WorkerImpl worker) {
this.connection = connection;
// Note: currently, we only report console messages for pages and they always have a page.
// However, in the future we might report console messages for service workers or something else,
// where page() would be null.
if (initializer.has("page")) {
page = connection.getExistingObject(initializer.getAsJsonObject("page").get("guid").getAsString());
}
this.page = page;
this.worker = worker;
this.initializer = initializer;
}
@Override
public double timestamp() {
return initializer.get("timestamp").getAsDouble();
}
public String type() {
return initializer.get("type").getAsString();
}
@Override
public Worker worker() {
return worker;
}
public String text() {
return initializer.get("text").getAsString();
}
@@ -0,0 +1,86 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Debugger;
import com.microsoft.playwright.options.Location;
import com.microsoft.playwright.options.DebuggerPausedDetails;
import java.util.ArrayList;
import java.util.List;
import static com.microsoft.playwright.impl.Serialization.gson;
class DebuggerImpl extends ChannelOwner implements Debugger {
private final List<Runnable> pausedStateChangedHandlers = new ArrayList<>();
private DebuggerPausedDetails pausedDetails;
DebuggerImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@Override
protected void handleEvent(String event, JsonObject params) {
if ("pausedStateChanged".equals(event)) {
if (params.has("pausedDetails") && !params.get("pausedDetails").isJsonNull()) {
pausedDetails = gson().fromJson(params.get("pausedDetails"), DebuggerPausedDetails.class);
} else {
pausedDetails = null;
}
for (Runnable handler : new ArrayList<>(pausedStateChangedHandlers)) {
handler.run();
}
}
}
@Override
public void onPausedStateChanged(Runnable handler) {
pausedStateChangedHandlers.add(handler);
}
@Override
public void offPausedStateChanged(Runnable handler) {
pausedStateChangedHandlers.remove(handler);
}
@Override
public DebuggerPausedDetails pausedDetails() {
return pausedDetails;
}
@Override
public void requestPause() {
sendMessage("requestPause", new JsonObject(), NO_TIMEOUT);
}
@Override
public void resume() {
sendMessage("resume", new JsonObject(), NO_TIMEOUT);
}
@Override
public void next() {
sendMessage("next", new JsonObject(), NO_TIMEOUT);
}
@Override
public void runTo(Location location) {
JsonObject params = gson().toJsonTree(location).getAsJsonObject();
sendMessage("runTo", params, NO_TIMEOUT);
}
}
@@ -43,7 +43,11 @@ class DialogImpl extends ChannelOwner implements Dialog {
@Override
public void dismiss() {
sendMessage("dismiss");
try {
sendMessage("dismiss");
} catch (TargetClosedError e) {
// Swallow TargetClosedErrors for beforeunload dialogs.
}
}
@Override
@@ -0,0 +1,30 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
class DisposableObject extends ChannelOwner implements AutoCloseable {
DisposableObject(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@Override
public void close() {
sendMessage("dispose", new JsonObject(), NO_TIMEOUT);
}
}
@@ -0,0 +1,34 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
class DisposableStub implements AutoCloseable {
private Runnable dispose;
DisposableStub(Runnable dispose) {
this.dispose = dispose;
}
@Override
public void close() {
if (dispose == null)
return;
Runnable d = dispose;
dispose = null;
d.run();
}
}
@@ -229,15 +229,18 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void click(String selector, ClickOptions options) {
clickImpl(selector, options);
clickImpl(selector, options, null);
}
void clickImpl(String selector, ClickOptions options) {
void clickImpl(String selector, ClickOptions options, Integer steps) {
if (options == null) {
options = new ClickOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
if (steps != null) {
params.addProperty("steps", steps);
}
sendMessage("click", params, timeout(options.timeout));
}
@@ -248,11 +251,18 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void dblclick(String selector, DblclickOptions options) {
dblclickImpl(selector, options, null);
}
void dblclickImpl(String selector, DblclickOptions options, Integer steps) {
if (options == null) {
options = new DblclickOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
if (steps != null) {
params.addProperty("steps", steps);
}
sendMessage("dblclick", params, timeout(options.timeout));
}
@@ -440,16 +450,19 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void dragAndDrop(String source, String target, DragAndDropOptions options) {
dragAndDropImpl(source, target, options);
dragAndDropImpl(source, target, options, null);
}
void dragAndDropImpl(String source, String target, DragAndDropOptions options) {
void dragAndDropImpl(String source, String target, DragAndDropOptions options, Integer steps) {
if (options == null) {
options = new DragAndDropOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("source", source);
params.addProperty("target", target);
if (steps != null) {
params.addProperty("steps", steps);
}
sendMessage("dragAndDrop", params, timeout(options.timeout));
}
@@ -627,7 +640,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
params.addProperty("key", key);
sendMessage("press", params, timeout(options.timeout));
}
@Override
public List<String> selectOption(String selector, SelectOption[] values, SelectOptionOptions options) {
return selectOptionImpl(selector, values, options);
@@ -16,6 +16,8 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Request;
@@ -82,7 +84,7 @@ public class HARRouter {
if (status == -1) {
return;
}
Map<String, String> headers = fromNameValues(response.getAsJsonArray("headers"));
Map<String, String> headers = mergeSetCookieHeaders(response.getAsJsonArray("headers"));
byte[] buffer = Base64.getDecoder().decode(response.get("body").getAsString());
route.fulfill(new Route.FulfillOptions()
.setStatus(status)
@@ -105,6 +107,25 @@ public class HARRouter {
route.abort();
}
private static Map<String, String> mergeSetCookieHeaders(JsonArray headersArray) {
Map<String, String> result = new java.util.LinkedHashMap<>();
for (JsonElement element : headersArray) {
JsonObject pair = element.getAsJsonObject();
String name = pair.get("name").getAsString();
String value = pair.get("value").getAsString();
if ("set-cookie".equalsIgnoreCase(name)) {
if (!result.containsKey("set-cookie")) {
result.put("set-cookie", value);
} else {
result.put("set-cookie", result.get("set-cookie") + "\n" + value);
}
} else {
result.put(name, value);
}
}
return result;
}
void dispose() {
JsonObject params = new JsonObject();
params.addProperty("harId", harId);
@@ -34,13 +34,20 @@ public class LocalUtils extends ChannelOwner {
return initializer.getAsJsonArray("deviceDescriptors");
}
void zip(Path zipFile, JsonArray entries, String stacksId, boolean appendMode, boolean includeSources) {
void zip(Path zipFile, JsonArray entries, String stacksId, boolean appendMode, boolean includeSources, List<String> additionalSources) {
JsonObject params = new JsonObject();
params.addProperty("zipFile", zipFile.toString());
params.add("entries", entries);
params.addProperty("mode", appendMode ? "append" : "write");
params.addProperty("stacksId", stacksId);
params.addProperty("includeSources", includeSources);
if (!additionalSources.isEmpty()) {
JsonArray sourcesArray = new JsonArray();
for (String source : additionalSources) {
sourcesArray.add(source);
}
params.add("additionalSources", sourcesArray);
}
sendMessage("zip", params, NO_TIMEOUT);
}
@@ -16,7 +16,6 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
@@ -25,6 +24,7 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.LocatorUtils.*;
@@ -108,6 +108,14 @@ class LocatorImpl implements Locator {
return (List<String>) frame.evalOnSelectorAll(selector, "ee => ee.map(e => e.textContent || '')");
}
@Override
public Locator normalize() {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
JsonObject result = frame.sendMessage("resolveSelector", params, ChannelOwner.NO_TIMEOUT).getAsJsonObject();
return new LocatorImpl(frame, result.get("resolvedSelector").getAsString(), null);
}
@Override
public Locator and(Locator locator) {
LocatorImpl other = (LocatorImpl) locator;
@@ -161,7 +169,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new ClickOptions();
}
frame.click(selector, convertType(options, Frame.ClickOptions.class).setStrict(true));
frame.clickImpl(selector, convertType(options, Frame.ClickOptions.class).setStrict(true), options.steps);
}
@Override
@@ -174,12 +182,33 @@ class LocatorImpl implements Locator {
return locator(describeSelector(description));
}
@Override
public String description() {
// Match internal:describe= at the end of the selector with a JSON string
// Pattern matches: >> internal:describe="..." where ... is a JSON-encoded string
Pattern pattern = Pattern.compile(" >> internal:describe=(\"(?:[^\"\\\\]|\\\\.)*\")$");
Matcher matcher = pattern.matcher(selector);
if (matcher.find()) {
String jsonString = matcher.group(1);
try {
// Deserialize the JSON string
return gson().fromJson(jsonString, String.class);
} catch (Exception e) {
// If we can't parse it, return null
return null;
}
}
return null;
}
@Override
public void dblclick(DblclickOptions options) {
if (options == null) {
options = new DblclickOptions();
}
frame.dblclick(selector, convertType(options, Frame.DblclickOptions.class).setStrict(true));
frame.dblclickImpl(selector, convertType(options, Frame.DblclickOptions.class).setStrict(true), options.steps);
}
@Override
@@ -197,7 +226,7 @@ class LocatorImpl implements Locator {
}
Frame.DragAndDropOptions frameOptions = convertType(options, Frame.DragAndDropOptions.class);
frameOptions.setStrict(true);
frame.dragAndDrop(selector, ((LocatorImpl) target).selector, frameOptions);
frame.dragAndDropImpl(selector, ((LocatorImpl) target).selector, frameOptions, options.steps);
}
@Override
@@ -627,6 +656,10 @@ class LocatorImpl implements Locator {
@Override
public String toString() {
String description = description();
if (description != null) {
return description;
}
return "Locator@" + selector;
}
@@ -31,6 +31,7 @@ import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Serialization.parseError;
import static com.microsoft.playwright.impl.Utils.*;
import static com.microsoft.playwright.options.ScreenshotType.JPEG;
import static com.microsoft.playwright.options.ScreenshotType.PNG;
@@ -45,6 +46,7 @@ public class PageImpl extends ChannelOwner implements Page {
private final KeyboardImpl keyboard;
private final MouseImpl mouse;
private final TouchscreenImpl touchscreen;
private final ScreencastImpl screencast;
final Waitable<?> waitableClosedOrCrashed;
private ViewportSize viewport;
private final Router routes = new Router();
@@ -134,6 +136,7 @@ public class PageImpl extends ChannelOwner implements Page {
keyboard = new KeyboardImpl(this);
mouse = new MouseImpl(this);
touchscreen = new TouchscreenImpl(this);
screencast = new ScreencastImpl(this);
frames.add(mainFrame);
timeoutSettings = new TimeoutSettings(browserContext.timeoutSettings);
waitableClosedOrCrashed = createWaitForCloseHelper();
@@ -142,6 +145,13 @@ public class PageImpl extends ChannelOwner implements Page {
} else {
opener = null;
}
if (initializer.has("video")) {
String artifactGuid = initializer.getAsJsonObject("video").get("guid").getAsString();
ArtifactImpl artifact = connection.getExistingObject(artifactGuid);
video = new VideoImpl(this, artifact);
} else {
video = new VideoImpl(this);
}
}
@Override
@@ -218,14 +228,12 @@ public class PageImpl extends ChannelOwner implements Page {
if (!webSocketRoutes.handle(route)) {
browserContext.handleWebSocketRoute(route);
}
} else if ("video".equals(event)) {
String artifactGuid = params.getAsJsonObject("artifact").get("guid").getAsString();
ArtifactImpl artifact = connection.getExistingObject(artifactGuid);
forceVideo().setArtifact(artifact);
} else if ("crash".equals(event)) {
listeners.notify(EventType.CRASH, this);
} else if ("close".equals(event)) {
didClose();
} else if ("screencastFrame".equals(event)) {
screencast.handleScreencastFrame(params);
}
}
@@ -236,7 +244,6 @@ public class PageImpl extends ChannelOwner implements Page {
void didClose() {
isClosed = true;
browserContext.pages.remove(this);
browserContext.backgroundPages.remove(this);
listeners.notify(EventType.CLOSE, this);
}
@@ -567,6 +574,17 @@ public class PageImpl extends ChannelOwner implements Page {
return mainFrame.querySelectorAll(selector);
}
@Override
public List<Request> requests() {
JsonObject json = sendMessage("requests", new JsonObject(), NO_TIMEOUT).getAsJsonObject();
JsonArray requests = json.getAsJsonArray("requests");
List<Request> result = new ArrayList<>();
for (JsonElement item : requests) {
result.add(connection.getExistingObject(item.getAsJsonObject().get("guid").getAsString()));
}
return result;
}
@Override
public void addLocatorHandler(Locator locator, Consumer<Locator> handler, AddLocatorHandlerOptions options) {
LocatorImpl locatorImpl = (LocatorImpl) locator;
@@ -622,6 +640,16 @@ public class PageImpl extends ChannelOwner implements Page {
}
}
@Override
public String ariaSnapshot(AriaSnapshotOptions options) {
if (options == null) {
options = new AriaSnapshotOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
JsonObject result = mainFrame.sendMessage("ariaSnapshot", params, mainFrame.timeout(options.timeout)).getAsJsonObject();
return result.get("snapshot").getAsString();
}
@Override
public Object evalOnSelector(String selector, String pageFunction, Object arg, EvalOnSelectorOptions options) {
return mainFrame.evalOnSelectorImpl(
@@ -634,25 +662,26 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public void addInitScript(String script) {
addInitScriptImpl(script);
public AutoCloseable addInitScript(String script) {
return addInitScriptImpl(script);
}
@Override
public void addInitScript(Path path) {
public AutoCloseable addInitScript(Path path) {
try {
byte[] bytes = readAllBytes(path);
String script = addSourceUrlToScript(new String(bytes, UTF_8), path);
addInitScriptImpl(script);
return addInitScriptImpl(script);
} catch (IOException e) {
throw new PlaywrightException("Failed to read script from file", e);
}
}
private void addInitScriptImpl(String script) {
private AutoCloseable addInitScriptImpl(String script) {
JsonObject params = new JsonObject();
params.addProperty("source", script);
sendMessage("addInitScript", params, NO_TIMEOUT);
JsonObject result = sendMessage("addInitScript", params, NO_TIMEOUT).getAsJsonObject();
return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString());
}
@Override
@@ -677,7 +706,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void click(String selector, ClickOptions options) {
mainFrame.clickImpl(selector, convertType(options, Frame.ClickOptions.class));
mainFrame.clickImpl(selector, convertType(options, Frame.ClickOptions.class), null);
}
@Override
@@ -692,7 +721,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void dblclick(String selector, DblclickOptions options) {
mainFrame.dblclick(selector, convertType(options, Frame.DblclickOptions.class));
mainFrame.dblclickImpl(selector, convertType(options, Frame.DblclickOptions.class), null);
}
@Override
@@ -724,11 +753,11 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public void exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
exposeBindingImpl(name, playwrightBinding, options);
public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
return exposeBindingImpl(name, playwrightBinding, options);
}
private void exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
if (bindings.containsKey(name)) {
throw new PlaywrightException("Function \"" + name + "\" has been already registered");
}
@@ -742,12 +771,13 @@ public class PageImpl extends ChannelOwner implements Page {
if (options != null && options.handle != null && options.handle) {
params.addProperty("needsHandle", true);
}
sendMessage("exposeBinding", params, NO_TIMEOUT);
JsonObject result = sendMessage("exposeBinding", params, NO_TIMEOUT).getAsJsonObject();
return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString());
}
@Override
public void exposeFunction(String name, FunctionCallback playwrightFunction) {
exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args), null);
public AutoCloseable exposeFunction(String name, FunctionCallback playwrightFunction) {
return exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args), null);
}
@Override
@@ -925,7 +955,10 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void dragAndDrop(String source, String target, DragAndDropOptions options) {
mainFrame.dragAndDropImpl(source, target, convertType(options, Frame.DragAndDropOptions.class));
if (options == null) {
options = new DragAndDropOptions();
}
mainFrame.dragAndDropImpl(source, target, convertType(options, Frame.DragAndDropOptions.class), options.steps);
}
@Override
@@ -983,6 +1016,40 @@ public class PageImpl extends ChannelOwner implements Page {
return keyboard;
}
@Override
public List<ConsoleMessage> consoleMessages(ConsoleMessagesOptions options) {
JsonObject params = options != null ? gson().toJsonTree(options).getAsJsonObject() : new JsonObject();
JsonObject json = sendMessage("consoleMessages", params, NO_TIMEOUT).getAsJsonObject();
JsonArray messages = json.getAsJsonArray("messages");
List<ConsoleMessage> result = new ArrayList<>();
for (JsonElement item : messages) {
result.add(new ConsoleMessageImpl(connection, item.getAsJsonObject(), this, null));
}
return result;
}
@Override
public void clearConsoleMessages() {
sendMessage("clearConsoleMessages", new JsonObject(), NO_TIMEOUT);
}
@Override
public void clearPageErrors() {
sendMessage("clearPageErrors", new JsonObject(), NO_TIMEOUT);
}
@Override
public List<String> pageErrors() {
JsonObject json = sendMessage("pageErrors", new JsonObject(), NO_TIMEOUT).getAsJsonObject();
JsonArray errors = json.getAsJsonArray("errors");
List<String> result = new ArrayList<>();
for (JsonElement item : errors) {
String errorStr = parseError(item.getAsJsonObject());
result.add(errorStr);
}
return result;
}
@Override
public Locator locator(String selector, LocatorOptions options) {
return mainFrame.locator(selector, convertType(options, Frame.LocatorOptions.class));
@@ -1006,17 +1073,29 @@ public class PageImpl extends ChannelOwner implements Page {
return opener;
}
@Override
public void cancelPickLocator() {
sendMessage("cancelPickLocator", new JsonObject(), NO_TIMEOUT);
}
@Override
public Locator pickLocator() {
JsonObject result = sendMessage("pickLocator", new JsonObject(), NO_TIMEOUT).getAsJsonObject();
return new LocatorImpl(mainFrame, result.get("selector").getAsString(), null);
}
@Override
public void pause() {
Double defaultNavigationTimeout = browserContext.timeoutSettings.defaultNavigationTimeout();
Double defaultTimeout = browserContext.timeoutSettings.defaultTimeout();
browserContext.setDefaultNavigationTimeout(0.0);
browserContext.setDefaultTimeout(0.0);
TimeoutSettings settings = browserContext.timeoutSettings;
Double defaultNavigationTimeout = settings.defaultNavigationTimeout();
Double defaultTimeout = settings.defaultTimeout();
settings.setDefaultNavigationTimeout(0.0);
settings.setDefaultTimeout(0.0);
try {
runUntil(() -> {}, new WaitableRace<>(asList(context().pause(), (Waitable<JsonElement>) waitableClosedOrCrashed)));
} finally {
browserContext.setDefaultNavigationTimeout(defaultNavigationTimeout);
browserContext.setDefaultTimeout(defaultTimeout);
settings.setDefaultNavigationTimeout(defaultNavigationTimeout);
settings.setDefaultTimeout(defaultTimeout);
}
}
@@ -1067,18 +1146,21 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public void route(String url, Consumer<Route> handler, RouteOptions options) {
public AutoCloseable route(String url, Consumer<Route> handler, RouteOptions options) {
route(UrlMatcher.forGlob(browserContext.baseUrl(), url, this.connection.localUtils, false), handler, options);
return new DisposableStub(() -> unroute(url, handler));
}
@Override
public void route(Pattern url, Consumer<Route> handler, RouteOptions options) {
public AutoCloseable route(Pattern url, Consumer<Route> handler, RouteOptions options) {
route(new UrlMatcher(url), handler, options);
return new DisposableStub(() -> unroute(url, handler));
}
@Override
public void route(Predicate<String> url, Consumer<Route> handler, RouteOptions options) {
public AutoCloseable route(Predicate<String> url, Consumer<Route> handler, RouteOptions options) {
route(new UrlMatcher(url), handler, options);
return new DisposableStub(() -> unroute(url, handler));
}
@Override
@@ -1166,18 +1248,8 @@ public class PageImpl extends ChannelOwner implements Page {
}
}
}
List<Locator> mask = options.mask;
options.mask = null;
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
options.mask = mask;
params.remove("path");
if (mask != null) {
JsonArray maskArray = new JsonArray();
for (Locator locator: mask) {
maskArray.add(((LocatorImpl) locator).toProtocol());
}
params.add("mask", maskArray);
}
JsonObject json = sendMessage("screenshot", params, timeoutSettings.timeout(options.timeout)).getAsJsonObject();
byte[] buffer = Base64.getDecoder().decode(json.get("binary").getAsString());
@@ -1279,6 +1351,11 @@ public class PageImpl extends ChannelOwner implements Page {
return touchscreen;
}
@Override
public Screencast screencast() {
return screencast;
}
@Override
public void type(String selector, String text, TypeOptions options) {
mainFrame.type(selector, text, convertType(options, Frame.TypeOptions.class));
@@ -1329,22 +1406,9 @@ public class PageImpl extends ChannelOwner implements Page {
}
private VideoImpl forceVideo() {
if (video == null) {
video = new VideoImpl(this);
}
return video;
}
@Override
public VideoImpl video() {
// Note: we are creating Video object lazily, because we do not know
// BrowserContextOptions when constructing the page - it is assigned
// too late during launchPersistentContext.
if (browserContext.videosDir() == null) {
return null;
}
return forceVideo();
return video;
}
@Override
@@ -1367,9 +1431,12 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void waitForLoadState(LoadState state, WaitForLoadStateOptions options) {
withWaitLogging("Page.waitForLoadState", logger -> {
mainFrame.waitForLoadStateImpl(state, convertType(options, Frame.WaitForLoadStateOptions.class), logger);
return null;
final LoadState loadState = state == null ? LoadState.LOAD : state;
withTitle("Wait for load state \"" + loadState.toString().toLowerCase() + "\"", () -> {
withWaitLogging("Page.waitForLoadState", logger -> {
mainFrame.waitForLoadStateImpl(loadState, convertType(options, Frame.WaitForLoadStateOptions.class), logger);
return null;
});
});
}
@@ -114,6 +114,7 @@ class FrameExpectOptions {
class FrameExpectResult {
boolean matches;
SerializedValue received;
String errorMessage;
List<String> log;
}
@@ -42,6 +42,7 @@ public class RequestImpl extends ChannelOwner implements Request {
String failure;
Timing timing;
boolean didFailOrFinish;
ResponseImpl existingResponse;
private FallbackOverrides fallbackOverrides;
static class FallbackOverrides {
@@ -66,6 +67,11 @@ public class RequestImpl extends ChannelOwner implements Request {
}
}
@Override
public ResponseImpl existingResponse() {
return existingResponse;
}
@Override
public Map<String, String> allHeaders() {
return getRawHeaders().headers();
@@ -36,12 +36,13 @@ import static java.util.Arrays.asList;
public class ResponseImpl extends ChannelOwner implements Response {
private final RawHeaders headers;
private RawHeaders rawHeaders;
private final RequestImpl request;
final RequestImpl request;
ResponseImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
headers = new RawHeaders(asList(gson().fromJson(initializer.getAsJsonArray("headers"), HttpHeader[].class)));
request = connection.getExistingObject(initializer.getAsJsonObject("request").get("guid").getAsString());
request.existingResponse = this;
request.timing = gson().fromJson(initializer.get("timing"), Timing.class);
}
@@ -86,6 +87,12 @@ public class ResponseImpl extends ChannelOwner implements Response {
return initializer.get("fromServiceWorker").getAsBoolean();
}
@Override
public String httpVersion() {
JsonObject result = sendMessage("httpVersion").getAsJsonObject();
return result.get("value").getAsString();
}
@Override
public Map<String, String> headers() {
return headers.headers();
@@ -0,0 +1,158 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Screencast;
import com.microsoft.playwright.options.ScreencastFrame;
import java.nio.file.Path;
import java.util.function.Consumer;
import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT;
import static com.microsoft.playwright.impl.Serialization.gson;
class ScreencastImpl implements Screencast {
private final PageImpl page;
private boolean started;
private Path savePath;
private Consumer<ScreencastFrame> onFrame;
private ArtifactImpl artifact;
ScreencastImpl(PageImpl page) {
this.page = page;
}
void handleScreencastFrame(JsonObject params) {
if (onFrame == null) {
return;
}
String dataBase64 = params.get("data").getAsString();
byte[] data = java.util.Base64.getDecoder().decode(dataBase64);
onFrame.accept(new ScreencastFrame(data));
}
@Override
public AutoCloseable start(StartOptions options) {
if (started) {
throw new PlaywrightException("Screencast is already started");
}
started = true;
JsonObject params = new JsonObject();
if (options != null) {
if (options.onFrame != null) {
onFrame = options.onFrame;
}
if (options.quality != null) {
params.addProperty("quality", options.quality);
}
params.addProperty("sendFrames", options.onFrame != null);
params.addProperty("record", options.path != null);
savePath = options.path;
} else {
params.addProperty("sendFrames", false);
params.addProperty("record", false);
}
JsonObject result = page.sendMessage("screencastStart", params, NO_TIMEOUT).getAsJsonObject();
if (result.has("artifact")) {
String artifactGuid = result.getAsJsonObject("artifact").get("guid").getAsString();
artifact = page.connection.getExistingObject(artifactGuid);
}
return new DisposableStub(this::stop);
}
@Override
public void stop() {
started = false;
onFrame = null;
page.sendMessage("screencastStop", new JsonObject(), NO_TIMEOUT);
if (savePath != null && artifact != null) {
artifact.saveAs(savePath);
}
artifact = null;
savePath = null;
}
@Override
public AutoCloseable showOverlay(String html, ShowOverlayOptions options) {
JsonObject params = new JsonObject();
params.addProperty("html", html);
if (options != null && options.duration != null) {
params.addProperty("duration", options.duration);
}
JsonObject result = (JsonObject) page.sendMessage("screencastShowOverlay", params, NO_TIMEOUT);
String id = result.get("id").getAsString();
return () -> {
JsonObject removeParams = new JsonObject();
removeParams.addProperty("id", id);
page.sendMessage("screencastRemoveOverlay", removeParams, NO_TIMEOUT);
};
}
@Override
public void showChapter(String title, ShowChapterOptions options) {
JsonObject params = new JsonObject();
params.addProperty("title", title);
if (options != null) {
if (options.description != null) {
params.addProperty("description", options.description);
}
if (options.duration != null) {
params.addProperty("duration", options.duration);
}
}
page.sendMessage("screencastChapter", params, NO_TIMEOUT);
}
@Override
public AutoCloseable showActions(ShowActionsOptions options) {
JsonObject params = new JsonObject();
if (options != null) {
if (options.duration != null) {
params.addProperty("duration", options.duration);
}
if (options.fontSize != null) {
params.addProperty("fontSize", options.fontSize);
}
if (options.position != null) {
params.add("position", gson().toJsonTree(options.position));
}
}
page.sendMessage("screencastShowActions", params, NO_TIMEOUT);
return new DisposableStub(this::hideActions);
}
@Override
public void showOverlays() {
JsonObject params = new JsonObject();
params.addProperty("visible", true);
page.sendMessage("screencastSetOverlayVisible", params, NO_TIMEOUT);
}
@Override
public void hideActions() {
page.sendMessage("screencastHideActions", new JsonObject(), NO_TIMEOUT);
}
@Override
public void hideOverlays() {
JsonObject params = new JsonObject();
params.addProperty("visible", false);
page.sendMessage("screencastSetOverlayVisible", params, NO_TIMEOUT);
}
}
@@ -58,6 +58,8 @@ class Serialization {
.registerTypeAdapter(ScreenshotCaret.class, new ToLowerCaseSerializer<ScreenshotCaret>())
.registerTypeAdapter(ServiceWorkerPolicy.class, new ToLowerCaseAndDashSerializer<ServiceWorkerPolicy>())
.registerTypeAdapter(MouseButton.class, new ToLowerCaseSerializer<MouseButton>())
.registerTypeAdapter(ConsoleMessagesFilter.class, new ConsoleMessagesFilterSerializer())
.registerTypeAdapter(AriaSnapshotMode.class, new ToLowerCaseSerializer<AriaSnapshotMode>())
.registerTypeAdapter(LoadState.class, new ToLowerCaseSerializer<LoadState>())
.registerTypeAdapter(WaitUntilState.class, new ToLowerCaseSerializer<WaitUntilState>())
.registerTypeAdapter(WaitForSelectorState.class, new ToLowerCaseSerializer<WaitForSelectorState>())
@@ -66,6 +68,7 @@ class Serialization {
.registerTypeHierarchyAdapter(JSHandleImpl.class, new HandleSerializer())
.registerTypeAdapter((new TypeToken<Map<String, String>>(){}).getType(), new StringMapSerializer())
.registerTypeAdapter((new TypeToken<Map<String, Object>>(){}).getType(), new FirefoxUserPrefsSerializer())
.registerTypeAdapter(LocatorImpl.class, new LocatorImplSerializer())
.registerTypeHierarchyAdapter(Path.class, new PathSerializer()).create();
static Gson gson() {
@@ -422,6 +425,18 @@ class Serialization {
return result;
}
static String parseError(JsonObject object) {
SerializedError error = gson().fromJson(object, SerializedError.class);
String errorStr = "";
if (error.error != null) {
errorStr = error.error.name + ": " + error.error.message;
if (error.error.stack != null && !error.error.stack.isEmpty()) {
errorStr += "\n" + error.error.stack;
}
}
return errorStr;
}
private static class OptionalSerializer implements JsonSerializer<Optional<?>> {
private static boolean isSupported(Type type) {
return new TypeToken<Optional<Media>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
@@ -469,6 +484,17 @@ class Serialization {
}
}
private static class ConsoleMessagesFilterSerializer implements JsonSerializer<ConsoleMessagesFilter> {
@Override
public JsonElement serialize(ConsoleMessagesFilter src, Type typeOfSrc, JsonSerializationContext context) {
switch (src) {
case ALL: return new JsonPrimitive("all");
case SINCE_NAVIGATION: return new JsonPrimitive("since-navigation");
default: throw new PlaywrightException("Unknown ConsoleMessagesFilter: " + src);
}
}
}
private static class ToLowerCaseAndDashSerializer<E extends Enum<E>> implements JsonSerializer<E> {
@Override
public JsonElement serialize(E src, Type typeOfSrc, JsonSerializationContext context) {
@@ -490,6 +516,13 @@ class Serialization {
}
}
private static class LocatorImplSerializer implements JsonSerializer<LocatorImpl> {
@Override
public JsonElement serialize(LocatorImpl src, Type typeOfSrc, JsonSerializationContext context) {
return src.toProtocol();
}
}
private static class SameSiteAdapter extends TypeAdapter<SameSiteAttribute> {
@Override
public void write(JsonWriter out, SameSiteAttribute value) throws IOException {
@@ -21,6 +21,10 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.Tracing;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static com.microsoft.playwright.impl.Serialization.gson;
@@ -29,6 +33,7 @@ class TracingImpl extends ChannelOwner implements Tracing {
private Path tracesDir;
private boolean isTracing;
private String stacksId;
private final Set<String> additionalSources = new HashSet<>();
TracingImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -42,6 +47,9 @@ class TracingImpl extends ChannelOwner implements Tracing {
}
JsonObject params = new JsonObject();
List<String> capturedAdditionalSources = new ArrayList<>(additionalSources);
additionalSources.clear();
// Not interested in artifacts.
if (path == null) {
params.addProperty("mode", "discard");
@@ -57,7 +65,7 @@ class TracingImpl extends ChannelOwner implements Tracing {
params.addProperty("mode", "entries");
JsonObject json = sendMessage("tracingStopChunk", params, NO_TIMEOUT).getAsJsonObject();
JsonArray entries = json.getAsJsonArray("entries");
connection.localUtils.zip(path, entries, stacksId, false, includeSources);
connection.localUtils.zip(path, entries, stacksId, false, includeSources, capturedAdditionalSources);
return;
}
@@ -74,7 +82,7 @@ class TracingImpl extends ChannelOwner implements Tracing {
artifact.saveAs(path);
artifact.delete();
connection.localUtils.zip(path, new JsonArray(), stacksId, true, includeSources);
connection.localUtils.zip(path, new JsonArray(), stacksId, true, includeSources, capturedAdditionalSources);
}
@Override
@@ -86,14 +94,18 @@ class TracingImpl extends ChannelOwner implements Tracing {
}
@Override
public void group(String name, GroupOptions options) {
public AutoCloseable group(String name, GroupOptions options) {
groupImpl(name, options);
return new DisposableStub(this::groupEnd);
}
private void groupImpl(String name, GroupOptions options) {
if (options == null) {
options = new GroupOptions();
}
if (options.location != null) {
additionalSources.add(options.location.file);
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("name", name);
sendMessage("tracingGroup", params, NO_TIMEOUT);
@@ -22,31 +22,23 @@ import com.microsoft.playwright.Video;
import java.nio.file.Path;
import java.nio.file.Paths;
import static java.util.Arrays.asList;
class VideoImpl implements Video {
private final PageImpl page;
private final WaitableResult<ArtifactImpl> waitableArtifact = new WaitableResult<>();
ArtifactImpl artifact;
VideoImpl(PageImpl page) {
this.page = page;
}
void setArtifact(ArtifactImpl artifact) {
waitableArtifact.complete(artifact);
}
private ArtifactImpl waitForArtifact() {
Waitable<ArtifactImpl> waitable = new WaitableRace<>(asList(waitableArtifact, (Waitable<ArtifactImpl>) page.waitableClosedOrCrashed));
return page.runUntil(() -> {}, waitable);
VideoImpl(PageImpl page, ArtifactImpl artifact) {
this.page = page;
this.artifact = artifact;
}
@Override
public void delete() {
try {
waitForArtifact().delete();
} catch (PlaywrightException e) {
}
if (artifact != null)
artifact.delete();
}
@Override
@@ -54,22 +46,15 @@ class VideoImpl implements Video {
if (page.connection.isRemote) {
throw new PlaywrightException("Path is not available when using browserType.connect(). Use saveAs() to save a local copy.");
}
try {
return Paths.get(waitForArtifact().initializer.get("absolutePath").getAsString());
} catch (PlaywrightException e) {
throw new PlaywrightException("Page did not produce any video frames", e);
}
if (artifact == null)
throw new PlaywrightException("Video recording has not been started.");
return Paths.get(artifact.initializer.get("absolutePath").getAsString());
}
@Override
public void saveAs(Path path) {
if (!page.isClosed()) {
throw new PlaywrightException("Page is not yet closed. Close the page prior to calling saveAs");
}
try {
waitForArtifact().saveAs(path);
} catch (PlaywrightException e) {
throw new PlaywrightException("Page did not produce any video frames", e);
}
if (artifact == null)
throw new PlaywrightException("Video recording has not been started.");
artifact.saveAs(path);
}
}
@@ -160,9 +160,9 @@ class WebSocketRouteImpl extends ChannelOwner implements WebSocketRoute {
sendMessageAsync("sendToPage", messageParams);
}
} else if ("closePage".equals(event)) {
int code = params.get("code").getAsInt();
String reason = params.get("reason").getAsString();
boolean wasClean = params.get("wasClean").getAsBoolean();
Integer code = params.has("code") ? params.get("code").getAsInt() : null;
String reason = params.has("reason") ? params.get("reason").getAsString() : null;
boolean wasClean = params.has("wasClean") && params.get("wasClean").getAsBoolean();
if (onPageClose != null) {
onPageClose.accept(code, reason);
} else {
@@ -173,9 +173,9 @@ class WebSocketRouteImpl extends ChannelOwner implements WebSocketRoute {
sendMessageAsync("closeServer", closeParams);
}
} else if ("closeServer".equals(event)) {
int code = params.get("code").getAsInt();
String reason = params.get("reason").getAsString();
boolean wasClean = params.get("wasClean").getAsBoolean();
Integer code = params.has("code") ? params.get("code").getAsInt() : null;
String reason = params.has("reason") ? params.get("reason").getAsString() : null;
boolean wasClean = params.has("wasClean") && params.get("wasClean").getAsBoolean();
if (onServerClose != null) {
onServerClose.accept(code, reason);
} else {
@@ -18,21 +18,25 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.ConsoleMessage;
import com.microsoft.playwright.JSHandle;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Worker;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static com.microsoft.playwright.impl.Serialization.*;
class WorkerImpl extends ChannelOwner implements Worker {
private final ListenerCollection<EventType> listeners = new ListenerCollection<>();
final ListenerCollection<EventType> listeners = new ListenerCollection<>();
PageImpl page;
enum EventType {
CLOSE,
CONSOLE,
}
WorkerImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -49,9 +53,19 @@ class WorkerImpl extends ChannelOwner implements Worker {
listeners.remove(EventType.CLOSE, handler);
}
private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Double timeout) {
@Override
public void onConsole(Consumer<ConsoleMessage> handler) {
listeners.add(EventType.CONSOLE, handler);
}
@Override
public void offConsole(Consumer<ConsoleMessage> handler) {
listeners.remove(EventType.CONSOLE, handler);
}
private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Predicate<T> predicate, Double timeout) {
List<Waitable<T>> waitables = new ArrayList<>();
waitables.add(new WaitableEvent<>(listeners, eventType));
waitables.add(new WaitableEvent<>(listeners, eventType, predicate));
waitables.add(page.createWaitForCloseHelper());
waitables.add(page.createWaitableTimeout(timeout));
return runUntil(code, new WaitableRace<>(waitables));
@@ -62,11 +76,23 @@ class WorkerImpl extends ChannelOwner implements Worker {
return withWaitLogging("Worker.waitForClose", logger -> waitForCloseImpl(options, code));
}
@Override
public ConsoleMessage waitForConsoleMessage(WaitForConsoleMessageOptions options, Runnable code) {
return withWaitLogging("Worker.waitForConsoleMessage", logger -> waitForConsoleMessageImpl(options, code));
}
private ConsoleMessage waitForConsoleMessageImpl(WaitForConsoleMessageOptions options, Runnable code) {
if (options == null) {
options = new WaitForConsoleMessageOptions();
}
return waitForEventWithTimeout(EventType.CONSOLE, code, options.predicate, options.timeout);
}
private Worker waitForCloseImpl(WaitForCloseOptions options, Runnable code) {
if (options == null) {
options = new WaitForCloseOptions();
}
return waitForEventWithTimeout(EventType.CLOSE, code, options.timeout);
return waitForEventWithTimeout(EventType.CLOSE, code, null, options.timeout);
}
@Override
@@ -36,7 +36,7 @@ public class PlaywrightExtension implements ParameterResolver {
// There should be at most one instance of PlaywrightRegistry per test run, it keeps
// track of all created Playwright instances and calls `close()` on each of them after
// the tests finished.
static class PlaywrightRegistry implements ExtensionContext.Store.CloseableResource {
static class PlaywrightRegistry implements AutoCloseable {
private final List<Playwright> playwrightList = Collections.synchronizedList(new ArrayList<>());
static synchronized PlaywrightRegistry getOrCreateFor(ExtensionContext extensionContext) {
@@ -59,7 +59,7 @@ public class PlaywrightExtension implements ParameterResolver {
// This is a workaround for JUnit's lack of an "AfterTestRun" hook
// This will be called once after all tests have completed.
@Override
public void close() throws Throwable {
public void close() {
for (Playwright playwright : playwrightList) {
playwright.close();
}
@@ -36,7 +36,7 @@ package com.microsoft.playwright.junit;
* public Options getOptions() {
* return new Options()
* .setHeadless(false)
* .setContextOption(new Browser.NewContextOptions()
* .setContextOptions(new Browser.NewContextOptions()
* .setBaseURL("https://github.com"))
* .setApiRequestOptions(new APIRequest.NewContextOptions()
* .setBaseURL("https://playwright.dev"));
@@ -0,0 +1,54 @@
/*
* 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 Annotate {
/**
* How long each annotation is displayed in milliseconds. Defaults to {@code 500}.
*/
public Double duration;
/**
* Position of the action title overlay. Defaults to {@code "top-right"}.
*/
public AnnotatePosition position;
/**
* Font size of the action title in pixels. Defaults to {@code 24}.
*/
public Integer fontSize;
/**
* How long each annotation is displayed in milliseconds. Defaults to {@code 500}.
*/
public Annotate setDuration(double duration) {
this.duration = duration;
return this;
}
/**
* Position of the action title overlay. Defaults to {@code "top-right"}.
*/
public Annotate setPosition(AnnotatePosition position) {
this.position = position;
return this;
}
/**
* Font size of the action title in pixels. Defaults to {@code 24}.
*/
public Annotate setFontSize(int fontSize) {
this.fontSize = fontSize;
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 AnnotatePosition {
TOP_LEFT,
TOP,
TOP_RIGHT,
BOTTOM_LEFT,
BOTTOM,
BOTTOM_RIGHT
}
@@ -0,0 +1,22 @@
/*
* 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 AriaSnapshotMode {
AI,
DEFAULT
}
@@ -0,0 +1,22 @@
/*
* 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 BindResult {
public String endpoint;
}
@@ -0,0 +1,22 @@
/*
* 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 ConsoleMessagesFilter {
ALL,
SINCE_NAVIGATION
}
@@ -20,16 +20,16 @@ public class Cookie {
public String name;
public String value;
/**
* Either url or domain / path are required. Optional.
* Either {@code url} or both {@code domain} and {@code path} are required. Optional.
*/
public String url;
/**
* For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url or
* domain / path are required. Optional.
* For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either {@code
* url} or both {@code domain} and {@code path} are required. Optional.
*/
public String domain;
/**
* Either url or domain / path are required Optional.
* Either {@code url} or both {@code domain} and {@code path} are required. Optional.
*/
public String path;
/**
@@ -60,22 +60,22 @@ public class Cookie {
this.value = value;
}
/**
* Either url or domain / path are required. Optional.
* Either {@code url} or both {@code domain} and {@code path} are required. Optional.
*/
public Cookie setUrl(String url) {
this.url = url;
return this;
}
/**
* For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url or
* domain / path are required. Optional.
* For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either {@code
* url} or both {@code domain} and {@code path} are required. Optional.
*/
public Cookie setDomain(String domain) {
this.domain = domain;
return this;
}
/**
* Either url or domain / path are required Optional.
* Either {@code url} or both {@code domain} and {@code path} are required. Optional.
*/
public Cookie setPath(String path) {
this.path = path;
@@ -0,0 +1,23 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class DebuggerPausedDetails {
public Location location;
public String title;
}
@@ -0,0 +1,32 @@
/*
* 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;
/**
* A single screencast frame delivered to {@link com.microsoft.playwright.Screencast#start Screencast.start()}'s
* {@code onFrame} callback.
*/
public class ScreencastFrame {
/**
* JPEG-encoded frame data.
*/
public byte[] data;
public ScreencastFrame(byte[] data) {
this.data = data;
}
}
@@ -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.options;
public class Size {
/**
* Video frame width.
*/
public int width;
/**
* Video frame height.
*/
public int height;
public Size(int width, int height) {
this.width = width;
this.height = height;
}
}
@@ -23,6 +23,7 @@ import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.zip.GZIPOutputStream;
import static com.microsoft.playwright.Utils.copy;
@@ -40,6 +41,7 @@ public class Server implements HttpHandler {
private final Map<String, String> csp = Collections.synchronizedMap(new HashMap<>());
private final Map<String, HttpHandler> routes = Collections.synchronizedMap(new HashMap<>());
private final Set<String> gzipRoutes = Collections.synchronizedSet(new HashSet<>());
private Function<String, InputStream> resourceProvider;
private static class Auth {
public final String user;
@@ -75,6 +77,8 @@ public class Server implements HttpHandler {
server.createContext("/", this);
server.setExecutor(null); // creates a default executor
server.start();
// Resources from "src/test/resources/" are copied to "resources/" directory in the jar.
resourceProvider = path -> Server.class.getClassLoader().getResourceAsStream("resources" + path);
}
public void stop() {
@@ -93,6 +97,10 @@ public class Server implements HttpHandler {
gzipRoutes.add(path);
}
void setResourceProvider(Function<String, InputStream> resourceProvider) {
this.resourceProvider = resourceProvider;
}
static class Request {
public final String url;
public final String method;
@@ -187,18 +195,16 @@ public class Server implements HttpHandler {
path = "/index.html";
}
// Resources from "src/test/resources/" are copied to "resources/" directory in the jar.
String resourcePath = "resources" + path;
InputStream resource = getClass().getClassLoader().getResourceAsStream(resourcePath);
InputStream resource = resourceProvider.apply(path);
if (resource == null) {
exchange.getResponseHeaders().add("Content-Type", "text/plain");
exchange.sendResponseHeaders(404, 0);
try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("File not found: " + resourcePath);
writer.write("File not found: " + path);
}
return;
}
exchange.getResponseHeaders().add("Content-Type", mimeType(new File(resourcePath)));
exchange.getResponseHeaders().add("Content-Type", mimeType(new File(path)));
ByteArrayOutputStream body = new ByteArrayOutputStream();
OutputStream output = body;
if (gzipRoutes.contains(path)) {
@@ -45,12 +45,12 @@ public class TestBase {
static final boolean isMac = Utils.getOS() == Utils.OS.MAC;
static final boolean isLinux = Utils.getOS() == Utils.OS.LINUX;
static final boolean isWindows = Utils.getOS() == Utils.OS.WINDOWS;
static final boolean headful;
static final boolean headed;
static final SameSiteAttribute defaultSameSiteCookieValue;
static {
String headfulEnv = System.getenv("HEADFUL");
headful = headfulEnv != null && !"0".equals(headfulEnv) && !"false".equals(headfulEnv);
String headedEnv = System.getenv("HEADED");
headed = headedEnv != null && !"0".equals(headedEnv) && !"false".equals(headedEnv);
defaultSameSiteCookieValue = initSameSiteAttribute();
}
@@ -58,8 +58,8 @@ public class TestBase {
Page page;
BrowserContext context;
static boolean isHeadful() {
return headful;
static boolean isHeaded() {
return headed;
}
static boolean isChromium() {
@@ -81,7 +81,7 @@ public class TestBase {
static BrowserType.LaunchOptions createLaunchOptions() {
BrowserType.LaunchOptions options;
options = new BrowserType.LaunchOptions();
options.headless = !headful;
options.headless = !headed;
options.channel = getBrowserChannelFromEnv();
return options;
}
@@ -0,0 +1,49 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.BindResult;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TestBrowserBind extends TestBase {
@Test
void shouldBindAndUnbindBrowser() {
BindResult serverInfo = browser.bind("default");
try {
assertNotNull(serverInfo);
assertNotNull(serverInfo.endpoint);
assertFalse(serverInfo.endpoint.isEmpty());
} finally {
browser.unbind();
}
}
@Test
void shouldBindWithCustomTitleAndOptions() {
BindResult serverInfo = browser.bind("my-title",
new Browser.BindOptions().setHost("127.0.0.1").setPort(0));
try {
assertNotNull(serverInfo);
assertNotNull(serverInfo.endpoint);
assertFalse(serverInfo.endpoint.isEmpty());
} finally {
browser.unbind();
}
}
}
@@ -155,6 +155,26 @@ public class TestBrowserContextCDPSession extends TestBase {
assertEquals(2, events.size());
}
@Test
void shouldEmitEventForEachCDPEvent() {
CDPSession client = page.context().newCDPSession(page);
client.send("Network.enable");
List<JsonObject> events = new ArrayList<>();
client.on("event", events::add);
page.navigate(server.EMPTY_PAGE);
assertTrue(events.size() > 0);
JsonObject requestEvent = null;
for (JsonObject e : events) {
if ("Network.requestWillBeSent".equals(e.get("method").getAsString())) {
requestEvent = e;
break;
}
}
assertNotNull(requestEvent);
assertEquals(server.EMPTY_PAGE,
requestEvent.getAsJsonObject("params").getAsJsonObject("request").get("url").getAsString());
}
@Test
void shouldRemoveEventListeners() {
CDPSession cdpSession = page.context().newCDPSession(page);
@@ -27,7 +27,7 @@ public class TestBrowserContextCredentials extends TestBase {
static boolean isChromiumHeadedLike() {
// --headless=new, the default in all Chromium channels, is like headless.
return isChromium() && (isHeadful() || getBrowserChannelFromEnv() != null);
return isChromium() && (isHeaded() || getBrowserChannelFromEnv() != null);
}
@Test
@@ -129,12 +129,12 @@ public class TestBrowserContextProxy extends TestBase {
context.close();
}
static boolean isChromiumHeadful() {
return isChromium() && isHeadful();
static boolean isChromiumHeaded() {
return isChromium() && isHeaded();
}
@Test
@DisabledIf(value="isChromiumHeadful", disabledReason="fixme")
@DisabledIf(value="isChromiumHeaded", disabledReason="fixme")
void shouldExcludePatterns() {
server.setRoute("/target.html", exchange -> {
exchange.sendResponseHeaders(200, 0);
@@ -31,6 +31,7 @@ import static com.microsoft.playwright.Utils.assertJsonEquals;
import static com.microsoft.playwright.Utils.mapOf;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestBrowserContextStorageState extends TestBase {
@@ -276,4 +277,12 @@ public class TestBrowserContextStorageState extends TestBase {
assertEquals("{\"cookies\":[],\"origins\":[]}", context.storageState(
new BrowserContext.StorageStateOptions().setIndexedDB(false)));
}
@Test
void setStorageStateShouldHandleMissingFile(@TempDir Path tempDir) {
Path file = tempDir.resolve("does-not-exist.json");
PlaywrightException e = org.junit.jupiter.api.Assertions.assertThrows(
PlaywrightException.class, () -> context.setStorageState(file));
assertTrue(e.getMessage().contains("Failed to read storage state from file"), e.getMessage());
}
}
@@ -355,9 +355,8 @@ public class TestClick extends TestBase {
page.evalOnSelector("button", "button => button.style.borderWidth = '8px'");
page.click("button", new Page.ClickOptions().setPosition(20, 10));
assertEquals(page.evaluate("result"), "Clicked");
// Safari reports border-relative offsetX/offsetY.
assertEquals(isWebKit() ? 20 + 8 : 20, page.evaluate("offsetX"));
assertEquals(isWebKit() ? 10 + 8 : 10, page.evaluate("offsetY"));
assertEquals(20, page.evaluate("offsetX"));
assertEquals(10, page.evaluate("offsetY"));
}
@Test
@@ -367,9 +366,8 @@ public class TestClick extends TestBase {
page.evalOnSelector("button", "button => button.style.fontSize = '12px'");
page.click("button", new Page.ClickOptions().setPosition(20, 10));
assertEquals("Clicked", page.evaluate("result"));
// Safari reports border-relative offsetX/offsetY.
assertEquals(isWebKit() ? 12 * 2 + 20 : 20, page.evaluate("offsetX"));
assertEquals(isWebKit() ? 12 * 2 + 10 : 10, page.evaluate("offsetY"));
assertEquals(20, page.evaluate("offsetX"));
assertEquals(10, page.evaluate("offsetY"));
}
@Test
@@ -379,9 +377,8 @@ public class TestClick extends TestBase {
page.evalOnSelector("button", "button => button.style.height = button.style.width = '2000px'");
page.click("button", new Page.ClickOptions().setPosition(1900, 1910));
assertEquals("Clicked", page.evaluate("() => window['result']"));
// Safari reports border-relative offsetX/offsetY.
assertEquals(isWebKit() ? 1900 + 8 : 1900, page.evaluate("offsetX"));
assertEquals(isWebKit() ? 1910 + 8 : 1910, page.evaluate("offsetY"));
assertEquals(1900, page.evaluate("offsetX"));
assertEquals(1910, page.evaluate("offsetY"));
}
@Test
@@ -400,9 +397,8 @@ public class TestClick extends TestBase {
"}");
page.click("button", new Page.ClickOptions().setPosition(1900, 1910));
assertEquals("Clicked", page.evaluate("() => window['result']"));
// Safari reports border-relative offsetX/offsetY.
assertEquals(isWebKit() ? 1900 + 8 : 1900, page.evaluate("offsetX"));
assertEquals(isWebKit() ? 1910 + 8 : 1910, page.evaluate("offsetY"));
assertEquals(1900, page.evaluate("offsetX"));
assertEquals(1910, page.evaluate("offsetY"));
}
private static void expectCloseTo(double expected, double actual) {
@@ -0,0 +1,101 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.DebuggerPausedDetails;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TestDebugger extends TestBase {
@Test
void shouldReturnNullPausedDetailsInitially() {
Debugger dbg = context.debugger();
assertNull(dbg.pausedDetails());
}
@Test
void shouldPauseAtNextAndResume() {
page.setContent("<div>click me</div>");
Debugger dbg = context.debugger();
assertNull(dbg.pausedDetails());
dbg.requestPause();
boolean[] paused = {false};
dbg.onPausedStateChanged(() -> {
if (!paused[0]) {
paused[0] = true;
DebuggerPausedDetails details = dbg.pausedDetails();
assertNotNull(details);
assertTrue(details.title.contains("Click"), "title: " + details.title);
dbg.resume();
}
});
page.click("div"); // blocks until dbg.resume() is called
assertNull(dbg.pausedDetails());
}
@Test
void shouldStepWithNext() {
page.setContent("<div>click me</div>");
Debugger dbg = context.debugger();
assertNull(dbg.pausedDetails());
dbg.requestPause();
boolean[] paused = {false};
dbg.onPausedStateChanged(() -> {
if (!paused[0]) {
paused[0] = true;
DebuggerPausedDetails details = dbg.pausedDetails();
assertNotNull(details);
assertTrue(details.title.contains("Click"), "title: " + details.title);
dbg.next();
} else if (dbg.pausedDetails() != null) {
dbg.resume();
}
});
page.click("div");
assertNull(dbg.pausedDetails());
}
@Test
void shouldPauseAtPauseCall() {
page.setContent("<div>click me</div>");
Debugger dbg = context.debugger();
assertNull(dbg.pausedDetails());
dbg.requestPause();
boolean[] paused = {false};
dbg.onPausedStateChanged(() -> {
if (!paused[0]) {
paused[0] = true;
DebuggerPausedDetails details = dbg.pausedDetails();
assertNotNull(details);
assertTrue(details.title.contains("Pause"), "title: " + details.title);
dbg.resume();
}
});
page.pause(); // blocks until dbg.resume() is called from event handler
assertNull(dbg.pausedDetails());
}
}
@@ -49,7 +49,7 @@ public class TestDefaultBrowserContext2 extends TestBase {
@TempDir Path tempDir;
@AfterEach
private void closePersistentContext() {
void closePersistentContext() {
if (persistentContext != null) {
persistentContext.close();
persistentContext = null;
@@ -128,7 +128,7 @@ public class TestDefaultBrowserContext2 extends TestBase {
@Test
void shouldSupportExtraHTTPHeadersOption() throws ExecutionException, InterruptedException {
// TODO: test.flaky(browserName === "firefox" && headful && platform === "linux", "Intermittent timeout on bots");
// TODO: test.flaky(browserName === "firefox" && headed && platform === "linux", "Intermittent timeout on bots");
Page page = launchPersistent(new BrowserType.LaunchPersistentContextOptions().setExtraHTTPHeaders(mapOf("foo", "bar")));
Future<Server.Request> request = server.futureRequest("/empty.html");
page.navigate(server.EMPTY_PAGE);
@@ -93,18 +93,12 @@ public class TestDownload extends TestBase {
assertTrue(Files.exists(path));
byte[] bytes = readAllBytes(path);
assertEquals("Hello world", new String(bytes, UTF_8));
if (isChromium()) {
assertNotNull(error[0]);
assertTrue(error[0].getMessage().contains("net::ERR_ABORTED"));
assertEquals("about:blank", page.url());
} else if (isWebKit()) {
assertNotNull(error[0]);
assertTrue(error[0].getMessage().contains("Download is starting"));
assertEquals("about:blank", page.url());
} else {
assertNotNull(error[0]);
assertNotNull(error[0]);
if (!chromiumVersionLessThan(browser.version(), "140.0.0.0")) {
assertTrue(error[0].getMessage().contains("Download is starting"));
}
if (!isFirefox())
assertEquals("about:blank", page.url());
page.close();
}
@@ -347,14 +341,14 @@ public class TestDownload extends TestBase {
}
static boolean isChromiumHeadful() {
return isChromium() && isHeadful();
static boolean isChromiumHeaded() {
return isChromium() && isHeaded();
}
@Test
@DisabledIf(value="isChromiumHeadful", disabledReason="fixme")
@DisabledIf(value="isChromiumHeaded", disabledReason="fixme")
void shouldReportNewWindowDownloads() throws IOException {
// TODO: - the test fails in headful Chromium as the popup page gets closed along
// TODO: - the test fails in headed Chromium as the popup page gets closed along
// with the session before download completed event arrives.
// - WebKit doesn't close the popup page
Page page = browser.newPage(new Browser.NewPageOptions().setAcceptDownloads(true));
@@ -26,12 +26,12 @@ import static org.junit.jupiter.api.Assertions.*;
public class TestElementHandleBoundingBox extends TestBase {
static boolean isFirefoxHeadful() {
return isFirefox() && isHeadful();
static boolean isFirefoxHeaded() {
return isFirefox() && isHeaded();
}
@Test
@DisabledIf(value="isFirefoxHeadful", disabledReason="fail")
@DisabledIf(value="isFirefoxHeaded", disabledReason="fail")
void shouldWork() {
page.setViewportSize(500, 500);
page.navigate(server.PREFIX + "/grid.html");
@@ -0,0 +1,46 @@
/*
* 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 org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestExpectMisc extends TestBase {
@Test
void strictModeViolationErrorFormat() {
page.setContent(" <div>hello</div><div>hi</div>");
AssertionFailedError error = assertThrows(AssertionFailedError.class, () ->
assertThat(page.locator("div")).isVisible());
assertTrue(error.getMessage().contains("Locator expected to be visible"), error.getMessage());
assertTrue(error.getMessage().contains("Error: strict mode violation: locator(\"div\") resolved to 2 elements:"), error.getMessage());
}
@Test
void invalidSelectorErrorFormat() {
page.setContent("<div>hello</div><div>hi</div>");
AssertionFailedError error = assertThrows(AssertionFailedError.class, () ->
assertThat(page.locator("##")).isVisible());
assertTrue(error.getMessage().contains("Locator expected to be visible"), error.getMessage());
assertTrue(error.getMessage().contains("Error: Unexpected token \"#\" while parsing css selector \"##\"."), error.getMessage());
}
}
@@ -1,88 +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 org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import static com.microsoft.playwright.Utils.mapOf;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
public class TestLaunch extends TestBase {
@Override
@BeforeAll
// Hide base class method to not launch browser.
void launchBrowser() {
}
@Override
void createContextAndPage() {
// Do nothing
}
@Test
void passEnvVar() {
BrowserType.LaunchOptions options = new BrowserType.LaunchOptions();
options.setEnv(mapOf("DEBUG", "pw:protocol"));
launchBrowser(options);
}
public static boolean canRunHeaded() {
// On linux headed browser requires xvfb.
return isHeadful() || isMac || isWindows;
}
public static boolean canRunExtensionTest() {
return canRunHeaded() && isChromium();
}
@Test
@EnabledIf(value="com.microsoft.playwright.TestLaunch#canRunExtensionTest", disabledReason="Only Chromium Headed")
void shouldReturnBackgroundPages(@TempDir Path tmpDir) throws IOException {
Path profileDir = tmpDir.resolve("profile");
Files.createDirectories(profileDir);
String extensionPath = Paths.get("src/test/resources/simple-extension").toAbsolutePath().toString();
initBrowserType();
BrowserContext context = browserType.launchPersistentContext(profileDir, new BrowserType.LaunchPersistentContextOptions()
.setHeadless(false)
.setArgs(asList(
"--disable-extensions-except=" + extensionPath,
"--load-extension=" + extensionPath
)));
List<Page> backgroundPages = context.backgroundPages();
context.onBackgroundPage(page1 -> backgroundPages.add(page1));
context.waitForCondition(() -> !backgroundPages.isEmpty(),
new BrowserContext.WaitForConditionOptions().setTimeout(10_000));
Page backgroundPage = backgroundPages.get(0);
assertNotNull(backgroundPage);
assertTrue(context.backgroundPages().contains(backgroundPage));
assertFalse(context.pages().contains(backgroundPage));
context.close();
assertEquals(0, context.pages().size());
assertEquals(0, context.backgroundPages().size());
}
}
@@ -223,7 +223,7 @@ public class TestLocatorAssertions extends TestBase {
});
assertEquals("[Text 1, Text 3, Extra]", e.getExpected().getStringRepresentation());
assertEquals("[Text 1, Text 3]", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have text: [Text 1, Text 3, Extra]"), e.getMessage());
assertTrue(e.getMessage().contains("Locator expected to have text\nExpected: [Text 1, Text 3, Extra]"), e.getMessage());
assertTrue(e.getMessage().contains("Received: [Text 1, Text 3]"), e.getMessage());
}
@@ -272,7 +272,7 @@ public class TestLocatorAssertions extends TestBase {
});
assertEquals("foo", e.getExpected().getStringRepresentation());
assertEquals("node", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have attribute 'id': foo\nReceived: node"), e.getMessage());
assertTrue(e.getMessage().contains("Locator expected to have attribute 'id'\nExpected: foo\nReceived: node"), e.getMessage());
}
@Test
@@ -291,7 +291,7 @@ public class TestLocatorAssertions extends TestBase {
});
assertEquals(".Nod..", e.getExpected().getStringRepresentation());
assertEquals("node", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have attribute 'id' matching regex: .Nod..\nReceived: node"), e.getMessage());
assertTrue(e.getMessage().contains("Locator expected to have attribute 'id' matching regex\nExpected: .Nod..\nReceived: node"), e.getMessage());
}
@Test
@@ -629,7 +629,7 @@ public class TestLocatorAssertions extends TestBase {
" </select>");
Locator locator = page.locator("select");
locator.selectOption(new String[] {"B"});
PlaywrightException e = assertThrows(PlaywrightException.class, () -> {
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> {
assertThat(locator).hasValues(new Pattern[]{ Pattern.compile("R"), Pattern.compile("G")});
});
assertTrue(e.getMessage().contains("Not a select element with a multiple attribute"), e.getMessage());
@@ -639,7 +639,7 @@ public class TestLocatorAssertions extends TestBase {
void hasValuesFailsWhenNotASelectElement() {
page.setContent("<input value=\"foo\" />");
Locator locator = page.locator("input");
PlaywrightException e = assertThrows(PlaywrightException.class, () -> {
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> {
assertThat(locator).hasValues(new Pattern[]{ Pattern.compile("R"), Pattern.compile("G")}, new LocatorAssertions.HasValuesOptions().setTimeout(1000));
});
assertTrue(e.getMessage().contains("Not a select element with a multiple attribute"), e.getMessage());
@@ -661,7 +661,7 @@ public class TestLocatorAssertions extends TestBase {
});
assertEquals("checked", e.getExpected().getStringRepresentation());
assertEquals("unchecked", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to be: checked"), e.getMessage());
assertTrue(e.getMessage().contains("Locator expected to be\nExpected: checked"), e.getMessage());
}
@Test
@@ -674,7 +674,7 @@ public class TestLocatorAssertions extends TestBase {
assertEquals("checked", e.getExpected().getStringRepresentation());
assertEquals("checked", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected not to be: checked"), e.getMessage());
assertTrue(e.getMessage().contains("Locator expected not to be\nExpected: checked"), e.getMessage());
}
@Test
@@ -690,7 +690,7 @@ public class TestLocatorAssertions extends TestBase {
Locator locator = page.locator("input");
AssertionFailedError error = assertThrows(AssertionFailedError.class,
() -> assertThat(locator).isChecked(new LocatorAssertions.IsCheckedOptions().setChecked(false).setTimeout(1000)));
assertTrue(error.getMessage().contains("Locator expected to be: unchecked"), error.getMessage());
assertTrue(error.getMessage().contains("Locator expected to be\nExpected: unchecked"), error.getMessage());
}
@Test
@@ -796,7 +796,7 @@ public class TestLocatorAssertions extends TestBase {
void isEditableThrowsOnNonInputElement() {
page.setContent("<button>");
Locator locator = page.locator("button");
PlaywrightException e = assertThrows(PlaywrightException.class, () -> assertThat(locator).isEditable());
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> assertThat(locator).isEditable());
assertTrue(e.getMessage().contains("Element is not an <input>, <textarea>, <select> or [contenteditable] and does not have a role allowing [aria-readonly]"), e.getMessage());
}
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -206,7 +206,7 @@ public class TestLocatorAssertions2 extends TestBase {
page.setContent("<input type=checkbox></input>");
page.locator("input").evaluate("e => e.indeterminate = true");
Locator locator = page.locator("input");
PlaywrightException e = assertThrows(PlaywrightException.class, () ->
AssertionFailedError e = assertThrows(AssertionFailedError.class, () ->
assertThat(locator).isChecked(new LocatorAssertions.IsCheckedOptions().setIndeterminate(true).setChecked(false)));
assertTrue(e.getMessage().contains("Can't assert indeterminate and checked at the same time"), e.getMessage());
}
@@ -220,4 +220,5 @@ public class TestLocatorAssertions2 extends TestBase {
// TODO: should be "assertThat().isChecked() with timeout 1000ms"
assertTrue(e.getMessage().contains("Assert \"isChecked\" with timeout 1000ms"), e.getMessage());
}
}
@@ -65,4 +65,41 @@ public class TestLocatorClick extends TestBase {
page.getByText("Go").click(new Locator.ClickOptions().setModifiers(asList(KeyboardModifier.CONTROLORMETA))));
assertThat(newPage).hasURL(server.PREFIX + "/title.html");
}
@Test
void shouldClickWithTweenedMouseMovement() {
page.setContent(
"<body style=\"margin: 0; padding: 0; height: 500px; width: 500px;\">\n" +
" <div style=\"position: relative; top: 280px; left: 150px; width: 100px; height: 40px\">Click me</div>\n" +
"</body>"
);
// The test becomes flaky on WebKit without next line.
if (isWebKit()) {
page.evaluate("() => new Promise(requestAnimationFrame)");
}
page.mouse().move(100, 100);
page.evaluate("() => {\n" +
" window['result'] = [];\n" +
" document.addEventListener('mousemove', event => {\n" +
" window['result'].push([event.clientX, event.clientY]);\n" +
" });\n" +
"}");
// Centerpoint at 150 + 100/2, 280 + 40/2 = 200, 300
page.locator("div").click(new Locator.ClickOptions().setSteps(5));
assertEquals(
asList(
asList(120, 140),
asList(140, 180),
asList(160, 220),
asList(180, 260),
asList(200, 300)
),
page.evaluate("result")
);
}
}
@@ -186,4 +186,53 @@ public class TestLocatorConvenience extends TestBase {
page.setContent("<div>A</div><div>B</div><div>C</div>");
assertEquals(asList("A", "B", "C"), page.locator("div").allInnerTexts());
}
@Test
void descriptionShouldReturnNullForUndescribedLocators() {
page.setContent("<div>Hello</div>");
Locator locator = page.locator("div");
assertNull(locator.description());
}
@Test
void descriptionShouldReturnSimpleDescription() {
page.setContent("<div>Hello</div>");
Locator locator = page.locator("div").describe("my div");
assertEquals("my div", locator.description());
}
@Test
void descriptionShouldHandleSpecialCharacters() {
page.setContent("<div>Hello</div>");
Locator locator = page.locator("div").describe("título 😊");
assertEquals("título 😊", locator.description());
}
@Test
void descriptionShouldWorkWithChainedLocators() {
page.setContent("<div><span>Hello</span></div>");
Locator locator = page.locator("div").describe("container").locator("span");
assertNull(locator.description());
}
@Test
void descriptionShouldReturnLastDescriptionForMultipleCalls() {
page.setContent("<div>Hello</div>");
Locator locator = page.locator("div").describe("first").describe("second");
assertEquals("second", locator.description());
}
@Test
void toStringShouldReturnFormattedLocator() {
page.setContent("<div>Hello</div>");
Locator locator = page.locator("div");
assertTrue(locator.toString().startsWith("Locator@"));
}
@Test
void toStringShouldPreferDescription() {
page.setContent("<div>Hello</div>");
Locator locator = page.locator("div").describe("my div");
assertEquals("my div", locator.toString());
}
}
@@ -35,13 +35,13 @@ public class TestOptionsFactories {
public static BrowserType.LaunchOptions createLaunchOptions() {
BrowserType.LaunchOptions options;
options = new BrowserType.LaunchOptions();
options.headless = !getHeadful();
options.headless = !getHeaded();
return options;
}
private static boolean getHeadful() {
String headfulEnv = System.getenv("HEADFUL");
return headfulEnv != null && !"0".equals(headfulEnv) && !"false".equals(headfulEnv);
private static boolean getHeaded() {
String headedEnv = System.getenv("HEADED");
return headedEnv != null && !"0".equals(headedEnv) && !"false".equals(headedEnv);
}
public static String getBrowserName() {
@@ -127,4 +127,218 @@ public class TestPageAriaSnapshot {
" - listitem: \"1.2\"");
});
}
@Test
void matchValuesBothAgainstRegexAndString(Page page) {
page.setContent("<a href=\"/auth?r=/\">Log in</a>");
checkAndMatchSnapshot(page.locator("body"),
"- link \"Log in\":\n" +
" - /url: /auth?r=/");
}
@Test
void shouldSnapshotIntegration(Page page) {
page.setContent(
"<h1>Microsoft</h1>" +
"<div>Open source projects and samples from Microsoft</div>" +
"<ul>" +
"<li><details><summary>Verified</summary><div><div>" +
"<p>We've verified that the organization <strong>microsoft</strong> controls the domain:</p>" +
"<ul><li class=\"mb-1\"><strong>opensource.microsoft.com</strong></li></ul>" +
"<div><a href=\"about: blank\">Learn more about verified organizations</a></div>" +
"</div></div></details></li>" +
"<li><a href=\"about:blank\"><summary title=\"Label: GitHub Sponsor\">Sponsor</summary></a></li>" +
"</ul>");
checkAndMatchSnapshot(page.locator("body"),
"- heading \"Microsoft\" [level=1]\n" +
"- text: Open source projects and samples from Microsoft\n" +
"- list:\n" +
" - listitem:\n" +
" - group: Verified\n" +
" - listitem:\n" +
" - link \"Sponsor\":\n" +
" - /url: about:blank");
}
@Test
void shouldSupportMultilineText(Page page) {
page.setContent("<p>\n Line 1\n Line 2\n Line 3\n </p>");
checkAndMatchSnapshot(page.locator("body"), "- paragraph: Line 1 Line 2 Line 3");
assertThat(page.locator("body")).matchesAriaSnapshot(
" - paragraph: |\n" +
" Line 1\n" +
" Line 2\n" +
" Line 3");
}
@Test
void shouldConcatenateSpanText(Page page) {
page.setContent("<span>One</span> <span>Two</span> <span>Three</span>");
checkAndMatchSnapshot(page.locator("body"), "- text: One Two Three");
}
@Test
void shouldConcatenateSpanText2(Page page) {
page.setContent("<span>One </span><span>Two </span><span>Three</span>");
checkAndMatchSnapshot(page.locator("body"), "- text: One Two Three");
}
@Test
void shouldConcatenateDivTextWithSpaces(Page page) {
page.setContent("<div>One</div><div>Two</div><div>Three</div>");
checkAndMatchSnapshot(page.locator("body"), "- text: One Two Three");
}
@Test
void shouldIncludePseudoInText(Page page) {
page.setContent(
"<style>span:before { content: 'world'; } div:after { content: 'bye'; }</style>" +
"<a href=\"about:blank\"><span>hello</span><div>hello</div></a>");
checkAndMatchSnapshot(page.locator("body"),
"- link \"worldhello hellobye\":\n" +
" - /url: about:blank");
}
@Test
void shouldNotIncludeHiddenPseudoInText(Page page) {
page.setContent(
"<style>span:before { content: 'world'; display: none; } div:after { content: 'bye'; visibility: hidden; }</style>" +
"<a href=\"about:blank\"><span>hello</span><div>hello</div></a>");
checkAndMatchSnapshot(page.locator("body"),
"- link \"hello hello\":\n" +
" - /url: about:blank");
}
@Test
void shouldIncludeNewLineForBlockPseudo(Page page) {
page.setContent(
"<style>span:before { content: 'world'; display: block; } div:after { content: 'bye'; display: block; }</style>" +
"<a href=\"about:blank\"><span>hello</span><div>hello</div></a>");
checkAndMatchSnapshot(page.locator("body"),
"- link \"world hello hello bye\":\n" +
" - /url: about:blank");
}
@Test
void shouldWorkWithSlots(Page page) {
// Text "foo" is assigned to the slot, should not be used twice.
page.setContent(
"<button><div>foo</div></button>" +
"<script>(() => {" +
" const container = document.querySelector('div');" +
" const shadow = container.attachShadow({ mode: 'open' });" +
" const slot = document.createElement('slot');" +
" shadow.appendChild(slot);" +
"})()</script>");
checkAndMatchSnapshot(page.locator("body"), "- button \"foo\"");
// Text "foo" is assigned to the slot, should be used instead of slot content.
page.setContent(
"<div>foo</div>" +
"<script>(() => {" +
" const container = document.querySelector('div');" +
" const shadow = container.attachShadow({ mode: 'open' });" +
" const button = document.createElement('button');" +
" shadow.appendChild(button);" +
" const slot = document.createElement('slot');" +
" button.appendChild(slot);" +
" const span = document.createElement('span');" +
" span.textContent = 'pre';" +
" slot.appendChild(span);" +
"})()</script>");
checkAndMatchSnapshot(page.locator("body"), "- button \"foo\"");
// Nothing is assigned to the slot, should use slot content.
page.setContent(
"<div></div>" +
"<script>(() => {" +
" const container = document.querySelector('div');" +
" const shadow = container.attachShadow({ mode: 'open' });" +
" const button = document.createElement('button');" +
" shadow.appendChild(button);" +
" const slot = document.createElement('slot');" +
" button.appendChild(slot);" +
" const span = document.createElement('span');" +
" span.textContent = 'pre';" +
" slot.appendChild(span);" +
"})()</script>");
checkAndMatchSnapshot(page.locator("body"), "- button \"pre\"");
}
@Test
void shouldSnapshotInnerText(Page page) {
page.setContent(
"<div role=\"listitem\"><div><div><span title=\"a.test.ts\">a.test.ts</span></div>" +
"<div><button title=\"Run\"></button><button title=\"Show source\"></button><button title=\"Watch\"></button></div></div></div>" +
"<div role=\"listitem\"><div><div><span title=\"snapshot\">snapshot</span></div>" +
"<div class=\"ui-mode-list-item-time\">30ms</div>" +
"<div><button title=\"Run\"></button><button title=\"Show source\"></button><button title=\"Watch\"></button></div></div></div>");
checkAndMatchSnapshot(page.locator("body"),
" - listitem:\n" +
" - text: a.test.ts\n" +
" - button \"Run\"\n" +
" - button \"Show source\"\n" +
" - button \"Watch\"\n" +
" - listitem:\n" +
" - text: snapshot 30ms\n" +
" - button \"Run\"\n" +
" - button \"Show source\"\n" +
" - button \"Watch\"");
}
@Test
void checkAriaHiddenText(Page page) {
page.setContent("<p><span>hello</span><span aria-hidden=\"true\">world</span></p>");
checkAndMatchSnapshot(page.locator("body"), "- paragraph: hello");
}
@Test
void shouldIgnorePresentationAndNoneRoles(Page page) {
page.setContent("<ul><li role=\"presentation\">hello</li><li role=\"none\">world</li></ul>");
checkAndMatchSnapshot(page.locator("body"), "- list: hello world");
}
@Test
void shouldNotUseOnAsCheckboxValue(Page page) {
page.setContent("<input type=\"checkbox\"><input type=\"radio\">");
checkAndMatchSnapshot(page.locator("body"), "- checkbox\n- radio");
}
@Test
void shouldNotReportTextareaTextContent(Page page) {
page.setContent("<textarea>Before</textarea>");
checkAndMatchSnapshot(page.locator("body"), "- textbox: Before");
page.evaluate("document.querySelector('textarea').value = 'After'");
checkAndMatchSnapshot(page.locator("body"), "- textbox: After");
}
@Test
void shouldNotShowVisibleChildrenOfHiddenElements(Page page) {
page.setContent(
"<div style=\"visibility: hidden;\">" +
"<div style=\"visibility: visible;\"><button>Button</button></div>" +
"</div>");
assertEquals("", page.locator("body").ariaSnapshot());
}
@Test
void shouldNotShowUnhiddenChildrenOfAriaHiddenElements(Page page) {
page.setContent(
"<div aria-hidden=\"true\">" +
"<div aria-hidden=\"false\"><button>Button</button></div>" +
"</div>");
assertEquals("", page.locator("body").ariaSnapshot());
}
@Test
void shouldSnapshotPlaceholderWhenDifferentFromName(Page page) {
page.setContent("<input placeholder=\"Placeholder\">");
assertThat(page.locator("body")).matchesAriaSnapshot("- textbox \"Placeholder\"");
page.setContent("<input placeholder=\"Placeholder\" aria-label=\"Label\">");
assertThat(page.locator("body")).matchesAriaSnapshot(
"- textbox \"Label\":\n" +
" - /placeholder: Placeholder");
}
}
@@ -0,0 +1,191 @@
package com.microsoft.playwright;
import com.microsoft.playwright.junit.FixtureTest;
import com.microsoft.playwright.junit.UsePlaywright;
import com.microsoft.playwright.options.AriaSnapshotMode;
import org.junit.jupiter.api.Test;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
@FixtureTest
@UsePlaywright
public class TestPageAriaSnapshotAI {
private static String aiSnapshot(Page page) {
return page.ariaSnapshot(new Page.AriaSnapshotOptions().setMode(AriaSnapshotMode.AI));
}
@Test
void shouldGenerateRefs(Page page) {
page.setContent("<button>One</button><button>Two</button><button>Three</button>");
String snapshot1 = aiSnapshot(page);
assertTrue(snapshot1.contains("button \"One\" [ref=e2]"), snapshot1);
assertTrue(snapshot1.contains("button \"Two\" [ref=e3]"), snapshot1);
assertTrue(snapshot1.contains("button \"Three\" [ref=e4]"), snapshot1);
assertThat(page.locator("aria-ref=e2")).hasText("One");
assertThat(page.locator("aria-ref=e3")).hasText("Two");
assertThat(page.locator("aria-ref=e4")).hasText("Three");
page.locator("aria-ref=e3").evaluate("e => e.textContent = 'Not Two'");
String snapshot2 = aiSnapshot(page);
assertTrue(snapshot2.contains("button \"One\" [ref=e2]"), snapshot2);
assertTrue(snapshot2.contains("button \"Not Two\" [ref=e5]"), snapshot2);
assertTrue(snapshot2.contains("button \"Three\" [ref=e4]"), snapshot2);
}
@Test
void shouldListIframes(Page page) {
page.setContent(
"<h1>Hello</h1>" +
"<iframe name=\"foo\" src=\"data:text/html,<h1>World</h1>\">");
String snapshot = aiSnapshot(page);
assertTrue(snapshot.contains("- iframe"), snapshot);
String frameSnapshot = page.frameLocator("iframe").locator("body").ariaSnapshot();
assertEquals("- heading \"World\" [level=1]", frameSnapshot);
}
@Test
void shouldSnapshotLocatorInsideIframe(Page page) {
page.setContent(
"<h1>Main Page</h1>" +
"<iframe srcdoc=\"<ul><li>Item 1</li><li>Item 2</li></ul>\"></iframe>");
Locator list = page.frames().get(1).locator("ul");
String snapshot = list.ariaSnapshot(new Locator.AriaSnapshotOptions().setMode(AriaSnapshotMode.AI));
assertTrue(snapshot.contains("list [ref=f1e1]"), snapshot);
assertTrue(snapshot.contains("listitem [ref=f1e2]: Item 1"), snapshot);
assertTrue(snapshot.contains("listitem [ref=f1e3]: Item 2"), snapshot);
}
@Test
void shouldCollapseGenericNodes(Page page) {
page.setContent("<div><div><div><button>Button</button></div></div></div>");
String snapshot = aiSnapshot(page);
assertTrue(snapshot.contains("button \"Button\" [ref=e5]"), snapshot);
}
@Test
void shouldIncludeCursorPointerHint(Page page) {
page.setContent("<button style=\"cursor: pointer\">Button</button>");
String snapshot = aiSnapshot(page);
assertTrue(snapshot.contains("button \"Button\" [ref=e2] [cursor=pointer]"), snapshot);
}
@Test
void shouldNotNestCursorPointerHints(Page page) {
page.setContent(
"<a style=\"cursor: pointer\" href=\"about:blank\">" +
"Link with a button <button style=\"cursor: pointer\">Button</button>" +
"</a>");
String snapshot = aiSnapshot(page);
assertTrue(snapshot.contains("link \"Link with a button Button\" [ref=e2] [cursor=pointer]"), snapshot);
// The button inside a cursor-pointer link should not get a redundant [cursor=pointer]
assertTrue(snapshot.contains("button \"Button\" [ref=e3]"), snapshot);
assertFalse(snapshot.contains("button \"Button\" [ref=e3] [cursor=pointer]"), snapshot);
}
@Test
void shouldShowVisibleChildrenOfHiddenElements(Page page) {
page.setContent(
"<div style=\"visibility: hidden\">" +
" <div style=\"visibility: visible\"><button>Visible</button></div>" +
" <div style=\"visibility: hidden\"><button style=\"visibility: visible\">Visible</button></div>" +
" <div>" +
" <div style=\"visibility: visible\"><button style=\"visibility: hidden\">Hidden</button></div>" +
" <button>Hidden</button>" +
" </div>" +
"</div>");
String snapshot = aiSnapshot(page);
assertEquals(
"- generic [active] [ref=e1]:\n" +
" - button \"Visible\" [ref=e3]\n" +
" - button \"Visible\" [ref=e4]",
snapshot);
}
@Test
void shouldIncludeActiveElementInformation(Page page) {
page.setContent(
"<button id=\"btn1\">Button 1</button>" +
"<button id=\"btn2\" autofocus>Button 2</button>" +
"<div>Not focusable</div>");
page.waitForFunction("document.activeElement?.id === 'btn2'");
String snapshot = aiSnapshot(page);
assertTrue(snapshot.contains("button \"Button 2\" [active] [ref=e3]"), snapshot);
assertFalse(snapshot.contains("button \"Button 1\" [active]"), snapshot);
}
@Test
void shouldUpdateActiveElementOnFocus(Page page) {
page.setContent(
"<input id=\"input1\" placeholder=\"First input\">" +
"<input id=\"input2\" placeholder=\"Second input\">");
String initialSnapshot = aiSnapshot(page);
assertTrue(initialSnapshot.contains("textbox \"First input\" [ref=e2]"), initialSnapshot);
assertTrue(initialSnapshot.contains("textbox \"Second input\" [ref=e3]"), initialSnapshot);
assertFalse(initialSnapshot.contains("textbox \"First input\" [active]"), initialSnapshot);
assertFalse(initialSnapshot.contains("textbox \"Second input\" [active]"), initialSnapshot);
page.locator("#input2").focus();
String afterFocusSnapshot = aiSnapshot(page);
assertTrue(afterFocusSnapshot.contains("textbox \"Second input\" [active] [ref=e3]"), afterFocusSnapshot);
assertFalse(afterFocusSnapshot.contains("textbox \"First input\" [active]"), afterFocusSnapshot);
}
@Test
void shouldCollapseInlineGenericNodes(Page page) {
page.setContent(
"<ul>" +
"<li><b>3</b> <abbr>bds</abbr></li>" +
"<li><b>2</b> <abbr>ba</abbr></li>" +
"<li><b>1,200</b> <abbr>sqft</abbr></li>" +
"</ul>");
String snapshot = aiSnapshot(page);
assertTrue(snapshot.contains("listitem [ref=e3]: 3 bds"), snapshot);
assertTrue(snapshot.contains("listitem [ref=e4]: 2 ba"), snapshot);
assertTrue(snapshot.contains("listitem [ref=e5]: 1,200 sqft"), snapshot);
}
@Test
void shouldNotRemoveGenericNodesWithTitle(Page page) {
page.setContent("<div title=\"Element title\">Element content</div>");
String snapshot = aiSnapshot(page);
assertTrue(snapshot.contains("generic \"Element title\" [ref=e2]"), snapshot);
}
@Test
void shouldLimitDepth(Page page) {
page.setContent(
"<ul>" +
"<li>item1</li>" +
"<a href=\"about:blank\" style=\"cursor:pointer\">link</a>" +
"<li><ul id=\"target\"><li>item2</li><li><ul><li>item3</li></ul></li></ul></li>" +
"</ul>");
String snapshot1 = page.ariaSnapshot(new Page.AriaSnapshotOptions().setMode(AriaSnapshotMode.AI).setDepth(1));
assertTrue(snapshot1.contains("listitem [ref=e3]: item1"), snapshot1);
assertFalse(snapshot1.contains("item2"), snapshot1);
assertFalse(snapshot1.contains("item3"), snapshot1);
String snapshot2 = page.ariaSnapshot(new Page.AriaSnapshotOptions().setMode(AriaSnapshotMode.AI).setDepth(3));
assertTrue(snapshot2.contains("item1"), snapshot2);
assertTrue(snapshot2.contains("item2"), snapshot2);
assertFalse(snapshot2.contains("item3"), snapshot2);
String snapshot3 = page.ariaSnapshot(new Page.AriaSnapshotOptions().setMode(AriaSnapshotMode.AI).setDepth(100));
assertTrue(snapshot3.contains("item1"), snapshot3);
assertTrue(snapshot3.contains("item2"), snapshot3);
assertTrue(snapshot3.contains("item3"), snapshot3);
String snapshot4 = page.locator("#target").ariaSnapshot(new Locator.AriaSnapshotOptions().setMode(AriaSnapshotMode.AI).setDepth(1));
assertTrue(snapshot4.contains("listitem [ref=e7]: item2"), snapshot4);
assertFalse(snapshot4.contains("item3"), snapshot4);
}
}
@@ -107,7 +107,7 @@ public class TestPageAssertions extends TestBase {
});
assertEquals("foo", e.getExpected().getValue());
assertEquals("Woof-Woof", e.getActual().getValue());
assertTrue(e.getMessage().contains("Page title expected to be: foo\nReceived: Woof-Woof"), e.getMessage());
assertTrue(e.getMessage().contains("Page title expected to be\nExpected: foo\nReceived: Woof-Woof"), e.getMessage());
}
@Test
@@ -124,7 +124,7 @@ public class TestPageAssertions extends TestBase {
});
assertEquals("^foo[AB]", e.getExpected().getStringRepresentation());
assertEquals("Woof-Woof", e.getActual().getValue());
assertTrue(e.getMessage().contains("Page title expected to match regex: ^foo[AB]\nReceived: Woof-Woof"), e.getMessage());
assertTrue(e.getMessage().contains("Page title expected to match regex\nExpected: ^foo[AB]\nReceived: Woof-Woof"), e.getMessage());
}
@Test
@@ -356,4 +356,10 @@ public class TestPageBasic extends TestBase {
assertTrue(e.getMessage().contains("Can't add a null listener"));
}
@Test
void pagePauseShouldNotThrow() {
page.pause();
}
}
@@ -124,4 +124,94 @@ public class TestPageDrag extends TestBase {
page.locator("#source").dragTo(page.locator("#target"));
assertEquals(true, page.evalOnSelector("#target", "target => target.contains(document.querySelector('#source'))"));
}
@Test
void shouldDragAndDropWithTweenedMouseMovement() {
page.setContent(
"<body style='margin: 0; padding: 0;'>\n" +
" <div style='width:100px;height:100px;background:red;' id='red'></div>\n" +
" <div style='width:300px;height:100px;background:blue;' id='blue'></div>\n" +
"</body>"
);
JSHandle eventsHandle = page.evaluateHandle("() => {\n" +
" const events = [];\n" +
" document.addEventListener('mousedown', event => {\n" +
" events.push({ type: 'mousedown', x: event.pageX, y: event.pageY });\n" +
" });\n" +
" document.addEventListener('mouseup', event => {\n" +
" events.push({ type: 'mouseup', x: event.pageX, y: event.pageY });\n" +
" });\n" +
" document.addEventListener('mousemove', event => {\n" +
" events.push({ type: 'mousemove', x: event.pageX, y: event.pageY });\n" +
" });\n" +
" return events;\n" +
"}");
// Red div center is at (50, 50), blue div center is at (150, 50)
// With 4 steps, we expect intermediate positions at (75, 50), (100, 50), (125, 50)
page.dragAndDrop("#red", "#blue", new Page.DragAndDropOptions().setSteps(4));
Object json = eventsHandle.jsonValue();
// Expected sequence: mousemove to (50,50), mousedown at (50,50),
// then 3 mousemove events at (75,50), (100,50), (125,50),
// and mouseup at (150,50)
assertJsonEquals(
"[" +
"{type: \"mousemove\", x: 50, y: 50}," +
"{type: \"mousedown\", x: 50, y: 50}," +
"{type: \"mousemove\", x: 75, y: 75}," +
"{type: \"mousemove\", x: 100, y: 100}," +
"{type: \"mousemove\", x: 125, y: 125}," +
"{type: \"mousemove\", x: 150, y: 150}," +
"{type: \"mouseup\", x: 150, y: 150}" +
"]",
json
);
}
@Test
void shouldDragToWithTweenedMouseMovement() {
page.setContent(
"<body style='margin: 0; padding: 0;'>\n" +
" <div style='width:100px;height:100px;background:red;' id='red'></div>\n" +
" <div style='width:300px;height:100px;background:blue;' id='blue'></div>\n" +
"</body>"
);
JSHandle eventsHandle = page.evaluateHandle("() => {\n" +
" const events = [];\n" +
" document.addEventListener('mousedown', event => {\n" +
" events.push({ type: 'mousedown', x: event.pageX, y: event.pageY });\n" +
" });\n" +
" document.addEventListener('mouseup', event => {\n" +
" events.push({ type: 'mouseup', x: event.pageX, y: event.pageY });\n" +
" });\n" +
" document.addEventListener('mousemove', event => {\n" +
" events.push({ type: 'mousemove', x: event.pageX, y: event.pageY });\n" +
" });\n" +
" return events;\n" +
"}");
// Red div center is at (50, 50), blue div center is at (150, 50)
// With 4 steps, we expect intermediate positions at (75, 50), (100, 50), (125, 50)
page.locator("#red").dragTo(page.locator("#blue"), new Locator.DragToOptions().setSteps(4));
Object json = eventsHandle.jsonValue();
// Expected sequence: mousemove to (50,50), mousedown at (50,50),
// then 3 mousemove events at (75,50), (100,50), (125,50),
// and mouseup at (150,50)
assertJsonEquals(
"[" +
"{type: \"mousemove\", x: 50, y: 50}," +
"{type: \"mousedown\", x: 50, y: 50}," +
"{type: \"mousemove\", x: 75, y: 75}," +
"{type: \"mousemove\", x: 100, y: 100}," +
"{type: \"mousemove\", x: 125, y: 125}," +
"{type: \"mousemove\", x: 150, y: 150}," +
"{type: \"mouseup\", x: 150, y: 150}" +
"]",
json
);
}
}
@@ -16,6 +16,7 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.ConsoleMessagesFilter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
@@ -26,8 +27,7 @@ import static com.microsoft.playwright.Utils.getOS;
import static com.microsoft.playwright.Utils.mapOf;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
public class TestPageEventConsole extends TestBase {
@Test
@@ -103,7 +103,7 @@ public class TestPageEventConsole extends TestBase {
ConsoleMessage message = page.waitForConsoleMessage(() -> {
page.evaluate("async url => fetch(url).catch(e => {})", server.EMPTY_PAGE);
});
assertTrue(message.text().contains("Access-Control-Allow-Origin"));
assertTrue(message.text().contains("Access-Control-Allow-Origin") || message.text().contains("blocked by CORS policy"), message.text());
assertEquals("error", message.type());
}
@@ -130,4 +130,79 @@ public class TestPageEventConsole extends TestBase {
assertEquals("2", message.text());
assertEquals("info", message.type());
}
@Test
void consoleMessagesShouldWork() {
page.evaluate("() => {\n" +
" for (let i = 0; i < 301; i++)\n" +
" console.log('message' + i);\n" +
"}");
List<ConsoleMessage> messages = page.consoleMessages();
assertTrue(messages.size() >= 100, "should be at least 100 messages");
int firstIndex = messages.size() - 100;
for (int i = 0; i < 100; i++) {
ConsoleMessage message = messages.get(firstIndex + i);
assertEquals("message" + (201 + i), message.text());
assertEquals("log", message.type());
assertEquals(page, message.page());
}
}
@Test
void shouldHaveTimestamp() {
double before = (double) System.currentTimeMillis() - 1;
ConsoleMessage message = page.waitForConsoleMessage(
() -> page.evaluate("() => console.log('timestamp test')"));
double after = (double) System.currentTimeMillis() + 1;
assertTrue(message.timestamp() >= before,
"timestamp " + message.timestamp() + " should be >= " + before);
assertTrue(message.timestamp() <= after,
"timestamp " + message.timestamp() + " should be <= " + after);
}
@Test
void shouldHaveIncreasingTimestamps() {
List<ConsoleMessage> messages = new ArrayList<>();
page.onConsoleMessage(messages::add);
page.evaluate("() => { console.log('first'); console.log('second'); console.log('third'); }");
assertEquals(3, messages.size());
for (int i = 1; i < messages.size(); i++)
assertTrue(messages.get(i).timestamp() >= messages.get(i - 1).timestamp());
}
@Test
void clearConsoleMessagesShouldWork() {
page.evaluate("() => { console.log('message1'); console.log('message2'); }");
List<ConsoleMessage> messages = page.consoleMessages();
assertTrue(messages.stream().anyMatch(m -> "message1".equals(m.text())));
assertTrue(messages.stream().anyMatch(m -> "message2".equals(m.text())));
page.clearConsoleMessages();
messages = page.consoleMessages();
assertEquals(0, messages.size());
page.waitForConsoleMessage(() -> page.evaluate("() => console.log('message3')"));
messages = page.consoleMessages();
assertEquals(1, messages.size());
assertEquals("message3", messages.get(0).text());
}
@Test
void consoleMessagesSinceNavigationFilterShouldWork() {
page.evaluate("() => console.log('before navigation')");
page.navigate(server.EMPTY_PAGE);
page.evaluate("() => console.log('after navigation')");
List<ConsoleMessage> all = page.consoleMessages(
new Page.ConsoleMessagesOptions().setFilter(ConsoleMessagesFilter.ALL));
assertTrue(all.stream().anyMatch(m -> "before navigation".equals(m.text())));
assertTrue(all.stream().anyMatch(m -> "after navigation".equals(m.text())));
// sinceNavigation is the default
List<ConsoleMessage> sinceNav = page.consoleMessages();
assertFalse(sinceNav.stream().anyMatch(m -> "before navigation".equals(m.text())));
assertTrue(sinceNav.stream().anyMatch(m -> "after navigation".equals(m.text())));
}
}

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