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

Compare commits

...

61 Commits

Author SHA1 Message Date
Gerard Mista 59a8490540 Artifact should have same version as release. (#974) 2022-07-01 14:21:42 -07:00
Yury Semikhatsky a127bfefb3 cherry-pick(#978,#984): recent test fixes (#985) 2022-06-30 14:47:57 -07:00
Yury Semikhatsky 06082438fe cherry-pick(#982): feat: roll driver to 1.23.1-beta, implement routeFromHar.update (#983) 2022-06-30 13:17:18 -07:00
Yury Semikhatsky b8bd59a55c chore: mark 1.23.0 (#971) 2022-06-28 09:14:00 -07:00
Yury Semikhatsky e42d7bdc9a cherry-pick(#969): feat: support ignoreCase option (#970) 2022-06-28 08:50:34 -07:00
Yury Semikhatsky 3f60144e0f chore: update driver to 1.23.0 (#968) 2022-06-27 15:33:44 -07:00
Yury Semikhatsky 8004e5d0ff test: 403 response still has postBody (#967) 2022-06-27 14:13:40 -07:00
Yury Semikhatsky 3604aab710 chore: store LocalUtils on Connection (#963) 2022-06-24 16:25:58 -07:00
Yury Semikhatsky 2fdb89c94e fix: match against updated url (#962) 2022-06-24 15:13:41 -07:00
Yury Semikhatsky 4fee61a655 test: unflake TestHar.shouldAttachContent (#961) 2022-06-24 14:50:24 -07:00
Yury Semikhatsky efb281e016 feat: implement routeFromHAR (#960) 2022-06-24 13:44:57 -07:00
Yury Semikhatsky fdec32c650 chore: simplify handler result (#959) 2022-06-23 18:59:04 -07:00
Yury Semikhatsky 7e285ffe44 feat: route.fallback with overrides (#958) 2022-06-23 16:48:32 -07:00
Yury Semikhatsky edf0e45fb4 feat: roll to 1.23.0-beta-1655926399000 (#956) 2022-06-23 10:11:38 -07:00
Yury Semikhatsky c8eb4f9eeb feat: route chaining (#950) 2022-06-11 11:24:30 -07:00
dependabot[bot] e4ec9b8dbe chore(deps): bump gson from 2.8.6 to 2.8.9 in /tools/api-generator (#937) 2022-06-11 09:37:35 -07:00
dependabot[bot] ef13ab86b8 chore(deps): bump gson from 2.8.6 to 2.8.9 in /tools/test-local-installation (#938) 2022-06-11 09:37:09 -07:00
dependabot[bot] a48fef6b01 chore(deps): bump gson from 2.8.6 to 2.8.9 (#936) 2022-06-11 09:36:35 -07:00
Yury Semikhatsky 1c1f3d43ac docs: update docs link (#945) 2022-06-06 09:42:53 -07:00
Max Schmitt 8f59cd73f5 chore: fix examples which could not be executed (#941) 2022-05-31 09:33:33 +02:00
Yury Semikhatsky e04ef2132c test: make install test not skip browser downloads (#931) 2022-05-13 15:31:37 -07:00
Yury Semikhatsky 34d23a833e devops: bump version to 1.23.0-SNAPSHOT (#930) 2022-05-13 11:14:15 -07:00
Yury Semikhatsky 15eefc54af feat: roll beta driver, remove layout selectors (#926) 2022-05-13 00:49:59 -07:00
Yury Semikhatsky abf245ccc7 feat: implement Locator.filter (#925) 2022-05-12 09:39:10 -07:00
Gabriel Gavrilov 10592ce5c7 Added Selectors and Keyboard Manipulation Example (#920) 2022-05-11 09:20:51 -07:00
Yury Semikhatsky ef7f50c48a feat: roll driver, implement locator filters (#921) 2022-05-10 10:43:30 -07:00
Andrey Lushnikov 473b1ce794 devops: mark docker image as playwright official (#889) 2022-05-06 19:07:44 -07:00
Yury Semikhatsky 98ecb7e0a0 test: unflake TestInstall.shouldThrowWhenBrowserPathIsInvalid (#916) 2022-05-03 13:50:20 -07:00
Yury Semikhatsky 04eb228813 devops: add workflow triggering internal tests (#915) 2022-05-02 18:01:40 -07:00
Max Schmitt 298e01ee80 chore: add arm64 Docker image (#890) 2022-05-02 14:46:05 +01:00
Yury Semikhatsky b6b54af13c Revert "Revert "Add Linux/arm64 support (#883)" (#892)" (#905)
This reverts commit 536af6b3d8.
2022-04-27 17:24:12 -07:00
Yury Semikhatsky b8d2ccae08 fix: respect isChecked options (#911) 2022-04-26 17:01:50 -07:00
Yury Semikhatsky 59e7c0cc94 fix: convert file path to absolute in setInputFiles (#903) 2022-04-15 12:11:52 -07:00
Yury Semikhatsky 54d0366b9e fix: get docker version from pom.xml (#899) (#901) 2022-04-12 12:15:06 -07:00
Andrey Lushnikov 9845a05544 devops: fix docker publish workflow (#898) 2022-04-12 11:40:21 -07:00
Yury Semikhatsky 41fd9a6f75 chore: bump dev version to 1.22 (#897) 2022-04-12 10:46:31 -07:00
Yury Semikhatsky 483cf0d473 chore: revert some inadvertent changes (#894) 2022-04-12 08:55:05 -07:00
Yury Semikhatsky 1681c410dd feat: large file uploads (#891) 2022-04-12 08:33:40 -07:00
Yury Semikhatsky 9f6860539a fix: links to documentation (#893) 2022-04-11 19:20:29 -07:00
Yury Semikhatsky 536af6b3d8 Revert "Add Linux/arm64 support (#883)" (#892) 2022-04-11 17:25:13 -07:00
Yury Semikhatsky 8ce193d144 chore: roll to 1.21 beta driver (#888) 2022-04-11 13:04:57 -07:00
Michael S. Fischer 7eddd2d2b2 Add Linux/arm64 support (#883) 2022-04-06 18:42:39 -07:00
uchagani 447578c582 fix(driver): set driver instance to null if an exception is thrown du… (#879) 2022-04-04 12:25:37 -07:00
Leonard Brünings 58013adfac fix(tracing): support explicit StartOptions().setSources(false) (#866) 2022-03-30 10:42:54 -07:00
Alex (Huy Tran) 43d12a7662 feat: enable loading alternative driver via system properties (#873) 2022-03-30 10:42:18 -07:00
Yury Semikhatsky 1deccbb55d chore: add logging to driver installation (#865) 2022-03-25 12:02:47 -07:00
Yury Semikhatsky 1b2d33402e docs: add driver update instructions 2022-03-18 09:00:34 -07:00
Yury Semikhatsky d315e7b5bf fix: docker publishing (#851) (#852) 2022-03-14 20:31:16 -07:00
Yury Semikhatsky 7dc22aa08a devops: do not cache mvn packages (#848) 2022-03-14 19:01:28 -07:00
Yury Semikhatsky 5b0ef8b7bf chore: update current version to 1.21.0-SNAPSHOT (#847) 2022-03-14 17:16:37 -07:00
Yury Semikhatsky 43ba37817b fix: send x-playwright-browser (#844) 2022-03-14 15:31:19 -07:00
Yury Semikhatsky 4916ba22af chore: roll driver (#838) 2022-03-14 12:04:20 -07:00
uchagani 94f72694f1 chore: move chmod command to after browsers are installed and consolidate co… (#835)
Co-authored-by: Max Schmitt <max@schmitt.mx>
2022-03-09 18:22:00 +01:00
Max Schmitt be167a161b devops: trigger Docker tests on Dockerfile changes (#839) 2022-03-09 09:08:36 -08:00
Yury Semikhatsky 187d2ad6c6 devops: restore test docker trigger (#840) 2022-03-09 09:07:07 -08:00
Max Schmitt 68a7dbc1e3 devops: publish no arm Docker for now (#837) 2022-03-08 22:01:54 +01:00
Yury Semikhatsky 1153d473fb feat: roll driver to 3/3/22 (#832) 2022-03-04 08:31:13 -08:00
Max Schmitt 00d53bd1ea devops: fix Docker publishing 2 2022-03-04 00:05:14 +01:00
Max Schmitt d423733d9d devops: fix Docker publishing 2022-03-04 00:00:16 +01:00
Max Schmitt e127abb68e chore: align Docker with upstream Docker infra (#831) 2022-03-03 22:55:21 +01:00
Yury Semikhatsky 5ee8f23380 fix(docs): drop trailing slash from link path (#818) 2022-02-17 15:26:10 -08:00
129 changed files with 7047 additions and 1270 deletions
+6 -7
View File
@@ -17,10 +17,9 @@ jobs:
username: playwright
password: ${{ secrets.DOCKER_PASSWORD }}
- uses: actions/checkout@v2
- name: Build Docker image
run: docker build -t playwright-java:localbuild-focal -f Dockerfile.focal .
- name: tag & publish
run: |
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:next
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:next-focal
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:sha-${{ github.sha }}
- name: Set up Docker QEMU for arm64 docker builds
uses: docker/setup-qemu-action@v1
with:
platforms: arm64
- name: publish docker canary
run: ./utils/docker/publish_docker.sh canary
+13 -16
View File
@@ -3,6 +3,11 @@ on:
release:
types: [published]
workflow_dispatch:
inputs:
is_release:
required: true
type: boolean
description: "Is this a release image?"
branches:
- release-*
jobs:
@@ -17,20 +22,12 @@ jobs:
login-server: playwright.azurecr.io
username: playwright
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up Docker QEMU for arm64 docker builds
uses: docker/setup-qemu-action@v1
with:
platforms: arm64
- uses: actions/checkout@v2
- name: Build Docker image
run: docker build -t playwright-java:localbuild-focal -f Dockerfile.focal .
- name: tag & publish
run: |
# GITHUB_REF has a form of `refs/tags/v1.3.0`.
# TAG_NAME would be `v1.3.0`
TAG_NAME=${GITHUB_REF#refs/tags/}
if [[ ! "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]];
then
echo "Wrong TAG_NAME format: $TAG_NAME"
exit 1
fi
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:latest
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:focal
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:${TAG_NAME}
./scripts/tag_image_and_push.sh playwright-java:localbuild-focal playwright.azurecr.io/public/playwright/java:${TAG_NAME}-focal
- run: ./utils/docker/publish_docker.sh stable
if: (github.event_name != 'workflow_dispatch' && !github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && github.event.inputs.is_release == 'true')
- run: ./utils/docker/publish_docker.sh canary
if: (github.event_name != 'workflow_dispatch' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && github.event.inputs.is_release != 'true')
+9 -29
View File
@@ -19,23 +19,17 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1.5.0
- uses: microsoft/playwright-github-action@v1
- name: Set up JDK 1.8
uses: actions/setup-java@v2
with:
distribution: zulu
java-version: 8
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Download drivers
shell: bash
run: scripts/download_driver_for_all_platforms.sh
- name: Build with Maven
run: mvn -B package -D skipTests --no-transfer-progress
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress --fail-at-end
env:
@@ -50,7 +44,6 @@ jobs:
env:
BROWSER: ${{ matrix.browser }}
run: |
mvn -B install -D skipTests --no-transfer-progress
cd tools/test-spring-boot-starter
mvn package -D skipTests --no-transfer-progress
java -jar target/test-spring-boot*.jar
@@ -70,7 +63,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1.5.0
- uses: microsoft/playwright-github-action@v1
- name: Install Media Pack
if: matrix.os == 'windows-latest'
shell: powershell
@@ -80,17 +73,11 @@ jobs:
with:
distribution: zulu
java-version: 8
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Download drivers
shell: bash
run: scripts/download_driver_for_all_platforms.sh
- name: Build with Maven
run: mvn -B package -D skipTests --no-transfer-progress
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress --fail-at-end
env:
@@ -106,23 +93,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1.5.0
- uses: microsoft/playwright-github-action@v1
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
distribution: adopt
java-version: 17
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: m2-${{ hashFiles('**/pom.xml') }}
restore-keys: m2
- name: Download drivers
shell: bash
run: scripts/download_driver_for_all_platforms.sh
- name: Build with Maven
run: mvn -B package -D skipTests --no-transfer-progress
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress --fail-at-end
env:
@@ -132,7 +113,6 @@ jobs:
env:
BROWSER: ${{ matrix.browser }}
run: |
mvn -B install -D skipTests --no-transfer-progress
cd tools/test-spring-boot-starter
mvn package -D skipTests --no-transfer-progress
java -jar target/test-spring-boot*.jar
+3 -3
View File
@@ -3,14 +3,14 @@ on:
push:
paths:
- '.github/workflows/test_docker.yml'
- 'Dockerfile*'
- '**/Dockerfile*'
branches:
- main
- release-*
pull_request:
paths:
- .github/workflows/test_docker.yml
- Dockerfile.*
- '**/Dockerfile*'
- scripts/CLI_VERSION
- '**/pom.xml'
branches:
@@ -23,7 +23,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Build Docker image
run: docker build -t playwright-java:localbuild-focal -f Dockerfile.focal .
run: bash utils/docker/build.sh --amd64 focal playwright-java:localbuild-focal
- name: Test
run: |
CONTAINER_ID="$(docker run --rm --ipc=host -v $(pwd):/root/playwright --name playwright-docker-test -d -t playwright-java:localbuild-focal /bin/bash)"
@@ -0,0 +1,21 @@
name: "Internal Tests"
on:
push:
branches:
- main
- release-*
jobs:
trigger:
name: "trigger"
runs-on: ubuntu-20.04
steps:
- run: |
curl -X POST \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${GH_TOKEN}" \
--data "{\"event_type\": \"playwright_tests_java\", \"client_payload\": {\"ref\": \"${GITHUB_SHA}\"}}" \
https://api.github.com/repos/microsoft/playwright-internal/dispatches
env:
GH_TOKEN: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
+1 -7
View File
@@ -20,13 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1.5.0
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- uses: microsoft/playwright-github-action@v1
- name: Download drivers
run: scripts/download_driver_for_all_platforms.sh
- name: Regenerate APIs
+14 -1
View File
@@ -8,7 +8,7 @@ Install git, Java JDK (version >= 8), Maven (tested with version 3.6.3), on Ubun
just run the following command:
```sh
sudo apt-get install git openjdk-11-jdk maven
sudo apt-get install git openjdk-11-jdk maven unzip
```
### Getting the Code
@@ -49,6 +49,19 @@ Java interfaces for the current driver run the following commands:
./scripts/generate_api.sh
```
#### Updating driver version
Driver version is read from [scripts/CLI_VERSION](https://github.com/microsoft/playwright-java/blob/main/scripts/CLI_VERSION) and can be found in the upstream [GHA build](https://github.com/microsoft/playwright/actions/workflows/publish_canary.yml) logs. To update the driver to a particular version run the following commands:
```bash
cat > scripts/CLI_VERSION
<paste new version>
^D
./scripts/download_driver_for_all_platforms.sh -f
./scripts/generate_api.sh
./scripts/update_readme.sh
```
### Code Style
- We try to follow [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)
-32
View File
@@ -1,32 +0,0 @@
FROM ubuntu:focal
# === INSTALL JDK and Maven ===
RUN apt-get update && apt-get install -y --no-install-recommends \
openjdk-11-jdk maven
ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
# Install utilities required for downloading driver
RUN apt-get update && apt-get install -y --no-install-recommends \
curl unzip
# === INSTALL playwright maven modules & browsers ===
# Browsers will remain downloaded in `/ms-playwright`.
# Note: make sure to set 777 to the registry so that any user can access
# registry.
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN mkdir /ms-playwright && chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH
RUN mkdir /tmp/pw-java
COPY . /tmp/pw-java
RUN cd /tmp/pw-java && \
./scripts/download_driver_for_all_platforms.sh && \
mvn install -D skipTests --no-transfer-progress && \
DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="install" -f playwright/pom.xml --no-transfer-progress && \
rm -rf /tmp/pw-java
+4 -4
View File
@@ -11,9 +11,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->100.0.4863.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->104.0.5112.20<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->15.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->96.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->100.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all the browsers on all platforms. Check out [system requirements](https://playwright.dev/java/docs/next/intro/#system-requirements) for details.
@@ -43,7 +43,7 @@ To run Playwright simply add following dependency to your Maven project:
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.17.0</version>
<version>1.23.0</version>
</dependency>
```
@@ -173,7 +173,7 @@ public class InterceptNetworkRequests {
## Documentation
Check out our [new documentation site](https://playwright.dev/java)!.
Check out our official [documentation site](https://playwright.dev/java).
You can also browse [javadoc online](https://www.javadoc.io/doc/com.microsoft.playwright/playwright/latest/index.html).
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.19.0-SNAPSHOT</version>
<version>1.23.0</version>
</parent>
<artifactId>driver-bundle</artifactId>
@@ -37,11 +37,13 @@ public class DriverJar extends Driver {
? Files.createTempDirectory(prefix)
: Files.createTempDirectory(Paths.get(alternativeTmpdir), prefix);
driverTempDir.toFile().deleteOnExit();
logMessage("created DriverJar: " + driverTempDir);
}
@Override
protected void initialize(Map<String, String> env, Boolean installBrowsers) throws Exception {
extractDriverToTempDir();
logMessage("extracted driver from jar to " + driverPath());
if (installBrowsers)
installBrowsers(env);
}
@@ -137,11 +139,17 @@ public class DriverJar extends Driver {
private static String platformDir() {
String name = System.getProperty("os.name").toLowerCase();
String arch = System.getProperty("os.arch").toLowerCase();
if (name.contains("windows")) {
return "win32_x64";
}
if (name.contains("linux")) {
return "linux";
if (arch.equals("aarch64")) {
return "linux-arm64";
} else {
return "linux";
}
}
if (name.contains("mac os x")) {
return "mac";
@@ -18,24 +18,74 @@ package com.microsoft.playwright;
import com.microsoft.playwright.impl.Driver;
import com.microsoft.playwright.impl.DriverJar;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
public class TestInstall {
private static boolean isPortAvailable(int port) {
try (ServerSocket ignored = new ServerSocket(port)) {
return true;
} catch (IOException ignored) {
return false;
}
}
private static int unusedPort() {
for (int i = 10000; i < 11000; i++) {
if (isPortAvailable(i)) {
return i;
}
}
throw new RuntimeException("Cannot find unused local port");
}
@BeforeEach
void clearSystemProperties() {
// Clear system property to ensure that the driver is loaded from jar.
System.clearProperty("playwright.cli.dir");
System.clearProperty("playwright.driver.tmpdir");
// Clear system property to ensure that the default driver is loaded.
System.clearProperty("playwright.driver.impl");
}
@Test
void shouldThrowWhenBrowserPathIsInvalid(@TempDir Path tmpDir) throws MalformedURLException, ClassNotFoundException, NoSuchMethodException, NoSuchFieldException, IllegalAccessException {
Map<String,String> env = new HashMap<>();
// On macOS we can only use 127.0.0.1, so pick unused port instead.
// https://superuser.com/questions/458875/how-do-you-get-loopback-addresses-other-than-127-0-0-1-to-work-on-os-x
env.put("PLAYWRIGHT_DOWNLOAD_HOST", "https://127.0.0.1:" + unusedPort());
// Make sure the browsers are not installed yet by pointing at an empty dir.
env.put("PLAYWRIGHT_BROWSERS_PATH", tmpDir.toString());
env.put("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "false");
// Reset instance field value to null for the test.
Field field = Driver.class.getDeclaredField("instance");
field.setAccessible(true);
Object value = field.get(Driver.class);
field.set(Driver.class, null);
for (int i = 0; i < 2; i++){
RuntimeException exception = assertThrows(RuntimeException.class, () -> Driver.ensureDriverInstalled(env, true));
String message = exception.getMessage();
assertTrue(message.contains("Failed to create driver"), message);
}
field.set(Driver.class, value);
}
@Test
@@ -57,4 +107,19 @@ public class TestInstall {
DriverJar driver = new DriverJar();
assertTrue(driver.driverPath().startsWith(tmpdir), "Driver path: " + driver.driverPath() + " tmp: " + tmpdir);
}
@Test
void playwrightDriverDefaultImpl() {
assertDoesNotThrow(() -> Driver.ensureDriverInstalled(Collections.emptyMap(), false));
}
@Test
void playwrightDriverAlternativeImpl() {
System.setProperty("playwright.driver.impl", "com.microsoft.playwright.impl.AlternativeDriver");
RuntimeException thrown =
assertThrows(
RuntimeException.class,
() -> Driver.ensureDriverInstalled(Collections.emptyMap(), false));
assertEquals("Failed to create driver", thrown.getMessage());
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.19.0-SNAPSHOT</version>
<version>1.23.0</version>
</parent>
<artifactId>driver</artifactId>
@@ -18,8 +18,11 @@ package com.microsoft.playwright.impl;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.ZonedDateTime;
import java.util.Map;
import static com.microsoft.playwright.impl.DriverLogging.logWithTimestamp;
/**
* This class provides access to playwright-cli. It can be either preinstalled
* in the host system and its path is passed as a system property or it can be
@@ -31,6 +34,7 @@ public abstract class Driver {
private static class PreinstalledDriver extends Driver {
private final Path driverDir;
PreinstalledDriver(Path driverDir) {
logMessage("created PreinstalledDriver: " + driverDir);
this.driverDir = driverDir;
}
@@ -49,8 +53,11 @@ public abstract class Driver {
if (instance == null) {
try {
instance = createDriver();
logMessage("initializing driver");
instance.initialize(env, installBrowsers);
logMessage("driver initialized.");
} catch (Exception exception) {
instance = null;
throw new RuntimeException("Failed to create driver", exception);
}
}
@@ -92,9 +99,16 @@ public abstract class Driver {
return new PreinstalledDriver(Paths.get(pathFromProperty));
}
Class<?> jarDriver = Class.forName("com.microsoft.playwright.impl.DriverJar");
String driverImpl =
System.getProperty("playwright.driver.impl", "com.microsoft.playwright.impl.DriverJar");
Class<?> jarDriver = Class.forName(driverImpl);
return (Driver) jarDriver.getDeclaredConstructor().newInstance();
}
abstract Path driverDir();
protected static void logMessage(String message) {
// This matches log format produced by the server.
logWithTimestamp("pw:install " + message);
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
class DriverLogging {
private static final boolean isEnabled;
static {
String debug = System.getenv("DEBUG");
isEnabled = (debug != null) && debug.contains("pw:install");
}
private static final DateTimeFormatter timestampFormat = DateTimeFormatter.ofPattern(
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX").withZone(ZoneId.of("UTC"));
static void logWithTimestamp(String message) {
if (!isEnabled) {
return;
}
// This matches log format produced by the server.
String timestamp = ZonedDateTime.now().format(timestampFormat);
System.err.println(timestamp + " " + message);
}
}
+2 -2
View File
@@ -6,7 +6,7 @@
<groupId>org.example</groupId>
<artifactId>examples</artifactId>
<version>1.19.0-SNAPSHOT</version>
<version>1.23.0</version>
<name>Playwright Client Examples</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -15,7 +15,7 @@
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.17.0</version>
<version>1.23.0</version>
</dependency>
</dependencies>
<build>
@@ -0,0 +1,36 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.example;
import java.nio.file.Paths;
import com.microsoft.playwright.*;
public class SelectorsAndKeyboardManipulation {
public static void main(String[] args) {
try(Playwright playwright = Playwright.create()) {
Browser browser = playwright.firefox().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://playwright.dev/java/");
page.locator("text=SearchK").click();
page.locator("[placeholder=\"Search docs\"]").fill("getting started");
page.locator("div[role=\"button\"]:has-text(\"CancelIntroductionGetting startedInstallationGetting startedUsageGetting start\")").click();
page.waitForSelector("h1:has-text(\"Getting started\")"); // Waits for the new page to load before screenshotting.
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("Screenshot.png")));
}
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.19.0-SNAPSHOT</version>
<version>1.23.0</version>
</parent>
<artifactId>playwright</artifactId>
@@ -21,9 +21,9 @@ import java.nio.file.Path;
import java.util.*;
/**
* Exposes API that can be used for the Web API testing. Each Playwright browser context has a APIRequestContext instance
* attached which shares cookies with the page context. Its also possible to create a new APIRequestContext instance
* manually. For more information see <a href="https://playwright.dev/java/docs/class-apirequestcontext">here</a>.
* Exposes API that can be used for the Web API testing. This class is used for creating {@code APIRequestContext} instance which
* in turn can be used for sending web requests. An instance of this class can be obtained via {@link Playwright#request
* Playwright.request()}. For more information see {@code APIRequestContext}.
*/
public interface APIRequest {
class NewContextOptions {
@@ -21,9 +21,24 @@ import java.nio.file.Path;
/**
* This API is used for the Web API testing. You can use it to trigger API endpoints, configure micro-services, prepare
* environment or the service to your e2e test. When used on {@code Page} or a {@code BrowserContext}, this API will automatically use
* the cookies from the corresponding {@code BrowserContext}. This means that if you log in using this API, your e2e test will be
* logged in and vice versa.
* environment or the service to your e2e test.
*
* <p> Each Playwright browser context has associated with it {@code APIRequestContext} instance which shares cookie storage with the
* browser context and can be accessed via {@link BrowserContext#request BrowserContext.request()} or {@link Page#request
* Page.request()}. It is also possible to create a new APIRequestContext instance manually by calling {@link
* APIRequest#newContext APIRequest.newContext()}.
*
* <p> **Cookie management**
*
* <p> {@code APIRequestContext} retuned by {@link BrowserContext#request BrowserContext.request()} and {@link Page#request
* Page.request()} shares cookie storage with the corresponding {@code BrowserContext}. Each API request will have {@code Cookie}
* header populated with the values from the browser context. If the API response contains {@code Set-Cookie} header it will
* automatically update {@code BrowserContext} cookies and requests made from the page will pick them up. This means that if you
* log in using this API, your e2e test will be logged in and vice versa.
*
* <p> If you want API requests to not interfere with the browser cookies you shoud create a new {@code APIRequestContext} by calling
* {@link APIRequest#newContext APIRequest.newContext()}. Such {@code APIRequestContext} object will have its own isolated cookie
* storage.
*/
public interface APIRequestContext {
class StorageStateOptions {
@@ -20,6 +20,7 @@ import com.microsoft.playwright.options.*;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.regex.Pattern;
/**
* A Browser is created via {@link BrowserType#launch BrowserType.launch()}. An example of using a {@code Browser} to create a
@@ -143,6 +144,17 @@ public interface Browser extends AutoCloseable {
* 'http://per-context' } })}.
*/
public Proxy proxy;
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
*/
public HarContentPolicy recordHarContent;
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
*/
public HarMode recordHarMode;
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
@@ -153,6 +165,7 @@ public interface Browser extends AutoCloseable {
* BrowserContext.close()} for the HAR to be saved.
*/
public Path recordHarPath;
public Object recordHarUrlFilter;
/**
* Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure
* to call {@link BrowserContext#close BrowserContext.close()} for videos to be saved.
@@ -174,6 +187,15 @@ public interface Browser extends AutoCloseable {
* is set.
*/
public ScreenSize screenSize;
/**
* Whether to allow sites to register Service workers. Defaults to {@code "allow"}.
* <ul>
* <li> {@code "allow"}: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a> can be
* registered.</li>
* <li> {@code "block"}: Playwright will block all registration of Service Workers.</li>
* </ul>
*/
public ServiceWorkerPolicy serviceWorkers;
/**
* Populates context with given storage state. This option can be used to initialize context with logged-in information
* obtained via {@link BrowserContext#storageState BrowserContext.storageState()}.
@@ -186,7 +208,7 @@ public interface Browser extends AutoCloseable {
*/
public Path storageStatePath;
/**
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* that imply single target DOM element will throw when more than one element matches the selector. See {@code Locator} to learn
* more about the strict mode.
*/
@@ -363,6 +385,23 @@ public interface Browser extends AutoCloseable {
this.proxy = proxy;
return this;
}
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
*/
public NewContextOptions setRecordHarContent(HarContentPolicy recordHarContent) {
this.recordHarContent = recordHarContent;
return this;
}
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
*/
public NewContextOptions setRecordHarMode(HarMode recordHarMode) {
this.recordHarMode = recordHarMode;
return this;
}
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
@@ -379,6 +418,14 @@ public interface Browser extends AutoCloseable {
this.recordHarPath = recordHarPath;
return this;
}
public NewContextOptions setRecordHarUrlFilter(String recordHarUrlFilter) {
this.recordHarUrlFilter = recordHarUrlFilter;
return this;
}
public NewContextOptions setRecordHarUrlFilter(Pattern recordHarUrlFilter) {
this.recordHarUrlFilter = recordHarUrlFilter;
return this;
}
/**
* Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure
* to call {@link BrowserContext#close BrowserContext.close()} for videos to be saved.
@@ -427,6 +474,18 @@ public interface Browser extends AutoCloseable {
this.screenSize = screenSize;
return this;
}
/**
* Whether to allow sites to register Service workers. Defaults to {@code "allow"}.
* <ul>
* <li> {@code "allow"}: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a> can be
* registered.</li>
* <li> {@code "block"}: Playwright will block all registration of Service Workers.</li>
* </ul>
*/
public NewContextOptions setServiceWorkers(ServiceWorkerPolicy serviceWorkers) {
this.serviceWorkers = serviceWorkers;
return this;
}
/**
* Populates context with given storage state. This option can be used to initialize context with logged-in information
* obtained via {@link BrowserContext#storageState BrowserContext.storageState()}.
@@ -445,7 +504,7 @@ public interface Browser extends AutoCloseable {
return this;
}
/**
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* that imply single target DOM element will throw when more than one element matches the selector. See {@code Locator} to learn
* more about the strict mode.
*/
@@ -571,6 +630,17 @@ public interface Browser extends AutoCloseable {
* 'http://per-context' } })}.
*/
public Proxy proxy;
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
*/
public HarContentPolicy recordHarContent;
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
*/
public HarMode recordHarMode;
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
@@ -581,6 +651,7 @@ public interface Browser extends AutoCloseable {
* BrowserContext.close()} for the HAR to be saved.
*/
public Path recordHarPath;
public Object recordHarUrlFilter;
/**
* Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure
* to call {@link BrowserContext#close BrowserContext.close()} for videos to be saved.
@@ -602,6 +673,15 @@ public interface Browser extends AutoCloseable {
* is set.
*/
public ScreenSize screenSize;
/**
* Whether to allow sites to register Service workers. Defaults to {@code "allow"}.
* <ul>
* <li> {@code "allow"}: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a> can be
* registered.</li>
* <li> {@code "block"}: Playwright will block all registration of Service Workers.</li>
* </ul>
*/
public ServiceWorkerPolicy serviceWorkers;
/**
* Populates context with given storage state. This option can be used to initialize context with logged-in information
* obtained via {@link BrowserContext#storageState BrowserContext.storageState()}.
@@ -614,7 +694,7 @@ public interface Browser extends AutoCloseable {
*/
public Path storageStatePath;
/**
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* that imply single target DOM element will throw when more than one element matches the selector. See {@code Locator} to learn
* more about the strict mode.
*/
@@ -791,6 +871,23 @@ public interface Browser extends AutoCloseable {
this.proxy = proxy;
return this;
}
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
*/
public NewPageOptions setRecordHarContent(HarContentPolicy recordHarContent) {
this.recordHarContent = recordHarContent;
return this;
}
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
*/
public NewPageOptions setRecordHarMode(HarMode recordHarMode) {
this.recordHarMode = recordHarMode;
return this;
}
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
@@ -807,6 +904,14 @@ public interface Browser extends AutoCloseable {
this.recordHarPath = recordHarPath;
return this;
}
public NewPageOptions setRecordHarUrlFilter(String recordHarUrlFilter) {
this.recordHarUrlFilter = recordHarUrlFilter;
return this;
}
public NewPageOptions setRecordHarUrlFilter(Pattern recordHarUrlFilter) {
this.recordHarUrlFilter = recordHarUrlFilter;
return this;
}
/**
* Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure
* to call {@link BrowserContext#close BrowserContext.close()} for videos to be saved.
@@ -855,6 +960,18 @@ public interface Browser extends AutoCloseable {
this.screenSize = screenSize;
return this;
}
/**
* Whether to allow sites to register Service workers. Defaults to {@code "allow"}.
* <ul>
* <li> {@code "allow"}: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a> can be
* registered.</li>
* <li> {@code "block"}: Playwright will block all registration of Service Workers.</li>
* </ul>
*/
public NewPageOptions setServiceWorkers(ServiceWorkerPolicy serviceWorkers) {
this.serviceWorkers = serviceWorkers;
return this;
}
/**
* Populates context with given storage state. This option can be used to initialize context with logged-in information
* obtained via {@link BrowserContext#storageState BrowserContext.storageState()}.
@@ -873,7 +990,7 @@ public interface Browser extends AutoCloseable {
return this;
}
/**
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* that imply single target DOM element will throw when more than one element matches the selector. See {@code Locator} to learn
* more about the strict mode.
*/
@@ -947,6 +1064,10 @@ public interface Browser extends AutoCloseable {
return this;
}
}
/**
* Get the browser type (chromium, firefox or webkit) that the browser belongs to.
*/
BrowserType browserType();
/**
* In case this browser is obtained using {@link BrowserType#launch BrowserType.launch()}, closes the browser and all of
* its pages (if any were opened).
@@ -1017,8 +1138,9 @@ public interface Browser extends AutoCloseable {
Page newPage(NewPageOptions options);
/**
* <strong>NOTE:</strong> This API controls <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">Chromium Tracing</a>
* which is a low-level chromium-specific debugging tool. API to control <a href="../trace-viewer">Playwright Tracing</a>
* could be found <a href="https://playwright.dev/java/docs/class-tracing">here</a>.
* which is a low-level chromium-specific debugging tool. API to control <a
* href="https://playwright.dev/java/docs/trace-viewer">Playwright Tracing</a> could be found <a
* href="https://playwright.dev/java/docs/api/class-tracing">here</a>.
*
* <p> You can use {@link Browser#startTracing Browser.startTracing()} and {@link Browser#stopTracing Browser.stopTracing()} to
* create a trace file that can be opened in Chrome DevTools performance panel.
@@ -1036,8 +1158,9 @@ public interface Browser extends AutoCloseable {
}
/**
* <strong>NOTE:</strong> This API controls <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">Chromium Tracing</a>
* which is a low-level chromium-specific debugging tool. API to control <a href="../trace-viewer">Playwright Tracing</a>
* could be found <a href="https://playwright.dev/java/docs/class-tracing">here</a>.
* which is a low-level chromium-specific debugging tool. API to control <a
* href="https://playwright.dev/java/docs/trace-viewer">Playwright Tracing</a> could be found <a
* href="https://playwright.dev/java/docs/api/class-tracing">here</a>.
*
* <p> You can use {@link Browser#startTracing Browser.startTracing()} and {@link Browser#stopTracing Browser.stopTracing()} to
* create a trace file that can be opened in Chrome DevTools performance panel.
@@ -1053,8 +1176,9 @@ public interface Browser extends AutoCloseable {
}
/**
* <strong>NOTE:</strong> This API controls <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">Chromium Tracing</a>
* which is a low-level chromium-specific debugging tool. API to control <a href="../trace-viewer">Playwright Tracing</a>
* could be found <a href="https://playwright.dev/java/docs/class-tracing">here</a>.
* which is a low-level chromium-specific debugging tool. API to control <a
* href="https://playwright.dev/java/docs/trace-viewer">Playwright Tracing</a> could be found <a
* href="https://playwright.dev/java/docs/api/class-tracing">here</a>.
*
* <p> You can use {@link Browser#startTracing Browser.startTracing()} and {@link Browser#stopTracing Browser.stopTracing()} to
* create a trace file that can be opened in Chrome DevTools performance panel.
@@ -1070,8 +1194,9 @@ public interface Browser extends AutoCloseable {
void startTracing(Page page, StartTracingOptions options);
/**
* <strong>NOTE:</strong> This API controls <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">Chromium Tracing</a>
* which is a low-level chromium-specific debugging tool. API to control <a href="../trace-viewer">Playwright Tracing</a>
* could be found <a href="https://playwright.dev/java/docs/class-tracing">here</a>.
* which is a low-level chromium-specific debugging tool. API to control <a
* href="https://playwright.dev/java/docs/trace-viewer">Playwright Tracing</a> could be found <a
* href="https://playwright.dev/java/docs/api/class-tracing">here</a>.
*
* <p> Returns the buffer with trace data.
*/
@@ -174,6 +174,62 @@ public interface BrowserContext extends AutoCloseable {
return this;
}
}
class RouteFromHAROptions {
/**
* <ul>
* <li> If set to 'abort' any request not found in the HAR file will be aborted.</li>
* <li> If set to 'fallback' falls through to the next route handler in the handler chain.</li>
* </ul>
*
* <p> Defaults to abort.
*/
public HarNotFound notFound;
/**
* If specified, updates the given HAR with the actual network information instead of serving from file.
*/
public Boolean update;
/**
* A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
* will be surved from the HAR file. If not specified, all requests are served from the HAR file.
*/
public Object url;
/**
* <ul>
* <li> If set to 'abort' any request not found in the HAR file will be aborted.</li>
* <li> If set to 'fallback' falls through to the next route handler in the handler chain.</li>
* </ul>
*
* <p> Defaults to abort.
*/
public RouteFromHAROptions setNotFound(HarNotFound notFound) {
this.notFound = notFound;
return this;
}
/**
* If specified, updates the given HAR with the actual network information instead of serving from file.
*/
public RouteFromHAROptions setUpdate(boolean update) {
this.update = update;
return this;
}
/**
* A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
* will be surved from the HAR file. If not specified, all requests are served from the HAR file.
*/
public RouteFromHAROptions setUrl(String url) {
this.url = url;
return this;
}
/**
* A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
* will be surved from the HAR file. If not specified, all requests are served from the HAR file.
*/
public RouteFromHAROptions setUrl(Pattern url) {
this.url = url;
return this;
}
}
class StorageStateOptions {
/**
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
@@ -497,7 +553,6 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "midi"}</li>
* <li> {@code "midi-sysex"} (system-exclusive midi)</li>
* <li> {@code "notifications"}</li>
* <li> {@code "push"}</li>
* <li> {@code "camera"}</li>
* <li> {@code "microphone"}</li>
* <li> {@code "background-sync"}</li>
@@ -524,7 +579,6 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "midi"}</li>
* <li> {@code "midi-sysex"} (system-exclusive midi)</li>
* <li> {@code "notifications"}</li>
* <li> {@code "push"}</li>
* <li> {@code "camera"}</li>
* <li> {@code "microphone"}</li>
* <li> {@code "background-sync"}</li>
@@ -555,9 +609,9 @@ public interface BrowserContext extends AutoCloseable {
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.route()} will not intercept requests intercepted by Service Worker. See <a
* <p> <strong>NOTE:</strong> {@link BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by Service Worker. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception. Via {@code await context.addInitScript(() => delete window.navigator.serviceWorker);}
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -607,9 +661,9 @@ public interface BrowserContext extends AutoCloseable {
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.route()} will not intercept requests intercepted by Service Worker. See <a
* <p> <strong>NOTE:</strong> {@link BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by Service Worker. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception. Via {@code await context.addInitScript(() => delete window.navigator.serviceWorker);}
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -657,9 +711,9 @@ public interface BrowserContext extends AutoCloseable {
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.route()} will not intercept requests intercepted by Service Worker. See <a
* <p> <strong>NOTE:</strong> {@link BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by Service Worker. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception. Via {@code await context.addInitScript(() => delete window.navigator.serviceWorker);}
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -709,9 +763,9 @@ public interface BrowserContext extends AutoCloseable {
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.route()} will not intercept requests intercepted by Service Worker. See <a
* <p> <strong>NOTE:</strong> {@link BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by Service Worker. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception. Via {@code await context.addInitScript(() => delete window.navigator.serviceWorker);}
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -759,9 +813,9 @@ public interface BrowserContext extends AutoCloseable {
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.route()} will not intercept requests intercepted by Service Worker. See <a
* <p> <strong>NOTE:</strong> {@link BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by Service Worker. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception. Via {@code await context.addInitScript(() => delete window.navigator.serviceWorker);}
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -811,9 +865,9 @@ public interface BrowserContext extends AutoCloseable {
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once route
* is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.route()} will not intercept requests intercepted by Service Worker. See <a
* <p> <strong>NOTE:</strong> {@link BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by Service Worker. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception. Via {@code await context.addInitScript(() => delete window.navigator.serviceWorker);}
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* <p> An example of a naive handler that aborts all image requests:
* <pre>{@code
@@ -857,6 +911,32 @@ public interface BrowserContext extends AutoCloseable {
* @param handler handler function to route the request.
*/
void route(Predicate<String> url, Consumer<Route> handler, RouteOptions options);
/**
* If specified the network requests that are made in the context will be served from the HAR file. Read more about <a
* href="https://playwright.dev/java/docs/network#replaying-from-har">Replaying from HAR</a>.
*
* <p> Playwright will not serve requests intercepted by Service Worker from the HAR file. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* @param har Path to a <a href="http://www.softwareishard.com/blog/har-12-spec">HAR</a> file with prerecorded network data. If {@code path}
* is a relative path, then it is resolved relative to the current working directory.
*/
default void routeFromHAR(Path har) {
routeFromHAR(har, null);
}
/**
* If specified the network requests that are made in the context will be served from the HAR file. Read more about <a
* href="https://playwright.dev/java/docs/network#replaying-from-har">Replaying from HAR</a>.
*
* <p> Playwright will not serve requests intercepted by Service Worker from the HAR file. See <a
* href="https://github.com/microsoft/playwright/issues/1090">this</a> issue. We recommend disabling Service Workers when
* using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
*
* @param har Path to a <a href="http://www.softwareishard.com/blog/har-12-spec">HAR</a> file with prerecorded network data. If {@code path}
* is a relative path, then it is resolved relative to the current working directory.
*/
void routeFromHAR(Path har, RouteFromHAROptions options);
/**
* This setting will change the default maximum navigation time for the following methods and related shortcuts:
* <ul>
@@ -19,6 +19,7 @@ package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
/**
* BrowserType provides methods to launch a specific browser instance or connect to an existing one. The following is a
@@ -52,8 +53,7 @@ public interface BrowserType {
*/
public Double slowMo;
/**
* Maximum time in milliseconds to wait for the connection to be established. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to
* disable timeout.
* Maximum time in milliseconds to wait for the connection to be established. Defaults to {@code 0} (no timeout).
*/
public Double timeout;
@@ -73,8 +73,7 @@ public interface BrowserType {
return this;
}
/**
* Maximum time in milliseconds to wait for the connection to be established. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to
* disable timeout.
* Maximum time in milliseconds to wait for the connection to be established. Defaults to {@code 0} (no timeout).
*/
public ConnectOptions setTimeout(double timeout) {
this.timeout = timeout;
@@ -130,7 +129,7 @@ public interface BrowserType {
/**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers/#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public Object channel;
/**
@@ -222,7 +221,7 @@ public interface BrowserType {
/**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers/#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public LaunchOptions setChannel(BrowserChannel channel) {
this.channel = channel;
@@ -231,7 +230,7 @@ public interface BrowserType {
/**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers/#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public LaunchOptions setChannel(String channel) {
this.channel = channel;
@@ -399,7 +398,7 @@ public interface BrowserType {
/**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers/#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public Object channel;
/**
@@ -517,6 +516,17 @@ public interface BrowserType {
* Network proxy settings.
*/
public Proxy proxy;
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
*/
public HarContentPolicy recordHarContent;
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
*/
public HarMode recordHarMode;
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
@@ -527,6 +537,7 @@ public interface BrowserType {
* BrowserContext.close()} for the HAR to be saved.
*/
public Path recordHarPath;
public Object recordHarUrlFilter;
/**
* Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure
* to call {@link BrowserContext#close BrowserContext.close()} for videos to be saved.
@@ -548,12 +559,21 @@ public interface BrowserType {
* is set.
*/
public ScreenSize screenSize;
/**
* Whether to allow sites to register Service workers. Defaults to {@code "allow"}.
* <ul>
* <li> {@code "allow"}: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a> can be
* registered.</li>
* <li> {@code "block"}: Playwright will block all registration of Service Workers.</li>
* </ul>
*/
public ServiceWorkerPolicy serviceWorkers;
/**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
*/
public Double slowMo;
/**
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* that imply single target DOM element will throw when more than one element matches the selector. See {@code Locator} to learn
* more about the strict mode.
*/
@@ -625,7 +645,7 @@ public interface BrowserType {
/**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers/#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public LaunchPersistentContextOptions setChannel(BrowserChannel channel) {
this.channel = channel;
@@ -634,7 +654,7 @@ public interface BrowserType {
/**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers/#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
*/
public LaunchPersistentContextOptions setChannel(String channel) {
this.channel = channel;
@@ -845,6 +865,23 @@ public interface BrowserType {
this.proxy = proxy;
return this;
}
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
*/
public LaunchPersistentContextOptions setRecordHarContent(HarContentPolicy recordHarContent) {
this.recordHarContent = recordHarContent;
return this;
}
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
*/
public LaunchPersistentContextOptions setRecordHarMode(HarMode recordHarMode) {
this.recordHarMode = recordHarMode;
return this;
}
/**
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
*/
@@ -861,6 +898,14 @@ public interface BrowserType {
this.recordHarPath = recordHarPath;
return this;
}
public LaunchPersistentContextOptions setRecordHarUrlFilter(String recordHarUrlFilter) {
this.recordHarUrlFilter = recordHarUrlFilter;
return this;
}
public LaunchPersistentContextOptions setRecordHarUrlFilter(Pattern recordHarUrlFilter) {
this.recordHarUrlFilter = recordHarUrlFilter;
return this;
}
/**
* Enables video recording for all pages into the specified directory. If not specified videos are not recorded. Make sure
* to call {@link BrowserContext#close BrowserContext.close()} for videos to be saved.
@@ -909,6 +954,18 @@ public interface BrowserType {
this.screenSize = screenSize;
return this;
}
/**
* Whether to allow sites to register Service workers. Defaults to {@code "allow"}.
* <ul>
* <li> {@code "allow"}: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a> can be
* registered.</li>
* <li> {@code "block"}: Playwright will block all registration of Service Workers.</li>
* </ul>
*/
public LaunchPersistentContextOptions setServiceWorkers(ServiceWorkerPolicy serviceWorkers) {
this.serviceWorkers = serviceWorkers;
return this;
}
/**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
*/
@@ -917,7 +974,7 @@ public interface BrowserType {
return this;
}
/**
* It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors
* that imply single target DOM element will throw when more than one element matches the selector. See {@code Locator} to learn
* more about the strict mode.
*/
@@ -971,7 +1028,7 @@ public interface BrowserType {
}
}
/**
* This methods attaches Playwright to an existing browser instance.
* This method attaches Playwright to an existing browser instance.
*
* @param wsEndpoint A browser websocket endpoint to connect to.
*/
@@ -979,13 +1036,13 @@ public interface BrowserType {
return connect(wsEndpoint, null);
}
/**
* This methods attaches Playwright to an existing browser instance.
* This method attaches Playwright to an existing browser instance.
*
* @param wsEndpoint A browser websocket endpoint to connect to.
*/
Browser connect(String wsEndpoint, ConnectOptions options);
/**
* This methods attaches Playwright to an existing browser instance using the Chrome DevTools Protocol.
* This method attaches Playwright to an existing browser instance using the Chrome DevTools Protocol.
*
* <p> The default browser context is accessible via {@link Browser#contexts Browser.contexts()}.
*
@@ -998,7 +1055,7 @@ public interface BrowserType {
return connectOverCDP(endpointURL, null);
}
/**
* This methods attaches Playwright to an existing browser instance using the Chrome DevTools Protocol.
* This method attaches Playwright to an existing browser instance using the Chrome DevTools Protocol.
*
* <p> The default browser context is accessible via {@link Browser#contexts Browser.contexts()}.
*
@@ -19,7 +19,28 @@ package com.microsoft.playwright;
import java.util.*;
/**
* {@code ConsoleMessage} objects are dispatched by page via the {@link Page#onConsoleMessage Page.onConsoleMessage()} event.
* {@code ConsoleMessage} objects are dispatched by page via the {@link Page#onConsoleMessage Page.onConsoleMessage()} event. For
* each console messages logged in the page there will be corresponding event in the Playwright context.
* <pre>{@code
* // Listen for all System.out.printlns
* page.onConsoleMessage(msg -> System.out.println(msg.text()));
*
* // Listen for all console events and handle errors
* page.onConsoleMessage(msg -> {
* if ("error".equals(msg.type()))
* System.out.println("Error text: " + msg.text());
* });
*
* // Get the next System.out.println
* ConsoleMessage msg = page.waitForConsoleMessage(() -> {
* // Issue console.log inside the page
* page.evaluate("console.log('hello', 42, { foo: 'bar' });");
* });
*
* // Deconstruct console.log arguments
* msg.args().get(0).jsonValue() // hello
* msg.args().get(1).jsonValue() // 42
* }</pre>
*/
public interface ConsoleMessage {
/**
File diff suppressed because it is too large Load Diff
@@ -74,50 +74,50 @@ public interface FileChooser {
Page page();
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
* they are resolved relative to the current working directory. For empty array, clears the selected files.
*/
default void setFiles(Path files) {
setFiles(files, null);
}
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
* they are resolved relative to the current working directory. For empty array, clears the selected files.
*/
void setFiles(Path files, SetFilesOptions options);
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
* they are resolved relative to the current working directory. For empty array, clears the selected files.
*/
default void setFiles(Path[] files) {
setFiles(files, null);
}
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
* they are resolved relative to the current working directory. For empty array, clears the selected files.
*/
void setFiles(Path[] files, SetFilesOptions options);
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
* they are resolved relative to the current working directory. For empty array, clears the selected files.
*/
default void setFiles(FilePayload files) {
setFiles(files, null);
}
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
* they are resolved relative to the current working directory. For empty array, clears the selected files.
*/
void setFiles(FilePayload files, SetFilesOptions options);
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
* they are resolved relative to the current working directory. For empty array, clears the selected files.
*/
default void setFiles(FilePayload[] files) {
setFiles(files, null);
}
/**
* Sets the value of the file input this chooser is associated with. If some of the {@code filePaths} are relative paths, then
* they are resolved relative to the the current working directory. For empty array, clears the selected files.
* they are resolved relative to the current working directory. For empty array, clears the selected files.
*/
void setFiles(FilePayload[] files, SetFilesOptions options);
}
File diff suppressed because it is too large Load Diff
@@ -57,8 +57,9 @@ public interface FrameLocator {
*/
public Locator has;
/**
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example,
* {@code "Playwright"} matches {@code <article><div>Playwright</div></article>}.
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a
* [string], matching is case-insensitive and searches for a substring. For example, {@code "Playwright"} matches
* {@code <article><div>Playwright</div></article>}.
*/
public Object hasText;
@@ -73,16 +74,18 @@ public interface FrameLocator {
return this;
}
/**
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example,
* {@code "Playwright"} matches {@code <article><div>Playwright</div></article>}.
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a
* [string], matching is case-insensitive and searches for a substring. For example, {@code "Playwright"} matches
* {@code <article><div>Playwright</div></article>}.
*/
public LocatorOptions setHasText(String hasText) {
this.hasText = hasText;
return this;
}
/**
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example,
* {@code "Playwright"} matches {@code <article><div>Playwright</div></article>}.
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a
* [string], matching is case-insensitive and searches for a substring. For example, {@code "Playwright"} matches
* {@code <article><div>Playwright</div></article>}.
*/
public LocatorOptions setHasText(Pattern hasText) {
this.hasText = hasText;
@@ -97,7 +100,7 @@ public interface FrameLocator {
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
* that iframe.
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors/">working with
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors">working with
* selectors</a> for more details.
*/
FrameLocator frameLocator(String selector);
@@ -108,7 +111,7 @@ public interface FrameLocator {
/**
* The method finds an element matching the specified selector in the FrameLocator's subtree.
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors/">working with
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors">working with
* selectors</a> for more details.
*/
default Locator locator(String selector) {
@@ -117,12 +120,12 @@ public interface FrameLocator {
/**
* The method finds an element matching the specified selector in the FrameLocator's subtree.
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors/">working with
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors">working with
* selectors</a> for more details.
*/
Locator locator(String selector, LocatorOptions options);
/**
* Returns locator to the n-th matching frame.
* Returns locator to the n-th matching frame. It's zero based, {@code nth(0)} selects the first frame.
*/
FrameLocator nth(int index);
}
File diff suppressed because it is too large Load Diff
@@ -122,12 +122,12 @@ public interface Mouse {
}
class MoveOptions {
/**
* defaults to 1. Sends intermediate {@code mousemove} events.
* Defaults to 1. Sends intermediate {@code mousemove} events.
*/
public Integer steps;
/**
* defaults to 1. Sends intermediate {@code mousemove} events.
* Defaults to 1. Sends intermediate {@code mousemove} events.
*/
public MoveOptions setSteps(int steps) {
this.steps = steps;
File diff suppressed because it is too large Load Diff
@@ -70,7 +70,7 @@ public interface Playwright extends AutoCloseable {
APIRequest request();
/**
* Selectors can be used to install custom selector engines. See <a
* href="https://playwright.dev/java/docs/selectors/">Working with selectors</a> for more information.
* href="https://playwright.dev/java/docs/selectors">Working with selectors</a> for more information.
*/
Selectors selectors();
/**
@@ -58,8 +58,9 @@ public interface Request {
*/
Frame frame();
/**
* **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use {@link Request#allHeaders
* Request.allHeaders()} instead.
* An object with the request HTTP headers. The header names are lower-cased. Note that this method does not return
* security-related headers, including cookie-related ones. You can use {@link Request#allHeaders Request.allHeaders()} for
* complete list of headers that include {@code cookie} information.
*/
Map<String, String> headers();
/**
@@ -40,8 +40,14 @@ public interface Response {
*/
Frame frame();
/**
* **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use {@link Response#allHeaders
* Response.allHeaders()} instead.
* Indicates whether this Response was fullfilled by a Service Worker's Fetch Handler (i.e. via <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith">FetchEvent.respondWith</a>).
*/
boolean fromServiceWorker();
/**
* An object with the response HTTP headers. The header names are lower-cased. Note that this method does not return
* security-related headers, including cookie-related ones. You can use {@link Response#allHeaders Response.allHeaders()}
* for complete list of headers that include {@code cookie} information.
*/
Map<String, String> headers();
/**
@@ -22,6 +22,8 @@ import java.util.*;
/**
* Whenever a network route is set up with {@link Page#route Page.route()} or {@link BrowserContext#route
* BrowserContext.route()}, the {@code Route} object allows to handle the route.
*
* <p> Learn more about <a href="https://playwright.dev/java/docs/network">networking</a>.
*/
public interface Route {
class ResumeOptions {
@@ -78,6 +80,62 @@ public interface Route {
return this;
}
}
class FallbackOptions {
/**
* If set changes the request HTTP headers. Header values will be converted to a string.
*/
public Map<String, String> headers;
/**
* If set changes the request method (e.g. GET or POST)
*/
public String method;
/**
* If set changes the post data of request
*/
public Object postData;
/**
* If set changes the request URL. New URL must have same protocol as original one. Changing the URL won't affect the route
* matching, all the routes are matched using the original request URL.
*/
public String url;
/**
* If set changes the request HTTP headers. Header values will be converted to a string.
*/
public FallbackOptions setHeaders(Map<String, String> headers) {
this.headers = headers;
return this;
}
/**
* If set changes the request method (e.g. GET or POST)
*/
public FallbackOptions setMethod(String method) {
this.method = method;
return this;
}
/**
* If set changes the post data of request
*/
public FallbackOptions setPostData(String postData) {
this.postData = postData;
return this;
}
/**
* If set changes the post data of request
*/
public FallbackOptions setPostData(byte[] postData) {
this.postData = postData;
return this;
}
/**
* If set changes the request URL. New URL must have same protocol as original one. Changing the URL won't affect the route
* matching, all the routes are matched using the original request URL.
*/
public FallbackOptions setUrl(String url) {
this.url = url;
return this;
}
}
class FulfillOptions {
/**
* Optional response body as text.
@@ -198,8 +256,8 @@ public interface Route {
* page.route("**\/*", route -> {
* // Override headers
* Map<String, String> headers = new HashMap<>(route.request().headers());
* headers.put("foo", "bar"); // set "foo" header
* headers.remove("origin"); // remove "origin" header
* headers.put("foo", "foo-value"); // set "foo" header
* headers.remove("bar"); // remove "bar" header
* route.resume(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
@@ -213,13 +271,133 @@ public interface Route {
* page.route("**\/*", route -> {
* // Override headers
* Map<String, String> headers = new HashMap<>(route.request().headers());
* headers.put("foo", "bar"); // set "foo" header
* headers.remove("origin"); // remove "origin" header
* headers.put("foo", "foo-value"); // set "foo" header
* headers.remove("bar"); // remove "bar" header
* route.resume(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
*/
void resume(ResumeOptions options);
/**
* When several routes match the given pattern, they run in the order opposite to their registration. That way the last
* registered route can always override all the previos ones. In the example below, request will be handled by the
* bottom-most handler first, then it'll fall back to the previous one and in the end will be aborted by the first
* registered route.
* <pre>{@code
* page.route("**\/*", route -> {
* // Runs last.
* route.abort();
* });
*
* page.route("**\/*", route -> {
* // Runs second.
* route.fallback();
* });
*
* page.route("**\/*", route -> {
* // Runs first.
* route.fallback();
* });
* }</pre>
*
* <p> Registering multiple routes is useful when you want separate handlers to handle different kinds of requests, for example
* API calls vs page resources or GET requests vs POST requests as in the example below.
* <pre>{@code
* // Handle GET requests.
* page.route("**\/*", route -> {
* if (!route.request().method().equals("GET")) {
* route.fallback();
* return;
* }
* // Handling GET only.
* // ...
* });
*
* // Handle POST requests.
* page.route("**\/*", route -> {
* if (!route.request().method().equals("POST")) {
* route.fallback();
* return;
* }
* // Handling POST only.
* // ...
* });
* }</pre>
*
* <p> One can also modify request while falling back to the subsequent handler, that way intermediate route handler can modify
* url, method, headers and postData of the request.
* <pre>{@code
* page.route("**\/*", route -> {
* // Override headers
* Map<String, String> headers = new HashMap<>(route.request().headers());
* headers.put("foo", "foo-value"); // set "foo" header
* headers.remove("bar"); // remove "bar" header
* route.fallback(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
*/
default void fallback() {
fallback(null);
}
/**
* When several routes match the given pattern, they run in the order opposite to their registration. That way the last
* registered route can always override all the previos ones. In the example below, request will be handled by the
* bottom-most handler first, then it'll fall back to the previous one and in the end will be aborted by the first
* registered route.
* <pre>{@code
* page.route("**\/*", route -> {
* // Runs last.
* route.abort();
* });
*
* page.route("**\/*", route -> {
* // Runs second.
* route.fallback();
* });
*
* page.route("**\/*", route -> {
* // Runs first.
* route.fallback();
* });
* }</pre>
*
* <p> Registering multiple routes is useful when you want separate handlers to handle different kinds of requests, for example
* API calls vs page resources or GET requests vs POST requests as in the example below.
* <pre>{@code
* // Handle GET requests.
* page.route("**\/*", route -> {
* if (!route.request().method().equals("GET")) {
* route.fallback();
* return;
* }
* // Handling GET only.
* // ...
* });
*
* // Handle POST requests.
* page.route("**\/*", route -> {
* if (!route.request().method().equals("POST")) {
* route.fallback();
* return;
* }
* // Handling POST only.
* // ...
* });
* }</pre>
*
* <p> One can also modify request while falling back to the subsequent handler, that way intermediate route handler can modify
* url, method, headers and postData of the request.
* <pre>{@code
* page.route("**\/*", route -> {
* // Override headers
* Map<String, String> headers = new HashMap<>(route.request().headers());
* headers.put("foo", "foo-value"); // set "foo" header
* headers.remove("bar"); // remove "bar" header
* route.fallback(new Route.ResumeOptions().setHeaders(headers));
* });
* }</pre>
*/
void fallback(FallbackOptions options);
/**
* Fulfills route's request with given response.
*
@@ -20,7 +20,7 @@ import java.nio.file.Path;
/**
* Selectors can be used to install custom selector engines. See <a
* href="https://playwright.dev/java/docs/selectors/">Working with selectors</a> for more information.
* href="https://playwright.dev/java/docs/selectors">Working with selectors</a> for more information.
*/
public interface Selectors {
class RegisterOptions {
@@ -20,7 +20,7 @@ import java.nio.file.Path;
/**
* API for collecting and saving Playwright traces. Playwright traces can be opened in <a
* href="https://playwright.dev/java/docs/trace-viewer/">Trace Viewer</a> after Playwright script runs.
* href="https://playwright.dev/java/docs/trace-viewer">Trace Viewer</a> after Playwright script runs.
*
* <p> Start recording a trace before performing actions. At the end, stop tracing and save it to a file.
* <pre>{@code
@@ -56,7 +56,8 @@ public interface Tracing {
public Boolean snapshots;
/**
* Whether to include source files for trace actions. List of the directories with source code for the application must be
* provided via {@code PLAYWRIGHT_JAVA_SRC} environment variable.
* provided via {@code PLAYWRIGHT_JAVA_SRC} environment variable (the paths should be separated by ';' on Windows and by ':' on
* other platforms).
*/
public Boolean sources;
/**
@@ -92,7 +93,8 @@ public interface Tracing {
}
/**
* Whether to include source files for trace actions. List of the directories with source code for the application must be
* provided via {@code PLAYWRIGHT_JAVA_SRC} environment variable.
* provided via {@code PLAYWRIGHT_JAVA_SRC} environment variable (the paths should be separated by ';' on Windows and by ':' on
* other platforms).
*/
public StartOptions setSources(boolean sources) {
this.sources = sources;
@@ -156,6 +156,11 @@ public interface LocatorAssertions {
}
}
class ContainsTextOptions {
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
*/
public Boolean ignoreCase;
/**
* Time to retry the assertion for.
*/
@@ -165,6 +170,14 @@ public interface LocatorAssertions {
*/
public Boolean useInnerText;
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
*/
public ContainsTextOptions setIgnoreCase(boolean ignoreCase) {
this.ignoreCase = ignoreCase;
return this;
}
/**
* Time to retry the assertion for.
*/
@@ -265,6 +278,11 @@ public interface LocatorAssertions {
}
}
class HasTextOptions {
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
*/
public Boolean ignoreCase;
/**
* Time to retry the assertion for.
*/
@@ -274,6 +292,14 @@ public interface LocatorAssertions {
*/
public Boolean useInnerText;
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
*/
public HasTextOptions setIgnoreCase(boolean ignoreCase) {
this.ignoreCase = ignoreCase;
return this;
}
/**
* Time to retry the assertion for.
*/
@@ -303,6 +329,20 @@ public interface LocatorAssertions {
return this;
}
}
class HasValuesOptions {
/**
* Time to retry the assertion for.
*/
public Double timeout;
/**
* Time to retry the assertion for.
*/
public HasValuesOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
/**
* Makes the assertion check for the opposite condition. For example, this code tests that the Locator doesn't contain text
* {@code "error"}:
@@ -328,7 +368,11 @@ public interface LocatorAssertions {
*/
void isChecked(IsCheckedOptions options);
/**
* Ensures the {@code Locator} points to a disabled element.
* Ensures the {@code Locator} points to a disabled element. Element is disabled if it has "disabled" attribute or is disabled
* via <a
* href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-disabled">'aria-disabled'</a>.
* Note that only native control elements such as HTML {@code button}, {@code input}, {@code select}, {@code textarea}, {@code option}, {@code optgroup} can be
* disabled by setting "disabled" attribute. "disabled" attribute on other elements is ignored by the browser.
* <pre>{@code
* assertThat(page.locator("button.submit")).isDisabled();
* }</pre>
@@ -337,7 +381,11 @@ public interface LocatorAssertions {
isDisabled(null);
}
/**
* Ensures the {@code Locator} points to a disabled element.
* Ensures the {@code Locator} points to a disabled element. Element is disabled if it has "disabled" attribute or is disabled
* via <a
* href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-disabled">'aria-disabled'</a>.
* Note that only native control elements such as HTML {@code button}, {@code input}, {@code select}, {@code textarea}, {@code option}, {@code optgroup} can be
* disabled by setting "disabled" attribute. "disabled" attribute on other elements is ignored by the browser.
* <pre>{@code
* assertThat(page.locator("button.submit")).isDisabled();
* }</pre>
@@ -409,7 +457,7 @@ public interface LocatorAssertions {
void isFocused(IsFocusedOptions options);
/**
* Ensures the {@code Locator} points to a hidden DOM node, which is the opposite of <a
* href="https://playwright.dev/java/docs/actionability/#visible">visible</a>.
* href="https://playwright.dev/java/docs/api/actionability#visible">visible</a>.
* <pre>{@code
* assertThat(page.locator(".my-element")).isHidden();
* }</pre>
@@ -419,27 +467,27 @@ public interface LocatorAssertions {
}
/**
* Ensures the {@code Locator} points to a hidden DOM node, which is the opposite of <a
* href="https://playwright.dev/java/docs/actionability/#visible">visible</a>.
* href="https://playwright.dev/java/docs/api/actionability#visible">visible</a>.
* <pre>{@code
* assertThat(page.locator(".my-element")).isHidden();
* }</pre>
*/
void isHidden(IsHiddenOptions options);
/**
* Ensures the {@code Locator} points to a <a href="https://playwright.dev/java/docs/actionability/#visible">visible</a> DOM
* Ensures the {@code Locator} points to a <a href="https://playwright.dev/java/docs/api/actionability#visible">visible</a> DOM
* node.
* <pre>{@code
* assertThat(page.locator(".my-element")).toBeVisible();
* assertThat(page.locator(".my-element")).isVisible();
* }</pre>
*/
default void isVisible() {
isVisible(null);
}
/**
* Ensures the {@code Locator} points to a <a href="https://playwright.dev/java/docs/actionability/#visible">visible</a> DOM
* Ensures the {@code Locator} points to a <a href="https://playwright.dev/java/docs/api/actionability#visible">visible</a> DOM
* node.
* <pre>{@code
* assertThat(page.locator(".my-element")).toBeVisible();
* assertThat(page.locator(".my-element")).isVisible();
* }</pre>
*/
void isVisible(IsVisibleOptions options);
@@ -1035,5 +1083,61 @@ public interface LocatorAssertions {
* @param value Expected value.
*/
void hasValue(Pattern value, HasValueOptions options);
/**
* Ensures the {@code Locator} points to multi-select/combobox (i.e. a {@code select} with the {@code multiple} attribute) and the specified
* values are selected.
*
* <p> For example, given the following element:
* <pre>{@code
* page.locator("id=favorite-colors").selectOption(["R", "G"]);
* assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
* }</pre>
*
* @param values Expected options currently selected.
*/
default void hasValues(String[] values) {
hasValues(values, null);
}
/**
* Ensures the {@code Locator} points to multi-select/combobox (i.e. a {@code select} with the {@code multiple} attribute) and the specified
* values are selected.
*
* <p> For example, given the following element:
* <pre>{@code
* page.locator("id=favorite-colors").selectOption(["R", "G"]);
* assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
* }</pre>
*
* @param values Expected options currently selected.
*/
void hasValues(String[] values, HasValuesOptions options);
/**
* Ensures the {@code Locator} points to multi-select/combobox (i.e. a {@code select} with the {@code multiple} attribute) and the specified
* values are selected.
*
* <p> For example, given the following element:
* <pre>{@code
* page.locator("id=favorite-colors").selectOption(["R", "G"]);
* assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
* }</pre>
*
* @param values Expected options currently selected.
*/
default void hasValues(Pattern[] values) {
hasValues(values, null);
}
/**
* Ensures the {@code Locator} points to multi-select/combobox (i.e. a {@code select} with the {@code multiple} attribute) and the specified
* values are selected.
*
* <p> For example, given the following element:
* <pre>{@code
* page.locator("id=favorite-colors").selectOption(["R", "G"]);
* assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
* }</pre>
*
* @param values Expected options currently selected.
*/
void hasValues(Pattern[] values, HasValuesOptions options);
}
@@ -24,8 +24,8 @@ import com.microsoft.playwright.impl.LocatorAssertionsImpl;
import com.microsoft.playwright.impl.PageAssertionsImpl;
/**
* The {@code PlaywrightAssertions} class provides convenience methods for creating assertions that will wait until the expected
* condition is met.
* Playwright gives you Web-First Assertions with convenience methods for creating assertions that will wait and retry
* until the expected condition is met.
*
* <p> Consider the following example:
* <pre>{@code
@@ -20,23 +20,23 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.BindingCallback;
import com.microsoft.playwright.options.Cookie;
import com.microsoft.playwright.options.FunctionCallback;
import com.microsoft.playwright.options.Geolocation;
import com.microsoft.playwright.options.*;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes;
import static java.util.Arrays.asList;
@@ -54,7 +54,17 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
final TimeoutSettings timeoutSettings = new TimeoutSettings();
Path videosDir;
URL baseUrl;
Path recordHarPath;
final Map<String, HarRecorder> harRecorders = new HashMap<>();
static class HarRecorder {
final Path path;
final HarContentPolicy contentPolicy;
HarRecorder(Path har, HarContentPolicy policy) {
path = har;
contentPolicy = policy;
}
}
enum EventType {
CLOSE,
@@ -77,6 +87,12 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
this.request = connection.getExistingObject(initializer.getAsJsonObject("APIRequestContext").get("guid").getAsString());
}
void setRecordHar(Path path, HarContentPolicy policy) {
if (path != null) {
harRecorders.put("", new HarRecorder(path, policy));
}
}
void setBaseUrl(String spec) {
try {
this.baseUrl = new URL(spec);
@@ -181,15 +197,31 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
isClosedOrClosing = true;
try {
if (recordHarPath != null) {
JsonObject json = sendMessage("harExport").getAsJsonObject();
for (Map.Entry<String, HarRecorder> entry : harRecorders.entrySet()) {
JsonObject params = new JsonObject();
params.addProperty("harId", entry.getKey());
JsonObject json = sendMessage("harExport", params).getAsJsonObject();
ArtifactImpl artifact = connection.getExistingObject(json.getAsJsonObject("artifact").get("guid").getAsString());
// In case of CDP connection browser is null but since the connection is established by
// the driver it is safe to consider the artifact local.
if (browser() != null && browser().isRemote) {
artifact.isRemote = true;
}
artifact.saveAs(recordHarPath);
// Server side will compress artifact if content is attach or if file is .zip.
HarRecorder harParams = entry.getValue();
boolean isCompressed = harParams.contentPolicy == HarContentPolicy.ATTACH || harParams.path.toString().endsWith(".zip");
boolean needCompressed = harParams.path.toString().endsWith(".zip");
if (isCompressed && !needCompressed) {
String tmpPath = harParams.path + ".tmp";
artifact.saveAs(Paths.get(tmpPath));
JsonObject unzipParams = new JsonObject();
unzipParams.addProperty("zipFile", tmpPath);
unzipParams.addProperty("harFile", harParams.path.toString());
connection.localUtils.sendMessage("harUnzip", unzipParams);
} else {
artifact.saveAs(harParams.path);
}
artifact.delete();
}
@@ -336,7 +368,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public void route(String url, Consumer<Route> handler, RouteOptions options) {
route(new UrlMatcher(this.baseUrl, url), handler, options);
route(new UrlMatcher(baseUrl, url), handler, options);
}
@Override
@@ -349,6 +381,21 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
route(new UrlMatcher(url), handler, options);
}
@Override
public void routeFromHAR(Path har, RouteFromHAROptions options) {
if (options == null) {
options = new RouteFromHAROptions();
}
if (options.update != null && options.update) {
recordIntoHar(null, har, options);
return;
}
UrlMatcher matcher = UrlMatcher.forOneOf(baseUrl, options.url);
HARRouter harRouter = new HARRouter(connection.localUtils, har, options.notFound);
onClose(context -> harRouter.dispose());
route(matcher, route -> harRouter.handle(route), null);
}
private void route(UrlMatcher matcher, Consumer<Route> handler, RouteOptions options) {
withLogging("BrowserContext.route", () -> {
routes.add(matcher, handler, options == null ? null : options.times);
@@ -360,6 +407,22 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
});
}
void recordIntoHar(PageImpl page, Path har, RouteFromHAROptions options) {
JsonObject params = new JsonObject();
if (page != null) {
params.add("page", page.toProtocolRef());
}
JsonObject jsonOptions = new JsonObject();
jsonOptions.addProperty("path", har.toAbsolutePath().toString());
jsonOptions.addProperty("content", HarContentPolicy.ATTACH.name().toLowerCase());
jsonOptions.addProperty("mode", HarMode.MINIMAL.name().toLowerCase());
addHarUrlFilter(jsonOptions, options.url);
params.add("options", jsonOptions);
JsonObject json = sendMessage("harStart", params).getAsJsonObject();
String harId = json.get("harId").getAsString();
harRecorders.put(harId, new HarRecorder(har, HarContentPolicy.ATTACH));
}
@Override
public void setDefaultNavigationTimeout(double timeout) {
withLogging("BrowserContext.setDefaultNavigationTimeout", () -> {
@@ -474,11 +537,12 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
}
void handleRoute(Route route) {
boolean handled = routes.handle(route);
if (handled) {
void handleRoute(RouteImpl route) {
Router.HandleResult handled = routes.handle(route);
if (handled == Router.HandleResult.FoundMatchingHandler) {
maybeDisableNetworkInterception();
} else {
}
if (!route.isHandled()){
route.resume();
}
}
@@ -490,7 +554,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
protected void handleEvent(String event, JsonObject params) {
if ("route".equals(event)) {
Route route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
RouteImpl route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
handleRoute(route);
} else if ("page".equals(event)) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
@@ -560,4 +624,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
listeners.notify(EventType.CLOSE, this);
}
WritableStream createTempFile(String name) {
JsonObject params = new JsonObject();
params.addProperty("name", name);
JsonObject json = sendMessage("createTempFile", params).getAsJsonObject();
return connection.getExistingObject(json.getAsJsonObject("writableStream").get("guid").getAsString());
}
}
@@ -19,20 +19,21 @@ package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.HarContentPolicy;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.*;
import static com.microsoft.playwright.impl.Utils.convertType;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
class BrowserImpl extends ChannelOwner implements Browser {
final Set<BrowserContextImpl> contexts = new HashSet<>();
@@ -40,7 +41,7 @@ class BrowserImpl extends ChannelOwner implements Browser {
boolean isRemote;
boolean isConnectedOverWebSocket;
private boolean isConnected = true;
LocalUtils localUtils;
BrowserTypeImpl browserType;
enum EventType {
DISCONNECTED,
@@ -60,6 +61,11 @@ class BrowserImpl extends ChannelOwner implements Browser {
listeners.remove(EventType.DISCONNECTED, handler);
}
@Override
public BrowserType browserType() {
return browserType;
}
@Override
public void close() {
withLogging("Browser.close", () -> closeImpl());
@@ -112,6 +118,9 @@ class BrowserImpl extends ChannelOwner implements Browser {
private BrowserContextImpl newContextImpl(NewContextOptions options) {
if (options == null) {
options = new NewContextOptions();
} else {
// Make a copy so that we can nullify some fields below.
options = convertType(options, NewContextOptions.class);
}
if (options.storageStatePath != null) {
try {
@@ -127,21 +136,50 @@ class BrowserImpl extends ChannelOwner implements Browser {
storageState = new Gson().fromJson(options.storageState, JsonObject.class);
options.storageState = null;
}
JsonObject recordHar = null;
Path recordHarPath = options.recordHarPath;
HarContentPolicy harContentPolicy = null;
if (options.recordHarPath != null) {
recordHar = new JsonObject();
recordHar.addProperty("path", options.recordHarPath.toString());
if (options.recordHarContent != null) {
harContentPolicy = options.recordHarContent;
} else if (options.recordHarOmitContent != null && options.recordHarOmitContent) {
harContentPolicy = HarContentPolicy.OMIT;
}
if (harContentPolicy != null) {
recordHar.addProperty("content", harContentPolicy.name().toLowerCase());
}
if (options.recordHarMode != null) {
recordHar.addProperty("mode", options.recordHarMode.name().toLowerCase());
}
addHarUrlFilter(recordHar, options.recordHarUrlFilter);
options.recordHarPath = null;
options.recordHarMode = null;
options.recordHarOmitContent = null;
options.recordHarContent = null;
options.recordHarUrlFilter = null;
} else {
if (options.recordHarOmitContent != null) {
throw new PlaywrightException("recordHarOmitContent is set but recordHarPath is null");
}
if (options.recordHarUrlFilter != null) {
throw new PlaywrightException("recordHarUrlFilter is set but recordHarPath is null");
}
if (options.recordHarMode != null) {
throw new PlaywrightException("recordHarMode is set but recordHarPath is null");
}
if (options.recordHarContent != null) {
throw new PlaywrightException("recordHarContent is set but recordHarPath is null");
}
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
if (storageState != null) {
params.add("storageState", storageState);
}
if (options.recordHarPath != null) {
JsonObject recordHar = new JsonObject();
recordHar.addProperty("path", options.recordHarPath.toString());
if (options.recordHarOmitContent != null) {
recordHar.addProperty("omitContent", true);
}
params.remove("recordHarPath");
params.remove("recordHarOmitContent");
if (recordHar != null) {
params.add("recordHar", recordHar);
} else if (options.recordHarOmitContent != null) {
throw new PlaywrightException("recordHarOmitContent is set but recordHarPath is null");
}
if (options.recordVideoDir != null) {
JsonObject recordVideo = new JsonObject();
@@ -171,8 +209,7 @@ class BrowserImpl extends ChannelOwner implements Browser {
if (options.baseURL != null) {
context.setBaseUrl(options.baseURL);
}
context.recordHarPath = options.recordHarPath;
context.tracing().localUtils = localUtils;
context.setRecordHar(recordHarPath, harContentPolicy);
contexts.add(context);
return context;
}
@@ -193,9 +230,7 @@ class BrowserImpl extends ChannelOwner implements Browser {
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
if (page != null) {
JsonObject jsonPage = new JsonObject();
jsonPage.addProperty("guid", ((PageImpl) page).guid);
params.add("page", jsonPage);
params.add("page", ((PageImpl) page).toProtocolRef());
}
sendMessage("startTracing", params);
}
@@ -22,12 +22,15 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.HarContentPolicy;
import java.io.IOException;
import java.nio.file.Path;
import java.util.function.Consumer;
import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.convertType;
class BrowserTypeImpl extends ChannelOwner implements BrowserType {
LocalUtils localUtils;
@@ -48,7 +51,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
JsonElement result = sendMessage("launch", params);
BrowserImpl browser = connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("browser").get("guid").getAsString());
browser.localUtils = localUtils;
browser.browserType = this;
return browser;
}
@@ -64,6 +67,22 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
// We don't use gson() here as the headers map should be serialized to a json object.
JsonObject params = new Gson().toJsonTree(options).getAsJsonObject();
params.addProperty("wsEndpoint", wsEndpoint);
if (!params.has("headers")) {
params.add("headers", new JsonObject());
}
JsonObject headers = params.get("headers").getAsJsonObject();
boolean foundBrowserHeader = false;
for (String name : headers.keySet()) {
if ("x-playwright-browser".equalsIgnoreCase(name)) {
foundBrowserHeader = true;
break;
}
}
if (!foundBrowserHeader) {
headers.addProperty("x-playwright-browser", name());
}
JsonObject json = sendMessage("connect", params).getAsJsonObject();
JsonPipe pipe = connection.getExistingObject(json.getAsJsonObject("pipe").get("guid").getAsString());
Connection connection = new Connection(pipe);
@@ -80,7 +99,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
BrowserImpl browser = connection.getExistingObject(playwright.initializer.getAsJsonObject("preLaunchedBrowser").get("guid").getAsString());
browser.isRemote = true;
browser.isConnectedOverWebSocket = true;
browser.localUtils = localUtils;
browser.browserType = this;
Consumer<JsonPipe> connectionCloseListener = t -> browser.notifyRemoteClosed();
pipe.onClose(connectionCloseListener);
browser.onDisconnected(b -> {
@@ -114,7 +133,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
BrowserImpl browser = connection.getExistingObject(json.getAsJsonObject("browser").get("guid").getAsString());
browser.isRemote = true;
browser.localUtils = localUtils;
browser.browserType = this;
if (json.has("defaultContext")) {
String contextId = json.getAsJsonObject("defaultContext").get("guid").getAsString();
BrowserContextImpl defaultContext = connection.getExistingObject(contextId);
@@ -136,20 +155,52 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
private BrowserContextImpl launchPersistentContextImpl(Path userDataDir, LaunchPersistentContextOptions options) {
if (options == null) {
options = new LaunchPersistentContextOptions();
} else {
// Make a copy so that we can nullify some fields below.
options = convertType(options, LaunchPersistentContextOptions.class);
}
JsonObject recordHar = null;
Path recordHarPath = options.recordHarPath;
HarContentPolicy harContentPolicy = null;
if (options.recordHarPath != null) {
recordHar = new JsonObject();
recordHar.addProperty("path", options.recordHarPath.toString());
if (options.recordHarContent != null) {
harContentPolicy = options.recordHarContent;
} else if (options.recordHarOmitContent != null && options.recordHarOmitContent) {
harContentPolicy = HarContentPolicy.OMIT;
}
if (harContentPolicy != null) {
recordHar.addProperty("content", harContentPolicy.name().toLowerCase());
}
if (options.recordHarMode != null) {
recordHar.addProperty("mode", options.recordHarMode.toString().toLowerCase());
}
addHarUrlFilter(recordHar, options.recordHarUrlFilter);
options.recordHarPath = null;
options.recordHarMode = null;
options.recordHarOmitContent = null;
options.recordHarContent = null;
options.recordHarUrlFilter = null;
} else {
if (options.recordHarOmitContent != null) {
throw new PlaywrightException("recordHarOmitContent is set but recordHarPath is null");
}
if (options.recordHarUrlFilter != null) {
throw new PlaywrightException("recordHarUrlFilter is set but recordHarPath is null");
}
if (options.recordHarMode != null) {
throw new PlaywrightException("recordHarMode is set but recordHarPath is null");
}
if (options.recordHarContent != null) {
throw new PlaywrightException("recordHarContent is set but recordHarPath is null");
}
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("userDataDir", userDataDir.toString());
if (options.recordHarPath != null) {
JsonObject recordHar = new JsonObject();
recordHar.addProperty("path", options.recordHarPath.toString());
if (options.recordHarOmitContent != null) {
recordHar.addProperty("omitContent", true);
}
params.remove("recordHarPath");
params.remove("recordHarOmitContent");
if (recordHar != null) {
params.add("recordHar", recordHar);
} else if (options.recordHarOmitContent != null) {
throw new PlaywrightException("recordHarOmitContent is set but recordHarPath is null");
}
if (options.recordVideoDir != null) {
JsonObject recordVideo = new JsonObject();
@@ -179,8 +230,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
if (options.baseURL != null) {
context.setBaseUrl(options.baseURL);
}
context.recordHarPath = options.recordHarPath;
context.tracing().localUtils = localUtils;
context.setRecordHar(recordHarPath, harContentPolicy);
return context;
}
@@ -108,4 +108,10 @@ class ChannelOwner extends LoggingSupport {
void handleEvent(String event, JsonObject parameters) {
}
JsonObject toProtocolRef() {
JsonObject json = new JsonObject();
json.addProperty("guid", guid);
return json;
}
}
@@ -64,6 +64,7 @@ public class Connection {
String debug = System.getenv("DEBUG");
isLogging = (debug != null) && debug.contains("pw:channel");
}
LocalUtils localUtils;
class Root extends ChannelOwner {
Root(Connection connection) {
@@ -138,6 +139,10 @@ public class Connection {
return (PlaywrightImpl) this.root.initialize();
}
LocalUtils localUtils() {
return localUtils;
}
public <T> T getExistingObject(String guid) {
@SuppressWarnings("unchecked") T result = (T) objects.get(guid);
if (result == null)
@@ -270,7 +275,8 @@ public class Connection {
result = new JsonPipe(parent, type, guid, initializer);
break;
case "LocalUtils":
result = new LocalUtils(parent, type, guid, initializer);
localUtils = new LocalUtils(parent, type, guid, initializer);
result = localUtils;
break;
case "Page":
result = new PageImpl(parent, type, guid, initializer);
@@ -302,6 +308,9 @@ public class Connection {
case "Worker":
result = new WorkerImpl(parent, type, guid, initializer);
break;
case "WritableStream":
result = new WritableStream(parent, type, guid, initializer);
break;
default:
throw new PlaywrightException("Unknown type " + type);
}
@@ -33,7 +33,8 @@ import java.util.Base64;
import java.util.List;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.convertType;
import static com.microsoft.playwright.impl.Utils.*;
import static com.microsoft.playwright.impl.Utils.addLargeFileUploadParams;
import static com.microsoft.playwright.options.ScreenshotType.JPEG;
import static com.microsoft.playwright.options.ScreenshotType.PNG;
@@ -299,7 +300,7 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
}
@Override
public Frame ownerFrame() {
public FrameImpl ownerFrame() {
return withLogging("ElementHandle.ownerFrame", () -> {
JsonObject json = sendMessage("ownerFrame").getAsJsonObject();
if (!json.has("frame")) {
@@ -455,7 +456,24 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
@Override
public void setInputFiles(Path[] files, SetInputFilesOptions options) {
setInputFiles(Utils.toFilePayloads(files), options);
withLogging("ElementHandle.setInputFiles", () -> setInputFilesImpl(files, options));
}
void setInputFilesImpl(Path[] files, SetInputFilesOptions options) {
FrameImpl frame = ownerFrame();
if (frame == null) {
throw new Error("Cannot set input files to detached element");
}
if (hasLargeFile(files)) {
if (options == null) {
options = new SetInputFilesOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
addLargeFileUploadParams(files, params, frame.page().context());
sendMessage("setInputFilePaths", params);
} else {
setInputFilesImpl(Utils.toFilePayloads(files), options);
}
}
@Override
@@ -469,6 +487,7 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
}
void setInputFilesImpl(FilePayload[] files, SetInputFilesOptions options) {
checkFilePayloadSize(files);
if (options == null) {
options = new SetInputFilesOptions();
}
@@ -58,7 +58,8 @@ class FileChooserImpl implements FileChooser {
@Override
public void setFiles(Path[] files, SetFilesOptions options) {
setFiles(Utils.toFilePayloads(files), options);
page.withLogging("FileChooser.setInputFiles",
() -> element.setInputFilesImpl(files, convertType(options, ElementHandle.SetInputFilesOptions.class)));
}
@Override
@@ -69,6 +70,6 @@ class FileChooserImpl implements FileChooser {
@Override
public void setFiles(FilePayload[] files, SetFilesOptions options) {
page.withLogging("FileChooser.setInputFiles",
() -> element.setInputFilesImpl(files, convertType(options, ElementHandle.SetInputFilesOptions.class)));
() -> element.setInputFilesImpl(files, convertType(options, ElementHandle.SetInputFilesOptions.class)));
}
}
@@ -31,7 +31,7 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Utils.convertType;
import static com.microsoft.playwright.impl.Utils.*;
import static com.microsoft.playwright.options.WaitUntilState.*;
import static com.microsoft.playwright.impl.Serialization.*;
@@ -686,21 +686,32 @@ public class FrameImpl extends ChannelOwner implements Frame {
withLogging("Frame.setInputFiles", () -> setInputFilesImpl(selector, files, options));
}
void setInputFilesImpl(String selector, Path[] files, SetInputFilesOptions options) {
if (hasLargeFile(files)) {
if (options == null) {
options = new SetInputFilesOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
addLargeFileUploadParams(files, params, page.context());
params.addProperty("selector", selector);
sendMessage("setInputFilePaths", params);
} else {
setInputFilesImpl(selector, Utils.toFilePayloads(files), options);
}
}
@Override
public void setInputFiles(String selector, FilePayload files, SetInputFilesOptions options) {
setInputFiles(selector, new FilePayload[]{files}, options);
}
void setInputFilesImpl(String selector, Path[] files, SetInputFilesOptions options) {
setInputFiles(selector, Utils.toFilePayloads(files), options);
}
@Override
public void setInputFiles(String selector, FilePayload[] files, SetInputFilesOptions options) {
withLogging("Frame.setInputFiles", () -> setInputFilesImpl(selector, files, options));
}
void setInputFilesImpl(String selector, FilePayload[] files, SetInputFilesOptions options) {
checkFilePayloadSize(files);
if (options == null) {
options = new SetInputFilesOptions();
}
@@ -1024,12 +1035,25 @@ public class FrameImpl extends ChannelOwner implements Frame {
return result.get("value").getAsInt();
}
void highlightImpl(String selector) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
sendMessage("highlight", params);
}
protected void handleEvent(String event, JsonObject params) {
if ("loadstate".equals(event)) {
JsonElement add = params.get("add");
if (add != null) {
WaitUntilState state = loadStateFromProtocol(add.getAsString());
loadStates.add(state);
if (parentFrame == null && page != null) {
if (state == LOAD) {
page.listeners.notify(PageImpl.EventType.LOAD, page);
} else if (state == DOMCONTENTLOADED) {
page.listeners.notify(PageImpl.EventType.DOMCONTENTLOADED, page);
}
}
internalListeners.notify(InternalEventType.LOADSTATE, state);
}
JsonElement remove = params.get("remove");
@@ -0,0 +1,104 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Request;
import com.microsoft.playwright.Route;
import com.microsoft.playwright.options.HarNotFound;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Map;
import static com.microsoft.playwright.impl.LoggingSupport.logApi;
import static com.microsoft.playwright.impl.Serialization.fromNameValues;
import static com.microsoft.playwright.impl.Serialization.gson;
public class HARRouter {
private final LocalUtils localUtils;
private final HarNotFound defaultAction;
private final String harId;
HARRouter(LocalUtils localUtils, Path harFile, HarNotFound defaultAction) {
this.localUtils = localUtils;
this.defaultAction = defaultAction;
JsonObject params = new JsonObject();
params.addProperty("file", harFile.toString());
JsonObject json = localUtils.sendMessage("harOpen", params).getAsJsonObject();
if (json.has("error")) {
throw new PlaywrightException(json.get("error").getAsString());
}
harId = json.get("harId").getAsString();
}
void handle(Route route) {
Request request = route.request();
JsonObject params = new JsonObject();
params.addProperty("harId", harId);
params.addProperty("url", request.url());
params.addProperty("method", request.method());
params.add("headers", gson().toJsonTree(request.headersArray()));
if (request.postDataBuffer() != null) {
String base64 = Base64.getEncoder().encodeToString(request.postDataBuffer());
params.addProperty("postData", base64);
}
params.addProperty("isNavigationRequest", request.isNavigationRequest());
JsonObject response = localUtils.sendMessage("harLookup", params).getAsJsonObject();
String action = response.get("action").getAsString();
if ("redirect".equals(action)) {
String redirectURL = response.get("redirectURL").getAsString();
logApi("HAR: " + route.request().url() + " redirected to " + redirectURL);
((RouteImpl) route).redirectNavigationRequest(redirectURL);
return;
}
if ("fulfill".equals(action)) {
int status = response.get("status").getAsInt();
Map<String, String> headers = fromNameValues(response.getAsJsonArray("headers"));
byte[] buffer = Base64.getDecoder().decode(response.get("body").getAsString());
route.fulfill(new Route.FulfillOptions()
.setStatus(status)
.setHeaders(headers)
.setBodyBytes(buffer));
return;
}
if ("error".equals(action)) {
logApi("HAR: " + response.get("message").getAsString());
// Report the error, but fall through to the default handler.
}
if (defaultAction == HarNotFound.FALLBACK) {
route.fallback();
return;
}
// By default abort not matching requests.
route.abort();
}
void dispose() {
JsonObject params = new JsonObject();
params.addProperty("harId", harId);
localUtils.sendMessageAsync("harClose", params);
}
}
@@ -19,6 +19,7 @@ package com.microsoft.playwright.impl;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.assertions.LocatorAssertions;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
@@ -39,6 +40,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
public void containsText(String text, ContainsTextOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, text, "Locator expected to contain text", convertType(options, FrameExpectOptions.class));
@@ -47,6 +49,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
@Override
public void containsText(Pattern pattern, ContainsTextOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, pattern, "Locator expected to contain regex", convertType(options, FrameExpectOptions.class));
@@ -58,6 +61,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
for (String text : strings) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
list.add(expected);
@@ -70,6 +74,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
List<ExpectedTextValue> list = new ArrayList<>();
for (Pattern pattern : patterns) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
list.add(expected);
@@ -203,6 +208,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
public void hasText(String text, HasTextOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = false;
expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, text, "Locator expected to have text", convertType(options, FrameExpectOptions.class));
@@ -211,6 +217,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
@Override
public void hasText(Pattern pattern, HasTextOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
// Just match substring, same as containsText.
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
@@ -223,6 +230,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
for (String text : strings) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = false;
expected.normalizeWhiteSpace = true;
list.add(expected);
@@ -235,6 +243,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
List<ExpectedTextValue> list = new ArrayList<>();
for (Pattern pattern : patterns) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
list.add(expected);
@@ -255,9 +264,32 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expectImpl("to.have.value", expected, pattern, "Locator expected to have value matching regex", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasValues(String[] values, HasValuesOptions options) {
List<ExpectedTextValue> list = new ArrayList<>();
for (String text : values) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
list.add(expected);
}
expectImpl("to.have.values", list, values, "Locator expected to have values", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasValues(Pattern[] patterns, HasValuesOptions options) {
List<ExpectedTextValue> list = new ArrayList<>();
for (Pattern pattern : patterns) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.matchSubstring = true;
list.add(expected);
}
expectImpl("to.have.values", list, patterns, "Locator expected to have values matching regex", convertType(options, FrameExpectOptions.class));
}
@Override
public void isChecked(IsCheckedOptions options) {
expectTrue("to.be.checked", "Locator expected to be checked", convertType(options, FrameExpectOptions.class));
String expression = (options != null && options.checked != null && !options.checked) ? "to.be.unchecked" : "to.be.checked";
expectTrue(expression, "Locator expected to be checked", convertType(options, FrameExpectOptions.class));
}
@Override
@@ -304,5 +336,17 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
public LocatorAssertions not() {
return new LocatorAssertionsImpl(actualLocator, !isNot);
}
}
private static Boolean shouldIgnoreCase(Object options) {
if (options == null) {
return null;
}
try {
Field fromField = options.getClass().getDeclaredField("ignoreCase");
Object value = fromField.get(options);
return (Boolean) value;
} catch (NoSuchFieldException | IllegalAccessException e) {
return null;
}
}
}
@@ -1,6 +1,5 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
@@ -9,8 +8,11 @@ import com.microsoft.playwright.options.FilePayload;
import com.microsoft.playwright.options.SelectOption;
import com.microsoft.playwright.options.WaitForSelectorState;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
@@ -22,6 +24,43 @@ class LocatorImpl implements Locator {
private final FrameImpl frame;
private final String selector;
private static class Filters {
private final Map<Field, String> filterFieldToEngine = new LinkedHashMap<>();
private void addFilter(String name, String engine) throws NoSuchFieldException {
filterFieldToEngine.put(LocatorOptions.class.getField(name), engine);
}
{
try {
addFilter("has", "has");
// addFilter("leftOf", "left-of");
// addFilter("rightOf", "right-of");
// addFilter("above", "above");
// addFilter("below", "below");
// addFilter("near", "near");
} catch (NoSuchFieldException e) {
throw new InternalError(e);
}
}
String addFiltersToSelector(String selector, LocatorOptions options, Frame frame) {
try {
for (Map.Entry<Field, String> p : filterFieldToEngine.entrySet()) {
LocatorImpl filter = (LocatorImpl) p.getKey().get(options);
if (filter == null) {
continue;
}
if (filter.frame != frame) {
throw new PlaywrightException("Inner '" + p.getKey().getName() + "' locator must belong to the same frame.");
}
selector += " >> " + p.getValue() + "=" + gson().toJson(filter.selector);
}
} catch (IllegalAccessException e) {
throw new PlaywrightException("Unexpected options", e);
}
return selector;
}
}
private static final Filters filters = new Filters();
public LocatorImpl(FrameImpl frame, String selector, LocatorOptions options) {
this.frame = frame;
if (options != null) {
@@ -34,14 +73,7 @@ class LocatorImpl implements Locator {
selector += " >> :scope:has-text(" + escapeWithQuotes(text) + ")";
}
}
if (options.has != null) {
LocatorImpl has = (LocatorImpl) options.has;
if (has.frame != frame) {
throw new PlaywrightException("Inner 'has' locator must belong to the same frame.");
}
selector += " >> has=" + gson().toJson(has.selector);
}
selector = filters.addFiltersToSelector(selector, options, frame);
}
this.selector = selector;
}
@@ -170,6 +202,11 @@ class LocatorImpl implements Locator {
frame.fill(selector, value, convertType(options, Frame.FillOptions.class).setStrict(true));
}
@Override
public Locator filter(FilterOptions options) {
return new LocatorImpl(frame, selector, convertType(options,LocatorOptions.class));
}
@Override
public Locator first() {
return new LocatorImpl(frame, selector + " >> nth=0", null);
@@ -196,6 +233,11 @@ class LocatorImpl implements Locator {
return frame.getAttribute(selector, name, convertType(options, Frame.GetAttributeOptions.class).setStrict(true));
}
@Override
public void highlight() {
frame.highlightImpl(selector);
}
@Override
public void hover(HoverOptions options) {
if (options == null) {
@@ -466,6 +508,13 @@ class LocatorImpl implements Locator {
return frame.withLogging("Locator.expect", () -> expectImpl(expression, options));
}
JsonObject toProtocol() {
JsonObject result = new JsonObject();
result.add("frame", frame.toProtocolRef());
result.addProperty("selector", selector);
return result;
}
private FrameExpectResult expectImpl(String expression, FrameExpectOptions options) {
if (options == null) {
options = new FrameExpectOptions();
@@ -60,7 +60,7 @@ class LoggingSupport {
System.err.println(timestamp + " " + message);
}
private void logApi(String message) {
static void logApi(String message) {
// This matches log format produced by the server.
logWithTimestamp("pw:api " + message);
}
@@ -28,11 +28,11 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.convertType;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
import static com.microsoft.playwright.options.ScreenshotType.JPEG;
import static com.microsoft.playwright.options.ScreenshotType.PNG;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes;
import static java.util.Arrays.asList;
@@ -175,10 +175,6 @@ public class PageImpl extends ChannelOwner implements Page {
}
}
}
} else if ("load".equals(event)) {
listeners.notify(EventType.LOAD, this);
} else if ("domcontentloaded".equals(event)) {
listeners.notify(EventType.DOMCONTENTLOADED, this);
} else if ("frameAttached".equals(event)) {
String guid = params.getAsJsonObject("frame").get("guid").getAsString();
FrameImpl frame = connection.getExistingObject(guid);
@@ -198,11 +194,12 @@ public class PageImpl extends ChannelOwner implements Page {
}
listeners.notify(EventType.FRAMEDETACHED, frame);
} else if ("route".equals(event)) {
Route route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
boolean handled = routes.handle(route);
if (handled) {
RouteImpl route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
Router.HandleResult handled = routes.handle(route);
if (handled == Router.HandleResult.FoundMatchingHandler) {
maybeDisableNetworkInterception();
} else {
}
if (!route.isHandled()) {
browserContext.handleRoute(route);
}
} else if ("video".equals(event)) {
@@ -972,6 +969,21 @@ public class PageImpl extends ChannelOwner implements Page {
route(new UrlMatcher(url), handler, options);
}
@Override
public void routeFromHAR(Path har, RouteFromHAROptions options) {
if (options == null) {
options = new RouteFromHAROptions();
}
if (options.update != null && options.update) {
browserContext.recordIntoHar(this, har, convertType(options, BrowserContext.RouteFromHAROptions.class));
return;
}
UrlMatcher matcher = UrlMatcher.forOneOf(browserContext.baseUrl, options.url);
HARRouter harRouter = new HARRouter(connection.localUtils, har, options.notFound);
onClose(context -> harRouter.dispose());
route(matcher, route -> harRouter.handle(route), null);
}
private void route(UrlMatcher matcher, Consumer<Route> handler, RouteOptions options) {
withLogging("Page.route", () -> {
routes.add(matcher, handler, options == null ? null : options.times);
@@ -1034,8 +1046,18 @@ public class PageImpl extends ChannelOwner implements Page {
}
}
}
List<Locator> mask = options.mask;
options.mask = null;
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
options.mask = mask;
params.remove("path");
if (mask != null) {
JsonArray maskArray = new JsonArray();
for (Locator locator: mask) {
maskArray.add(((LocatorImpl) locator).toProtocol());
}
params.add("mask", maskArray);
}
JsonObject json = sendMessage("screenshot", params).getAsJsonObject();
byte[] buffer = Base64.getDecoder().decode(json.get("binary").getAsString());
@@ -43,6 +43,8 @@ class SerializedValue{
}
O[] o;
Number h;
Integer id;
Integer ref;
}
class SerializedArgument{
@@ -83,6 +85,7 @@ class ExpectedTextValue {
String string;
String regexSource;
String regexFlags;
Boolean ignoreCase;
Boolean matchSubstring;
Boolean normalizeWhiteSpace;
}
@@ -42,6 +42,14 @@ public class RequestImpl extends ChannelOwner implements Request {
String failure;
Timing timing;
boolean didFailOrFinish;
private FallbackOverrides fallbackOverrides;
static class FallbackOverrides {
String url;
String method;
byte[] postData;
Map<String, String> headers;
}
RequestImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
@@ -75,6 +83,9 @@ public class RequestImpl extends ChannelOwner implements Request {
@Override
public Map<String, String> headers() {
if (fallbackOverrides != null && fallbackOverrides.headers != null) {
return new RawHeaders(Utils.toHeadersList(fallbackOverrides.headers)).headers();
}
return headers.headers();
}
@@ -95,19 +106,26 @@ public class RequestImpl extends ChannelOwner implements Request {
@Override
public String method() {
if (fallbackOverrides != null && fallbackOverrides.method != null) {
return fallbackOverrides.method;
}
return initializer.get("method").getAsString();
}
@Override
public String postData() {
if (postData == null) {
byte[] buffer = postDataBuffer();
if (buffer == null) {
return null;
}
return new String(postData, StandardCharsets.UTF_8);
return new String(buffer, StandardCharsets.UTF_8);
}
@Override
public byte[] postDataBuffer() {
if (fallbackOverrides != null && fallbackOverrides.postData != null) {
return fallbackOverrides.postData;
}
return postData;
}
@@ -156,6 +174,9 @@ public class RequestImpl extends ChannelOwner implements Request {
@Override
public String url() {
if (fallbackOverrides != null && fallbackOverrides.url != null) {
return fallbackOverrides.url;
}
return initializer.get("url").getAsString();
}
@@ -164,6 +185,9 @@ public class RequestImpl extends ChannelOwner implements Request {
}
private RawHeaders getRawHeaders() {
if (fallbackOverrides != null && fallbackOverrides.headers != null) {
return new RawHeaders(Utils.toHeadersList(fallbackOverrides.headers));
}
if (rawHeaders != null) {
return rawHeaders;
}
@@ -176,4 +200,26 @@ public class RequestImpl extends ChannelOwner implements Request {
rawHeaders = new RawHeaders(asList(gson().fromJson(rawHeadersJson, HttpHeader[].class)));
return rawHeaders;
}
void applyFallbackOverrides(FallbackOverrides overrides) {
if (fallbackOverrides == null) {
fallbackOverrides = new FallbackOverrides();
}
if (overrides.url != null) {
fallbackOverrides.url = overrides.url;
}
if (overrides.method != null) {
fallbackOverrides.method = overrides.method;
}
if (overrides.headers != null) {
fallbackOverrides.headers = overrides.headers;
}
if (overrides.postData != null) {
fallbackOverrides.postData = overrides.postData;
}
}
FallbackOverrides fallbackOverridesForResume() {
return fallbackOverrides;
}
}
@@ -83,6 +83,11 @@ public class ResponseImpl extends ChannelOwner implements Response {
return request().frame();
}
@Override
public boolean fromServiceWorker() {
return initializer.get("fromServiceWorker").getAsBoolean();
}
@Override
public Map<String, String> headers() {
return headers.headers();
@@ -17,6 +17,7 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Route;
@@ -27,6 +28,8 @@ import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import static com.microsoft.playwright.impl.Utils.convertType;
public class RouteImpl extends ChannelOwner implements Route {
private boolean handled;
@@ -44,41 +47,69 @@ public class RouteImpl extends ChannelOwner implements Route {
});
}
boolean isHandled() {
return handled;
}
@Override
public void resume(ResumeOptions options) {
startHandling();
withLogging("Route.resume", () -> resumeImpl(options));
applyOverrides(convertType(options, FallbackOptions.class));
withLogging("Route.resume", () -> resumeImpl(request().fallbackOverridesForResume()));
}
private void resumeImpl(ResumeOptions options) {
@Override
public void fallback(FallbackOptions options) {
if (handled) {
throw new PlaywrightException("Route is already handled!");
}
applyOverrides(options);
}
private void applyOverrides(FallbackOptions options) {
if (options == null) {
options = new ResumeOptions();
}
JsonObject params = new JsonObject();
if (options.url != null) {
params.addProperty("url", options.url);
}
if (options.method != null) {
params.addProperty("method", options.method);
}
if (options.headers != null) {
params.add("headers", Serialization.toProtocol(options.headers));
return;
}
RequestImpl.FallbackOverrides overrides = new RequestImpl.FallbackOverrides();
overrides.url = options.url;
overrides.method = options.method;
overrides.headers = options.headers;
if (options.postData != null) {
byte[] bytes = null;
if (options.postData instanceof byte[]) {
bytes = (byte[]) options.postData;
} else if (options.postData instanceof String) {
bytes = ((String) options.postData).getBytes(StandardCharsets.UTF_8);
} else {
throw new PlaywrightException("postData must be either String or byte[], found: " + options.postData.getClass().getName());
overrides.postData = getPostDataBytes(options.postData);
}
request().applyFallbackOverrides(overrides);
}
private void resumeImpl(RequestImpl.FallbackOverrides options) {
JsonObject params = new JsonObject();
if (options != null) {
if (options.url != null) {
params.addProperty("url", options.url);
}
if (options.method != null) {
params.addProperty("method", options.method);
}
if (options.headers != null) {
params.add("headers", Serialization.toProtocol(options.headers));
}
if (options.postData != null) {
String base64 = Base64.getEncoder().encodeToString(options.postData);
params.addProperty("postData", base64);
}
String base64 = Base64.getEncoder().encodeToString(bytes);
params.addProperty("postData", base64);
}
sendMessageAsync("continue", params);
}
private static byte[] getPostDataBytes(Object postData) {
if (postData instanceof byte[]) {
return (byte[]) postData;
}
if (postData instanceof String) {
return ((String) postData).getBytes(StandardCharsets.UTF_8);
}
throw new PlaywrightException("postData must be either String or byte[], found: " + postData.getClass().getName());
}
@Override
public void fulfill(FulfillOptions options) {
startHandling();
@@ -168,6 +199,14 @@ public class RouteImpl extends ChannelOwner implements Route {
return connection.getExistingObject(initializer.getAsJsonObject("request").get("guid").getAsString());
}
void redirectNavigationRequest(String redirectURL) {
startHandling();
JsonObject params = new JsonObject();
params.addProperty("url", redirectURL);
// TODO: _raceWithPageClose ?
sendMessageAsync("redirectNavigationRequest", params);
}
private void startHandling() {
if (handled) {
throw new PlaywrightException("Route is already handled!");
@@ -19,6 +19,7 @@ package com.microsoft.playwright.impl;
import com.microsoft.playwright.Route;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -37,7 +38,7 @@ class Router {
this.times = times;
}
boolean handle(Route route) {
boolean handle(RouteImpl route) {
if (times != null && times <= 0) {
return false;
}
@@ -70,15 +71,21 @@ class Router {
return routes.size();
}
boolean handle(Route route) {
for (RouteInfo info : routes) {
enum HandleResult { NoMatchingHandler, FoundMatchingHandler}
HandleResult handle(RouteImpl route) {
HandleResult result = HandleResult.NoMatchingHandler;
for (Iterator<RouteInfo> it = routes.iterator(); it.hasNext();) {
RouteInfo info = it.next();
if (info.handle(route)) {
result = HandleResult.FoundMatchingHandler;
if (info.isDone()) {
routes.remove(info);
it.remove();
}
if (route.isHandled()) {
break;
}
return true;
}
}
return false;
return result;
}
}
@@ -31,6 +31,9 @@ import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
class Serialization {
private static final Gson gson = new GsonBuilder()
@@ -40,7 +43,11 @@ class Serialization {
.registerTypeAdapter(Media.class, new ToLowerCaseSerializer<Media>())
.registerTypeAdapter(ForcedColors.class, new ToLowerCaseSerializer<ForcedColors>())
.registerTypeAdapter(ReducedMotion.class, new ToLowerCaseAndDashSerializer<ReducedMotion>())
.registerTypeAdapter(ScreenshotAnimations.class, new ToLowerCaseSerializer<ScreenshotAnimations>())
.registerTypeAdapter(ScreenshotType.class, new ToLowerCaseSerializer<ScreenshotType>())
.registerTypeAdapter(ScreenshotScale.class, new ToLowerCaseSerializer<ScreenshotScale>())
.registerTypeAdapter(ScreenshotCaret.class, new ToLowerCaseSerializer<ScreenshotCaret>())
.registerTypeAdapter(ServiceWorkerPolicy.class, new ToLowerCaseAndDashSerializer<ServiceWorkerPolicy>())
.registerTypeAdapter(MouseButton.class, new ToLowerCaseSerializer<MouseButton>())
.registerTypeAdapter(LoadState.class, new ToLowerCaseSerializer<LoadState>())
.registerTypeAdapter(WaitUntilState.class, new ToLowerCaseSerializer<WaitUntilState>())
@@ -68,82 +75,128 @@ class Serialization {
return result;
}
private static SerializedValue serializeValue(Object value, List<JSHandleImpl> handles, int depth) {
if (depth > 100) {
throw new PlaywrightException("Maximum argument depth exceeded");
private static class ValueSerializer {
// hashCode() of a map containing itself as a key will throw stackoverflow exception,
// so we user wrappers.
private static class HashableValue {
final Object value;
HashableValue(Object value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
return value == ((HashableValue) o).value;
}
@Override
public int hashCode() {
return System.identityHashCode(value);
}
}
SerializedValue result = new SerializedValue();
if (value instanceof JSHandleImpl) {
result.h = handles.size();
handles.add((JSHandleImpl) value);
private final Map<HashableValue, Integer> valueToId = new HashMap<>();
private int lastId = 0;
private final List<JSHandleImpl> handles = new ArrayList<>();
private final SerializedValue serializedValue;
ValueSerializer(Object value) {
serializedValue = serializeValue(value);
}
SerializedArgument toSerializedArgument() {
SerializedArgument result = new SerializedArgument();
result.value = serializedValue;
result.handles = new Channel[handles.size()];
int i = 0;
for (JSHandleImpl handle : handles) {
result.handles[i] = new Channel();
result.handles[i].guid = handle.guid;
++i;
}
return result;
}
if (value == null) {
result.v = "undefined";
} else if (value instanceof Double) {
double d = ((Double) value);
if (d == Double.POSITIVE_INFINITY) {
result.v = "Infinity";
} else if (d == Double.NEGATIVE_INFINITY) {
result.v = "-Infinity";
} else if (d == -0) {
result.v = "-0";
} else if (Double.isNaN(d)) {
result.v = "NaN";
private SerializedValue serializeValue(Object value) {
SerializedValue result = new SerializedValue();
if (value instanceof JSHandleImpl) {
result.h = handles.size();
handles.add((JSHandleImpl) value);
return result;
}
if (value == null) {
result.v = "undefined";
} else if (value instanceof Double) {
double d = ((Double) value);
if (d == Double.POSITIVE_INFINITY) {
result.v = "Infinity";
} else if (d == Double.NEGATIVE_INFINITY) {
result.v = "-Infinity";
} else if (d == -0) {
result.v = "-0";
} else if (Double.isNaN(d)) {
result.v = "NaN";
} else {
result.n = d;
}
} else if (value instanceof Boolean) {
result.b = (Boolean) value;
} else if (value instanceof Integer) {
result.n = (Integer) value;
} else if (value instanceof String) {
result.s = (String) value;
} else {
result.n = d;
HashableValue mapKey = new HashableValue(value);
Integer id = valueToId.get(mapKey);
if (id != null) {
result.ref = id;
} else {
result.id = ++lastId;
valueToId.put(mapKey, lastId);
if (value instanceof List) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (List<?>) value) {
list.add(serializeValue(o));
}
result.a = list.toArray(new SerializedValue[0]);
} else if (value instanceof Map) {
List<SerializedValue.O> list = new ArrayList<>();
@SuppressWarnings("unchecked")
Map<String, ?> map = (Map<String, ?>) value;
for (Map.Entry<String, ?> e : map.entrySet()) {
SerializedValue.O o = new SerializedValue.O();
o.k = e.getKey();
o.v = serializeValue(e.getValue());
list.add(o);
}
result.o = list.toArray(new SerializedValue.O[0]);
} else if (value instanceof Object[]) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (Object[]) value) {
list.add(serializeValue(o));
}
result.a = list.toArray(new SerializedValue[0]);
} else {
throw new PlaywrightException("Unsupported type of argument: " + value);
}
}
}
} else if (value instanceof Boolean) {
result.b = (Boolean) value;
} else if (value instanceof Integer) {
result.n = (Integer) value;
} else if (value instanceof String) {
result.s = (String) value;
} else if (value instanceof List) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (List<?>) value) {
list.add(serializeValue(o, handles, depth + 1));
}
result.a = list.toArray(new SerializedValue[0]);
} else if (value instanceof Map) {
List<SerializedValue.O> list = new ArrayList<>();
@SuppressWarnings("unchecked")
Map<String, ?> map = (Map<String, ?>) value;
for (Map.Entry<String, ?> e : map.entrySet()) {
SerializedValue.O o = new SerializedValue.O();
o.k = e.getKey();
o.v = serializeValue(e.getValue(), handles, depth + 1);
list.add(o);
}
result.o = list.toArray(new SerializedValue.O[0]);
} else if (value instanceof Object[]) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (Object[]) value) {
list.add(serializeValue(o, handles, depth + 1));
}
result.a = list.toArray(new SerializedValue[0]);
} else {
throw new PlaywrightException("Unsupported type of argument: " + value);
return result;
}
return result;
}
static SerializedArgument serializeArgument(Object arg) {
SerializedArgument result = new SerializedArgument();
List<JSHandleImpl> handles = new ArrayList<>();
result.value = serializeValue(arg, handles, 0);
result.handles = new Channel[handles.size()];
int i = 0;
for (JSHandleImpl handle : handles) {
result.handles[i] = new Channel();
result.handles[i].guid = handle.guid;
++i;
}
return result;
return new ValueSerializer(arg).toSerializedArgument();
}
static <T> T deserialize(SerializedValue value) {
return deserialize(value, new HashMap<>());
}
@SuppressWarnings("unchecked")
static <T> T deserialize(SerializedValue value) {
private static <T> T deserialize(SerializedValue value, Map<Integer, Object> idToValue) {
if (value.ref != null) {
return (T) idToValue.get(value.ref);
}
if (value.n != null) {
if (value.n.doubleValue() == (double) value.n.intValue()) {
return (T) Integer.valueOf(value.n.intValue());
@@ -174,15 +227,17 @@ class Serialization {
}
if (value.a != null) {
List<Object> list = new ArrayList<>();
idToValue.put(value.id, list);
for (SerializedValue v : value.a) {
list.add(deserialize(v));
list.add(deserialize(v, idToValue));
}
return (T) list;
}
if (value.o != null) {
Map<String, Object> map = new LinkedHashMap<>();
idToValue.put(value.id, map);
for (SerializedValue.O o : value.o) {
map.put(o.k, deserialize(o.v));
map.put(o.k, deserialize(o.v, idToValue));
}
return (T) map;
}
@@ -209,6 +264,14 @@ class Serialization {
}
}
static JsonArray toJsonArray(Path[] files) {
JsonArray jsonFiles = new JsonArray();
for (Path p : files) {
jsonFiles.add(p.toAbsolutePath().toString());
}
return jsonFiles;
}
static JsonArray toJsonArray(FilePayload[] files) {
JsonArray jsonFiles = new JsonArray();
for (FilePayload p : files) {
@@ -228,9 +291,7 @@ class Serialization {
static JsonArray toProtocol(ElementHandle[] handles) {
JsonArray jsonElements = new JsonArray();
for (ElementHandle handle : handles) {
JsonObject jsonHandle = new JsonObject();
jsonHandle.addProperty("guid", ((ElementHandleImpl) handle).guid);
jsonElements.add(jsonHandle);
jsonElements.add(((ElementHandleImpl) handle).toProtocolRef());
}
return jsonElements;
}
@@ -239,6 +300,16 @@ class Serialization {
return toNameValueArray(map);
}
static void addHarUrlFilter(JsonObject options, Object urlFilter) {
if (urlFilter instanceof String) {
options.addProperty("urlGlob", (String) urlFilter);
} else if (urlFilter instanceof Pattern) {
Pattern pattern = (Pattern) urlFilter;
options.addProperty("urlRegexSource", pattern.pattern());
options.addProperty("urlRegexFlags", toJsRegexFlags(pattern));
}
}
static JsonArray toNameValueArray(Map<String, ?> map) {
JsonArray array = new JsonArray();
for (Map.Entry<String, ?> e : map.entrySet()) {
@@ -250,6 +321,15 @@ class Serialization {
return array;
}
static Map<String, String> fromNameValues(JsonArray array) {
Map<String, String> map = new LinkedHashMap<>();
for (JsonElement element : array) {
JsonObject pair = element.getAsJsonObject();
map.put(pair.get("name").getAsString(), pair.get("value").getAsString());
}
return map;
}
static List<String> parseStringList(JsonArray array) {
List<String> result = new ArrayList<>();
for (JsonElement e : array) {
@@ -280,9 +360,7 @@ class Serialization {
private static class HandleSerializer implements JsonSerializer<JSHandleImpl> {
@Override
public JsonElement serialize(JSHandleImpl src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject json = new JsonObject();
json.addProperty("guid", src.guid);
return json;
return src.toProtocolRef();
}
}
@@ -26,9 +26,7 @@ import java.nio.file.Path;
import static com.microsoft.playwright.impl.Serialization.gson;
class TracingImpl extends ChannelOwner implements Tracing {
LocalUtils localUtils;
boolean isRemote;
private boolean includeSources;
TracingImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
@@ -61,7 +59,7 @@ class TracingImpl extends ChannelOwner implements Tracing {
// Add local sources to the remote trace if necessary.
if (isRemote && json.has("sourceEntries")) {
JsonArray entries = json.getAsJsonArray("sourceEntries");
localUtils.zip(path, entries);
connection.localUtils.zip(path, entries);
}
}
@@ -90,7 +88,7 @@ class TracingImpl extends ChannelOwner implements Tracing {
options = new StartOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
includeSources = options.sources != null;
boolean includeSources = options.sources != null && options.sources;
if (includeSources) {
if (!connection.isCollectingStacks()) {
throw new PlaywrightException("Source root directory must be specified via PLAYWRIGHT_JAVA_SRC environment variable when source collection is enabled");
@@ -16,6 +16,8 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.FilePayload;
import com.microsoft.playwright.options.HttpHeader;
@@ -23,6 +25,7 @@ import com.microsoft.playwright.options.HttpHeader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
@@ -30,6 +33,8 @@ import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.toJsonArray;
class Utils {
static <F, T> T convertType(F f, Class<T> t) {
if (f == null) {
@@ -144,6 +149,56 @@ class Utils {
return mimeType;
}
static final int maxUplodBufferSize = 50 * 1024 * 1024;
static boolean hasLargeFile(Path[] files) {
for (Path file: files) {
try {
if (Files.size(file)> maxUplodBufferSize) {
return true;
}
} catch (IOException e) {
throw new PlaywrightException("Cannot get file size.", e);
}
}
return false;
}
static void addLargeFileUploadParams(Path[] files, JsonObject params, BrowserContextImpl context) {
if (context.browser().isRemote) {
List<WritableStream> streams = new ArrayList<>();
JsonArray jsonStreams = new JsonArray();
for (Path path : files) {
WritableStream temp = context.createTempFile(path.getFileName().toString());
streams.add(temp);
try (OutputStream out = temp.stream()) {
Files.copy(path, out);
} catch (IOException e) {
throw new PlaywrightException("Failed to copy file to remote server.", e);
}
jsonStreams.add(temp.toProtocolRef());
}
params.add("streams", jsonStreams);
} else {
Path[] absolute = Arrays.stream(files).map(f -> {
try {
return f.toRealPath();
} catch (IOException e) {
throw new PlaywrightException("Cannot get absolute file path", e);
}
}).toArray(Path[]::new);
params.add("localPaths", toJsonArray(absolute));
}
}
static void checkFilePayloadSize(FilePayload[] files) {
for (FilePayload file: files) {
if (file.buffer.length > maxUplodBufferSize) {
throw new PlaywrightException("Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead.");
}
}
}
static FilePayload[] toFilePayloads(Path[] files) {
List<FilePayload> payloads = new ArrayList<>();
for (Path file : files) {
@@ -222,6 +277,17 @@ class Utils {
return map;
}
static List<HttpHeader> toHeadersList(Map<String, String> headers) {
List<HttpHeader> list = new ArrayList<>();
for (Map.Entry<String, String> entry: headers.entrySet()) {
HttpHeader header = new HttpHeader();
header.name = entry.getKey();
header.value = entry.getValue();
list.add(header);
}
return list;
}
static String toJsRegexFlags(Pattern pattern) {
String regexFlags = "";
if ((pattern.flags() & Pattern.CASE_INSENSITIVE) != 0) {
@@ -0,0 +1,33 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
class WritableStream extends ChannelOwner {
WritableStream(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
OutputStream stream() {
return new OutputStream() {
@Override
public void write(int b) throws IOException {
write(new byte[] { (byte) b });
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
JsonObject params = new JsonObject();
ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
ByteBuffer encoded = Base64.getEncoder().encode(buffer);
params.addProperty("binary", new String(encoded.array(), StandardCharsets.UTF_8));
sendMessage("write", params);
}
};
}
}
@@ -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 enum HarContentPolicy {
OMIT,
EMBED,
ATTACH
}
@@ -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 HarMode {
FULL,
MINIMAL
}
@@ -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 HarNotFound {
ABORT,
FALLBACK
}
@@ -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 ScreenshotAnimations {
DISABLED,
ALLOW
}
@@ -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 ScreenshotCaret {
HIDE,
INITIAL
}
@@ -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 ScreenshotScale {
CSS,
DEVICE
}
@@ -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 ServiceWorkerPolicy {
ALLOW,
BLOCK
}
@@ -0,0 +1,66 @@
package com.microsoft.playwright;
import com.sun.net.httpserver.HttpExchange;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MultipartFormData {
static MultipartFormData parseRequest(HttpExchange exchange) throws IOException {
ByteArrayOutputStream bodyBytes = new ByteArrayOutputStream();
try (OutputStream output = bodyBytes) {
Utils.copy(exchange.getRequestBody(), output);
}
String body = new String(bodyBytes.toByteArray(), StandardCharsets.UTF_8);
String contentType = exchange.getRequestHeaders().get("content-type").get(0);
Matcher matcher = Pattern.compile("boundary=(.*)$").matcher(contentType);
if (!matcher.find()) {
throw new RuntimeException("Boundary not found!");
}
String boundary = matcher.group(1);
return new MultipartFormData(body, boundary);
}
static class Field {
final String filename;
final String content;
Field(String filename, String content) {
this.filename = filename;
this.content = content;
}
}
final List<Field> fields = new ArrayList<>();
MultipartFormData(String body, String boundary) {
String[] parts = Pattern.compile("--" + boundary + "(--)?\r\n", Pattern.MULTILINE).split(body);
for (String part : parts) {
if (part.trim().length() == 0) {
continue;
}
String[] headersAndContent = Pattern.compile("\r\n\r\n", Pattern.MULTILINE).split(part);
if (headersAndContent.length != 2) {
throw new RuntimeException("Unexpected format: " + part);
}
String headers = headersAndContent[0];
String filename = null;
for (String header: Pattern.compile("\r\n", Pattern.MULTILINE).split(headers)) {
Matcher matcher = Pattern.compile("content-disposition: .*filename=\"([^\"]+)\"", Pattern.CASE_INSENSITIVE).matcher(header);
if (!matcher.find()) {
continue;
}
filename = matcher.group(1);
}
String content = headersAndContent[1];
content = content.substring(0, content.length() - "\r\n".length());
fields.add(new Field(filename, content));
}
}
}
@@ -97,4 +97,9 @@ public class TestBrowser extends TestBase {
assertNotNull(browser);
browser.close();
}
@Test
void shouldReturnBrowserType() {
assertEquals(browserType, browser.browserType());
}
}
@@ -346,7 +346,21 @@ public class TestBrowserContextAddCookies extends TestBase {
"}", server.CROSS_PROCESS_PREFIX + "/grid.html");
page.frames().get(1).evaluate("document.cookie = 'username=John Doe'");
page.waitForTimeout(2000);
boolean allowsThirdParty = isFirefox();
List<Cookie> cookies = context.cookies(server.CROSS_PROCESS_PREFIX + "/grid.html");
assertEquals(0, cookies.size());
if (allowsThirdParty) {
assertJsonEquals("[{\n" +
" 'domain': '127.0.0.1',\n" +
" 'expires': -1,\n" +
" 'httpOnly': false,\n" +
" 'name': 'username',\n" +
" 'path': '/',\n" +
" 'sameSite': 'NONE',\n" +
" 'secure': false,\n" +
" 'value': 'John Doe'\n" +
"}]", cookies);
} else {
assertEquals(0, cookies.size());
}
}
}
@@ -50,7 +50,7 @@ public class TestBrowserContextCookies extends TestBase {
" expires: -1,\n" +
" httpOnly: false,\n" +
" secure: false,\n" +
" sameSite: '" + (isChromium() || isFirefox() ? "LAX" : "NONE") +"'\n" +
" sameSite: '" + (isChromium() ? "LAX" : "NONE") +"'\n" +
" }]", cookies);
}
@@ -65,16 +65,22 @@ public class TestBrowserContextCookies extends TestBase {
" return document.cookie;\n" +
" }");
assertEquals("username=John Doe", documentCookie);
int timestamp = (Integer) page.evaluate("+(new Date('1/1/2038'))/1000");
Cookie cookie = context.cookies().get(0);
assertEquals("username", cookie.name);
assertEquals("John Doe", cookie.value);
assertEquals("localhost", cookie.domain);
assertEquals("/", cookie.path);
assertEquals(timestamp, cookie.expires);
// Browsers start to cap cookies with 400 days max expires value.
// See https://github.com/httpwg/http-extensions/pull/1732
// Chromium patch: https://chromium.googlesource.com/chromium/src/+/aaa5d2b55478eac2ee642653dcd77a50ac3faff6
// We want to make sure that expires date is at least 400 days in future.
Double timestamp = (Double) page.evaluate("const FOUR_HUNDRED_DAYS = 1000 * 60 * 60 * 24 * 400;\n" +
" const FIVE_MINUTES = 1000 * 60 * 5; // relax condition a bit to make sure test is not flaky.\n" +
" (Date.now() + FOUR_HUNDRED_DAYS - FIVE_MINUTES) / 1000;");
assertTrue(cookie.expires > timestamp, cookie.expires + " > " + timestamp + " failed.");
assertEquals(false, cookie.httpOnly);
assertEquals(false, cookie.secure);
if (isChromium() || isFirefox()) {
if (isChromium()) {
assertEquals(SameSiteAttribute.LAX, cookie.sameSite);
} else {
assertEquals(SameSiteAttribute.NONE, cookie.sameSite);
@@ -146,7 +152,7 @@ public class TestBrowserContextCookies extends TestBase {
" expires: -1,\n" +
" httpOnly: false,\n" +
" secure: false,\n" +
" sameSite: '" + (isChromium() || isFirefox() ? "LAX" : "NONE") +"'\n" +
" sameSite: '" + (isChromium() ? "LAX" : "NONE") +"'\n" +
" },\n" +
" {\n" +
" name: 'username',\n" +
@@ -156,7 +162,7 @@ public class TestBrowserContextCookies extends TestBase {
" expires: -1,\n" +
" httpOnly: false,\n" +
" secure: false,\n" +
" sameSite: '" + (isChromium() || isFirefox() ? "LAX" : "NONE") +"'\n" +
" sameSite: '" + (isChromium() ? "LAX" : "NONE") +"'\n" +
" }\n" +
"]", cookies);
}
@@ -0,0 +1,458 @@
/*
* 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.HarContentPolicy;
import com.microsoft.playwright.options.HarMode;
import com.microsoft.playwright.options.HarNotFound;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.regex.Pattern;
import static com.microsoft.playwright.Utils.copy;
import static com.microsoft.playwright.Utils.extractZip;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static com.microsoft.playwright.options.HarContentPolicy.ATTACH;
import static org.junit.jupiter.api.Assertions.*;
public class TestBrowserContextHar extends TestBase {
@Test
void shouldContextRouteFromHARMatchingTheMethodAndFollowingRedirects() {
Path path = Paths.get("src/test/resources/har-fulfill.har");
context.routeFromHAR(path);
Page page = context.newPage();
page.navigate("http://no.playwright/");
// HAR contains a redirect for the script that should be followed automatically.
assertEquals("foo", page.evaluate("window.value"));
// HAR contains a POST for the css file that should not be used.
assertThat(page.locator("body")).hasCSS("background-color", "rgb(255, 0, 0)");
}
@Test
void shouldPageRouteFromHARMatchingTheMethodAndFollowingRedirects() {
Path path = Paths.get("src/test/resources/har-fulfill.har");
Page page = context.newPage();
page.routeFromHAR(path);
page.navigate("http://no.playwright/");
// HAR contains a redirect for the script that should be followed automatically.
assertEquals("foo", page.evaluate("window.value"));
// HAR contains a POST for the css file that should not be used.
assertThat(page.locator("body")).hasCSS("background-color", "rgb(255, 0, 0)");
}
@Test
void fallbackContinueShouldContinueWhenNotFoundInHar() {
Path path = Paths.get("src/test/resources/har-fulfill.har");
context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setNotFound(HarNotFound.FALLBACK));
Page page = context.newPage();
page.navigate(server.PREFIX + "/one-style.html");
assertThat(page.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)");
}
@Test
void byDefaultShouldAbortRequestsNotFoundInHar() {
Path path = Paths.get("src/test/resources/har-fulfill.har");
context.routeFromHAR(path);
Page page = context.newPage();
try {
page.navigate(server.EMPTY_PAGE);
fail("did not throw");
} catch (PlaywrightException e) {
}
}
@Test
void fallbackContinueShouldContinueRequestsOnBadHar(@TempDir Path tmpDir) throws IOException {
Path path = tmpDir.resolve("test.har");
try (Writer stream = new OutputStreamWriter(Files.newOutputStream(path))) {
stream.write("{ \"log\" : {} }");
}
context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setNotFound(HarNotFound.FALLBACK));
Page page = context.newPage();
page.navigate(server.PREFIX + "/one-style.html");
assertThat(page.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)");
}
@Test
void shouldOnlyHandleRequestsMatchingUrlFilter() {
Path path = Paths.get("src/test/resources/har-fulfill.har");
context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setNotFound(HarNotFound.FALLBACK).setUrl("**/*.js"));
Page page = context.newPage();
context.route("http://no.playwright/", route -> {
assertEquals("http://no.playwright/", route.request().url());
route.fulfill(new Route.FulfillOptions()
.setStatus(200)
.setContentType("text/html")
.setBody("<script src='./script.js'></script><div>hello</div>"));
});
page.navigate("http://no.playwright/");
// HAR contains a redirect for the script that should be followed automatically.
assertEquals("foo", page.evaluate("window.value"));
assertThat(page.locator("body")).hasCSS("background-color", "rgba(0, 0, 0, 0)");
}
@Test
void shouldOnlyContextRouteFromHARRequestsMatchingUrlFilter() {
Path path = Paths.get("src/test/resources/har-fulfill.har");
context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setUrl("**/*.js"));
Page page = context.newPage();
context.route("http://no.playwright/", route -> {
assertEquals("http://no.playwright/", route.request().url());
route.fulfill(new Route.FulfillOptions()
.setStatus(200)
.setContentType("text/html")
.setBody("<script src='./script.js'></script><div>hello</div>"));
});
page.navigate("http://no.playwright/");
// HAR contains a redirect for the script that should be followed automatically.
assertEquals("foo", page.evaluate("window.value"));
assertThat(page.locator("body")).hasCSS("background-color", "rgba(0, 0, 0, 0)");
}
@Test
void shouldOnlyPageRouteFromHARRequestsMatchingUrlFilter() {
Path path = Paths.get("src/test/resources/har-fulfill.har");
Page page = context.newPage();
page.routeFromHAR(path, new Page.RouteFromHAROptions().setUrl("**/*.js"));
context.route("http://no.playwright/", route -> {
assertEquals("http://no.playwright/", route.request().url());
route.fulfill(new Route.FulfillOptions()
.setStatus(200)
.setContentType("text/html")
.setBody("<script src='./script.js'></script><div>hello</div>"));
});
page.navigate("http://no.playwright/");
// HAR contains a redirect for the script that should be followed automatically.
assertEquals("foo", page.evaluate("window.value"));
assertThat(page.locator("body")).hasCSS("background-color", "rgba(0, 0, 0, 0)");
}
@Test
void shouldSupportRegexFilter() {
Path path = Paths.get("src/test/resources/har-fulfill.har");
context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setUrl(Pattern.compile(".*(\\.js|.*\\.css|no.playwright\\/)$")));
Page page = context.newPage();
page.navigate("http://no.playwright/");
assertEquals("foo", page.evaluate("window.value"));
assertThat(page.locator("body")).hasCSS("background-color", "rgb(255, 0, 0)");
}
@Test
void newPageShouldFulfillFromHarMatchingTheMethodAndFollowingRedirects() {
Path path = Paths.get("src/test/resources/har-fulfill.har");
Page page = browser.newPage();
page.routeFromHAR(path);
page.navigate("http://no.playwright/");
// HAR contains a redirect for the script that should be followed automatically.
assertEquals("foo", page.evaluate("window.value"));
// HAR contains a POST for the css file that should not be used.
assertThat(page.locator("body")).hasCSS("background-color", "rgb(255, 0, 0)");
page.close();
}
@Test
void shouldChangeDocumentURLAfterRedirectedNavigation() {
Path path = Paths.get("src/test/resources/har-redirect.har");
context.routeFromHAR(path);
Page page = context.newPage();
Response response = page.waitForNavigation(() -> {
page.navigate("https://theverge.com/");
page.waitForURL("https://www.theverge.com/");
});
assertThat(page).hasURL("https://www.theverge.com/");
assertEquals("https://www.theverge.com/", response.request().url());
assertEquals("https://www.theverge.com/", page.evaluate("location.href"));
}
@Test
void shouldChangeDocumentURLAfterRedirectedNavigationOnClick() {
Path path = Paths.get("src/test/resources/har-redirect.har");
context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setUrl(Pattern.compile(".*theverge.*")));
Page page = context.newPage();
page.navigate(server.EMPTY_PAGE);
page.setContent("<a href='https://theverge.com/'>click me</a>");
Response response = page.waitForNavigation(() -> page.click("text=click me"));
assertThat(page).hasURL("https://www.theverge.com/");
assertEquals("https://www.theverge.com/", response.request().url());
assertEquals("https://www.theverge.com/", page.evaluate("location.href"));
}
@Test
void shouldGoBackToRedirectedNavigation() {
Path path = Paths.get("src/test/resources/har-redirect.har");
context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setUrl(Pattern.compile(".*theverge.*")));
Page page = context.newPage();
page.navigate("https://theverge.com/");
page.navigate(server.EMPTY_PAGE);
assertThat(page).hasURL(server.EMPTY_PAGE);
Response response = page.goBack();
assertThat(page).hasURL("https://www.theverge.com/");
assertEquals("https://www.theverge.com/", response.request().url());
assertEquals("https://www.theverge.com/", page.evaluate("location.href"));
}
@Test
@DisabledIf(value="isFirefox", disabledReason="Flaky in Firefox, upstream as well")
void shouldGoForwardToRedirectedNavigation() {
Path path = Paths.get("src/test/resources/har-redirect.har");
context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setUrl(Pattern.compile(".*theverge.*")));
Page page = context.newPage();
page.navigate(server.EMPTY_PAGE);
assertThat(page).hasURL(server.EMPTY_PAGE);
page.navigate("https://theverge.com/");
assertThat(page).hasURL("https://www.theverge.com/");
page.goBack();
assertThat(page).hasURL(server.EMPTY_PAGE);
Response response = page.goForward();
assertThat(page).hasURL("https://www.theverge.com/");
assertEquals("https://www.theverge.com/", response.request().url());
assertEquals("https://www.theverge.com/", page.evaluate("() => location.href"));
}
@Test
void shouldReloadRedirectedNavigation() {
Path path = Paths.get("src/test/resources/har-redirect.har");
context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setUrl(Pattern.compile(".*theverge.*")));
Page page = context.newPage();
page.navigate("https://theverge.com/");
assertThat(page).hasURL("https://www.theverge.com/");
Response response = page.reload();
assertThat(page).hasURL("https://www.theverge.com/");
assertEquals("https://www.theverge.com/", response.request().url());
assertEquals("https://www.theverge.com/", page.evaluate("() => location.href"));
}
@Test
void shouldFulfillFromHarWithContentInAFile() {
Path path = Paths.get("src/test/resources/har-sha1.har");
context.routeFromHAR(path);
Page page = context.newPage();
page.navigate("http://no.playwright/");
assertEquals("<html><head></head><body>Hello, world</body></html>", page.content());
}
@Test
void shouldRoundTripHarZip(@TempDir Path tmpDir) {
Path harPath = tmpDir.resolve("har.zip");
try (BrowserContext context1 = browser.newContext(new Browser.NewContextOptions()
.setRecordHarPath(harPath)
.setRecordHarMode(HarMode.MINIMAL))) {
Page page1 = context1.newPage();
page1.navigate(server.PREFIX + "/one-style.html");
}
try (BrowserContext context2 = browser.newContext()) {
context2.routeFromHAR(harPath, new BrowserContext.RouteFromHAROptions().setNotFound(HarNotFound.ABORT));
Page page2 = context2.newPage();
page2.navigate(server.PREFIX + "/one-style.html");
assertTrue(page2.content().contains("hello, world!"));
assertThat(page2.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)");
}
}
@Test
void shouldProduceExtractedZip(@TempDir Path tmpDir) throws IOException {
Path harPath = tmpDir.resolve("har.har");
try (BrowserContext context1 = browser.newContext(new Browser.NewContextOptions()
.setRecordHarPath(harPath)
.setRecordHarMode(HarMode.MINIMAL)
.setRecordHarContent(ATTACH))) {
Page page1 = context1.newPage();
page1.navigate(server.PREFIX + "/one-style.html");
}
assertTrue(Files.exists(harPath));
String har = new String(Files.readAllBytes(harPath), StandardCharsets.UTF_8);
assertFalse(har.contains("background-color"));
try (BrowserContext context2 = browser.newContext()) {
context2.routeFromHAR(harPath, new BrowserContext.RouteFromHAROptions().setNotFound(HarNotFound.ABORT));
Page page2 = context2.newPage();
page2.navigate(server.PREFIX + "/one-style.html");
assertTrue(page2.content().contains("hello, world!"));
assertThat(page2.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)");
}
}
@Test
void shouldRoundTripExtractedHarZip(@TempDir Path tmpDir) throws IOException {
Path harPath = tmpDir.resolve("har.zip");
try (BrowserContext context1 = browser.newContext(new Browser.NewContextOptions()
.setRecordHarPath(harPath)
.setRecordHarMode(HarMode.MINIMAL))) {
Page page1 = context1.newPage();
page1.navigate(server.PREFIX + "/one-style.html");
}
Path harDir = tmpDir.resolve("hardir");
extractZip(harPath, harDir);
try (BrowserContext context2 = browser.newContext()) {
context2.routeFromHAR(harDir.resolve("har.har"));
Page page2 = context2.newPage();
page2.navigate(server.PREFIX + "/one-style.html");
assertTrue(page2.content().contains("hello, world!"));
assertThat(page2.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)");
}
}
@Test
void shouldRoundTripHarWithPostData(@TempDir Path tmpDir) {
server.setRoute("/echo", exchange -> {
exchange.sendResponseHeaders(200, 0);
try (OutputStream out = exchange.getResponseBody()) {
copy(exchange.getRequestBody(), out);
}
});
String fetchFunction = "async body => {\n" +
" const response = await fetch('/echo', { method: 'POST', body });\n" +
" return await response.text();\n" +
" }\n";
Path harPath = tmpDir.resolve("har.zip");
try (BrowserContext context1 = browser.newContext(new Browser.NewContextOptions()
.setRecordHarPath(harPath)
.setRecordHarMode(HarMode.MINIMAL))) {
Page page1 = context1.newPage();
page1.navigate(server.EMPTY_PAGE);
assertEquals("1", page1.evaluate(fetchFunction, "1"));
assertEquals("2", page1.evaluate(fetchFunction, "2"));
assertEquals("3", page1.evaluate(fetchFunction, "3"));
}
server.reset();
try (BrowserContext context2 = browser.newContext()) {
context2.routeFromHAR(harPath);
Page page2 = context2.newPage();
page2.navigate(server.EMPTY_PAGE);
assertEquals("1", page2.evaluate(fetchFunction, "1"));
assertEquals("2", page2.evaluate(fetchFunction, "2"));
assertEquals("3", page2.evaluate(fetchFunction, "3"));
assertEquals("3", page2.evaluate(fetchFunction, "3"));
try {
Object result = page2.evaluate(fetchFunction, "4");
System.out.println(result);
fail("did not throw");
} catch (PlaywrightException e) {
}
}
}
@Test
void shouldDisambiguateByHeader(@TempDir Path tmpDir) {
server.setRoute("/echo", exchange -> {
exchange.sendResponseHeaders(200, 0);
try (OutputStream out = exchange.getResponseBody()) {
List<String> values = exchange.getRequestHeaders().get("baz");
try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write(values == null ? "<no header>" : String.join(", ", values));
}
}
});
String fetchFunction = "async bazValue => {\n" +
" const response = await fetch('/echo', {\n" +
" method: 'POST',\n" +
" body: '',\n" +
" headers: {\n" +
" foo: 'foo-value',\n" +
" bar: 'bar-value',\n" +
" baz: bazValue,\n" +
" }\n" +
" });\n" +
" return await response.text();\n" +
" }\n";
Path harPath = tmpDir.resolve("har.zip");
try (BrowserContext context1 = browser.newContext(new Browser.NewContextOptions()
.setRecordHarPath(harPath)
.setRecordHarMode(HarMode.MINIMAL))) {
Page page1 = context1.newPage();
page1.navigate(server.EMPTY_PAGE);
assertEquals("baz1", page1.evaluate(fetchFunction, "baz1"));
assertEquals("baz2", page1.evaluate(fetchFunction, "baz2"));
assertEquals("baz3", page1.evaluate(fetchFunction, "baz3"));
}
server.reset();
try (BrowserContext context2 = browser.newContext()) {
context2.routeFromHAR(harPath);
Page page2 = context2.newPage();
page2.navigate(server.EMPTY_PAGE);
assertEquals("baz1", page2.evaluate(fetchFunction, "baz1"));
assertEquals("baz2", page2.evaluate(fetchFunction, "baz2"));
assertEquals("baz3", page2.evaluate(fetchFunction, "baz3"));
assertEquals("baz1", page2.evaluate(fetchFunction, "baz4"));
}
}
@Test
void shouldUpdateHarZipForContext(@TempDir Path tmpDir) {
Path harPath = tmpDir.resolve("har.zip");
try (BrowserContext context1 = browser.newContext()) {
context1.routeFromHAR(harPath, new BrowserContext.RouteFromHAROptions().setUpdate(true));
Page page1 = context1.newPage();
page1.navigate(server.PREFIX + "/one-style.html");
}
try (BrowserContext context2 = browser.newContext()) {
context2.routeFromHAR(harPath, new BrowserContext.RouteFromHAROptions().setNotFound(HarNotFound.ABORT));
Page page2 = context2.newPage();
page2.navigate(server.PREFIX + "/one-style.html");
assertTrue(page2.content().contains("hello, world!"));
assertThat(page2.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)");
}
}
@Test
void shouldUpdateHarZipForPage(@TempDir Path tmpDir) {
Path harPath = tmpDir.resolve("har.zip");
try (BrowserContext context1 = browser.newContext()) {
Page page1 = context1.newPage();
page1.routeFromHAR(harPath, new Page.RouteFromHAROptions().setUpdate(true));
page1.navigate(server.PREFIX + "/one-style.html");
}
try (BrowserContext context2 = browser.newContext()) {
Page page2 = context2.newPage();
page2.routeFromHAR(harPath, new Page.RouteFromHAROptions().setNotFound(HarNotFound.ABORT));
page2.navigate(server.PREFIX + "/one-style.html");
assertTrue(page2.content().contains("hello, world!"));
assertThat(page2.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)");
}
}
@Test
void shouldUpdateExtractedHarZipForPage(@TempDir Path tmpDir) {
Path harPath = tmpDir.resolve("har.har");
try (BrowserContext context1 = browser.newContext()) {
Page page1 = context1.newPage();
page1.routeFromHAR(harPath, new Page.RouteFromHAROptions().setUpdate(true));
page1.navigate(server.PREFIX + "/one-style.html");
}
try (BrowserContext context2 = browser.newContext()) {
Page page2 = context2.newPage();
page2.routeFromHAR(harPath, new Page.RouteFromHAROptions().setNotFound(HarNotFound.ABORT));
page2.navigate(server.PREFIX + "/one-style.html");
assertTrue(page2.content().contains("hello, world!"));
assertThat(page2.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)");
}
}
}
@@ -28,19 +28,19 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestBrowserContextLocale extends TestBase {
@Test
void shouldAffectAcceptLanguageHeader() throws ExecutionException, InterruptedException {
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setLocale("fr-CH"));
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setLocale("fr-FR"));
Page page = context.newPage();
Future<Server.Request> request = server.futureRequest("/empty.html");
page.navigate(server.EMPTY_PAGE);
assertEquals("fr-CH", request.get().headers.get("accept-language").get(0).substring(0, 5));
assertEquals("fr-FR", request.get().headers.get("accept-language").get(0).substring(0, 5));
context.close();
}
@Test
void shouldAffectNavigatorLanguage() {
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setLocale("fr-CH"));
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setLocale("fr-FR"));
Page page = context.newPage();
assertEquals("fr-CH", page.evaluate("() => navigator.language"));
assertEquals("fr-FR", page.evaluate("() => navigator.language"));
context.close();
}
@@ -55,7 +55,7 @@ public class TestBrowserContextLocale extends TestBase {
context.close();
}
{
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setLocale("fr-CH"));
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setLocale("fr-FR"));
Page page = context.newPage();
page.navigate(server.EMPTY_PAGE);
assertEquals("1 000 000,5", page.evaluate("() => (1000000.50).toLocaleString().replace(/\\s/g, ' ')"));
@@ -87,7 +87,7 @@ public class TestBrowserContextLocale extends TestBase {
@Test
void shouldFormatNumberInPopups() {
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setLocale("fr-CH"));
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setLocale("fr-FR"));
Page page = context.newPage();
page.navigate(server.EMPTY_PAGE);
Page popup = page.waitForPopup(() -> page.evaluate(
@@ -100,14 +100,14 @@ public class TestBrowserContextLocale extends TestBase {
@Test
void shouldAffectNavigatorLanguageInPopups() {
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setLocale("fr-CH"));
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setLocale("fr-FR"));
Page page = context.newPage();
page.navigate(server.EMPTY_PAGE);
Page popup = page.waitForPopup(() -> page.evaluate(
"url => window.open(url)", server.PREFIX + "/formatted-number.html"));
popup.waitForLoadState(LoadState.DOMCONTENTLOADED);
Object result = popup.evaluate("window.initialNavigatorLanguage");
assertEquals("fr-CH", result);
assertEquals("fr-FR", result);
context.close();
}
@@ -155,7 +155,7 @@ public class TestBrowserContextLocale extends TestBase {
defaultLocale = getContextLocale.apply(context);
context.close();
}
String localeOverride = "ru-RU".equals(defaultLocale) ? "de-DE" : "ru-RU";
String localeOverride = "es-MX".equals(defaultLocale) ? "de-DE" : "es-MX";
{
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setLocale(localeOverride));
assertEquals(localeOverride, getContextLocale.apply(context));
@@ -16,6 +16,7 @@
package com.microsoft.playwright;
import com.sun.net.httpserver.Filter;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
@@ -33,10 +34,19 @@ public class TestBrowserContextNetworkEvents extends TestBase {
page.setContent("<a target=_blank rel=noopener href='/one-style.html'>yo</a>");
Page page1 = context.waitForPage(() -> page.click("a"));
page1.waitForLoadState();
assertEquals(asList(
server.EMPTY_PAGE,
server.PREFIX + "/one-style.html",
server.PREFIX + "/one-style.css"), requests);
// In firefox one-style.css is requested multiple times.
if (isFirefox()) {
assertEquals(asList(
server.EMPTY_PAGE,
server.PREFIX + "/one-style.html",
server.PREFIX + "/one-style.css",
server.PREFIX + "/one-style.css"), requests);
} else {
assertEquals(asList(
server.EMPTY_PAGE,
server.PREFIX + "/one-style.html",
server.PREFIX + "/one-style.css"), requests);
}
}
@Test
@@ -47,10 +57,19 @@ public class TestBrowserContextNetworkEvents extends TestBase {
page.setContent("<a target=_blank rel=noopener href='/one-style.html'>yo</a>");
Page page1 = context.waitForPage(() -> page.click("a"));
page1.waitForLoadState();
assertEquals(asList(
server.EMPTY_PAGE,
server.PREFIX + "/one-style.html",
server.PREFIX + "/one-style.css"), responses);
// In firefox one-style.css is requested multiple times.
if (isFirefox()) {
assertEquals(asList(
server.EMPTY_PAGE,
server.PREFIX + "/one-style.html",
server.PREFIX + "/one-style.css",
server.PREFIX + "/one-style.css"), responses);
} else {
assertEquals(asList(
server.EMPTY_PAGE,
server.PREFIX + "/one-style.html",
server.PREFIX + "/one-style.css"), responses);
}
}
@Test
@@ -59,7 +78,12 @@ public class TestBrowserContextNetworkEvents extends TestBase {
List<Request> failedRequests = new ArrayList<>();
context.onRequestFailed(request -> failedRequests.add(request));
page.navigate(server.PREFIX + "/one-style.html");
assertEquals(1, failedRequests.size());
// In firefox one-style.css is requested multiple times.
if (isFirefox()) {
assertTrue(failedRequests.size() > 0);
} else {
assertEquals(1, failedRequests.size());
}
assertTrue(failedRequests.get(0).url().contains("one-style.css"));
assertNull(failedRequests.get(0).response());
assertEquals("stylesheet", failedRequests.get(0).resourceType());
@@ -19,6 +19,7 @@ package com.microsoft.playwright;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
@@ -62,28 +63,28 @@ public class TestBrowserContextRoute extends TestBase {
List<Integer> intercepted = new ArrayList<>();
context.route("**/*", route -> {
intercepted.add(1);
route.resume();
route.fallback();
});
context.route("**/empty.html", route -> {
intercepted.add(2);
route.resume();
route.fallback();
});
context.route("**/empty.html", route -> {
intercepted.add(3);
route.resume();
route.fallback();
});
Consumer<Route> handler4 = route -> {
intercepted.add(4);
route.resume();
route.fallback();
};
context.route("**/empty.html", handler4);
page.navigate(server.EMPTY_PAGE);
assertEquals(asList(4), intercepted);
assertEquals(asList(4, 3, 2, 1), intercepted);
intercepted.clear();
context.unroute("**/empty.html", handler4);
page.navigate(server.EMPTY_PAGE);
assertEquals(asList(3), intercepted);
assertEquals(asList(3, 2, 1), intercepted);
intercepted.clear();
context.unroute("**/empty.html");
@@ -140,8 +141,10 @@ public class TestBrowserContextRoute extends TestBase {
@Test
void shouldOverwritePostBodyWithEmptyString() throws ExecutionException, InterruptedException {
boolean[] routeHandled = {false};
context.route("**/empty.html", route -> {
route.resume(new Route.ResumeOptions().setPostData(""));
routeHandled[0] = true;
});
Future<Server.Request> req = server.futureRequest("/empty.html");
@@ -153,7 +156,9 @@ public class TestBrowserContextRoute extends TestBase {
" });\n" +
" })()\n" +
" </script>");
while (!routeHandled[0]) {
page.waitForTimeout(100);
}
byte[] body = req.get().postBody;
assertEquals(0, body.length);
}
@@ -203,4 +208,118 @@ public class TestBrowserContextRoute extends TestBase {
assertTrue(e.getMessage().contains("New URL must have same protocol as overridden URL"), e.getMessage());
}
}
@Test
void shouldChainFallback() {
List<Integer> intercepted = new ArrayList<>();
context.route("**/empty.html", route -> {
intercepted.add(1);
route.fallback();
});
context.route("**/empty.html", route -> {
intercepted.add(2);
route.fallback();
});
context.route("**/empty.html", route -> {
intercepted.add(3);
route.fallback();
});
page.navigate(server.EMPTY_PAGE);
assertEquals(asList(3, 2, 1), intercepted);
}
@Test
void shouldNotChainFulfill() {
boolean[] failed = {false};
context.route("**/empty.html", route -> {
failed[0] = true;
});
context.route("**/empty.html", route -> {
route.fulfill(new Route.FulfillOptions().setStatus(200).setBody("fulfilled"));
});
context.route("**/empty.html", route -> {
route.fallback();
});
Response response = page.navigate(server.EMPTY_PAGE);
byte[] body = response.body();
assertEquals("fulfilled", new String(body, StandardCharsets.UTF_8));
assertFalse(failed[0]);
}
@Test
void shouldNotChainAbort() {
boolean[] failed = {false};
context.route("**/empty.html", route -> {
failed[0] = true;
});
context.route("**/empty.html", route -> {
route.abort();
});
context.route("**/empty.html", route -> {
route.fallback();
});
try {
page.navigate(server.EMPTY_PAGE);
fail("did not throw");
} catch (PlaywrightException e) {
assertNotNull(e);
}
assertFalse(failed[0]);
}
@Test
void shouldChainFallbackIntoPage() {
List<Integer> intercepted = new ArrayList<>();
context.route("**/empty.html", route -> {
intercepted.add(1);
route.fallback();
});
context.route("**/empty.html", route -> {
intercepted.add(2);
route.fallback();
});
context.route("**/empty.html", route -> {
intercepted.add(3);
route.fallback();
});
page.route("**/empty.html", route -> {
intercepted.add(4);
route.fallback();
});
page.route("**/empty.html", route -> {
intercepted.add(5);
route.fallback();
});
page.route("**/empty.html", route -> {
intercepted.add(6);
route.fallback();
});
page.navigate(server.EMPTY_PAGE);
assertEquals(asList(6, 5, 4, 3, 2, 1), intercepted);
}
@Test
void shouldFallBackAsync() {
List<Integer> intercepted = new ArrayList<>();
context.route("**/empty.html", route -> {
intercepted.add(1);
page.waitForTimeout(50);
route.fallback();
});
context.route("**/empty.html", route -> {
intercepted.add(2);
page.waitForTimeout(100);
route.fallback();
});
context.route("**/empty.html", route -> {
intercepted.add(3);
page.waitForTimeout(150);
route.fallback();
});
page.navigate(server.EMPTY_PAGE);
assertEquals(asList(3, 2, 1), intercepted);
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.ServiceWorkerPolicy;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class TestBrowserContextServiceWorkerPolicy extends TestBase {
@Test
void shouldAllowServiceWorkersByDefault() {
page.navigate(server.PREFIX + "/serviceworkers/empty/sw.html");
assertNotNull(page.evaluate("() => window['registrationPromise']"));
}
@Test
void blocksServiceWorkerRegistration() {
try (BrowserContext context = browser.newContext(new Browser.NewContextOptions().setServiceWorkers(ServiceWorkerPolicy.BLOCK))) {
Page page = context.newPage();
ConsoleMessage message = page.waitForConsoleMessage(new Page.WaitForConsoleMessageOptions()
.setPredicate(m -> "Service Worker registration blocked by Playwright".equals(m.text())),
() -> page.navigate(server.PREFIX + "/serviceworkers/empty/sw.html"));
assertNotNull(message);
}
}
}
@@ -109,7 +109,7 @@ public class TestBrowserContextStorageState extends TestBase {
" 'expires':-1,\n" +
" 'httpOnly':false,\n" +
" 'secure':false,\n" +
" 'sameSite':'" + (isChromium() || isFirefox() ? "Lax" : "None") + "'\n" +
" 'sameSite':'" + (isChromium() ? "Lax" : "None") + "'\n" +
" }],\n" +
" 'origins':[\n" +
" {\n" +
@@ -17,7 +17,6 @@
package com.microsoft.playwright;
import com.microsoft.playwright.impl.Driver;
import com.microsoft.playwright.options.WaitForSelectorState;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
@@ -30,10 +29,11 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static com.microsoft.playwright.Utils.mapOf;
import static com.microsoft.playwright.Utils.parseTrace;
import static com.microsoft.playwright.Utils.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
@@ -66,7 +66,7 @@ public class TestBrowserTypeConnect extends TestBase {
String cliJs = dir.resolve("package/lib/cli/cli.js").toString();
// We launch node process directly instead of using playwright.sh script as killing the script
// process will leave node process running and killing it would be more hassle.
ProcessBuilder pb = new ProcessBuilder(node, cliJs, "launch-server", browserType.name());
ProcessBuilder pb = new ProcessBuilder(node, cliJs, "launch-server", "--browser", browserType.name());
pb.directory(dir.toFile());
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
BrowserServer result = new BrowserServer();
@@ -166,6 +166,7 @@ public class TestBrowserTypeConnect extends TestBase {
}
assertNotNull(webSocketServer.lastClientHandshake);
assertEquals("Playwright", webSocketServer.lastClientHandshake.getFieldValue("User-Agent"));
assertEquals(browserType.name(), webSocketServer.lastClientHandshake.getFieldValue("x-playwright-browser"));
assertEquals("bar", webSocketServer.lastClientHandshake.getFieldValue("foo"));
}
}
@@ -497,7 +498,7 @@ public class TestBrowserTypeConnect extends TestBase {
Path trace = tmpDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(trace));
Map<String, byte[]> entries = parseTrace(trace);
Map<String, byte[]> entries = parseZip(trace);
Map<String, byte[]> sources = entries.entrySet().stream().filter(e -> e.getKey().endsWith(".txt")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
assertEquals(1, sources.size());
@@ -518,4 +519,44 @@ public class TestBrowserTypeConnect extends TestBase {
assertEquals(200, response.status());
assertEquals("{\"foo\": \"bar\"}\n", response.text());
}
@Test
void shouldUploadLargeFile(@TempDir Path tmpDir) throws IOException, ExecutionException, InterruptedException {
Assumptions.assumeTrue(3 <= (Runtime.getRuntime().maxMemory() >> 30), "Fails if max heap size is < 3Gb");
page.navigate(server.PREFIX + "/input/fileupload.html");
Path uploadFile = tmpDir.resolve("200MB.zip");
String str = String.join("", Collections.nCopies(4 * 1024, "A"));
try (Writer stream = new OutputStreamWriter(Files.newOutputStream(uploadFile))) {
for (int i = 0; i < 50 * 1024; i++) {
stream.write(str);
}
}
Locator input = page.locator("input[type='file']");
JSHandle events = input.evaluateHandle("e => {\n" +
" const events = [];\n" +
" e.addEventListener('input', () => events.push('input'));\n" +
" e.addEventListener('change', () => events.push('change'));\n" +
" return events;\n" +
" }");
input.setInputFiles(uploadFile);
assertEquals("200MB.zip", input.evaluate("e => e.files[0].name"));
assertEquals(asList("input", "change"), events.evaluate("e => e"));
CompletableFuture<MultipartFormData> formData = new CompletableFuture<>();
server.setRoute("/upload", exchange -> {
try {
MultipartFormData multipartFormData = MultipartFormData.parseRequest(exchange);
formData.complete(multipartFormData);
} catch (Exception e) {
e.printStackTrace();
formData.completeExceptionally(e);
}
exchange.sendResponseHeaders(200, -1);
});
page.click("input[type=submit]", new Page.ClickOptions().setTimeout(90_000));
List<MultipartFormData.Field> fields = formData.get().fields;
assertEquals(1, fields.size());
assertEquals("200MB.zip", fields.get(0).filename);
assertEquals(200 * 1024 * 1024, fields.get(0).content.length());
}
}
@@ -82,8 +82,8 @@ public class TestDefaultBrowserContext2 extends TestBase {
@Test
void shouldSupportLocaleOption() {
Page page = launchPersistent(new BrowserType.LaunchPersistentContextOptions()
.setLocale("fr-CH"));
assertEquals("fr-CH", page.evaluate("navigator.language"));
.setLocale("fr-FR"));
assertEquals("fr-FR", page.evaluate("navigator.language"));
}
@Test
@@ -48,6 +48,6 @@ public class TestElementHandlePress extends TestBase {
void shouldWorkWithNumberInput() {
page.setContent("<input type='number' value=2 />");
page.press("input", "1");
assertEquals("12", page.evalOnSelector("input", "input => input.value"));
assertEquals(isWebKit() ? "1" : "12", page.evalOnSelector("input", "input => input.value"));
}
}
@@ -48,6 +48,6 @@ public class TestElementHandleType extends TestBase {
void shouldWorkWithNumberInput() {
page.setContent("<input type='number' value=2 />");
page.type("input", "13");
assertEquals("132", page.evalOnSelector("input", "input => input.value"));
assertEquals(isWebKit() ? "13" : "132", page.evalOnSelector("input", "input => input.value"));
}
}
@@ -137,7 +137,7 @@ public class TestGlobalFetch extends TestBase {
Optional<HttpHeader> contentType = response.headersArray().stream().filter(h -> "content-type".equals(h.name.toLowerCase())).findFirst();
assertTrue(contentType.isPresent());
assertEquals("application/json", contentType.get().value);
assertEquals("", response.text());
assertEquals("{\"foo\": \"bar\"}\n", response.text());
}
@Test
@@ -16,47 +16,56 @@
package com.microsoft.playwright;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.*;
import com.microsoft.playwright.options.HarContentPolicy;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.api.io.TempDir;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Map;
import java.util.regex.Pattern;
import static com.microsoft.playwright.Utils.getOS;
import static com.microsoft.playwright.options.LoadState.DOMCONTENTLOADED;
import static org.junit.jupiter.api.Assertions.*;
public class TestHar extends TestBase {
private PageWithHar pageWithHar;
private static JsonObject parseHar(Path harFile) throws IOException {
try (FileReader json = new FileReader(harFile.toFile())) {
return new Gson().fromJson(json, JsonObject.class).getAsJsonObject("log");
}
}
private class PageWithHar {
final Path harFile;
final BrowserContext context;
final Page page;
PageWithHar() throws IOException {
harFile = Files.createTempFile("test-", ".har");
context = browser.newContext(new Browser.NewContextOptions()
this(new Browser.NewContextOptions(), null);
}
PageWithHar(Browser.NewContextOptions options, Path harFilePath) throws IOException {
harFile = harFilePath == null ? Files.createTempFile("test-", ".har") : harFilePath;
context = browser.newContext(options
.setRecordHarPath(harFile).setIgnoreHTTPSErrors(true));
page = context.newPage();
}
JsonObject log() throws IOException {
context.close();
try (FileReader json = new FileReader(harFile.toFile())) {
return new Gson().fromJson(json, JsonObject.class).getAsJsonObject("log");
}
return parseHar(harFile);
}
Map<String, byte[]> parseZip() throws IOException {
context.close();
return Utils.parseZip(harFile);
}
void dispose() throws IOException {
@@ -176,4 +185,144 @@ public class TestHar extends TestBase {
}
assertTrue(foundUserContentType);
}
@Test
void shouldFilterByGlob(@TempDir Path tmpDir) throws IOException {
Path harPath = tmpDir.resolve("test.har");
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setBaseURL(server.PREFIX)
.setRecordHarPath(harPath)
.setRecordHarUrlFilter("/*.css")
.setIgnoreHTTPSErrors(true));
Page page = context.newPage();
page.navigate("/har.html");
context.close();
JsonObject log = parseHar(harPath);
JsonArray entries = log.getAsJsonArray("entries");
// There are 2 entries for the same .css request in firefox.
if (isFirefox()) {
assertEquals(2, entries.size());
} else {
assertEquals(1, entries.size());
}
assertTrue(entries.get(0).getAsJsonObject().getAsJsonObject("request").get("url").getAsString().endsWith("one-style.css"));
}
@Test
void shouldFilterByRegexp(@TempDir Path tmpDir) throws IOException {
Path harPath = tmpDir.resolve("test.har");
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setBaseURL(server.PREFIX)
.setRecordHarPath(harPath)
.setRecordHarUrlFilter(Pattern.compile("HAR.X?HTML", Pattern.CASE_INSENSITIVE))
.setIgnoreHTTPSErrors(true));
Page page = context.newPage();
page.navigate(server.PREFIX + "/har.html");
context.close();
JsonObject log = parseHar(harPath);
JsonArray entries = log.getAsJsonArray("entries");
assertEquals(1, entries.size());
assertTrue(entries.get(0).getAsJsonObject().getAsJsonObject("request").get("url").getAsString().endsWith("har.html"));
}
@Test
void shouldOmitContent(@TempDir Path tmpDir) throws IOException {
Path harPath = tmpDir.resolve("test.har");
PageWithHar pageWithHar = new PageWithHar(new Browser.NewContextOptions()
.setRecordHarContent(HarContentPolicy.OMIT), harPath);
pageWithHar.page.navigate(server.PREFIX + "/har.html");
pageWithHar.page.evaluate("() => fetch('/pptr.png').then(r => r.arrayBuffer())");
JsonObject log = pageWithHar.log();
pageWithHar.dispose();
JsonArray entries = log.getAsJsonArray("entries");
assertFalse(entries.get(0).getAsJsonObject()
.getAsJsonObject("response")
.getAsJsonObject("content")
.has("text"));
assertFalse(entries.get(0).getAsJsonObject()
.getAsJsonObject("response")
.getAsJsonObject("content")
.has("_file"));
}
@Test
void shouldOmitContentLegacy(@TempDir Path tmpDir) throws IOException {
Path harPath = tmpDir.resolve("test.har");
PageWithHar pageWithHar = new PageWithHar(new Browser.NewContextOptions()
.setRecordHarOmitContent(true), harPath);
pageWithHar.page.navigate(server.PREFIX + "/har.html");
pageWithHar.page.evaluate("() => fetch('/pptr.png').then(r => r.arrayBuffer())");
JsonObject log = pageWithHar.log();
pageWithHar.dispose();
JsonArray entries = log.getAsJsonArray("entries");
assertFalse(entries.get(0).getAsJsonObject()
.getAsJsonObject("response")
.getAsJsonObject("content")
.has("text"));
assertFalse(entries.get(0).getAsJsonObject()
.getAsJsonObject("response")
.getAsJsonObject("content")
.has("_file"));
}
@Test
void shouldAttachContent(@TempDir Path tmpDir) throws IOException {
Path harPath = tmpDir.resolve("test.har.zip");
PageWithHar pageWithHar = new PageWithHar(new Browser.NewContextOptions()
.setRecordHarContent(HarContentPolicy.ATTACH), harPath);
pageWithHar.page.navigate(server.PREFIX + "/har.html");
pageWithHar.page.evaluate("() => fetch('/pptr.png').then(r => r.arrayBuffer())");
Map<String, byte[]> zip = pageWithHar.parseZip();
JsonObject log = new Gson().fromJson(new InputStreamReader(new ByteArrayInputStream(zip.get("har.har"))), JsonObject.class).getAsJsonObject("log");
pageWithHar.dispose();
JsonArray entries = log.getAsJsonArray("entries");
{
JsonObject content = firstEntryFor(entries, "har.html")
.getAsJsonObject("response")
.getAsJsonObject("content");
assertFalse(content.has("encoding"));
assertEquals("text/html", content.get("mimeType").getAsString());
assertTrue(content.get("_file").getAsString().contains("75841480e2606c03389077304342fac2c58ccb1b"));
assertTrue(content.get("size").getAsInt() >= 96);
assertEquals(0, content.get("compression").getAsInt());
}
{
// TODO: figure out why there is more than one entry in Firefox.
JsonObject content = firstEntryFor(entries, "one-style.css")
.getAsJsonObject("response")
.getAsJsonObject("content");
assertFalse(content.has("encoding"));
assertEquals("text/css", content.get("mimeType").getAsString());
assertTrue(content.get("_file").getAsString().contains("79f739d7bc88e80f55b9891a22bf13a2b4e18adb"));
assertTrue(content.get("size").getAsInt() >= 37);
assertEquals(0, content.get("compression").getAsInt());
}
{
JsonObject content = firstEntryFor(entries, "pptr.png")
.getAsJsonObject("response")
.getAsJsonObject("content");
assertFalse(content.has("encoding"));
assertEquals("image/png", content.get("mimeType").getAsString());
assertTrue(content.get("_file").getAsString().contains("a4c3a18f0bb83f5d9fe7ce561e065c36205762fa"));
assertTrue(content.get("size").getAsInt() >= 6000);
assertEquals(0, content.get("compression").getAsInt());
}
assertTrue(new String(zip.get("75841480e2606c03389077304342fac2c58ccb1b.html"), StandardCharsets.UTF_8).contains("HAR Page"));
assertTrue(new String(zip.get("79f739d7bc88e80f55b9891a22bf13a2b4e18adb.css"), StandardCharsets.UTF_8).contains("pink"));
assertEquals(firstEntryFor(entries, "pptr.png")
.getAsJsonObject("response")
.getAsJsonObject("content")
.get("size").getAsInt(), zip.get("a4c3a18f0bb83f5d9fe7ce561e065c36205762fa.png").length);
}
private static JsonObject firstEntryFor(JsonArray entries, String name) {
for (int i = 0; i < entries.size(); i++) {
JsonObject entry = entries.get(i).getAsJsonObject();
String url = entry.getAsJsonObject("request").get("url").getAsString();
if (url.endsWith(name)) {
return entry;
}
}
return null;
}
}
@@ -72,6 +72,28 @@ public class TestLocatorAssertions extends TestBase {
}
}
@Test
void containsTextWTextPass() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).containsText("Text");
// Should normalize whitespace.
assertThat(locator).containsText(" ext cont\n ");
// Should support ignoreCase.
assertThat(locator).containsText("EXT", new LocatorAssertions.ContainsTextOptions().setIgnoreCase(true));
// Should support falsy ignoreCase.
assertThat(locator).not().containsText("TEXT", new LocatorAssertions.ContainsTextOptions().setIgnoreCase(false));
}
@Test
void containsTextWTextArrayPass() {
page.setContent("<div>Text \n1</div><div>Text2</div><div>Text3</div>");
Locator locator = page.locator("div");
assertThat(locator).containsText(new String[] {"ext 1", "ext3"});
// Should support ignoreCase.
assertThat(locator).containsText(new String[] {"EXT 1", "eXt3"}, new LocatorAssertions.ContainsTextOptions().setIgnoreCase(true));
}
@Test
void hasTextWRegexPass() {
page.setContent("<div id=node>Text content</div>");
@@ -79,6 +101,10 @@ public class TestLocatorAssertions extends TestBase {
assertThat(locator).hasText(Pattern.compile("Te.t"));
// Should not normalize whitespace.
assertThat(locator).hasText(Pattern.compile("Text.+content"));
// Should respect ignoreCase.
assertThat(locator).hasText(Pattern.compile("text content"), new LocatorAssertions.HasTextOptions().setIgnoreCase(true));
// Should override regex flag with ignoreCase.
assertThat(locator).not().hasText(Pattern.compile("text content", Pattern.CASE_INSENSITIVE), new LocatorAssertions.HasTextOptions().setIgnoreCase(false));
}
@Test
@@ -101,6 +127,10 @@ public class TestLocatorAssertions extends TestBase {
Locator locator = page.locator("#node");
// Should normalize whitespace.
assertThat(locator).hasText("Text content");
// Should support ignoreCase.
assertThat(locator).hasText("text CONTENT", new LocatorAssertions.HasTextOptions().setIgnoreCase(true));
// Should support falsy ignoreCase.
assertThat(locator).not().hasText("TEXT", new LocatorAssertions.HasTextOptions().setIgnoreCase(false));
}
@Test
@@ -118,12 +148,21 @@ public class TestLocatorAssertions extends TestBase {
}
}
@Test
void hasTextWTextInnerTextPass() {
page.setContent("<div id=node>Text <span hidden>garbage</span> content</div>");
Locator locator = page.locator("#node");
assertThat(locator).hasText("Text content", new LocatorAssertions.HasTextOptions().setUseInnerText(true));
}
@Test
void hasTextWTextArrayPass() {
page.setContent("<div>Text \n1</div><div>Text 2a</div>");
Locator locator = page.locator("div");
// Should normalize whitespace.
assertThat(locator).hasText(new String[] {"Text 1", "Text 2a"});
// Should support ignoreCase.
assertThat(locator).hasText(new String[] {"tEXT 1", "TExt 2A"}, new LocatorAssertions.HasTextOptions().setIgnoreCase(true));
}
@Test
@@ -542,6 +581,109 @@ public class TestLocatorAssertions extends TestBase {
}
}
@Test
void hasValuesWorksWithText() {
page.setContent("<select multiple>\n" +
" <option value=\"R\">Red</option>\n" +
" <option value=\"G\">Green</option>\n" +
" <option value=\"B\">Blue</option>\n" +
" </select>");
Locator locator = page.locator("select");
locator.selectOption(new String[] {"R", "G"});
assertThat(locator).hasValues(new String[]{"R", "G"});
}
@Test
void hasValuesFollowsLabels() {
page.setContent("<label for=\"colors\">Pick a Color</label>\n" +
" <select id=\"colors\" multiple>\n" +
" <option value=\"R\">Red</option>\n" +
" <option value=\"G\">Green</option>\n" +
" <option value=\"B\">Blue</option>\n" +
" </select>");
Locator locator = page.locator("text=Pick a Color");
locator.selectOption(new String[] {"R", "G"});
assertThat(locator).hasValues(new String[]{"R", "G"});
}
@Test
void hasValuesExactMatchWithText() {
page.setContent("<select multiple>\n" +
" <option value=\"RR\">Red</option>\n" +
" <option value=\"GG\">Green</option>\n" +
" </select>");
Locator locator = page.locator("select");
locator.selectOption(new String[] {"RR", "GG"});
try {
assertThat(locator).hasValues(new String[]{"R", "G"}, new LocatorAssertions.HasValuesOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("[R, G]", e.getExpected().getStringRepresentation());
assertEquals("[RR, GG]", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have values"), e.getMessage());
}
}
@Test
void hasValuesWorksWithRegex() {
page.setContent("<select multiple>\n" +
" <option value=\"R\">Red</option>\n" +
" <option value=\"G\">Green</option>\n" +
" <option value=\"B\">Blue</option>\n" +
" </select>");
Locator locator = page.locator("select");
locator.selectOption(new String[] {"R", "G"});
assertThat(locator).hasValues(new Pattern[]{ Pattern.compile("R"), Pattern.compile("G")});
}
@Test
void hasValuesFailsWhenItemsNotSelected() {
page.setContent("<select multiple>\n" +
" <option value=\"R\">Red</option>\n" +
" <option value=\"G\">Green</option>\n" +
" <option value=\"B\">Blue</option>\n" +
" </select>");
Locator locator = page.locator("select");
locator.selectOption(new String[] {"B"}, new Locator.SelectOptionOptions().setTimeout(1000));
try {
assertThat(locator).hasValues(new Pattern[]{ Pattern.compile("R"), Pattern.compile("G")});
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("[R, G]", e.getExpected().getStringRepresentation());
assertEquals("[B]", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have values matching regex"), e.getMessage());
}
}
@Test
void hasValuesFailsWhenMultipleNotSpecified() {
page.setContent("<select>\n" +
" <option value=\"R\">Red</option>\n" +
" <option value=\"G\">Green</option>\n" +
" <option value=\"B\">Blue</option>\n" +
" </select>");
Locator locator = page.locator("select");
locator.selectOption(new String[] {"B"});
try {
assertThat(locator).hasValues(new Pattern[]{ Pattern.compile("R"), Pattern.compile("G")});
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Not a select element with a multiple attribute"), e.getMessage());
}
}
@Test
void hasValuesFailsWhenNotASelectElement() {
page.setContent("<input value=\"foo\" />");
Locator locator = page.locator("input");
try {
assertThat(locator).hasValues(new Pattern[]{ Pattern.compile("R"), Pattern.compile("G")}, new LocatorAssertions.HasValuesOptions().setTimeout(1000));
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Not a select element with a multiple attribute"), e.getMessage());
}
}
@Test
void isCheckedPass() {
page.setContent("<input type=checkbox checked></input>");
@@ -577,6 +719,13 @@ public class TestLocatorAssertions extends TestBase {
}
}
@Test
void isCheckedFalsePass() {
page.setContent("<input type=checkbox></input>");
Locator locator = page.locator("input");
assertThat(locator).isChecked(new LocatorAssertions.IsCheckedOptions().setChecked(false));
}
@Test
void isDisabledPass() {
page.setContent("<button disabled>Text</button>");
@@ -0,0 +1,23 @@
package com.microsoft.playwright;
import com.google.gson.Gson;
import com.microsoft.playwright.options.BoundingBox;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestLocatorHighlight extends TestBase {
@Disabled("Requires isUnderTest to be true https://github.com/microsoft/playwright/pull/12420")
@Test
void shouldHighlightLocator() {
page.setContent("<input type='text' />");
page.locator("input").highlight();
assertThat(page.locator("x-pw-tooltip")).hasText("input");
assertThat(page.locator("x-pw-highlight")).isVisible();
BoundingBox box1 = page.locator("input").boundingBox();
BoundingBox box2 = page.locator("x-pw-highlight").boundingBox();
assertEquals(new Gson().toJson(box2), new Gson().toJson(box1));
}
}
@@ -0,0 +1,66 @@
/*
* 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.LoadState;
import org.junit.jupiter.api.Test;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestPageAutowaitingBasic extends TestBase {
@Test
void shouldWorkWithNoWaitAfterTrue() {
server.setRoute("/empty.html", exchange -> {});
page.setContent("<a id='anchor' href='${server.EMPTY_PAGE}'>empty.html</a>");
page.click("a", new Page.ClickOptions().setNoWaitAfter(true));
}
@Test
void shouldWorkWithDblclickNoWaitAfterTrue() {
server.setRoute("/empty.html", exchange -> {});
page.setContent("<a id='anchor' href='${server.EMPTY_PAGE}'>empty.html</a>");
page.dblclick("a", new Page.DblclickOptions().setNoWaitAfter(true));
}
@Test
void shouldWorkWithWaitForLoadStateLoad() {
List<String> messages = new ArrayList<>();
server.setRoute("/empty.html", exchange -> {
messages.add("route");
exchange.getResponseHeaders().add("content-type", "text/html");
exchange.sendResponseHeaders(200, 0);
try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("<link rel='stylesheet' href='./one-style.css'>");
} catch (RuntimeException e) {
e.printStackTrace();
throw e;
}
});
page.setContent("<a id='anchor' href='" + server.EMPTY_PAGE + "'>empty.html</a>");
page.onLoad(p -> messages.add("clickload"));
page.click("a");
page.waitForLoadState(LoadState.LOAD);
messages.add("load");
assertEquals(asList("route", "clickload", "load"), messages);
}
}
@@ -164,6 +164,24 @@ public class TestPageBasic extends TestBase {
page.waitForLoadState(DOMCONTENTLOADED);
}
@Test
void shouldPassSelfAsArgumentToDomcontentloadedEvent() {
Page[] eventPage = {null};
page.onDOMContentLoaded(p -> eventPage[0] = p);
page.navigate("about:blank");
page.waitForLoadState(DOMCONTENTLOADED);
assertEquals(page, eventPage[0]);
}
@Test
void shouldPassSelfAsArgumentToLoadEvent() {
Page[] eventPage = {null};
page.onLoad(p -> eventPage[0] = p);
page.navigate("about:blank");
page.waitForLoadState(LOAD);
assertEquals(page, eventPage[0]);
}
// TODO: downloads
void shouldFailWithErrorUponDisconnect() {
}
@@ -345,31 +345,13 @@ public class TestPageEvaluate extends TestBase {
@Test
void shouldReturnUndefinedForNonSerializableObjects() {
assertEquals(null, page.evaluate("() => window"));
}
@Test
void shouldFailForCircularObject() {
Object result = page.evaluate("() => {\n" +
" const a = {};\n" +
" const b = { a };\n" +
" a.b = b;\n" +
" return a;\n" +
"}");
assertNull(result);
assertEquals(null, page.evaluate("() => () => {}"));
assertEquals("ref: <Window>", page.evaluate("() => window"));
}
@Test
void shouldBeAbleToThrowATrickyError() {
JSHandle windowHandle = page.evaluateHandle("() => window");
String errorText = null;
try {
windowHandle.jsonValue();
fail("did not throw");
} catch (PlaywrightException e) {
errorText = e.getMessage();
}
assertNotNull(errorText);
String errorText = "My error";
try {
page.evaluate("errorText => {\n" +
" throw new Error(errorText);\n" +
@@ -619,4 +601,23 @@ public class TestPageEvaluate extends TestBase {
JSHandle resultHandle = page.evaluateHandle("() => ({ toJSON: () => 'string', data: 'data' })");
assertEquals(mapOf("data", "data", "toJSON", emptyMap()), resultHandle.jsonValue());
}
@Test
void shouldAliasWindowDocumentAndNode() {
Object object = page.evaluate("[window, document, document.body]");
assertEquals(asList("ref: <Window>", "ref: <Document>", "ref: <Node>"), object);
}
@Test
void shouldWorkForCircularObject() {
Object result = page.evaluate("() => {\n" +
" const a = {};\n" +
" a.b = a;\n" +
" return a;\n" +
" }");
Map<String, Object> map = (Map<String, Object>) result;
assertEquals(1, map.size());
assertTrue(map == map.get("b"));
}
}
@@ -57,7 +57,12 @@ public class TestPageEventNetwork extends TestBase {
List<Request> failedRequests = new ArrayList<>();
page.onRequestFailed(request -> failedRequests.add(request));
page.navigate(server.PREFIX + "/one-style.html");
assertEquals(1, failedRequests.size());
// In firefox one-style.css is requested multiple times.
if (isFirefox()) {
assertTrue(failedRequests.size() > 0);
} else {
assertEquals(1, failedRequests.size());
}
assertTrue(failedRequests.get(0).url().contains("one-style.css"));
assertNull(failedRequests.get(0).response());
assertEquals("stylesheet", failedRequests.get(0).resourceType());
@@ -227,5 +227,15 @@ public class TestPageExposeFunction extends TestBase {
assertTrue(e.getMessage().contains("exposeBindingHandle supports a single argument, 2 received"));
}
}
@Test
void shouldSerializeCycles() {
Object[] object = { null };
page.exposeBinding("log", (source, obj) -> object[0] = obj[0]);
page.evaluate("const a = {}; a.b = a; window.log(a)");
Map<String, Object> map = (Map<String, Object>) object[0];
assertEquals(1, map.size());
assertTrue(map == map.get("b"));
}
}
@@ -0,0 +1,60 @@
package com.microsoft.playwright;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import static org.junit.jupiter.api.Assertions.*;
public class TestPageInterception extends TestBase {
@Test
void shouldWorkWithNavigationSmoke() {
HashMap<String, Request> requests = new HashMap<>();
page.route("**/*", route -> {
String[] parts = route.request().url().split("/");
requests.put(parts[parts.length - 1], route.request());
route.resume();
});
server.setRedirect("/rrredirect", "/frames/one-frame.html");
page.navigate(server.PREFIX + "/rrredirect");
assertTrue(requests.get("rrredirect").isNavigationRequest());
assertTrue(requests.get("frame.html").isNavigationRequest());
assertFalse(requests.get("script.js").isNavigationRequest());
assertFalse(requests.get("style.css").isNavigationRequest());
}
@Test
void shouldInterceptAfterAServiceWorker() {
page.navigate(server.PREFIX + "/serviceworkers/fetchdummy/sw.html");
page.evaluate("() => window['activationPromise']");
// Sanity check.
Object swResponse = page.evaluate("() => window['fetchDummy']('foo')");
assertEquals("responseFromServiceWorker:foo", swResponse);
page.route("**/foo", route -> {
int slash = route.request().url().lastIndexOf("/");
String name = route.request().url().substring(slash + 1);
route.fulfill(new Route.FulfillOptions()
.setStatus(200)
.setContentType("text/css").setBody("responseFromInterception:" + name));
});
// Page route is applied after service worker fetch event.
Object swResponse2 = page.evaluate("() => window['fetchDummy']('foo')");
assertEquals("responseFromServiceWorker:foo", swResponse2);
// Page route is not applied to service worker initiated fetch.
Object nonInterceptedResponse = page.evaluate("() => window['fetchDummy']('passthrough')");
assertEquals("FAILURE: Not Found", nonInterceptedResponse);
// Firefox does not want to fetch the redirect for some reason.
if (!isFirefox()) {
// Page route is not applied to service worker initiated fetch with redirect.
server.setRedirect("/serviceworkers/fetchdummy/passthrough", "/simple.json");
Object redirectedResponse = page.evaluate("() => window['fetchDummy']('passthrough')");
assertEquals("{\"foo\": \"bar\"}\n", redirectedResponse);
}
}
}
@@ -134,4 +134,19 @@ public class TestPageLocatorQuery extends TestBase {
assertThat(page.locator("div", new Page.LocatorOptions()
.setHas(page.locator("span")).setHasText("wor"))).hasCount(1);
}
@Test
void shouldSupportLocatorFilter() {
page.setContent("<section><div><span>hello</span></div><div><span>world</span></div></section>");
assertThat(page.locator("div").filter(new Locator.FilterOptions().setHasText("hello"))).hasCount(1);
assertThat(page.locator("div", new Page.LocatorOptions().setHasText("hello")).filter(new Locator.FilterOptions().setHasText("hello"))).hasCount(1);
assertThat(page.locator("div", new Page.LocatorOptions().setHasText("hello")).filter(new Locator.FilterOptions().setHasText("world"))).hasCount(0);
assertThat(page.locator("section", new Page.LocatorOptions().setHasText("hello")).filter(new Locator.FilterOptions().setHasText("world"))).hasCount(1);
assertThat(page.locator("div").filter(new Locator.FilterOptions().setHasText("hello")).locator("span")).hasCount(1);
assertThat(page.locator("div").filter(new Locator.FilterOptions().setHas(page.locator("span", new Page.LocatorOptions().setHasText("world"))))).hasCount(1);
assertThat(page.locator("div").filter(new Locator.FilterOptions().setHas(page.locator("span")))).hasCount(2);
assertThat(page.locator("div").filter(new Locator.FilterOptions()
.setHas(page.locator("span"))
.setHasText("world"))).hasCount(1);
}
}
@@ -20,7 +20,10 @@ import com.google.gson.Gson;
import com.microsoft.playwright.options.HttpHeader;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.stream.Collectors;
@@ -93,4 +96,21 @@ public class TestPageNetworkRequest extends TestBase {
String cookie = response.request().allHeaders().get("cookie");
assertEquals("myCookie=myValue; myOtherCookie=myOtherValue", cookie);
}
@Test
void shouldReportPostDataFor403Request() throws InterruptedException, ExecutionException {
server.setRoute("/upload", exchange -> {
exchange.sendResponseHeaders(403, 0);
exchange.getResponseBody().close();
});
Future<Server.Request> serverRequest = server.futureRequest("/upload");
page.navigate(server.EMPTY_PAGE);
Request request = page.waitForRequest("**/*", () -> {
page.evaluate("() => fetch('/upload', { method: 'POST', body: 'test'})");
});
assertEquals("test", new String(serverRequest.get().postBody, StandardCharsets.UTF_8));
assertEquals("test", request.postData());
assertEquals("POST", request.method());
assertEquals(403, request.response().status());
}
}
@@ -18,10 +18,52 @@ package com.microsoft.playwright;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.OutputStreamWriter;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
public class TestPageRequestContinue extends TestBase {
@Test
void shouldDeleteHeaderWithUndefinedValue() throws ExecutionException, InterruptedException {
// https://github.com/microsoft/playwright/issues/13106
page.navigate(server.PREFIX + "/empty.html");
Future<Server.Request> serverRequest = server.futureRequest("/something");
server.setRoute("/something", exchange -> {
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
exchange.sendResponseHeaders(200, 0);
try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("done");
}
});
Request[] interceptedRequest = {null};
page.route(server.PREFIX + "/something", route -> {
interceptedRequest[0] = route.request();
Map<String, String> headers = route.request().allHeaders();
headers.remove("foo");
route.resume(new Route.ResumeOptions().setHeaders(headers));
});
Object text = page.evaluate("async url => {\n" +
" const data = await fetch(url, {\n" +
" headers: {\n" +
" foo: 'a',\n" +
" bar: 'b',\n" +
" }\n" +
" });\n" +
" return data.text();\n" +
" }", server.PREFIX + "/something");
assertEquals("done", text);
assertNull(interceptedRequest[0].headers().get("foo"));
assertNull(serverRequest.get().headers.get("foo"));
assertEquals(asList("b"), serverRequest.get().headers.get("bar"));
}
@Test
void shouldNotThrowWhenContinuingAfterPageIsClosed() {
boolean[] done = {false};
@@ -0,0 +1,276 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import org.junit.jupiter.api.Test;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
public class TestPageRequestFallback extends TestBase {
@Test
void shouldWork() {
page.route("**/*", route -> route.fallback());
page.navigate(server.EMPTY_PAGE);
}
@Test
void shouldFallBack() {
List<Integer> intercepted = new ArrayList<>();
page.route("**/empty.html", route -> {
intercepted.add(1);
route.fallback();
});
page.route("**/empty.html", route -> {
intercepted.add(2);
route.fallback();
});
page.route("**/empty.html", route -> {
intercepted.add(3);
route.fallback();
});
page.navigate(server.EMPTY_PAGE);
assertEquals(asList(3, 2, 1), intercepted);
}
@Test
void shouldFallBackAsync() {
List<Integer> intercepted = new ArrayList<>();
page.route("**/empty.html", route -> {
intercepted.add(1);
page.waitForTimeout(50);
route.fallback();
});
page.route("**/empty.html", route -> {
intercepted.add(2);
page.waitForTimeout(100);
route.fallback();
});
page.route("**/empty.html", route -> {
intercepted.add(3);
page.waitForTimeout(150);
route.fallback();
});
page.navigate(server.EMPTY_PAGE);
assertEquals(asList(3, 2, 1), intercepted);
}
@Test
void shouldNotChainFulfill() {
boolean[] failed = {false};
page.route("**/empty.html", route -> {
failed[0] = true;
});
page.route("**/empty.html", route -> {
route.fulfill(new Route.FulfillOptions().setStatus(200).setBody("fulfilled"));
});
page.route("**/empty.html", route -> {
route.fallback();
});
Response response = page.navigate(server.EMPTY_PAGE);
assertEquals("fulfilled", response.text());
assertFalse(failed[0]);
}
@Test
void shouldNotChainAbort() {
boolean[] failed = {false};
page.route("**/empty.html", route -> {
failed[0] = true;
});
page.route("**/empty.html", route -> {
route.abort();
});
page.route("**/empty.html", route -> {
route.fallback();
});
try {
Response response = page.navigate(server.EMPTY_PAGE);
fail("did not throw");
assertEquals("fulfilled", response.text());
} catch (PlaywrightException e) {
}
assertFalse(failed[0]);
}
@Test
void shouldFallBackAfterException() {
page.route("**/empty.html", route -> {
route.resume();
});
page.route("**/empty.html", route -> {
try {
route.fulfill(new Route.FulfillOptions());
} catch (PlaywrightException e) {
route.fallback();
}
});
page.navigate(server.EMPTY_PAGE);
}
@Test
void shouldChainOnce() {
page.route("**/empty.html", route -> {
route.fulfill(new Route.FulfillOptions().setStatus(200).setBody("fulfilled one"));
}, new Page.RouteOptions().setTimes(1));
page.route("**/empty.html", route -> {
route.fallback();
}, new Page.RouteOptions().setTimes(1));
Response response = page.navigate(server.EMPTY_PAGE);
assertEquals("fulfilled one", response.text());
}
@Test
void shouldAmendHTTPHeaders() throws ExecutionException, InterruptedException {
List<String> values = new ArrayList<>();
page.route("**/sleep.zzz", route -> {
values.add(route.request().headers().get("foo"));
values.add(route.request().headerValue("FOO"));
route.resume();
});
page.route("**/*", route -> {
Map<String, String> headers = route.request().headers();
headers.put("FOO", "bar");
route.fallback(new Route.FallbackOptions().setHeaders(headers));
});
page.navigate(server.EMPTY_PAGE);
Future<Server.Request> request = server.futureRequest("/sleep.zzz");
page.evaluate("() => fetch('/sleep.zzz')");
values.addAll(request.get().headers.get("foo"));
assertEquals(asList("bar", "bar", "bar"), values);
}
@Test
void shouldDeleteHeaderWithUndefinedValue() throws ExecutionException, InterruptedException {
// https://github.com/microsoft/playwright/issues/13106
page.navigate(server.PREFIX + "/empty.html");
Future<Server.Request> serverRequest = server.futureRequest("/something");
server.setRoute("/something", exchange -> {
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
exchange.sendResponseHeaders(200, 0);
try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("done");
}
});
Request[] interceptedRequest = {null};
page.route(server.PREFIX + "/something", route -> {
interceptedRequest[0] = route.request();
route.resume();
});
page.route(server.PREFIX + "/something", route -> {
Map<String, String> headers = route.request().allHeaders();
headers.remove("foo");
route.fallback(new Route.FallbackOptions().setHeaders(headers));
});
Object text = page.evaluate("async url => {\n" +
" const data = await fetch(url, {\n" +
" headers: {\n" +
" foo: 'a',\n" +
" bar: 'b',\n" +
" }\n" +
" });\n" +
" return data.text();\n" +
" }", server.PREFIX + "/something");
assertEquals("done", text);
assertNull(interceptedRequest[0].headers().get("foo"));
assertNull(serverRequest.get().headers.get("foo"));
assertEquals(asList("b"), serverRequest.get().headers.get("bar"));
}
@Test
void shouldAmendMethod() throws ExecutionException, InterruptedException {
Future<Server.Request> sRequest = server.futureRequest("/sleep.zzz");
page.navigate(server.EMPTY_PAGE);
String[] method = {null};
page.route("**/*", route -> {
method[0] = route.request().method();
route.resume();
});
page.route("**/*", route -> route.fallback(new Route.FallbackOptions().setMethod("POST")));
Request request = page.waitForRequest("**/sleep.zzz", () -> page.evaluate("() => fetch('/sleep.zzz')"));
assertEquals("POST", method[0]);
assertEquals("POST", request.method());
assertEquals("POST", sRequest.get().method);
}
@Test
void shouldOverrideRequestUrl() throws ExecutionException, InterruptedException {
Future<Server.Request> request = server.futureRequest("/global-var.html");
String[] url = {null};
page.route("**/global-var.html", route -> {
url[0] = route.request().url();
route.resume();
});
page.route("**/foo", route -> route.fallback(new Route.FallbackOptions().setUrl(server.PREFIX + "/global-var.html")));
Response response = page.waitForResponse("**/*", () -> page.navigate(server.PREFIX + "/foo"));
assertEquals(server.PREFIX + "/global-var.html", url[0]);
assertEquals(server.PREFIX + "/foo", response.url());
assertEquals(123, page.evaluate("() => window['globalVar']"));
assertEquals("GET", request.get().method);
}
@Test
void shouldAmendPostData() throws ExecutionException, InterruptedException {
page.navigate(server.EMPTY_PAGE);
String[] postData = {null};
page.route("**/*", route -> {
postData[0] = route.request().postData();
route.resume();
});
page.route("**/*", route -> {
route.fallback(new Route.FallbackOptions().setPostData("doggo"));
});
Future<Server.Request> serverRequest = server.futureRequest("/sleep.zzz");
page.evaluate("() => fetch('/sleep.zzz', { method: 'POST', body: 'birdy' })");
assertEquals("doggo", postData[0]);
assertEquals("doggo", new String(serverRequest.get().postBody, StandardCharsets.UTF_8));
}
@Test
void shouldAmendBinaryPostData() throws ExecutionException, InterruptedException {
page.navigate(server.EMPTY_PAGE);
byte[] arr = new byte[256];
for (int i = 0; i < arr.length; i++) {
arr[i] = (byte) i;
}
byte[][] postDataBuffer = {null};
page.route("**/*", route -> {
postDataBuffer[0] = route.request().postDataBuffer();
route.resume();
});
page.route("**/*", route -> {
route.fallback(new Route.FallbackOptions().setPostData(arr));
});
Future<Server.Request> serverRequest = server.futureRequest("/sleep.zzz");
page.evaluate("() => fetch('/sleep.zzz', { method: 'POST', body: 'birdy' })");
byte[] buffer = serverRequest.get().postBody;
assertArrayEquals(arr, buffer);
assertArrayEquals(arr, postDataBuffer[0]);
}
}
@@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -66,29 +67,29 @@ public class TestPageRoute extends TestBase {
List<Integer> intercepted = new ArrayList<>();
page.route("**/*", route -> {
intercepted.add(1);
route.resume();
route.fallback();
});
page.route("**/empty.html", route -> {
intercepted.add(2);
route.resume();
route.fallback();
});
page.route("**/empty.html", route -> {
intercepted.add(3);
route.resume();
route.fallback();
});
Consumer<Route> handler4 = route -> {
intercepted.add(4);
route.resume();
route.fallback();
};
page.route("**/empty.html", handler4);
page.navigate(server.EMPTY_PAGE);
assertEquals(asList(4), intercepted);
assertEquals(asList(4, 3, 2, 1), intercepted);
intercepted.clear();
page.unroute("**/empty.html", handler4);
page.navigate(server.EMPTY_PAGE);
assertEquals(asList(3), intercepted);
assertEquals(asList(3, 2, 1), intercepted);
intercepted.clear();
page.unroute("**/empty.html");
@@ -102,25 +103,25 @@ public class TestPageRoute extends TestBase {
Predicate<String> predicate = r -> true;
page.route(predicate, route -> {
intercepted.add(1);
route.resume();
route.fallback();
});
page.route(predicate, route -> {
intercepted.add(2);
route.resume();
route.fallback();
});
Consumer<Route> handler3 = route -> {
intercepted.add(3);
route.resume();
route.fallback();
};
page.route(predicate, handler3);
page.navigate(server.EMPTY_PAGE);
assertEquals(asList(3), intercepted);
assertEquals(asList(3, 2, 1), intercepted);
intercepted.clear();
page.unroute(predicate, handler3);
page.navigate(server.EMPTY_PAGE);
assertEquals(asList(2), intercepted);
assertEquals(asList(2, 1), intercepted);
intercepted.clear();
page.unroute(predicate);
@@ -256,7 +257,7 @@ public class TestPageRoute extends TestBase {
}
assertNotNull(failedRequest[0]);
if (isWebKit()) {
assertEquals("Request intercepted", failedRequest[0].failure());
assertEquals("Blocked by Web Inspector", failedRequest[0].failure());
} else if (isFirefox()) {
assertEquals("NS_ERROR_OFFLINE", failedRequest[0].failure());
} else {
@@ -281,7 +282,7 @@ public class TestPageRoute extends TestBase {
fail("did not throw");
} catch (PlaywrightException e) {
if (isWebKit())
assertTrue(e.getMessage().contains("Request intercepted"));
assertTrue(e.getMessage().contains("Blocked by Web Inspector"), e.getMessage());
else if (isFirefox())
assertTrue(e.getMessage().contains("NS_ERROR_FAILURE"));
else
@@ -347,7 +348,12 @@ public class TestPageRoute extends TestBase {
assertEquals(200, response.status());
assertTrue(response.url().contains("one-style.html"));
assertEquals(2, intercepted.size());
// In firefox one-style.css is requested multiple times.
if (isFirefox()) {
assertTrue(intercepted.size() > 1);
} else {
assertEquals(2, intercepted.size());
}
assertEquals("document", intercepted.get(0).resourceType());
assertTrue(intercepted.get(0).url().contains("one-style.html"));
@@ -460,12 +466,17 @@ public class TestPageRoute extends TestBase {
});
Response response = page.navigate("data:text/html,<link rel='stylesheet' href='" + server.PREFIX + "/fonts?helvetica|arial'/>");
assertNull(response);
assertEquals(1, requests.size());
// In firefox linked resource is requested multiple times.
if (isFirefox()) {
assertTrue(requests.size() > 0);
} else {
assertEquals(1, requests.size());
}
assertEquals(400, (requests.get(0).response()).status());
}
@Test
void shouldNotThrowInvalidInterceptionIdIfTheRequestWasCancelled() {
void shouldThrowIfResumeIsCalledAfterRouteHandlerFinished() {
page.setContent("<iframe></iframe>");
Route[] route = {null};
page.route("**/*", r -> route[0] = r);
@@ -475,8 +486,9 @@ public class TestPageRoute extends TestBase {
page.evalOnSelector("iframe", "frame => frame.remove()");
try {
route[0].resume();
fail("did not throw");
} catch (PlaywrightException e) {
fail("Should not throw");
assertTrue(e.getMessage().contains("Route is already handled!"), e.getMessage());
}
}
@@ -524,9 +536,7 @@ public class TestPageRoute extends TestBase {
page.navigate(server.EMPTY_PAGE);
page.route("**/cars*", route -> {
Map<String, String> headers = new HashMap<>();
if (route.request().url().endsWith("allow")) {
headers.put("access-control-allow-origin", "*");
}
headers.put("access-control-allow-origin", route.request().url().endsWith("allow") ? "*" : "none");
route.fulfill(new Route.FulfillOptions()
.setStatus(200)
.setContentType("application/json")
@@ -690,4 +700,50 @@ public class TestPageRoute extends TestBase {
page.navigate(server.EMPTY_PAGE);
assertEquals(1, intercepted[0]);
}
@Test
void shouldAddAccessControlAllowOriginByDefaultWhenFulfill() {
page.navigate(server.EMPTY_PAGE);
page.route("**/cars", route -> {
route.fulfill(new Route.FulfillOptions()
.setContentType("application/json")
.setStatus(200)
.setBody("[\"electric\",\"gas\"]"));
});
Response response = page.waitForResponse("https://example.com/cars", () -> {
Object result = page.evaluate("async () => {\n" +
" const response = await fetch('https://example.com/cars', {\n" +
" method: 'POST',\n" +
" headers: { 'Content-Type': 'application/json' },\n" +
" mode: 'cors',\n" +
" body: JSON.stringify({ 'number': 1 })\n" +
" });\n" +
" return response.text();\n" +
" }");
assertEquals("[\"electric\",\"gas\"]", result);
});
assertEquals(server.PREFIX, response.headerValue("Access-Control-Allow-Origin"));
}
@Test
void shouldChainFallbackWDynamicURL() {
List<Integer> intercepted = new ArrayList<>();
page.route("**/bar", route -> {
intercepted.add(1);
route.fallback(new Route.FallbackOptions().setUrl(server.EMPTY_PAGE));
});
page.route("**/foo", route -> {
intercepted.add(2);
route.fallback(new Route.FallbackOptions().setUrl("http://localhost/bar"));
});
page.route("**/empty.html", route -> {
intercepted.add(3);
route.fallback(new Route.FallbackOptions().setUrl("http://localhost/foo"));
});
page.navigate(server.EMPTY_PAGE);
assertEquals(asList(3, 2, 1), intercepted);
}
}
@@ -17,14 +17,23 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.Clip;
import com.microsoft.playwright.options.ScreenshotAnimations;
import com.microsoft.playwright.options.ScreenshotCaret;
import com.microsoft.playwright.options.ScreenshotScale;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static com.microsoft.playwright.options.ScreenshotAnimations.DISABLED;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
// TODO: suite.skip(browserName === "firefox" && headful");
public class TestPageScreenshot extends TestBase {
@@ -50,4 +59,191 @@ public class TestPageScreenshot extends TestBase {
assertEquals(100, image.getHeight());
// expect(screenshot).toMatchSnapshot("screenshot-clip-rect.png");
}
static private void rafraf(Page page) {
// Do a double raf since single raf does not
// actually guarantee a new animation frame.
page.evaluate("() => new Promise(x => {\n" +
" requestAnimationFrame(() => requestAnimationFrame(x));\n" +
" })");
}
@Test
void shouldNotCaptureInfiniteCssAnimation() {
page.navigate(server.PREFIX + "/rotate-z.html");
Locator div = page.locator("div");
byte[] screenshot = div.screenshot(new Locator.ScreenshotOptions().setAnimations(DISABLED));
for (int i = 0; i < 10; ++i) {
rafraf(page);
byte[] newScreenshot = div.screenshot(new Locator.ScreenshotOptions().setAnimations(DISABLED));
assertArrayEquals(screenshot, newScreenshot);
}
}
@Test
void shouldNotCapturePseudoElementCssAnimation() {
page.navigate(server.PREFIX + "/rotate-pseudo.html");
Locator div = page.locator("div");
byte[] screenshot = div.screenshot(new Locator.ScreenshotOptions().setAnimations(DISABLED));
for (int i = 0; i < 10; ++i) {
rafraf(page);
byte[] newScreenshot = div.screenshot(new Locator.ScreenshotOptions().setAnimations(DISABLED));
assertArrayEquals(screenshot, newScreenshot);
}
}
@Test
void shouldNotCaptureCssAnimationsInShadowDOM() {
page.navigate(server.PREFIX + "/rotate-z-shadow-dom.html");
byte[] screenshot = page.screenshot(new Page.ScreenshotOptions().setAnimations(DISABLED));
for (int i = 0; i < 10; ++i) {
rafraf(page);
byte[] newScreenshot = page.screenshot(new Page.ScreenshotOptions().setAnimations(DISABLED));
assertArrayEquals(screenshot, newScreenshot);
}
}
@Test
void shouldResumeInfiniteAnimations() {
page.navigate(server.PREFIX + "/rotate-z.html");
page.screenshot(new Page.ScreenshotOptions().setAnimations(DISABLED));
byte[] buffer1 = page.screenshot();
rafraf(page);
byte[] buffer2 = page.screenshot();
try {
assertArrayEquals(buffer1, buffer2);
} catch (AssertionFailedError e) {
return;
}
fail("Screenshots are equal");
}
@Test
void shouldNotCaptureInfiniteWebAnimations() {
page.navigate(server.PREFIX + "/web-animation.html");
Locator div = page.locator("div");
byte[] screenshot = div.screenshot(new Locator.ScreenshotOptions().setAnimations(DISABLED));
for (int i = 0; i < 10; ++i) {
rafraf(page);
byte[] newScreenshot = div.screenshot(new Locator.ScreenshotOptions().setAnimations(DISABLED));
assertArrayEquals(screenshot, newScreenshot);
}
// Should resume infinite web animation.
byte[] buffer1 = page.screenshot();
rafraf(page);
byte[] buffer2 = page.screenshot();
try {
assertArrayEquals(buffer1, buffer2);
} catch (AssertionFailedError e) {
return;
}
fail("Screenshots are equal");
}
@Test
void maskShouldWork() {
page.setViewportSize(500, 500);
page.navigate(server.PREFIX + "/grid.html");
byte[] screenshot = page.screenshot(new Page.ScreenshotOptions()
.setMask(asList(page.locator("div").nth(5))));
// TODO: toMatchSnapshot is not present in java, so we only checks that masked screenshot is different.
byte[] originalScreenshot = page.screenshot();
try {
assertArrayEquals(screenshot, originalScreenshot);
} catch (AssertionFailedError e) {
return;
}
fail("Screenshots are equal");
}
@Test
void shouldWorkWithDeviceScaleFactorAndClip() {
try (BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setViewportSize(500, 500).setDeviceScaleFactor(3))) {
Page page = context.newPage();
page.navigate(server.PREFIX + "/grid.html");
byte[] screenshot = page.screenshot(new Page.ScreenshotOptions().setClip(50, 100, 150, 100));
assertNotNull(screenshot);
// TODO:
// expect(screenshot).toMatchSnapshot("screenshot-device-scale-factor-clip.png");
}
}
@Test
void shouldWorkWithDeviceScaleFactorAndScaleCss() {
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setViewportSize(320, 480).setDeviceScaleFactor(2));
Page page = context.newPage();
page.navigate(server.PREFIX + "/grid.html");
byte[] screenshot = page.screenshot(new Page.ScreenshotOptions().setScale(ScreenshotScale.CSS));
assertNotNull(screenshot);
// TODO:
// expect(screenshot).toMatchSnapshot("screenshot-device-scale-factor-css-size.png");
}
@Test
void shouldWorkWithDeviceScaleFactorAndScaleDevice() {
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setViewportSize(320, 480).setDeviceScaleFactor(2));
Page page = context.newPage();
page.navigate(server.PREFIX + "/grid.html");
byte[] screenshot = page.screenshot(new Page.ScreenshotOptions().setScale(ScreenshotScale.DEVICE));
assertNotNull(screenshot);
// TODO:
// expect(screenshot).toMatchSnapshot("screenshot-device-scale-factor-device-size.png");
}
@Test
void shouldNotCaptureBlinkingCaretByDefault() {
page.setContent("<!-- Refer to stylesheet from other origin. Accessing this\n" +
" stylesheet rules will throw.\n" +
" -->\n" +
" <link rel=stylesheet href=\"" + server.CROSS_PROCESS_PREFIX + "/injectedstyle.css\">\n" +
" <!-- make life harder: define caret color in stylesheet -->\n" +
" <style>\n" +
" div {\n" +
" caret-color: #000 !important;\n" +
" }\n" +
" </style>\n" +
" <div contenteditable=\"true\"></div>\n");
Locator div = page.locator("div");
div.type("foo bar");
byte[] screenshot = div.screenshot();
for (int i = 0; i < 10; ++i) {
// Caret blinking time is set to 500ms.
// Try to capture variety of screenshots to make
// sure we don"t capture blinking caret.
page.waitForTimeout(150);
byte[] newScreenshot = div.screenshot();
assertArrayEquals(screenshot, newScreenshot);
}
}
@Test
void shouldCaptureBlinkingCaretIfExplicitlyAskedFor() {
page.setContent(" <!-- Refer to stylesheet from other origin. Accessing this\n" +
" stylesheet rules will throw.\n" +
" -->\n" +
" <link rel=stylesheet href=\"" + server.CROSS_PROCESS_PREFIX + "/injectedstyle.css'}\">\n" +
" <!-- make life harder: define caret color in stylesheet -->\n" +
" <style>\n" +
" div {\n" +
" caret-color: #000 !important;\n" +
" }\n" +
" </style>\n" +
" <div contenteditable=\"true\"></div>\n");
Locator div = page.locator("div");
div.type("foo bar");
byte[] screenshot = div.screenshot();
boolean hasDifferentScreenshots = false;
for (int i = 0; !hasDifferentScreenshots && i < 10; ++i) {
// Caret blinking time is set to 500ms.
// Try to capture variety of screenshots to make
// sure we capture blinking caret.
page.waitForTimeout(150);
byte[] newScreenshot = div.screenshot(new Locator.ScreenshotOptions().setCaret(ScreenshotCaret.INITIAL));
hasDifferentScreenshots = !Arrays.equals(newScreenshot, screenshot);
}
assertTrue(hasDifferentScreenshots);
}
}
@@ -17,15 +17,22 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.FilePayload;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import static java.util.Arrays.asList;
@@ -49,6 +56,90 @@ public class TestPageSetInputFiles extends TestBase {
"}", input));
}
@Test
void shouldUploadLargeFile(@TempDir Path tmpDir) throws IOException, ExecutionException, InterruptedException {
Assumptions.assumeTrue(3 <= (Runtime.getRuntime().maxMemory() >> 30), "Fails if max heap size is < 3Gb");
page.navigate(server.PREFIX + "/input/fileupload.html");
Path uploadFile = tmpDir.resolve("200MB.zip");
String str = String.join("", Collections.nCopies(4 * 1024, "A"));
try (Writer stream = new OutputStreamWriter(Files.newOutputStream(uploadFile))) {
for (int i = 0; i < 50 * 1024; i++) {
stream.write(str);
}
}
Locator input = page.locator("input[type='file']");
JSHandle events = input.evaluateHandle("e => {\n" +
" const events = [];\n" +
" e.addEventListener('input', () => events.push('input'));\n" +
" e.addEventListener('change', () => events.push('change'));\n" +
" return events;\n" +
" }");
input.setInputFiles(uploadFile);
assertEquals("200MB.zip", input.evaluate("e => e.files[0].name"));
assertEquals(asList("input", "change"), events.evaluate("e => e"));
CompletableFuture<MultipartFormData> formData = new CompletableFuture<>();
server.setRoute("/upload", exchange -> {
try {
MultipartFormData multipartFormData = MultipartFormData.parseRequest(exchange);
formData.complete(multipartFormData);
} catch (Exception e) {
e.printStackTrace();
formData.completeExceptionally(e);
}
exchange.sendResponseHeaders(200, -1);
});
page.click("input[type=submit]", new Page.ClickOptions().setTimeout(90_000));
List<MultipartFormData.Field> fields = formData.get().fields;
assertEquals(1, fields.size());
assertEquals("200MB.zip", fields.get(0).filename);
assertEquals(200 * 1024 * 1024, fields.get(0).content.length());
}
@Test
void shouldUploadLargeFileWithRelativePath(@TempDir Path tmpDir) throws IOException, ExecutionException, InterruptedException {
Assumptions.assumeTrue(3 <= (Runtime.getRuntime().maxMemory() >> 30), "Fails if max heap size is < 3Gb");
page.navigate(server.PREFIX + "/input/fileupload.html");
Path uploadFile = tmpDir.resolve("200MB.zip");
String str = String.join("", Collections.nCopies(4 * 1024, "A"));
try (Writer stream = new OutputStreamWriter(Files.newOutputStream(uploadFile))) {
for (int i = 0; i < 50 * 1024; i++) {
stream.write(str);
}
}
Locator input = page.locator("input[type='file']");
JSHandle events = input.evaluateHandle("e => {\n" +
" const events = [];\n" +
" e.addEventListener('input', () => events.push('input'));\n" +
" e.addEventListener('change', () => events.push('change'));\n" +
" return events;\n" +
" }");
Path cwd = Paths.get("").toAbsolutePath();
Path relativeUploadPath = cwd.relativize(uploadFile);
assertFalse(relativeUploadPath.isAbsolute());
input.setInputFiles(relativeUploadPath);
assertEquals("200MB.zip", input.evaluate("e => e.files[0].name"));
assertEquals(asList("input", "change"), events.evaluate("e => e"));
CompletableFuture<MultipartFormData> formData = new CompletableFuture<>();
server.setRoute("/upload", exchange -> {
try {
MultipartFormData multipartFormData = MultipartFormData.parseRequest(exchange);
formData.complete(multipartFormData);
} catch (Exception e) {
e.printStackTrace();
formData.completeExceptionally(e);
}
exchange.sendResponseHeaders(200, -1);
});
page.click("input[type=submit]", new Page.ClickOptions().setTimeout(90_000));
List<MultipartFormData.Field> fields = formData.get().fields;
assertEquals(1, fields.size());
assertEquals("200MB.zip", fields.get(0).filename);
assertEquals(200 * 1024 * 1024, fields.get(0).content.length());
}
@Test
void shouldWork() {
page.setContent("<input type=file>");
@@ -41,7 +41,8 @@ public class TestPlaywrightCreate {
getBrowserTypeFromEnv(playwright).launch();
fail("Did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Looks like Playwright Test or Playwright was just installed or updated"), e.getMessage());
assertTrue(e.getMessage().contains("Looks like Playwright Test or Playwright was just installed or updated") ||
e.getMessage().contains("Looks like Playwright was just installed or updated."), e.getMessage());
}
try (DirectoryStream<Path> ds = Files.newDirectoryStream(browsersDir)) {

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