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

Compare commits

...

48 Commits

Author SHA1 Message Date
Yury Semikhatsky ae844728de chore: update version to 1.24.1 (#1010) 2022-07-26 10:32:49 -07:00
Yury Semikhatsky 472b523c77 chore: update driver to 1.24.1 (#1009) 2022-07-26 10:32:36 -07:00
Yury Semikhatsky 94d1393c88 chore: set version to 1.24.0 (#1006) 2022-07-25 15:56:10 -07:00
Yury Semikhatsky f759222755 fix: do not escape html symbols when serializing strings to json (#1005) 2022-07-25 15:19:48 -07:00
Max Schmitt d87c6b24ca chore(roll): roll 1.24.0 add regex, date, url serializers (#1003) 2022-07-25 22:53:53 +02:00
Max Schmitt 41355bd059 chore: add 'gpg' package to Docker images (#1004) 2022-07-25 12:49:19 +02:00
Yury Semikhatsky e372513fa4 test: unflake playwrightDriverAlternativeImpl (#986) 2022-07-08 16:22:48 -07:00
Yury Semikhatsky b8e1e1d935 chore: remove isolated tests (#984) 2022-06-30 14:09:09 -07:00
Yury Semikhatsky a0745735d9 feat: roll driver to 1.23.1-beta, implement routeFromHar.update (#982) 2022-06-30 12:04:22 -07:00
Yury Semikhatsky b90de26d23 feat: accept PLAYWRIGHT_JAVA_SRC in Playwright.create (#980) 2022-06-29 15:27:11 -07:00
Yury Semikhatsky adfdf92eaa test: use only 127.0.0.1 for loopback (#978) 2022-06-28 17:26:33 -07:00
Yury Semikhatsky 60cb6ea7b3 chore: remove extractZip logs from tests (#979) 2022-06-28 17:26:22 -07:00
Yury Semikhatsky 44cb76d92c chore: only log har when pw:api is enabled (#977) 2022-06-28 16:23:04 -07:00
Yury Semikhatsky 9fac877892 test: unflake TestWebSocket.shouldEmitError (#976) 2022-06-28 16:09:17 -07:00
Yury Semikhatsky ec8fb9f191 fix: use same eol setting in gitattributes as upstream (#973) 2022-06-28 12:29:27 -07:00
Yury Semikhatsky 844d1665b2 chore: set dev version to 1.24.0-SNAPSHOT (#972) 2022-06-28 09:14:57 -07:00
Yury Semikhatsky 484a255ec7 feat: support ignoreCase option (#969) 2022-06-28 08:43:20 -07:00
Yury Semikhatsky 3f60144e0f chore: update driver to 1.23.0 (#968) 2022-06-27 15:33:44 -07:00
Yury Semikhatsky 8004e5d0ff test: 403 response still has postBody (#967) 2022-06-27 14:13:40 -07:00
Yury Semikhatsky 3604aab710 chore: store LocalUtils on Connection (#963) 2022-06-24 16:25:58 -07:00
Yury Semikhatsky 2fdb89c94e fix: match against updated url (#962) 2022-06-24 15:13:41 -07:00
Yury Semikhatsky 4fee61a655 test: unflake TestHar.shouldAttachContent (#961) 2022-06-24 14:50:24 -07:00
Yury Semikhatsky efb281e016 feat: implement routeFromHAR (#960) 2022-06-24 13:44:57 -07:00
Yury Semikhatsky fdec32c650 chore: simplify handler result (#959) 2022-06-23 18:59:04 -07:00
Yury Semikhatsky 7e285ffe44 feat: route.fallback with overrides (#958) 2022-06-23 16:48:32 -07:00
Yury Semikhatsky edf0e45fb4 feat: roll to 1.23.0-beta-1655926399000 (#956) 2022-06-23 10:11:38 -07:00
Yury Semikhatsky c8eb4f9eeb feat: route chaining (#950) 2022-06-11 11:24:30 -07:00
dependabot[bot] e4ec9b8dbe chore(deps): bump gson from 2.8.6 to 2.8.9 in /tools/api-generator (#937) 2022-06-11 09:37:35 -07:00
dependabot[bot] ef13ab86b8 chore(deps): bump gson from 2.8.6 to 2.8.9 in /tools/test-local-installation (#938) 2022-06-11 09:37:09 -07:00
dependabot[bot] a48fef6b01 chore(deps): bump gson from 2.8.6 to 2.8.9 (#936) 2022-06-11 09:36:35 -07:00
Yury Semikhatsky 1c1f3d43ac docs: update docs link (#945) 2022-06-06 09:42:53 -07:00
Max Schmitt 8f59cd73f5 chore: fix examples which could not be executed (#941) 2022-05-31 09:33:33 +02:00
Yury Semikhatsky e04ef2132c test: make install test not skip browser downloads (#931) 2022-05-13 15:31:37 -07:00
Yury Semikhatsky 34d23a833e devops: bump version to 1.23.0-SNAPSHOT (#930) 2022-05-13 11:14:15 -07:00
Yury Semikhatsky 15eefc54af feat: roll beta driver, remove layout selectors (#926) 2022-05-13 00:49:59 -07:00
Yury Semikhatsky abf245ccc7 feat: implement Locator.filter (#925) 2022-05-12 09:39:10 -07:00
Gabriel Gavrilov 10592ce5c7 Added Selectors and Keyboard Manipulation Example (#920) 2022-05-11 09:20:51 -07:00
Yury Semikhatsky ef7f50c48a feat: roll driver, implement locator filters (#921) 2022-05-10 10:43:30 -07:00
Andrey Lushnikov 473b1ce794 devops: mark docker image as playwright official (#889) 2022-05-06 19:07:44 -07:00
Yury Semikhatsky 98ecb7e0a0 test: unflake TestInstall.shouldThrowWhenBrowserPathIsInvalid (#916) 2022-05-03 13:50:20 -07:00
Yury Semikhatsky 04eb228813 devops: add workflow triggering internal tests (#915) 2022-05-02 18:01:40 -07:00
Max Schmitt 298e01ee80 chore: add arm64 Docker image (#890) 2022-05-02 14:46:05 +01:00
Yury Semikhatsky b6b54af13c Revert "Revert "Add Linux/arm64 support (#883)" (#892)" (#905)
This reverts commit 536af6b3d8.
2022-04-27 17:24:12 -07:00
Yury Semikhatsky b8d2ccae08 fix: respect isChecked options (#911) 2022-04-26 17:01:50 -07:00
Yury Semikhatsky 59e7c0cc94 fix: convert file path to absolute in setInputFiles (#903) 2022-04-15 12:11:52 -07:00
Yury Semikhatsky 54d0366b9e fix: get docker version from pom.xml (#899) (#901) 2022-04-12 12:15:06 -07:00
Andrey Lushnikov 9845a05544 devops: fix docker publish workflow (#898) 2022-04-12 11:40:21 -07:00
Yury Semikhatsky 41fd9a6f75 chore: bump dev version to 1.22 (#897) 2022-04-12 10:46:31 -07:00
104 changed files with 5315 additions and 575 deletions
+4 -2
View File
@@ -1,3 +1,5 @@
# text files must be lf for golden file tests to work
*.txt eol=lf
*.json eol=lf
* text=auto eol=lf
# make project show as TS on GitHub
*.js linguist-detectable=false
@@ -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:
+3 -12
View File
@@ -31,18 +31,14 @@ jobs:
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test -DexcludedGroups=isolated --no-transfer-progress --fail-at-end
run: mvn test --no-transfer-progress --fail-at-end
env:
BROWSER: ${{ matrix.browser }}
- name: Run tracing tests w/ sources
run: mvn test -DexcludedGroups=isolated --no-transfer-progress --fail-at-end -D test=*TestTracing*
run: mvn test --no-transfer-progress --fail-at-end -D test=*TestTracing*
env:
BROWSER: ${{ matrix.browser }}
PLAYWRIGHT_JAVA_SRC: src/test/java
- name: Run driver throw tests
run: mvn test -Dgroups=driverThrowTest --no-transfer-progress --fail-at-end
env:
BROWSER: ${{ matrix.browser }}
- name: Test Spring Boot Starter
shell: bash
env:
@@ -83,12 +79,7 @@ jobs:
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test -DexcludedGroups=isolated --no-transfer-progress --fail-at-end
env:
BROWSER: chromium
BROWSER_CHANNEL: ${{ matrix.browser-channel }}
- name: Run driver throw tests
run: mvn test -Dgroups=driverThrowTest --no-transfer-progress --fail-at-end
run: mvn test --no-transfer-progress --fail-at-end
env:
BROWSER: chromium
BROWSER_CHANNEL: ${{ matrix.browser-channel }}
@@ -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 -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
+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 -->101.0.4951.15<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->15.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->98.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->104.0.5112.48<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->16.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->102.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all the browsers on all platforms. Check out [system requirements](https://playwright.dev/java/docs/next/intro/#system-requirements) for details.
@@ -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.21.0-SNAPSHOT</version>
<version>1.24.1</version>
</parent>
<artifactId>driver-bundle</artifactId>
@@ -139,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,9 +18,13 @@ package com.microsoft.playwright;
import com.microsoft.playwright.impl.Driver;
import com.microsoft.playwright.impl.DriverJar;
import org.junit.jupiter.api.*;
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.ServerSocket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
@@ -28,12 +32,26 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
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.
@@ -44,13 +62,29 @@ public class TestInstall {
}
@Test
@Tags({@Tag("isolated"), @Tag("driverThrowTest")})
void shouldThrowWhenBrowserPathIsInvalid() {
void shouldThrowWhenBrowserPathIsInvalid(@TempDir Path tmpDir) throws NoSuchFieldException, IllegalAccessException {
Map<String,String> env = new HashMap<>();
env.put("PLAYWRIGHT_BROWSERS_PATH", "/some/bad/path/that/should/not/exist/i/hope");
assertThrows(RuntimeException.class, () -> Driver.ensureDriverInstalled(env, true));
assertThrows(RuntimeException.class, () -> Driver.ensureDriverInstalled(env, true));
// 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
@@ -79,12 +113,20 @@ public class TestInstall {
}
@Test
void playwrightDriverAlternativeImpl() {
void playwrightDriverAlternativeImpl() throws NoSuchFieldException, IllegalAccessException {
// 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);
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());
field.set(Driver.class, value);
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.21.0-SNAPSHOT</version>
<version>1.24.1</version>
</parent>
<artifactId>driver</artifactId>
+2 -2
View File
@@ -6,7 +6,7 @@
<groupId>org.example</groupId>
<artifactId>examples</artifactId>
<version>1.21.0-SNAPSHOT</version>
<version>1.24.1</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.22.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.21.0-SNAPSHOT</version>
<version>1.24.1</version>
</parent>
<artifactId>playwright</artifactId>
@@ -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).
@@ -954,6 +1075,10 @@ public interface Browser extends AutoCloseable {
* <p> In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from the
* browser server.
*
* <p> <strong>NOTE:</strong> This is similar to force quitting the browser. Therefore, you should call {@link BrowserContext#close
* BrowserContext.close()} on any {@code BrowserContext}'s you explicitly created earlier with {@link Browser#newContext
* Browser.newContext()} **before** calling {@link Browser#close Browser.close()}.
*
* <p> The {@code Browser} object itself is considered to be disposed and cannot be used anymore.
*/
void close();
@@ -973,6 +1098,11 @@ public interface Browser extends AutoCloseable {
boolean isConnected();
/**
* Creates a new browser context. It won't share cookies/cache with other browser contexts.
*
* <p> <strong>NOTE:</strong> If directly using this method to create {@code BrowserContext}s, it is best practice to explicilty close the returned context
* via {@link BrowserContext#close BrowserContext.close()} when your code is done with the {@code BrowserContext}, and before
* calling {@link Browser#close Browser.close()}. This will ensure the {@code context} is closed gracefully and any
* artifacts—like HARs and videos—are fully flushed and saved.
* <pre>{@code
* Browser browser = playwright.firefox().launch(); // Or 'chromium' or 'webkit'.
* // Create a new incognito browser context.
@@ -980,6 +1110,10 @@ public interface Browser extends AutoCloseable {
* // Create a new page in a pristine context.
* Page page = context.newPage();
* page.navigate('https://example.com');
*
* // Gracefull close up everything
* context.close();
* browser.close();
* }</pre>
*/
default BrowserContext newContext() {
@@ -987,6 +1121,11 @@ public interface Browser extends AutoCloseable {
}
/**
* Creates a new browser context. It won't share cookies/cache with other browser contexts.
*
* <p> <strong>NOTE:</strong> If directly using this method to create {@code BrowserContext}s, it is best practice to explicilty close the returned context
* via {@link BrowserContext#close BrowserContext.close()} when your code is done with the {@code BrowserContext}, and before
* calling {@link Browser#close Browser.close()}. This will ensure the {@code context} is closed gracefully and any
* artifacts—like HARs and videos—are fully flushed and saved.
* <pre>{@code
* Browser browser = playwright.firefox().launch(); // Or 'chromium' or 'webkit'.
* // Create a new incognito browser context.
@@ -994,6 +1133,10 @@ public interface Browser extends AutoCloseable {
* // Create a new page in a pristine context.
* Page page = context.newPage();
* page.navigate('https://example.com');
*
* // Gracefull close up everything
* context.close();
* browser.close();
* }</pre>
*/
BrowserContext newContext(NewContextOptions options);
@@ -67,7 +67,7 @@ public interface BrowserContext extends AutoCloseable {
* done and its response has started loading in the popup.
* <pre>{@code
* Page newPage = context.waitForPage(() -> {
* page.click("a[target=_blank]");
* page.locator("a[target=_blank]").click();
* });
* System.out.println(newPage.evaluate("location.href"));
* }</pre>
@@ -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 served from the HAR file. If not specified, all requests are served from the HAR file.
*/
public Object url;
/**
* <ul>
* <li> If set to 'abort' any request not found in the HAR file will be aborted.</li>
* <li> If set to 'fallback' falls through to the next route handler in the handler chain.</li>
* </ul>
*
* <p> Defaults to abort.
*/
public RouteFromHAROptions setNotFound(HarNotFound notFound) {
this.notFound = notFound;
return this;
}
/**
* If specified, updates the given HAR with the actual network information instead of serving from file.
*/
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 served from the HAR file. If not specified, all requests are served from the HAR file.
*/
public RouteFromHAROptions setUrl(String url) {
this.url = url;
return this;
}
/**
* A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
* will be served from the HAR file. If not specified, all requests are served from the HAR file.
*/
public RouteFromHAROptions setUrl(Pattern url) {
this.url = url;
return this;
}
}
class StorageStateOptions {
/**
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
@@ -348,7 +404,7 @@ public interface BrowserContext extends AutoCloseable {
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>");
* page.click("button");
* page.locator("button").click();
* }
* }
* }
@@ -407,7 +463,7 @@ public interface BrowserContext extends AutoCloseable {
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>");
* page.click("button");
* page.locator("button").click();
* }
* }
* }
@@ -477,7 +533,7 @@ public interface BrowserContext extends AutoCloseable {
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>\n");
* page.click("button");
* page.locator("button").click();
* }
* }
* }
@@ -553,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
@@ -605,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
@@ -655,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
@@ -707,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
@@ -757,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
@@ -809,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
@@ -855,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
@@ -515,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}.
*/
@@ -525,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.
@@ -546,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.
*/
@@ -843,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}.
*/
@@ -859,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.
@@ -907,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.
*/
@@ -915,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.
*/
@@ -27,14 +27,14 @@ import java.nio.file.Path;
* <p> Download event is emitted once the download starts. Download path becomes available once download completes:
* <pre>{@code
* // wait for download to start
* Download download = page.waitForDownload(() -> page.click("a"));
* Download download = page.waitForDownload(() -> page.locator("a").click());
* // wait for download to complete
* Path path = download.path();
* }</pre>
* <pre>{@code
* // wait for download to start
* Download download = page.waitForDownload(() -> {
* page.click("a");
* page.locator("a").click();
* });
* // wait for download to complete
* Path path = download.path();
@@ -608,7 +608,9 @@ public interface ElementHandle extends JSHandle {
/**
* When set to {@code "css"}, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will
* keep screenshots small. Using {@code "device"} option will produce a single pixel per each device pixel, so screenhots of
* high-dpi devices will be twice as large or even larger. Defaults to {@code "device"}.
* high-dpi devices will be twice as large or even larger.
*
* <p> Defaults to {@code "device"}.
*/
public ScreenshotScale scale;
/**
@@ -679,7 +681,9 @@ public interface ElementHandle extends JSHandle {
/**
* When set to {@code "css"}, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will
* keep screenshots small. Using {@code "device"} option will produce a single pixel per each device pixel, so screenhots of
* high-dpi devices will be twice as large or even larger. Defaults to {@code "device"}.
* high-dpi devices will be twice as large or even larger.
*
* <p> Defaults to {@code "device"}.
*/
public ScreenshotOptions setScale(ScreenshotScale scale) {
this.scale = scale;
@@ -1210,7 +1214,7 @@ public interface ElementHandle extends JSHandle {
* This method returns the bounding box of the element, or {@code null} if the element is not visible. The bounding box is
* calculated relative to the main frame viewport - which is usually the same as the browser window.
*
* <p> Scrolling affects the returned bonding box, similarly to <a
* <p> Scrolling affects the returned bounding box, similarly to <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">Element.getBoundingClientRect</a>.
* That means {@code x} and/or {@code y} may be negative.
*
@@ -1601,13 +1605,21 @@ public interface ElementHandle extends JSHandle {
*/
String innerText();
/**
* Returns {@code input.value} for {@code <input>} or {@code <textarea>} or {@code <select>} element. Throws for non-input elements.
* Returns {@code input.value} for the selected {@code <input>} or {@code <textarea>} or {@code <select>} element.
*
* <p> Throws for non-input elements. However, if the element is inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, returns the value of the
* control.
*/
default String inputValue() {
return inputValue(null);
}
/**
* Returns {@code input.value} for {@code <input>} or {@code <textarea>} or {@code <select>} element. Throws for non-input elements.
* Returns {@code input.value} for the selected {@code <input>} or {@code <textarea>} or {@code <select>} element.
*
* <p> Throws for non-input elements. However, if the element is inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, returns the value of the
* control.
*/
String inputValue(InputValueOptions options);
/**
@@ -1709,19 +1721,27 @@ public interface ElementHandle extends JSHandle {
*/
List<ElementHandle> querySelectorAll(String selector);
/**
* Returns the buffer with the captured screenshot.
* This method captures a screenshot of the page, clipped to the size and position of this particular element. If the
* element is covered by other elements, it will not be actually visible on the screenshot. If the element is a scrollable
* container, only the currently scrolled content will be visible on the screenshot.
*
* <p> This method waits for the <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks, then
* scrolls element into view before taking a screenshot. If the element is detached from DOM, the method throws an error.
*
* <p> Returns the buffer with the captured screenshot.
*/
default byte[] screenshot() {
return screenshot(null);
}
/**
* Returns the buffer with the captured screenshot.
* This method captures a screenshot of the page, clipped to the size and position of this particular element. If the
* element is covered by other elements, it will not be actually visible on the screenshot. If the element is a scrollable
* container, only the currently scrolled content will be visible on the screenshot.
*
* <p> This method waits for the <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks, then
* scrolls element into view before taking a screenshot. If the element is detached from DOM, the method throws an error.
*
* <p> Returns the buffer with the captured screenshot.
*/
byte[] screenshot(ScreenshotOptions options);
/**
@@ -2071,6 +2091,10 @@ public interface ElementHandle extends JSHandle {
/**
* This method waits for <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks, then focuses
* the element and selects all its text content.
*
* <p> If the element is inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, focuses and selects text
* in the control instead.
*/
default void selectText() {
selectText(null);
@@ -2078,6 +2102,10 @@ public interface ElementHandle extends JSHandle {
/**
* This method waits for <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks, then focuses
* the element and selects all its text content.
*
* <p> If the element is inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, focuses and selects text
* in the control instead.
*/
void selectText(SelectTextOptions options);
/**
@@ -2121,75 +2149,99 @@ public interface ElementHandle extends JSHandle {
*/
void setChecked(boolean checked, SetCheckedOptions options);
/**
* This method expects {@code elementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
default void setInputFiles(Path files) {
setInputFiles(files, null);
}
/**
* This method expects {@code elementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
void setInputFiles(Path files, SetInputFilesOptions options);
/**
* This method expects {@code elementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
default void setInputFiles(Path[] files) {
setInputFiles(files, null);
}
/**
* This method expects {@code elementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
void setInputFiles(Path[] files, SetInputFilesOptions options);
/**
* This method expects {@code elementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
default void setInputFiles(FilePayload files) {
setInputFiles(files, null);
}
/**
* This method expects {@code elementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
void setInputFiles(FilePayload files, SetInputFilesOptions options);
/**
* This method expects {@code elementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
default void setInputFiles(FilePayload[] files) {
setInputFiles(files, null);
}
/**
* This method expects {@code elementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code ElementHandle} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
void setInputFiles(FilePayload[] files, SetInputFilesOptions options);
/**
@@ -22,7 +22,7 @@ import java.nio.file.Path;
/**
* {@code FileChooser} objects are dispatched by the page in the {@link Page#onFileChooser Page.onFileChooser()} event.
* <pre>{@code
* FileChooser fileChooser = page.waitForFileChooser(() -> page.click("upload"));
* FileChooser fileChooser = page.waitForFileChooser(() -> page.locator("upload").click());
* fileChooser.setFiles(Paths.get("myfile.pdf"));
* }</pre>
*/
@@ -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);
}
@@ -2366,9 +2366,25 @@ public interface Frame {
* @param eventInit Optional event-specific initialization properties.
*/
void dispatchEvent(String selector, String type, Object eventInit, DispatchEventOptions options);
/**
*
*
* @param source A selector to search for an element to drag. If there are multiple elements satisfying the selector, the first will be
* used. See <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
* @param target A selector to search for an element to drop onto. If there are multiple elements satisfying the selector, the first will
* be used. See <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
*/
default void dragAndDrop(String source, String target) {
dragAndDrop(source, target, null);
}
/**
*
*
* @param source A selector to search for an element to drag. If there are multiple elements satisfying the selector, the first will be
* used. See <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
* @param target A selector to search for an element to drop onto. If there are multiple elements satisfying the selector, the first will
* be used. See <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
*/
void dragAndDrop(String source, String target, DragAndDropOptions options);
/**
* Returns the return value of {@code expression}.
@@ -2868,7 +2884,11 @@ public interface Frame {
*/
String innerText(String selector, InnerTextOptions options);
/**
* Returns {@code input.value} for the selected {@code <input>} or {@code <textarea>} or {@code <select>} element. Throws for non-input elements.
* Returns {@code input.value} for the selected {@code <input>} or {@code <textarea>} or {@code <select>} element.
*
* <p> Throws for non-input elements. However, if the element is inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, returns the value of the
* control.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -2877,7 +2897,11 @@ public interface Frame {
return inputValue(selector, null);
}
/**
* Returns {@code input.value} for the selected {@code <input>} or {@code <textarea>} or {@code <select>} element. Throws for non-input elements.
* Returns {@code input.value} for the selected {@code <input>} or {@code <textarea>} or {@code <select>} element.
*
* <p> Throws for non-input elements. However, if the element is inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, returns the value of the
* control.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -2996,6 +3020,8 @@ public interface Frame {
* element immediately before performing an action, so a series of actions on the same locator can in fact be performed on
* different DOM elements. That would happen if the DOM structure between those actions has changed.
*
* <p> <a href="https://playwright.dev/java/docs/locators">Learn more about locators</a>.
*
* @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.
*/
@@ -3007,6 +3033,8 @@ public interface Frame {
* element immediately before performing an action, so a series of actions on the same locator can in fact be performed on
* different DOM elements. That would happen if the DOM structure between those actions has changed.
*
* <p> <a href="https://playwright.dev/java/docs/locators">Learn more about locators</a>.
*
* @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.
*/
@@ -3539,11 +3567,14 @@ public interface Frame {
*/
void setContent(String html, SetContentOptions options);
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -3552,22 +3583,28 @@ public interface Frame {
setInputFiles(selector, files, null);
}
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
*/
void setInputFiles(String selector, Path files, SetInputFilesOptions options);
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -3576,22 +3613,28 @@ public interface Frame {
setInputFiles(selector, files, null);
}
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
*/
void setInputFiles(String selector, Path[] files, SetInputFilesOptions options);
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -3600,22 +3643,28 @@ public interface Frame {
setInputFiles(selector, files, null);
}
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
*/
void setInputFiles(String selector, FilePayload files, SetInputFilesOptions options);
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -3624,11 +3673,14 @@ public interface Frame {
setInputFiles(selector, files, null);
}
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -590,6 +590,50 @@ public interface Locator {
return this;
}
}
class FilterOptions {
/**
* Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer one.
* For example, {@code article} that has {@code text=Playwright} matches {@code <article><div>Playwright</div></article>}.
*
* <p> Note that outer and inner locators must belong to the same frame. Inner locator must not contain {@code FrameLocator}s.
*/
public Locator has;
/**
* 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;
/**
* Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer one.
* For example, {@code article} that has {@code text=Playwright} matches {@code <article><div>Playwright</div></article>}.
*
* <p> Note that outer and inner locators must belong to the same frame. Inner locator must not contain {@code FrameLocator}s.
*/
public FilterOptions setHas(Locator has) {
this.has = has;
return this;
}
/**
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a
* [string], matching is case-insensitive and searches for a substring. For example, {@code "Playwright"} matches
* {@code <article><div>Playwright</div></article>}.
*/
public FilterOptions setHasText(String hasText) {
this.hasText = hasText;
return this;
}
/**
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When passed a
* [string], matching is case-insensitive and searches for a substring. For example, {@code "Playwright"} matches
* {@code <article><div>Playwright</div></article>}.
*/
public FilterOptions setHasText(Pattern hasText) {
this.hasText = hasText;
return this;
}
}
class FocusOptions {
/**
* Maximum time in milliseconds, defaults to 30 seconds, pass {@code 0} to disable timeout. The default value can be changed by
@@ -991,7 +1035,9 @@ public interface Locator {
/**
* When set to {@code "css"}, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will
* keep screenshots small. Using {@code "device"} option will produce a single pixel per each device pixel, so screenhots of
* high-dpi devices will be twice as large or even larger. Defaults to {@code "device"}.
* high-dpi devices will be twice as large or even larger.
*
* <p> Defaults to {@code "device"}.
*/
public ScreenshotScale scale;
/**
@@ -1062,7 +1108,9 @@ public interface Locator {
/**
* When set to {@code "css"}, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will
* keep screenshots small. Using {@code "device"} option will produce a single pixel per each device pixel, so screenhots of
* high-dpi devices will be twice as large or even larger. Defaults to {@code "device"}.
* high-dpi devices will be twice as large or even larger.
*
* <p> Defaults to {@code "device"}.
*/
public ScreenshotOptions setScale(ScreenshotScale scale) {
this.scale = scale;
@@ -1588,7 +1636,7 @@ public interface Locator {
* This method returns the bounding box of the element, or {@code null} if the element is not visible. The bounding box is
* calculated relative to the main frame viewport - which is usually the same as the browser window.
*
* <p> Scrolling affects the returned bonding box, similarly to <a
* <p> Scrolling affects the returned bounding box, similarly to <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">Element.getBoundingClientRect</a>.
* That means {@code x} and/or {@code y} may be negative.
*
@@ -1609,7 +1657,7 @@ public interface Locator {
* This method returns the bounding box of the element, or {@code null} if the element is not visible. The bounding box is
* calculated relative to the main frame viewport - which is usually the same as the browser window.
*
* <p> Scrolling affects the returned bonding box, similarly to <a
* <p> Scrolling affects the returned bounding box, similarly to <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">Element.getBoundingClientRect</a>.
* That means {@code x} and/or {@code y} may be negative.
*
@@ -2074,6 +2122,38 @@ public interface Locator {
* @param value Value to set for the {@code <input>}, {@code <textarea>} or {@code [contenteditable]} element.
*/
void fill(String value, FillOptions options);
/**
* This method narrows existing locator according to the options, for example filters by text. It can be chained to filter
* multiple times.
* <pre>{@code
* Locator rowLocator = page.locator("tr");
* // ...
* rowLocator
* .filter(new Locator.FilterOptions().setHasText("text in column 1"))
* .filter(new Locator.FilterOptions().setHas(
* page.locator("button", new Page.LocatorOptions().setHasText("column 2 button"))
* ))
* .screenshot();
* }</pre>
*/
default Locator filter() {
return filter(null);
}
/**
* This method narrows existing locator according to the options, for example filters by text. It can be chained to filter
* multiple times.
* <pre>{@code
* Locator rowLocator = page.locator("tr");
* // ...
* rowLocator
* .filter(new Locator.FilterOptions().setHasText("text in column 1"))
* .filter(new Locator.FilterOptions().setHas(
* page.locator("button", new Page.LocatorOptions().setHasText("column 2 button"))
* ))
* .screenshot();
* }</pre>
*/
Locator filter(FilterOptions options);
/**
* Returns locator to the first matching element.
*/
@@ -2174,13 +2254,21 @@ public interface Locator {
*/
String innerText(InnerTextOptions options);
/**
* Returns {@code input.value} for {@code <input>} or {@code <textarea>} or {@code <select>} element. Throws for non-input elements.
* Returns {@code input.value} for the selected {@code <input>} or {@code <textarea>} or {@code <select>} element.
*
* <p> Throws for non-input elements. However, if the element is inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, returns the value of the
* control.
*/
default String inputValue() {
return inputValue(null);
}
/**
* Returns {@code input.value} for {@code <input>} or {@code <textarea>} or {@code <select>} element. Throws for non-input elements.
* Returns {@code input.value} for the selected {@code <input>} or {@code <textarea>} or {@code <select>} element.
*
* <p> Throws for non-input elements. However, if the element is inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, returns the value of the
* control.
*/
String inputValue(InputValueOptions options);
/**
@@ -2252,7 +2340,8 @@ public interface Locator {
*/
Locator last();
/**
* The method finds an element matching the specified selector in the {@code Locator}'s subtree.
* The method finds an element matching the specified selector in the {@code Locator}'s subtree. It also accepts filter options,
* similar to {@link Locator#filter Locator.filter()} method.
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors">working with
* selectors</a> for more details.
@@ -2261,7 +2350,8 @@ public interface Locator {
return locator(selector, null);
}
/**
* The method finds an element matching the specified selector in the {@code Locator}'s subtree.
* The method finds an element matching the specified selector in the {@code Locator}'s subtree. It also accepts filter options,
* similar to {@link Locator#filter Locator.filter()} method.
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors">working with
* selectors</a> for more details.
@@ -2326,19 +2416,27 @@ public interface Locator {
*/
void press(String key, PressOptions options);
/**
* Returns the buffer with the captured screenshot.
* This method captures a screenshot of the page, clipped to the size and position of a particular element matching the
* locator. If the element is covered by other elements, it will not be actually visible on the screenshot. If the element
* is a scrollable container, only the currently scrolled content will be visible on the screenshot.
*
* <p> This method waits for the <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks, then
* scrolls element into view before taking a screenshot. If the element is detached from DOM, the method throws an error.
*
* <p> Returns the buffer with the captured screenshot.
*/
default byte[] screenshot() {
return screenshot(null);
}
/**
* Returns the buffer with the captured screenshot.
* This method captures a screenshot of the page, clipped to the size and position of a particular element matching the
* locator. If the element is covered by other elements, it will not be actually visible on the screenshot. If the element
* is a scrollable container, only the currently scrolled content will be visible on the screenshot.
*
* <p> This method waits for the <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks, then
* scrolls element into view before taking a screenshot. If the element is detached from DOM, the method throws an error.
*
* <p> Returns the buffer with the captured screenshot.
*/
byte[] screenshot(ScreenshotOptions options);
/**
@@ -2682,6 +2780,10 @@ public interface Locator {
/**
* This method waits for <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks, then focuses
* the element and selects all its text content.
*
* <p> If the element is inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, focuses and selects text
* in the control instead.
*/
default void selectText() {
selectText(null);
@@ -2689,6 +2791,10 @@ public interface Locator {
/**
* This method waits for <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks, then focuses
* the element and selects all its text content.
*
* <p> If the element is inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, focuses and selects text
* in the control instead.
*/
void selectText(SelectTextOptions options);
/**
@@ -2732,75 +2838,99 @@ public interface Locator {
*/
void setChecked(boolean checked, SetCheckedOptions options);
/**
* This method expects {@code element} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code Locator} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
default void setInputFiles(Path files) {
setInputFiles(files, null);
}
/**
* This method expects {@code element} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code Locator} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
void setInputFiles(Path files, SetInputFilesOptions options);
/**
* This method expects {@code element} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code Locator} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
default void setInputFiles(Path[] files) {
setInputFiles(files, null);
}
/**
* This method expects {@code element} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code Locator} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
void setInputFiles(Path[] files, SetInputFilesOptions options);
/**
* This method expects {@code element} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code Locator} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
default void setInputFiles(FilePayload files) {
setInputFiles(files, null);
}
/**
* This method expects {@code element} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code Locator} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
void setInputFiles(FilePayload files, SetInputFilesOptions options);
/**
* This method expects {@code element} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code Locator} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
default void setInputFiles(FilePayload[] files) {
setInputFiles(files, null);
}
/**
* This method expects {@code element} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code Locator} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*/
void setInputFiles(FilePayload[] files, SetInputFilesOptions options);
/**
@@ -1997,6 +1997,62 @@ public interface Page 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' missing requests will be sent to the network.</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 served from the HAR file. If not specified, all requests are served from the HAR file.
*/
public Object url;
/**
* <ul>
* <li> If set to 'abort' any request not found in the HAR file will be aborted.</li>
* <li> If set to 'fallback' missing requests will be sent to the network.</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 served from the HAR file. If not specified, all requests are served from the HAR file.
*/
public RouteFromHAROptions setUrl(String url) {
this.url = url;
return this;
}
/**
* A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
* will be served from the HAR file. If not specified, all requests are served from the HAR file.
*/
public RouteFromHAROptions setUrl(Pattern url) {
this.url = url;
return this;
}
}
class ScreenshotOptions {
/**
* When set to {@code "disabled"}, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment
@@ -2046,7 +2102,9 @@ public interface Page extends AutoCloseable {
/**
* When set to {@code "css"}, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will
* keep screenshots small. Using {@code "device"} option will produce a single pixel per each device pixel, so screenhots of
* high-dpi devices will be twice as large or even larger. Defaults to {@code "device"}.
* high-dpi devices will be twice as large or even larger.
*
* <p> Defaults to {@code "device"}.
*/
public ScreenshotScale scale;
/**
@@ -2138,7 +2196,9 @@ public interface Page extends AutoCloseable {
/**
* When set to {@code "css"}, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will
* keep screenshots small. Using {@code "device"} option will produce a single pixel per each device pixel, so screenhots of
* high-dpi devices will be twice as large or even larger. Defaults to {@code "device"}.
* high-dpi devices will be twice as large or even larger.
*
* <p> Defaults to {@code "device"}.
*/
public ScreenshotOptions setScale(ScreenshotScale scale) {
this.scale = scale;
@@ -3521,9 +3581,25 @@ public interface Page extends AutoCloseable {
* @param eventInit Optional event-specific initialization properties.
*/
void dispatchEvent(String selector, String type, Object eventInit, DispatchEventOptions options);
/**
*
*
* @param source A selector to search for an element to drag. If there are multiple elements satisfying the selector, the first will be
* used. See <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
* @param target A selector to search for an element to drop onto. If there are multiple elements satisfying the selector, the first will
* be used. See <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
*/
default void dragAndDrop(String source, String target) {
dragAndDrop(source, target, null);
}
/**
*
*
* @param source A selector to search for an element to drag. If there are multiple elements satisfying the selector, the first will be
* used. See <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
* @param target A selector to search for an element to drop onto. If there are multiple elements satisfying the selector, the first will
* be used. See <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
*/
void dragAndDrop(String source, String target, DragAndDropOptions options);
/**
* This method changes the {@code CSS media type} through the {@code media} argument, and/or the {@code "prefers-colors-scheme"} media
@@ -4201,8 +4277,8 @@ public interface Page extends AutoCloseable {
*/
Response goForward(GoForwardOptions options);
/**
* Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the
* last redirect.
* Returns the main resource response. In case of multiple redirects, the navigation will resolve with the first
* non-redirect response.
*
* <p> The method will throw an error if:
* <ul>
@@ -4233,8 +4309,8 @@ public interface Page extends AutoCloseable {
return navigate(url, null);
}
/**
* Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the
* last redirect.
* Returns the main resource response. In case of multiple redirects, the navigation will resolve with the first
* non-redirect response.
*
* <p> The method will throw an error if:
* <ul>
@@ -4337,7 +4413,11 @@ public interface Page extends AutoCloseable {
*/
String innerText(String selector, InnerTextOptions options);
/**
* Returns {@code input.value} for the selected {@code <input>} or {@code <textarea>} or {@code <select>} element. Throws for non-input elements.
* Returns {@code input.value} for the selected {@code <input>} or {@code <textarea>} or {@code <select>} element.
*
* <p> Throws for non-input elements. However, if the element is inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, returns the value of the
* control.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -4346,7 +4426,11 @@ public interface Page extends AutoCloseable {
return inputValue(selector, null);
}
/**
* Returns {@code input.value} for the selected {@code <input>} or {@code <textarea>} or {@code <select>} element. Throws for non-input elements.
* Returns {@code input.value} for the selected {@code <input>} or {@code <textarea>} or {@code <select>} element.
*
* <p> Throws for non-input elements. However, if the element is inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, returns the value of the
* control.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -4466,6 +4550,8 @@ public interface Page extends AutoCloseable {
* element immediately before performing an action, so a series of actions on the same locator can in fact be performed on
* different DOM elements. That would happen if the DOM structure between those actions has changed.
*
* <p> <a href="https://playwright.dev/java/docs/locators">Learn more about locators</a>.
*
* <p> Shortcut for main frame's {@link Frame#locator Frame.locator()}.
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors">working with
@@ -4479,6 +4565,8 @@ public interface Page extends AutoCloseable {
* element immediately before performing an action, so a series of actions on the same locator can in fact be performed on
* different DOM elements. That would happen if the DOM structure between those actions has changed.
*
* <p> <a href="https://playwright.dev/java/docs/locators">Learn more about locators</a>.
*
* <p> Shortcut for main frame's {@link Frame#locator Frame.locator()}.
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors">working with
@@ -4752,7 +4840,7 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.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
@@ -4805,7 +4893,7 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.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
@@ -4856,7 +4944,7 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.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
@@ -4909,7 +4997,7 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.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
@@ -4960,7 +5048,7 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.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
@@ -5013,7 +5101,7 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> {@link Page#route Page.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
@@ -5055,6 +5143,32 @@ public interface Page 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 page 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 page 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);
/**
* Returns the buffer with the captured screenshot.
*/
@@ -5552,11 +5666,14 @@ public interface Page extends AutoCloseable {
*/
void setExtraHTTPHeaders(Map<String, String> headers);
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -5565,22 +5682,28 @@ public interface Page extends AutoCloseable {
setInputFiles(selector, files, null);
}
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
*/
void setInputFiles(String selector, Path files, SetInputFilesOptions options);
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -5589,22 +5712,28 @@ public interface Page extends AutoCloseable {
setInputFiles(selector, files, null);
}
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
*/
void setInputFiles(String selector, Path[] files, SetInputFilesOptions options);
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -5613,22 +5742,28 @@ public interface Page extends AutoCloseable {
setInputFiles(selector, files, null);
}
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
*/
void setInputFiles(String selector, FilePayload files, SetInputFilesOptions options);
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -5637,11 +5772,14 @@ public interface Page extends AutoCloseable {
setInputFiles(selector, files, null);
}
/**
* This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>.
* Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the current working directory. For empty array, clears the selected files.
*
* <p> Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then they
* are resolved relative to the the current working directory. For empty array, clears the selected files.
* <p> This method expects {@code selector} to point to an <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input element</a>. However, if the element is
* inside the {@code <label>} element that has an associated <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control">control</a>, targets the control
* instead.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See
* <a href="https://playwright.dev/java/docs/selectors">working with selectors</a> for more details.
@@ -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 previous ones. In the example below, request will be handled by the
* bottom-most handler first, then it'll fall back to the previous one and in the end will be aborted by the first
* registered route.
* <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 previous ones. In the example below, request will be handled by the
* bottom-most handler first, then it'll fall back to the previous one and in the end will be aborted by the first
* registered route.
* <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.
*
@@ -63,7 +63,7 @@ public interface Selectors {
* // Use the selector prefixed with its name.
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* page.locator("tag=div >> text=\"Click me\"").click();
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.locator("tag=button").count();
* browser.close();
@@ -98,7 +98,7 @@ public interface Selectors {
* // Use the selector prefixed with its name.
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* page.locator("tag=div >> text=\"Click me\"").click();
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.locator("tag=button").count();
* browser.close();
@@ -131,7 +131,7 @@ public interface Selectors {
* // Use the selector prefixed with its name.
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* page.locator("tag=div >> text=\"Click me\"").click();
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.locator("tag=button").count();
* browser.close();
@@ -166,7 +166,7 @@ public interface Selectors {
* // Use the selector prefixed with its name.
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* page.locator("tag=div >> text=\"Click me\"").click();
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.locator("tag=button").count();
* browser.close();
@@ -192,7 +192,7 @@ public interface Tracing {
* page.navigate("https://playwright.dev");
*
* context.tracing().startChunk();
* page.click("text=Get Started");
* page.locator("text=Get Started").click();
* // Everything between startChunk and stopChunk will be recorded in the trace.
* context.tracing().stopChunk(new Tracing.StopChunkOptions()
* .setPath(Paths.get("trace1.zip")));
@@ -219,7 +219,7 @@ public interface Tracing {
* page.navigate("https://playwright.dev");
*
* context.tracing().startChunk();
* page.click("text=Get Started");
* page.locator("text=Get Started").click();
* // Everything between startChunk and stopChunk will be recorded in the trace.
* context.tracing().stopChunk(new Tracing.StopChunkOptions()
* .setPath(Paths.get("trace1.zip")));
@@ -31,7 +31,7 @@ import java.util.regex.Pattern;
* @Test
* void statusBecomesSubmitted() {
* ...
* page.click("#submit-button");
* page.locator("#submit-button").click();
* assertThat(page.locator(".status")).hasText("Submitted");
* }
* }
@@ -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>
@@ -429,7 +477,7 @@ public interface LocatorAssertions {
* 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() {
@@ -439,7 +487,7 @@ public interface LocatorAssertions {
* 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);
@@ -616,9 +664,11 @@ public interface LocatorAssertions {
*/
void hasAttribute(String name, Pattern value, HasAttributeOptions options);
/**
* Ensures the {@code Locator} points to an element with given CSS class.
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a relaxed
* regular expression.
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
@@ -632,9 +682,11 @@ public interface LocatorAssertions {
hasClass(expected, null);
}
/**
* Ensures the {@code Locator} points to an element with given CSS class.
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a relaxed
* regular expression.
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
@@ -646,9 +698,11 @@ public interface LocatorAssertions {
*/
void hasClass(String expected, HasClassOptions options);
/**
* Ensures the {@code Locator} points to an element with given CSS class.
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a relaxed
* regular expression.
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
@@ -662,9 +716,11 @@ public interface LocatorAssertions {
hasClass(expected, null);
}
/**
* Ensures the {@code Locator} points to an element with given CSS class.
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a relaxed
* regular expression.
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
@@ -676,9 +732,11 @@ public interface LocatorAssertions {
*/
void hasClass(Pattern expected, HasClassOptions options);
/**
* Ensures the {@code Locator} points to an element with given CSS class.
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a relaxed
* regular expression.
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
@@ -692,9 +750,11 @@ public interface LocatorAssertions {
hasClass(expected, null);
}
/**
* Ensures the {@code Locator} points to an element with given CSS class.
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a relaxed
* regular expression.
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
@@ -706,9 +766,11 @@ public interface LocatorAssertions {
*/
void hasClass(String[] expected, HasClassOptions options);
/**
* Ensures the {@code Locator} points to an element with given CSS class.
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a relaxed
* regular expression.
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
@@ -722,9 +784,11 @@ public interface LocatorAssertions {
hasClass(expected, null);
}
/**
* Ensures the {@code Locator} points to an element with given CSS class.
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a relaxed
* regular expression.
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
@@ -1035,5 +1099,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);
}
@@ -31,7 +31,7 @@ import java.util.regex.Pattern;
* @Test
* void navigatesToLoginPage() {
* ...
* page.click("#login");
* page.locator("#login").click();
* assertThat(page).hasURL(Pattern.compile(".*\/login"));
* }
* }
@@ -120,7 +120,7 @@ public interface PageAssertions {
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
* @param urlOrRegExp Expected URL string or RegExp.
*/
default void hasURL(String urlOrRegExp) {
hasURL(urlOrRegExp, null);
@@ -131,7 +131,7 @@ public interface PageAssertions {
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
* @param urlOrRegExp Expected URL string or RegExp.
*/
void hasURL(String urlOrRegExp, HasURLOptions options);
/**
@@ -140,7 +140,7 @@ public interface PageAssertions {
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
* @param urlOrRegExp Expected URL string or RegExp.
*/
default void hasURL(Pattern urlOrRegExp) {
hasURL(urlOrRegExp, null);
@@ -151,7 +151,7 @@ public interface PageAssertions {
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
* @param urlOrRegExp Expected URL string or RegExp.
*/
void hasURL(Pattern urlOrRegExp, HasURLOptions options);
}
@@ -37,7 +37,7 @@ import com.microsoft.playwright.impl.PageAssertionsImpl;
* @Test
* void statusBecomesSubmitted() {
* ...
* page.click("#submit-button");
* page.locator("#submit-button").click();
* assertThat(page.locator(".status")).hasText("Submitted");
* }
* }
@@ -20,21 +20,20 @@ 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 java.nio.charset.StandardCharsets.UTF_8;
@@ -54,7 +53,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 +86,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 +196,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 +367,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 +380,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 +406,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 +536,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 +553,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());
@@ -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;
}
@@ -82,7 +85,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
JsonObject json = sendMessage("connect", params).getAsJsonObject();
JsonPipe pipe = connection.getExistingObject(json.getAsJsonObject("pipe").get("guid").getAsString());
Connection connection = new Connection(pipe);
Connection connection = new Connection(pipe, this.connection.env);
PlaywrightImpl playwright = connection.initializePlaywright();
if (!playwright.initializer.has("preLaunchedBrowser")) {
try {
@@ -96,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 -> {
@@ -130,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);
@@ -152,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();
@@ -195,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,8 @@ public class Connection {
String debug = System.getenv("DEBUG");
isLogging = (debug != null) && debug.contains("pw:channel");
}
LocalUtils localUtils;
final Map<String, String> env;
class Root extends ChannelOwner {
Root(Connection connection) {
@@ -78,13 +80,14 @@ public class Connection {
}
}
Connection(Transport transport) {
Connection(Transport transport, Map<String, String> env) {
this.env = env;
if (isLogging) {
transport = new TransportLogger(transport);
}
this.transport = transport;
root = new Root(this);
stackTraceCollector = StackTraceCollector.createFromEnv();
stackTraceCollector = StackTraceCollector.createFromEnv(env);
}
boolean isCollectingStacks() {
@@ -138,6 +141,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 +277,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);
@@ -23,7 +23,6 @@ import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -1048,6 +1047,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
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,109 @@
/*
* 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.isApiLoggingEnabled;
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();
if (isApiLoggingEnabled()) {
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)) {
if (isApiLoggingEnabled()) {
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,26 +24,57 @@ 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) {
if (options.hasText != null) {
if (options.hasText instanceof Pattern) {
Pattern pattern = (Pattern) options.hasText;
selector += " >> :scope:text-matches(" + escapeWithQuotes(pattern.pattern()) + ", \"" + toJsRegexFlags(pattern) + "\")";
String jsRegex = "/" + pattern.pattern() + "/" + toJsRegexFlags(pattern);
selector += " >> has=" + gson().toJson("text=" + jsRegex);
} else if (options.hasText instanceof String) {
String text = (String) options.hasText;
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 +203,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);
@@ -473,9 +511,7 @@ class LocatorImpl implements Locator {
JsonObject toProtocol() {
JsonObject result = new JsonObject();
JsonObject frameJson = new JsonObject();
frameJson.addProperty("guid", frame.guid);
result.add("frame", frameJson);
result.add("frame", frame.toProtocolRef());
result.addProperty("selector", selector);
return result;
}
@@ -60,7 +60,11 @@ class LoggingSupport {
System.err.println(timestamp + " " + message);
}
private void logApi(String message) {
static boolean isApiLoggingEnabled() {
return isEnabled;
}
static void logApi(String message) {
// This matches log format produced by the server.
logWithTimestamp("pw:api " + message);
}
@@ -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);
@@ -43,7 +43,7 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
pb.environment().putAll(env);
Driver.setRequiredEnvironmentVariables(pb);
Process p = pb.start();
Connection connection = new Connection(new PipeTransport(p.getInputStream(), p.getOutputStream()));
Connection connection = new Connection(new PipeTransport(p.getInputStream(), p.getOutputStream()), env);
PlaywrightImpl result = connection.initializePlaywright();
result.driverProcess = p;
result.initSharedSelectors(null);
@@ -31,6 +31,7 @@ class SerializedValue{
// Possible values: { 'null, 'undefined, 'NaN, 'Infinity, '-Infinity, '-0 }
String v;
String d;
String u;
public static class R {
String p;
String f;
@@ -43,6 +44,8 @@ class SerializedValue{
}
O[] o;
Number h;
Integer id;
Integer ref;
}
class SerializedArgument{
@@ -83,6 +86,7 @@ class ExpectedTextValue {
String string;
String regexSource;
String regexFlags;
Boolean ignoreCase;
Boolean matchSubstring;
Boolean normalizeWhiteSpace;
}
@@ -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,10 +38,7 @@ class Router {
this.times = times;
}
boolean handle(Route route) {
if (times != null && times <= 0) {
return false;
}
boolean handle(RouteImpl route) {
if (!matcher.test(route.request().url())) {
return false;
}
@@ -70,15 +68,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;
}
}
@@ -28,12 +28,19 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Instant;
import java.util.*;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
import static com.microsoft.playwright.impl.Utils.fromJsRegexFlags;
class Serialization {
private static final Gson gson = new GsonBuilder()
private static final Gson gson = new GsonBuilder().disableHtmlEscaping()
.registerTypeAdapter(SameSiteAttribute.class, new SameSiteAdapter().nullSafe())
.registerTypeAdapter(BrowserChannel.class, new ToLowerCaseAndDashSerializer<BrowserChannel>())
.registerTypeAdapter(ColorScheme.class, new ToLowerCaseAndDashSerializer<ColorScheme>())
@@ -44,6 +51,7 @@ class Serialization {
.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>())
@@ -71,82 +79,136 @@ class Serialization {
return result;
}
private static SerializedValue serializeValue(Object value, List<JSHandleImpl> handles, int depth) {
if (depth > 100) {
throw new PlaywrightException("Maximum argument depth exceeded");
private static class ValueSerializer {
// hashCode() of a map containing itself as a key will throw stackoverflow exception,
// so we user wrappers.
private static class HashableValue {
final Object value;
HashableValue(Object value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
return value == ((HashableValue) o).value;
}
@Override
public int hashCode() {
return System.identityHashCode(value);
}
}
SerializedValue result = new SerializedValue();
if (value instanceof JSHandleImpl) {
result.h = handles.size();
handles.add((JSHandleImpl) value);
private final Map<HashableValue, Integer> valueToId = new HashMap<>();
private int lastId = 0;
private final List<JSHandleImpl> handles = new ArrayList<>();
private final SerializedValue serializedValue;
ValueSerializer(Object value) {
serializedValue = serializeValue(value);
}
SerializedArgument toSerializedArgument() {
SerializedArgument result = new SerializedArgument();
result.value = serializedValue;
result.handles = new Channel[handles.size()];
int i = 0;
for (JSHandleImpl handle : handles) {
result.handles[i] = new Channel();
result.handles[i].guid = handle.guid;
++i;
}
return result;
}
if (value == null) {
result.v = "undefined";
} else if (value instanceof Double) {
double d = ((Double) value);
if (d == Double.POSITIVE_INFINITY) {
result.v = "Infinity";
} else if (d == Double.NEGATIVE_INFINITY) {
result.v = "-Infinity";
} else if (d == -0) {
result.v = "-0";
} else if (Double.isNaN(d)) {
result.v = "NaN";
private SerializedValue serializeValue(Object value) {
SerializedValue result = new SerializedValue();
if (value instanceof JSHandleImpl) {
result.h = handles.size();
handles.add((JSHandleImpl) value);
return result;
}
if (value == null) {
result.v = "undefined";
} else if (value instanceof Double) {
double d = ((Double) value);
if (d == Double.POSITIVE_INFINITY) {
result.v = "Infinity";
} else if (d == Double.NEGATIVE_INFINITY) {
result.v = "-Infinity";
} else if (d == -0) {
result.v = "-0";
} else if (Double.isNaN(d)) {
result.v = "NaN";
} else {
result.n = d;
}
} else if (value instanceof Boolean) {
result.b = (Boolean) value;
} else if (value instanceof Integer) {
result.n = (Integer) value;
} else if (value instanceof String) {
result.s = (String) value;
} else if (value instanceof Date) {
result.d = ((Date)value).toInstant().toString();
} else if (value instanceof URL) {
result.u = ((URL)value).toString();
} else if (value instanceof Pattern) {
result.r = new SerializedValue.R();
result.r.p = ((Pattern)value).pattern();
result.r.f = toJsRegexFlags(((Pattern)value));
} else {
result.n = d;
HashableValue mapKey = new HashableValue(value);
Integer id = valueToId.get(mapKey);
if (id != null) {
result.ref = id;
} else {
result.id = ++lastId;
valueToId.put(mapKey, lastId);
if (value instanceof List) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (List<?>) value) {
list.add(serializeValue(o));
}
result.a = list.toArray(new SerializedValue[0]);
} else if (value instanceof Map) {
List<SerializedValue.O> list = new ArrayList<>();
@SuppressWarnings("unchecked")
Map<String, ?> map = (Map<String, ?>) value;
for (Map.Entry<String, ?> e : map.entrySet()) {
SerializedValue.O o = new SerializedValue.O();
o.k = e.getKey();
o.v = serializeValue(e.getValue());
list.add(o);
}
result.o = list.toArray(new SerializedValue.O[0]);
} else if (value instanceof Object[]) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (Object[]) value) {
list.add(serializeValue(o));
}
result.a = list.toArray(new SerializedValue[0]);
} else {
throw new PlaywrightException("Unsupported type of argument: " + value);
}
}
}
} else if (value instanceof Boolean) {
result.b = (Boolean) value;
} else if (value instanceof Integer) {
result.n = (Integer) value;
} else if (value instanceof String) {
result.s = (String) value;
} else if (value instanceof List) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (List<?>) value) {
list.add(serializeValue(o, handles, depth + 1));
}
result.a = list.toArray(new SerializedValue[0]);
} else if (value instanceof Map) {
List<SerializedValue.O> list = new ArrayList<>();
@SuppressWarnings("unchecked")
Map<String, ?> map = (Map<String, ?>) value;
for (Map.Entry<String, ?> e : map.entrySet()) {
SerializedValue.O o = new SerializedValue.O();
o.k = e.getKey();
o.v = serializeValue(e.getValue(), handles, depth + 1);
list.add(o);
}
result.o = list.toArray(new SerializedValue.O[0]);
} else if (value instanceof Object[]) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (Object[]) value) {
list.add(serializeValue(o, handles, depth + 1));
}
result.a = list.toArray(new SerializedValue[0]);
} else {
throw new PlaywrightException("Unsupported type of argument: " + value);
return result;
}
return result;
}
static SerializedArgument serializeArgument(Object arg) {
SerializedArgument result = new SerializedArgument();
List<JSHandleImpl> handles = new ArrayList<>();
result.value = serializeValue(arg, handles, 0);
result.handles = new Channel[handles.size()];
int i = 0;
for (JSHandleImpl handle : handles) {
result.handles[i] = new Channel();
result.handles[i].guid = handle.guid;
++i;
}
return result;
return new ValueSerializer(arg).toSerializedArgument();
}
static <T> T deserialize(SerializedValue value) {
return deserialize(value, new HashMap<>());
}
@SuppressWarnings("unchecked")
static <T> T deserialize(SerializedValue value) {
private static <T> T deserialize(SerializedValue value, Map<Integer, Object> idToValue) {
if (value.ref != null) {
return (T) idToValue.get(value.ref);
}
if (value.n != null) {
if (value.n.doubleValue() == (double) value.n.intValue()) {
return (T) Integer.valueOf(value.n.intValue());
@@ -157,6 +219,17 @@ class Serialization {
return (T) value.b;
if (value.s != null)
return (T) value.s;
if (value.u != null) {
try {
return (T)(new URL(value.u));
} catch (MalformedURLException e) {
throw new PlaywrightException("Unexpected value: " + value.u, e);
}
}
if (value.d != null)
return (T)(Date.from(Instant.parse(value.d)));
if (value.r != null)
return (T)(Pattern.compile(value.r.p, fromJsRegexFlags(value.r.f)));
if (value.v != null) {
switch (value.v) {
case "undefined":
@@ -177,15 +250,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;
}
@@ -215,7 +290,7 @@ class Serialization {
static JsonArray toJsonArray(Path[] files) {
JsonArray jsonFiles = new JsonArray();
for (Path p : files) {
jsonFiles.add(p.toString());
jsonFiles.add(p.toAbsolutePath().toString());
}
return jsonFiles;
}
@@ -239,9 +314,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;
}
@@ -250,6 +323,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()) {
@@ -261,6 +344,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) {
@@ -291,9 +383,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();
}
}
@@ -28,18 +28,25 @@ import java.util.*;
import java.util.stream.Collectors;
class StackTraceCollector {
static final String PLAYWRIGHT_JAVA_SRC = "PLAYWRIGHT_JAVA_SRC";
private final List<Path> srcDirs;
private final Map<Path, String> classToSourceCache = new HashMap<>();
static StackTraceCollector createFromEnv() {
String srcRoots = System.getenv("PLAYWRIGHT_JAVA_SRC");
static StackTraceCollector createFromEnv(Map<String, String> env) {
String srcRoots = null;
if (env != null) {
srcRoots = env.get(PLAYWRIGHT_JAVA_SRC);
}
if (srcRoots == null) {
srcRoots = System.getenv(PLAYWRIGHT_JAVA_SRC);
}
if (srcRoots == null) {
return null;
}
List<Path> srcDirs = Arrays.stream(srcRoots.split(File.pathSeparator)).map(p -> Paths.get(p)).collect(Collectors.toList());
for (Path srcDir: srcDirs) {
if (!Files.exists(srcDir.toAbsolutePath())) {
throw new PlaywrightException("Source location specified in PLAYWRIGHT_JAVA_SRC doesn't exist: '" + srcDir.toAbsolutePath() + "'");
throw new PlaywrightException("Source location specified in " + PLAYWRIGHT_JAVA_SRC + " doesn't exist: '" + srcDir.toAbsolutePath() + "'");
}
}
return new StackTraceCollector(srcDirs);
@@ -26,7 +26,6 @@ import java.nio.file.Path;
import static com.microsoft.playwright.impl.Serialization.gson;
class TracingImpl extends ChannelOwner implements Tracing {
LocalUtils localUtils;
boolean isRemote;
TracingImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -60,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);
}
}
@@ -176,11 +176,18 @@ class Utils {
} catch (IOException e) {
throw new PlaywrightException("Failed to copy file to remote server.", e);
}
jsonStreams.add(temp.toProtocol());
jsonStreams.add(temp.toProtocolRef());
}
params.add("streams", jsonStreams);
} else {
params.add("localPaths", toJsonArray(files));
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));
}
}
@@ -270,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) {
@@ -289,4 +307,18 @@ class Utils {
}
return regexFlags;
}
static int fromJsRegexFlags(String regexFlags) {
int flags = 0;
if (regexFlags.contains("i")) {
flags |= Pattern.CASE_INSENSITIVE;
}
if (regexFlags.contains("s")) {
flags |= Pattern.DOTALL;
}
if (regexFlags.contains("m")) {
flags |= Pattern.MULTILINE;
}
return flags;
}
}
@@ -30,10 +30,4 @@ class WritableStream extends ChannelOwner {
}
};
}
JsonObject toProtocol() {
JsonObject json = new JsonObject();
json.addProperty("guid", guid);
return json;
}
}
@@ -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
}
@@ -19,7 +19,8 @@ package com.microsoft.playwright.options;
import com.microsoft.playwright.impl.RequestOptionsImpl;
/**
* The {@code RequestOptions} allows to create form data to be sent via {@code APIRequestContext}.
* The {@code RequestOptions} allows to create form data to be sent via {@code APIRequestContext}. Playwright will automatically
* determine content type of the request.
* <pre>{@code
* context.request().post(
* "https://example.com/submit",
@@ -27,6 +28,33 @@ import com.microsoft.playwright.impl.RequestOptionsImpl;
* .setQueryParam("page", 1)
* .setData("My data"));
* }</pre>
*
* <p> **Uploading html form data**
*
* <p> {@code FormData} class can be used to send a form to the server, by default the request will use
* {@code application/x-www-form-urlencoded} encoding:
* <pre>{@code
* context.request().post("https://example.com/signup", RequestOptions.create().setForm(
* FormData.create()
* .set("firstName", "John")
* .set("lastName", "Doe")));
* }</pre>
*
* <p> You can also send files as fields of an html form. The data will be encoded using <a
* href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST">{@code multipart/form-data}</a>:
* <pre>{@code
* Path path = Paths.get("members.csv");
* APIResponse response = context.request().post("https://example.com/upload_members",
* RequestOptions.create().setMultipart(FormData.create().set("membersList", path)));
* }</pre>
*
* <p> Alternatively, you can build the file payload manually:
* <pre>{@code
* FilePayload filePayload = new FilePayload("members.csv", "text/csv",
* "Alice, 33\nJohn, 35\n".getBytes(StandardCharsets.UTF_8));
* APIResponse response = context.request().post("https://example.com/upload_members",
* RequestOptions.create().setMultipart(FormData.create().set("membersList", filePayload)));
* }</pre>
*/
public interface RequestOptions {
/**
@@ -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
}
@@ -72,8 +72,12 @@ public class TestBase {
return options;
}
Playwright.CreateOptions playwrightOptions() {
return null;
}
void initBrowserType() {
playwright = Playwright.create();
playwright = Playwright.create(playwrightOptions());
browserType = Utils.getBrowserTypeFromEnv(playwright);
}
@@ -97,4 +97,9 @@ public class TestBrowser extends TestBase {
assertNotNull(browser);
browser.close();
}
@Test
void shouldReturnBrowserType() {
assertEquals(browserType, browser.browserType());
}
}
@@ -65,13 +65,19 @@ 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()) {
@@ -0,0 +1,457 @@
/*
* 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 {
page2.evaluate(fetchFunction, "4");
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));
@@ -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");
@@ -207,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);
}
}
}
@@ -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;
@@ -499,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());
@@ -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
@@ -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;
}
}
@@ -0,0 +1,46 @@
package com.microsoft.playwright;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.stream.Collectors;
import static com.microsoft.playwright.Utils.mapOf;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestJavaSourceLocationInConstructor extends TestBase {
private static final String SRC_DIRS = System.getenv("PLAYWRIGHT_JAVA_SRC") == null ? "src/test/java" : System.getenv("PLAYWRIGHT_JAVA_SRC");
@Override
Playwright.CreateOptions playwrightOptions() {
return new Playwright.CreateOptions().setEnv(mapOf("PLAYWRIGHT_JAVA_SRC", SRC_DIRS));
}
@Test
void shouldSupportSourcesLocationPassedToPlaywrightCreate(@TempDir Path tmpDir) throws IOException {
context.tracing().start(new Tracing.StartOptions().setSources(true));
page.navigate(server.EMPTY_PAGE);
page.setContent("<button>Click</button>");
page.click("'Click'");
Path trace = tmpDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(trace));
Map<String, byte[]> entries = Utils.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());
String path = getClass().getName().replace('.', File.separatorChar);
String[] srcRoots = SRC_DIRS.split(File.pathSeparator);
// Resolve in the last specified source dir.
Path sourceFile = Paths.get(srcRoots[srcRoots.length - 1], path + ".java");
byte[] thisFile = Files.readAllBytes(sourceFile);
assertEquals(new String(thisFile, UTF_8), new String(sources.values().iterator().next(), UTF_8));
}
}
@@ -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,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() {
}
@@ -20,6 +20,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.Date;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.ZonedDateTime;
import static com.microsoft.playwright.Utils.mapOf;
import static java.util.Arrays.asList;
@@ -345,31 +350,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" +
@@ -592,20 +579,56 @@ public class TestPageEvaluate extends TestBase {
assertTrue(((String) error).contains("Error: error message"));
}
@Test
void shouldEvaluateDate() {
// TODO: Date values are not supported in java.
Object result = page.evaluate("() => ({ date: new Date('2020-05-27T01:31:38.506Z') })");
Date expected = Date.from(ZonedDateTime.parse("2020-05-27T01:31:38.506Z").toInstant());
assertEquals(mapOf("date", expected), result);
}
@Test
void shouldRoundtripDate() {
// TODO: Date values are not supported in java.
Date date = Date.from(ZonedDateTime.parse("2020-05-27T01:31:38.506Z").toInstant());
Object result = page.evaluate("date => date", date);
assertTrue(result instanceof Date);
assertEquals(date.toString(), result.toString());
}
@Test
void shouldRoundtripRegex() {
// Not applicable
Pattern regex = Pattern.compile("hello", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Object result = page.evaluate("regex => regex", regex);
assertTrue(result instanceof Pattern);
assertEquals(regex.toString(), result.toString());
assertEquals(regex.flags(), ((Pattern)result).flags());
}
@Test
void shouldJsonValueDate() {
// TODO: Date values are not supported in java.
JSHandle resultHandle = page.evaluateHandle("() => ({ date: new Date('2020-05-27T01:31:38.506Z') })");
assertEquals(mapOf("date", Date.from(ZonedDateTime.parse("2020-05-27T01:31:38.506Z").toInstant())), resultHandle.jsonValue());
}
@Test
void shouldEvaluateUrl() throws MalformedURLException {
Object result = page.evaluate("() => ({ url: new URL('https://example.com/') })");
assertEquals(mapOf("url", new URL("https://example.com/")), result);
}
@Test
void shouldRoundtripUrl() throws MalformedURLException {
URL url = new URL("https://example.com/");
Object result = page.evaluate("url => url", url);
assertTrue(result instanceof URL);
assertEquals(url.toString(), result.toString());
}
@Test
void shouldRoundtripComplexUrl() throws MalformedURLException {
URL url = new URL("https://user:password@www.contoso.com:80/Home/Index.htm?q1=v1&q2=v2#FragmentName");
Object result = page.evaluate("url => url", url);
assertTrue(result instanceof URL);
assertEquals(url.toString(), result.toString());
}
@Test
@@ -619,4 +642,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"));
}
}
@@ -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);
}
}
}
@@ -119,6 +119,33 @@ public class TestPageLocatorQuery extends TestBase {
assertEquals("Hello \"world\"", page.locator("div", new Page.LocatorOptions().setHasText(pattern)).textContent());
}
@Test
void shouldFilterByCaseInsensitiveRegexInAChild() {
page.setContent("<div class=\"test\"><h5>Title Text</h5></div>");
Pattern pattern = Pattern.compile("^title text$", Pattern.CASE_INSENSITIVE);
assertThat(page.locator("div", new Page.LocatorOptions().setHasText(pattern))).hasText("Title Text");
}
@Test
void shouldFilterByCaseInsensitiveRegexInMultipleChildren() {
page.setContent("<div class=\"test\"><h5>Title</h5> <h2><i>Text</i></h2></div>`");
Pattern pattern = Pattern.compile("^title text$", Pattern.CASE_INSENSITIVE);
assertThat(page.locator("div", new Page.LocatorOptions().setHasText(pattern))).hasClass("test");
}
@Test
void shouldFilterByRegexWithSpecialSymbols() {
page.setContent("<div class=\"test\"><h5>First/\"and\"</h5><h2><i>Second\\</i></h2></div>");
Pattern pattern = Pattern.compile("first\\/\".*\"second\\\\$", Pattern.CASE_INSENSITIVE);
assertThat(page.locator("div", new Page.LocatorOptions().setHasText(pattern))).hasClass("test");
}
@Test
void shouldFilterByTextWithAmpersand() {
page.setContent("<div>Save & Continue</div>");
assertEquals("Save & Continue", page.locator("div",
new Page.LocatorOptions().setHasText("Save & Continue")).textContent());
}
@Test
void shouldSupportHasLocator() {
page.setContent("<div><span>hello</span></div><div><span>world</span></div>");
@@ -134,4 +161,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,280 @@
/*
* 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 -> {
System.out.println("before fulfill");
route.fulfill(new Route.FulfillOptions().setStatus(200).setBody("fulfilled one"));
System.out.println("after fulfill");
}, new Page.RouteOptions().setTimes(1));
page.route("**/empty.html", route -> {
System.out.println("before fallback");
route.fallback();
System.out.println("after 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
@@ -475,7 +476,7 @@ public class TestPageRoute extends TestBase {
}
@Test
void shouldNotThrowInvalidInterceptionIdIfTheRequestWasCancelled() {
void shouldThrowIfResumeIsCalledAfterRouteHandlerFinished() {
page.setContent("<iframe></iframe>");
Route[] route = {null};
page.route("**/*", r -> route[0] = r);
@@ -485,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());
}
}
@@ -723,4 +725,25 @@ public class TestPageRoute extends TestBase {
});
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);
}
}
@@ -22,6 +22,7 @@ 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.junit.jupiter.api.condition.DisabledIf;
import org.opentest4j.AssertionFailedError;
import javax.imageio.ImageIO;
@@ -220,6 +221,7 @@ public class TestPageScreenshot extends TestBase {
}
@Test
@DisabledIf(value="com.microsoft.playwright.TestBase#isFirefox", disabledReason="fixme")
void shouldCaptureBlinkingCaretIfExplicitlyAskedFor() {
page.setContent(" <!-- Refer to stylesheet from other origin. Accessing this\n" +
" stylesheet rules will throw.\n" +
@@ -96,6 +96,50 @@ public class TestPageSetInputFiles extends TestBase {
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)) {
@@ -0,0 +1,221 @@
package com.microsoft.playwright;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TestSelectorsMisc extends TestBase {
@Test
void shouldWorkWithLayoutSelectors() {
/*
+--+ +--+
| 1| | 2|
+--+ ++-++
| 3| | 4|
+-------+ ++-++
| 0 | | 5|
| +--+ +--+--+
| | 6| | 7|
| +--+ +--+
| |
O-------+
+--+
| 8|
+--++--+
| 9|
+--+
*/
Object[][] boxes = {
// x, y, width, height
{0, 0, 150, 150},
{100, 200, 50, 50},
{200, 200, 50, 50},
{100, 150, 50, 50},
{201, 150, 50, 50},
{200, 100, 50, 50},
{50, 50, 50, 50},
{150, 50, 50, 50},
{150, -51, 50, 50},
{201, -101, 50, 50},
};
page.setContent("<container style='width: 500px; height: 500px; position: relative;'></container>");
page.evalOnSelector("container", "(container, boxes) => {\n" +
" for (let i = 0; i < boxes.length; i++) {\n" +
" const div = document.createElement('div');\n" +
" div.style.position = 'absolute';\n" +
" div.style.overflow = 'hidden';\n" +
" div.style.boxSizing = 'border-box';\n" +
" div.style.border = '1px solid black';\n" +
" div.id = 'id' + i;\n" +
" div.textContent = 'id' + i;\n" +
" const box = boxes[i];\n" +
" div.style.left = box[0] + 'px';\n" +
" // Note that top is a flipped y coordinate.\n" +
" div.style.top = (250 - box[1] - box[3]) + 'px';\n" +
" div.style.width = box[2] + 'px';\n" +
" div.style.height = box[3] + 'px';\n" +
" container.appendChild(div);\n" +
" const span = document.createElement('span');\n" +
" span.textContent = '' + i;\n" +
" div.appendChild(span);\n" +
" }\n" +
" }", boxes);
assertEquals("id7", page.evalOnSelector("div:right-of(#id6)", "e => e.id"));
// assertEquals("id7", page.evalOnSelector("div >> right-of=\"#id6\"", "e => e.id"));
// assertEquals("id7", page.locator("div", new Page.LocatorOptions().setRightOf(page.locator("#id6")))
// .first().evaluate("e => e.id"));
assertEquals("id2", page.evalOnSelector("div:right-of(#id1)", "e => e.id"));
// assertEquals("id2", page.evalOnSelector("div >> right-of=\"#id1\"", "e => e.id"));
assertEquals("id4", page.evalOnSelector("div:right-of(#id3)", "e => e.id"));
// assertEquals("id4", page.evalOnSelector("div >> right-of=\"#id3\"", "e => e.id"));
assertNull(page.querySelector("div:right-of(#id4)"));
// assertNull(page.querySelector("div >> right-of=\"#id4\""));
assertEquals("id7", page.evalOnSelector("div:right-of(#id0)", "e => e.id"));
// assertEquals("id7", page.evalOnSelector("div >> right-of=\"#id0\"", "e => e.id"));
assertEquals("id9", page.evalOnSelector("div:right-of(#id8)", "e => e.id"));
// assertEquals("id9", page.evalOnSelector("div >> right-of=\"#id8\"", "e => e.id"));
assertEquals("id4,id2,id5,id7,id8,id9", page.evalOnSelectorAll("div:right-of(#id3)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id4,id2,id5,id7,id8,id9", page.evalOnSelectorAll("div >> right-of=\"#id3\"", "els => els.map(e => e.id).join(',')"));
// assertEquals("4,2,5,7,8,9", page.locator("div",
// new Page.LocatorOptions().setRightOf(page.locator("#id3"))).locator("span")
// .evaluateAll("els => els.map(e => e.textContent).join(',')"));
assertEquals("id2,id5,id7,id8", page.evalOnSelectorAll("div:right-of(#id3, 50)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id2,id5,id7,id8", page.evalOnSelectorAll("div >> right-of=\"#id3\",50", "els => els.map(e => e.id).join(',')"));
// assertEquals("2,5,7,8", page.evalOnSelectorAll("div >> right-of=\"#id3\",50 >> span", "els => els.map(e => e.textContent).join(',')"));
// assertEquals("4,2,5,7,8,9", page.locator("div", new Page.LocatorOptions().setRightOf(page.locator("#id3")))
// .locator("span").evaluateAll("els => els.map(e => e.textContent).join(',')"));
assertEquals("id7,id8", page.evalOnSelectorAll("div:right-of(#id3, 49)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id7,id8", page.evalOnSelectorAll("div >> right-of=\"#id3\",49", "els => els.map(e => e.id).join(',')"));
// assertEquals("7,8", page.evalOnSelectorAll("div >> right-of=\"#id3\",49 >> span", "els => els.map(e => e.textContent).join(',')"));
// assertEquals("4,2,5,7,8,9", page.locator("div", new Page.LocatorOptions().setRightOf(page.locator("#id3")))
// .locator("span").evaluateAll("els => els.map(e => e.textContent).join(',')"));
assertEquals("id1", page.evalOnSelector("div:left-of(#id2)", "e => e.id"));
// assertEquals("id1", page.evalOnSelector("div >> left-of=\"#id2\"", "e => e.id"));
// assertEquals("id1", page.locator("div", new Page.LocatorOptions().setLeftOf(page.locator("#id2"))).first().evaluate("e => e.id"));
assertNull(page.querySelector("div:left-of(#id0)"));
// assertNull(page.querySelector("div >> left-of=\"#id0\""));
assertEquals("id0", page.evalOnSelector("div:left-of(#id5)", "e => e.id"));
// assertEquals("id0", page.evalOnSelector("div >> left-of=\"#id5\"", "e => e.id"));
assertEquals("id8", page.evalOnSelector("div:left-of(#id9)", "e => e.id"));
// assertEquals("id8", page.evalOnSelector("div >> left-of=\"#id9\"", "e => e.id"));
assertEquals("id3", page.evalOnSelector("div:left-of(#id4)", "e => e.id"));
// assertEquals("id3", page.evalOnSelector("div >> left-of=\"#id4\"", "e => e.id"));
assertEquals("id0,id7,id3,id1,id6,id8", page.evalOnSelectorAll("div:left-of(#id5)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id0,id7,id3,id1,id6,id8", page.evalOnSelectorAll("div >> left-of=\"#id5\"", "els => els.map(e => e.id).join(',')"));
assertEquals("id7,id8", page.evalOnSelectorAll("div:left-of(#id5, 3)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id7,id8", page.evalOnSelectorAll("div >> left-of=\"#id5\",3", "els => els.map(e => e.id).join(',')"));
// assertEquals("7,8", page.evalOnSelectorAll("div >> left-of=\"#id5\",3 >> span", "els => els.map(e => e.textContent).join(',')"));
assertEquals("id3", page.evalOnSelector("div:above(#id0)", "e => e.id"));
// assertEquals("id3", page.evalOnSelector("div >> above=\"#id0\"", "e => e.id"));
// assertEquals("id3", page.locator("div", new Page.LocatorOptions().setAbove(page.locator("#id0")))
// .first().evaluate("e => e.id"));
assertEquals("id4", page.evalOnSelector("div:above(#id5)", "e => e.id"));
// assertEquals("id4", page.evalOnSelector("div >> above=\"#id5\"", "e => e.id"));
assertEquals("id5", page.evalOnSelector("div:above(#id7)", "e => e.id"));
// assertEquals("id5", page.evalOnSelector("div >> above=\"#id7\"", "e => e.id"));
assertEquals("id0", page.evalOnSelector("div:above(#id8)", "e => e.id"));
// assertEquals("id0", page.evalOnSelector("div >> above=\"#id8\"", "e => e.id"));
assertEquals("id8", page.evalOnSelector("div:above(#id9)", "e => e.id"));
// assertEquals("id8", page.evalOnSelector("div >> above=\"#id9\"", "e => e.id"));
assertNull(page.querySelector("div:above(#id2)"));
// assertNull(page.querySelector("div >> above=\"#id2\""));
assertEquals("id4,id2,id3,id1", page.evalOnSelectorAll("div:above(#id5)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id4,id2,id3,id1", page.evalOnSelectorAll("div >> above=\"#id5\"", "els => els.map(e => e.id).join(',')"));
assertEquals("id4,id3", page.evalOnSelectorAll("div:above(#id5, 20)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id4,id3", page.evalOnSelectorAll("div >> above=\"#id5\",20", "els => els.map(e => e.id).join(',')"));
assertEquals("id5", page.evalOnSelector("div:below(#id4)", "e => e.id"));
// assertEquals("id5", page.evalOnSelector("div >> below=\"#id4\"", "e => e.id"));
// assertEquals("id5", page.locator("div", new Page.LocatorOptions().setBelow(page.locator("#id4")))
// .first().evaluate("e => e.id"));
assertEquals("id0", page.evalOnSelector("div:below(#id3)", "e => e.id"));
// assertEquals("id0", page.evalOnSelector("div >> below=\"#id3\"", "e => e.id"));
assertEquals("id4", page.evalOnSelector("div:below(#id2)", "e => e.id"));
// assertEquals("id4", page.evalOnSelector("div >> below=\"#id2\"", "e => e.id"));
assertEquals("id8", page.evalOnSelector("div:below(#id6)", "e => e.id"));
// assertEquals("id8", page.evalOnSelector("div >> below=\"#id6\"", "e => e.id"));
assertEquals("id8", page.evalOnSelector("div:below(#id7)", "e => e.id"));
// assertEquals("id8", page.evalOnSelector("div >> below=\"#id7\"", "e => e.id"));
assertEquals("id9", page.evalOnSelector("div:below(#id8)", "e => e.id"));
// assertEquals("id9", page.evalOnSelector("div >> below=\"#id8\"", "e => e.id"));
assertNull(page.querySelector("div:below(#id9)"));
// assertNull(page.querySelector("div >> below=\"#id9\""));
assertEquals("id0,id5,id6,id7,id8,id9", page.evalOnSelectorAll("div:below(#id3)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id0,id5,id6,id7,id8,id9", page.evalOnSelectorAll("div >> below=\"#id3\"", "els => els.map(e => e.id).join(',')"));
assertEquals("id0,id5,id6,id7", page.evalOnSelectorAll("div:below(#id3, 105)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id0,id5,id6,id7", page.evalOnSelectorAll("div >> below=\"#id3\" , 105", "els => els.map(e => e.id).join(',')"));
assertEquals("id3", page.evalOnSelector("div:near(#id0)", "e => e.id"));
// assertEquals("id3", page.evalOnSelector("div >> near=\"#id0\"", "e => e.id"));
// assertEquals("id3", page.locator("div", new Page.LocatorOptions().setNear(page.locator("#id0")))
// .first().evaluate("e => e.id"));
assertEquals("id0,id5,id3,id6", page.evalOnSelectorAll("div:near(#id7)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id0,id5,id3,id6", page.evalOnSelectorAll("div >> near=\"#id7\"", "els => els.map(e => e.id).join(',')"));
assertEquals("id3,id6,id7,id8,id1,id5", page.evalOnSelectorAll("div:near(#id0)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id3,id6,id7,id8,id1,id5", page.evalOnSelectorAll("div >> near=\"#id0\"", "els => els.map(e => e.id).join(',')"));
assertEquals("id0,id3,id7", page.evalOnSelectorAll("div:near(#id6)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id0,id3,id7", page.evalOnSelectorAll("div >> near=\"#id6\"", "els => els.map(e => e.id).join(',')"));
assertEquals("id0", page.evalOnSelectorAll("div:near(#id6, 10)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id0", page.evalOnSelectorAll("div >> near=\"#id6\",10", "els => els.map(e => e.id).join(',')"));
assertEquals("id3,id6,id7,id8,id1,id5,id4,id2", page.evalOnSelectorAll("div:near(#id0, 100)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id3,id6,id7,id8,id1,id5,id4,id2", page.evalOnSelectorAll("div >> near=\"#id0\",100", "els => els.map(e => e.id).join(',')"));
assertEquals("id7,id6", page.evalOnSelectorAll("div:below(#id5):above(#id8)", "els => els.map(e => e.id).join(',')"));
// assertEquals("id7,id6", page.evalOnSelectorAll("div >> below=\"#id5\" >> above=\"#id8\"", "els => els.map(e => e.id).join(',')"));
assertEquals("id7", page.evalOnSelector("div:below(#id5):above(#id8)", "e => e.id"));
// assertEquals("id7", page.evalOnSelector("div >> below=\"#id5\" >> above=\"#id8\"", "e => e.id"));
// assertEquals("id7", page.locator("div", new Page.LocatorOptions()
// .setBelow(page.locator("#id5"))
// .setAbove(page.locator("#id8"))).first().evaluate("e => e.id"));
assertEquals("id5,id6,id3", page.evalOnSelectorAll("div:right-of(#id0) + div:above(#id8)", "els => els.map(e => e.id).join(',')"));
try {
ElementHandle error = page.querySelector(":near(50)");
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("\"near\" engine expects a selector list and optional maximum distance in pixels"), e.getMessage());
}
try {
ElementHandle error1 = page.querySelector("div >> left-of=abc");
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Malformed selector: left-of=abc"));
}
try {
ElementHandle error2 = page.querySelector("left-of=\"div\"");
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("\"left-of\" selector cannot be first"), e.getMessage());
}
try {
ElementHandle error3 = page.querySelector("div >> left-of=33");
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Malformed selector: left-of=33"));
}
try {
ElementHandle error4 = page.querySelector("div >> left-of='span','foo'");
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Malformed selector: left-of='span','foo'"));
}
try {
ElementHandle error5 = page.querySelector("div >> left-of='span',3,4");
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Malformed selector: left-of='span',3,4"));
}
}
}
@@ -113,7 +113,7 @@ public class TestTracing extends TestBase {
Path trace = tmpDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(trace));
Map<String, byte[]> entries = Utils.parseTrace(trace);
Map<String, byte[]> entries = Utils.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());
@@ -25,6 +25,7 @@ import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
@@ -139,9 +140,13 @@ public class TestWebSocket extends TestBase {
boolean[] socketError = {false};
String[] error = {null};
page.onWebSocket(ws -> {
ws.onSocketError(e -> {
error[0] = e;
socketError[0] = true;
ws.onSocketError(new Consumer<String>() {
@Override
public void accept(String e) {
ws.offSocketError(this);
error[0] = e;
socketError[0] = true;
}
});
});
page.evaluate("port => {\n" +
@@ -24,6 +24,24 @@ import static com.microsoft.playwright.Utils.mapOf;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestWheel extends TestBase {
private void expectEvent(Map<String, Object> expected) {
// Chromium reports deltaX/deltaY scaled by host device scale factor.
// https://bugs.chromium.org/p/chromium/issues/detail?id=1324819
// https://github.com/microsoft/playwright/issues/7362
// Different bots have different scale factors (usually 1 or 2), so we just ignore the values
// instead of guessing the host scale factor.
// This first appeared in Chromium 102.
boolean ignoreDelta = isChromium() && isMac;
Map<String, Object> received = (Map<String, Object>) page.evaluate("window.lastEvent");
if (ignoreDelta) {
expected.remove("deltaX");
expected.remove("deltaY");
received.remove("deltaX");
received.remove("deltaY");
}
assertEquals(expected, received);
}
@Test
void shouldDispatchWheelEvents() {
page.setContent("<div style='width: 5000px; height: 5000px;'></div>");
@@ -42,7 +60,7 @@ public class TestWheel extends TestBase {
"altKey", false,
"metaKey", false);
page.waitForFunction("window.scrollY === 100");
assertEquals(expected, page.evaluate("window.lastEvent"));
expectEvent(expected);
}
@Test
@@ -71,7 +89,7 @@ public class TestWheel extends TestBase {
"shiftKey", true,
"altKey", false,
"metaKey", false);
assertEquals(expected, page.evaluate("window.lastEvent"));
expectEvent(expected);
}
@Test
@@ -90,7 +108,7 @@ public class TestWheel extends TestBase {
"shiftKey", false,
"altKey", false,
"metaKey", false);
assertEquals(expected, page.evaluate("window.lastEvent"));
expectEvent(expected);
page.waitForFunction("window.scrollX === 100");
}
@@ -113,7 +131,7 @@ public class TestWheel extends TestBase {
"shiftKey", false,
"altKey", false,
"metaKey", false);
assertEquals(expected, page.evaluate("window.lastEvent"));
expectEvent(expected);
// give the page a chacne to scroll
page.waitForTimeout(100);
// ensure that it did not.
@@ -22,7 +22,7 @@ import com.google.gson.JsonParser;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
@@ -89,7 +89,7 @@ class Utils {
}
}
static Map<String, byte[]> parseTrace(Path trace) throws IOException {
static Map<String, byte[]> parseZip(Path trace) throws IOException {
Map<String, byte[]> entries = new HashMap<>();
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(trace.toFile()))) {
for (ZipEntry zipEntry = zis.getNextEntry(); zipEntry != null; zipEntry = zis.getNextEntry()) {
@@ -104,6 +104,24 @@ class Utils {
return entries;
}
static Map<String, byte[]> extractZip(Path zipPath, Path toDir) throws IOException {
Map<String, byte[]> entries = new HashMap<>();
Files.createDirectories(toDir);
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipPath.toFile()))) {
for (ZipEntry zipEntry = zis.getNextEntry(); zipEntry != null; zipEntry = zis.getNextEntry()) {
Path toPath = toDir.resolve(zipEntry.getName());
if (zipEntry.isDirectory()) {
Files.createDirectories(toPath);
} else {
Files.copy(zis, toPath);
}
zis.closeEntry();
}
}
return entries;
}
enum OS { WINDOWS, MAC, LINUX, UNKNOWN }
static OS getOS() {
String name = System.getProperty("os.name").toLowerCase();
@@ -0,0 +1,366 @@
{
"log": {
"version": "1.2",
"creator": {
"name": "Playwright",
"version": "1.23.0-next"
},
"browser": {
"name": "chromium",
"version": "103.0.5060.33"
},
"pages": [
{
"startedDateTime": "2022-06-10T04:27:32.125Z",
"id": "page@b17b177f1c2e66459db3dcbe44636ffd",
"title": "Hey",
"pageTimings": {
"onContentLoad": 70,
"onLoad": 70
}
}
],
"entries": [
{
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
"_monotonicTime": 270572145.898,
"startedDateTime": "2022-06-10T04:27:32.146Z",
"time": 8.286,
"request": {
"method": "GET",
"url": "http://no.playwright/",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "Accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
},
{
"name": "Upgrade-Insecure-Requests",
"value": "1"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36"
}
],
"queryString": [],
"headersSize": 326,
"bodySize": 0
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "content-length",
"value": "111"
},
{
"name": "content-type",
"value": "text/html"
}
],
"content": {
"size": 111,
"mimeType": "text/html",
"compression": 0,
"text": "<title>Hey</title><link rel='stylesheet' href='./style.css'><script src='./script.js'></script><div>hello</div>"
},
"headersSize": 65,
"bodySize": 170,
"redirectURL": "",
"_transferSize": 170
},
"cache": {
"beforeRequest": null,
"afterRequest": null
},
"timings": {
"dns": -1,
"connect": -1,
"ssl": -1,
"send": 0,
"wait": 8.286,
"receive": -1
},
"pageref": "page@b17b177f1c2e66459db3dcbe44636ffd",
"_securityDetails": {}
},
{
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
"_monotonicTime": 270572174.683,
"startedDateTime": "2022-06-10T04:27:32.172Z",
"time": 7.132,
"request": {
"method": "POST",
"url": "http://no.playwright/style.css",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "Accept",
"value": "text/css,*/*;q=0.1"
},
{
"name": "Referer",
"value": "http://no.playwright/"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36"
}
],
"queryString": [],
"headersSize": 220,
"bodySize": 0
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "content-length",
"value": "24"
},
{
"name": "content-type",
"value": "text/css"
}
],
"content": {
"size": 24,
"mimeType": "text/css",
"compression": 0,
"text": "body { background:cyan }"
},
"headersSize": 63,
"bodySize": 81,
"redirectURL": "",
"_transferSize": 81
},
"cache": {
"beforeRequest": null,
"afterRequest": null
},
"timings": {
"dns": -1,
"connect": -1,
"ssl": -1,
"send": 0,
"wait": 8.132,
"receive": -1
},
"pageref": "page@b17b177f1c2e66459db3dcbe44636ffd",
"_securityDetails": {}
},
{
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
"_monotonicTime": 270572174.683,
"startedDateTime": "2022-06-10T04:27:32.174Z",
"time": 8.132,
"request": {
"method": "GET",
"url": "http://no.playwright/style.css",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "Accept",
"value": "text/css,*/*;q=0.1"
},
{
"name": "Referer",
"value": "http://no.playwright/"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36"
}
],
"queryString": [],
"headersSize": 220,
"bodySize": 0
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "content-length",
"value": "24"
},
{
"name": "content-type",
"value": "text/css"
}
],
"content": {
"size": 24,
"mimeType": "text/css",
"compression": 0,
"text": "body { background: red }"
},
"headersSize": 63,
"bodySize": 81,
"redirectURL": "",
"_transferSize": 81
},
"cache": {
"beforeRequest": null,
"afterRequest": null
},
"timings": {
"dns": -1,
"connect": -1,
"ssl": -1,
"send": 0,
"wait": 8.132,
"receive": -1
},
"pageref": "page@b17b177f1c2e66459db3dcbe44636ffd",
"_securityDetails": {}
},
{
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
"_monotonicTime": 270572175.042,
"startedDateTime": "2022-06-10T04:27:32.175Z",
"time": 15.997,
"request": {
"method": "GET",
"url": "http://no.playwright/script.js",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "Accept",
"value": "*/*"
},
{
"name": "Referer",
"value": "http://no.playwright/"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36"
}
],
"queryString": [],
"headersSize": 205,
"bodySize": 0
},
"response": {
"status": 301,
"statusText": "Moved Permanently",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "location",
"value": "http://no.playwright/script2.js"
}
],
"content": {
"size": -1,
"mimeType": "x-unknown",
"compression": 0
},
"headersSize": 77,
"bodySize": 0,
"redirectURL": "http://no.playwright/script2.js",
"_transferSize": 77
},
"cache": {
"beforeRequest": null,
"afterRequest": null
},
"timings": {
"dns": -1,
"connect": -1,
"ssl": -1,
"send": 0,
"wait": 7.673,
"receive": 8.324
},
"pageref": "page@b17b177f1c2e66459db3dcbe44636ffd",
"_securityDetails": {}
},
{
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
"_monotonicTime": 270572181.822,
"startedDateTime": "2022-06-10T04:27:32.182Z",
"time": 6.735,
"request": {
"method": "GET",
"url": "http://no.playwright/script2.js",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "Accept",
"value": "*/*"
},
{
"name": "Referer",
"value": "http://no.playwright/"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36"
}
],
"queryString": [],
"headersSize": 206,
"bodySize": 0
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "content-length",
"value": "18"
},
{
"name": "content-type",
"value": "text/javascript"
}
],
"content": {
"size": 18,
"mimeType": "text/javascript",
"compression": 0,
"text": "window.value='foo'"
},
"headersSize": 70,
"bodySize": 82,
"redirectURL": "",
"_transferSize": 82
},
"cache": {
"beforeRequest": null,
"afterRequest": null
},
"timings": {
"dns": -1,
"connect": -1,
"ssl": -1,
"send": 0,
"wait": 6.735,
"receive": -1
},
"pageref": "page@b17b177f1c2e66459db3dcbe44636ffd",
"_securityDetails": {}
}
]
}
}
@@ -0,0 +1,620 @@
{
"log": {
"version": "1.2",
"creator": {
"name": "Playwright",
"version": "1.23.0-next"
},
"browser": {
"name": "chromium",
"version": "103.0.5060.42"
},
"pages": [
{
"startedDateTime": "2022-06-16T21:41:23.901Z",
"id": "page@8f314969edc000996eb5c2ab22f0e6b3",
"title": "Microsoft",
"pageTimings": {
"onContentLoad": 8363,
"onLoad": 8896
}
}
],
"entries": [
{
"_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f",
"_monotonicTime": 110928357.437,
"startedDateTime": "2022-06-16T21:41:23.951Z",
"time": 93.99,
"request": {
"method": "GET",
"url": "https://theverge.com/",
"httpVersion": "HTTP/2.0",
"cookies": [],
"headers": [
{
"name": ":authority",
"value": "theverge.com"
},
{
"name": ":method",
"value": "GET"
},
{
"name": ":path",
"value": "/"
},
{
"name": ":scheme",
"value": "https"
},
{
"name": "accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
},
{
"name": "accept-encoding",
"value": "gzip, deflate, br"
},
{
"name": "accept-language",
"value": "en-US,en;q=0.9"
},
{
"name": "sec-ch-ua",
"value": "\"Chromium\";v=\"103\", \".Not/A)Brand\";v=\"99\""
},
{
"name": "sec-ch-ua-mobile",
"value": "?0"
},
{
"name": "sec-ch-ua-platform",
"value": "\"Linux\""
},
{
"name": "sec-fetch-dest",
"value": "document"
},
{
"name": "sec-fetch-mode",
"value": "navigate"
},
{
"name": "sec-fetch-site",
"value": "none"
},
{
"name": "sec-fetch-user",
"value": "?1"
},
{
"name": "upgrade-insecure-requests",
"value": "1"
},
{
"name": "user-agent",
"value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36"
}
],
"queryString": [],
"headersSize": 644,
"bodySize": 0
},
"response": {
"status": 301,
"statusText": "",
"httpVersion": "HTTP/2.0",
"cookies": [
{
"name": "vmidv1",
"value": "9faf31ab-1415-4b90-b367-24b670205f41",
"expires": "2027-06-15T21:41:24.000Z",
"domain": "theverge.com",
"path": "/",
"sameSite": "Lax",
"secure": true
}
],
"headers": [
{
"name": "accept-ranges",
"value": "bytes"
},
{
"name": "content-length",
"value": "0"
},
{
"name": "date",
"value": "Thu, 16 Jun 2022 21:41:24 GMT"
},
{
"name": "location",
"value": "http://www.theverge.com/"
},
{
"name": "retry-after",
"value": "0"
},
{
"name": "server",
"value": "Varnish"
},
{
"name": "set-cookie",
"value": "vmidv1=9faf31ab-1415-4b90-b367-24b670205f41;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=theverge.com;Path=/;SameSite=Lax;Secure"
},
{
"name": "via",
"value": "1.1 varnish"
},
{
"name": "x-cache",
"value": "HIT"
},
{
"name": "x-cache-hits",
"value": "0"
},
{
"name": "x-served-by",
"value": "cache-pao17442-PAO"
},
{
"name": "x-timer",
"value": "S1655415684.005867,VS0,VE0"
}
],
"content": {
"size": -1,
"mimeType": "x-unknown",
"compression": 0
},
"headersSize": 425,
"bodySize": 0,
"redirectURL": "http://www.theverge.com/",
"_transferSize": 425
},
"cache": {
"beforeRequest": null,
"afterRequest": null
},
"timings": {
"dns": 0,
"connect": 34.151,
"ssl": 28.074,
"send": 0,
"wait": 27.549,
"receive": 4.216
},
"pageref": "page@8f314969edc000996eb5c2ab22f0e6b3",
"serverIPAddress": "151.101.65.52",
"_serverPort": 443,
"_securityDetails": {
"protocol": "TLS 1.2",
"subjectName": "*.americanninjawarriornation.com",
"issuer": "GlobalSign Atlas R3 DV TLS CA 2022 Q1",
"validFrom": 1644853133,
"validTo": 1679153932
}
},
{
"_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f",
"_monotonicTime": 110928427.603,
"startedDateTime": "2022-06-16T21:41:24.022Z",
"time": 44.39499999999999,
"request": {
"method": "GET",
"url": "http://www.theverge.com/",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "Accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate"
},
{
"name": "Accept-Language",
"value": "en-US,en;q=0.9"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Host",
"value": "www.theverge.com"
},
{
"name": "Upgrade-Insecure-Requests",
"value": "1"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36"
}
],
"queryString": [],
"headersSize": 423,
"bodySize": 0
},
"response": {
"status": 301,
"statusText": "Moved Permanently",
"httpVersion": "HTTP/1.1",
"cookies": [
{
"name": "_chorus_geoip_continent",
"value": "NA"
},
{
"name": "vmidv1",
"value": "4e0c1265-10f8-4cb1-a5de-1c3cf70b531c",
"expires": "2027-06-15T21:41:24.000Z",
"domain": "www.theverge.com",
"path": "/",
"sameSite": "Lax",
"secure": true
}
],
"headers": [
{
"name": "Accept-Ranges",
"value": "bytes"
},
{
"name": "Age",
"value": "2615"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Content-Length",
"value": "0"
},
{
"name": "Content-Type",
"value": "text/html"
},
{
"name": "Date",
"value": "Thu, 16 Jun 2022 21:41:24 GMT"
},
{
"name": "Location",
"value": "https://www.theverge.com/"
},
{
"name": "Server",
"value": "nginx"
},
{
"name": "Set-Cookie",
"value": "_chorus_geoip_continent=NA; expires=Fri, 17 Jun 2022 21:41:24 GMT; path=/;"
},
{
"name": "Set-Cookie",
"value": "vmidv1=4e0c1265-10f8-4cb1-a5de-1c3cf70b531c;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=www.theverge.com;Path=/;SameSite=Lax;Secure"
},
{
"name": "Vary",
"value": "X-Forwarded-Proto, Cookie, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region, Accept-Encoding"
},
{
"name": "Via",
"value": "1.1 varnish"
},
{
"name": "X-Cache",
"value": "HIT"
},
{
"name": "X-Cache-Hits",
"value": "2"
},
{
"name": "X-Served-By",
"value": "cache-pao17450-PAO"
},
{
"name": "X-Timer",
"value": "S1655415684.035748,VS0,VE0"
}
],
"content": {
"size": -1,
"mimeType": "text/html",
"compression": 0
},
"headersSize": 731,
"bodySize": 0,
"redirectURL": "https://www.theverge.com/",
"_transferSize": 731
},
"cache": {
"beforeRequest": null,
"afterRequest": null
},
"timings": {
"dns": 2.742,
"connect": 10.03,
"ssl": 14.123,
"send": 0,
"wait": 15.023,
"receive": 2.477
},
"pageref": "page@8f314969edc000996eb5c2ab22f0e6b3",
"serverIPAddress": "151.101.189.52",
"_serverPort": 80,
"_securityDetails": {}
},
{
"_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f",
"_monotonicTime": 110928455.901,
"startedDateTime": "2022-06-16T21:41:24.050Z",
"time": 50.29199999999999,
"request": {
"method": "GET",
"url": "https://www.theverge.com/",
"httpVersion": "HTTP/2.0",
"cookies": [
{
"name": "vmidv1",
"value": "9faf31ab-1415-4b90-b367-24b670205f41"
},
{
"name": "_chorus_geoip_continent",
"value": "NA"
}
],
"headers": [
{
"name": ":authority",
"value": "www.theverge.com"
},
{
"name": ":method",
"value": "GET"
},
{
"name": ":path",
"value": "/"
},
{
"name": ":scheme",
"value": "https"
},
{
"name": "accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
},
{
"name": "accept-encoding",
"value": "gzip, deflate, br"
},
{
"name": "accept-language",
"value": "en-US,en;q=0.9"
},
{
"name": "cookie",
"value": "vmidv1=9faf31ab-1415-4b90-b367-24b670205f41; _chorus_geoip_continent=NA"
},
{
"name": "sec-ch-ua",
"value": "\"Chromium\";v=\"103\", \".Not/A)Brand\";v=\"99\""
},
{
"name": "sec-ch-ua-mobile",
"value": "?0"
},
{
"name": "sec-ch-ua-platform",
"value": "\"Linux\""
},
{
"name": "sec-fetch-dest",
"value": "document"
},
{
"name": "sec-fetch-mode",
"value": "navigate"
},
{
"name": "sec-fetch-site",
"value": "none"
},
{
"name": "sec-fetch-user",
"value": "?1"
},
{
"name": "upgrade-insecure-requests",
"value": "1"
},
{
"name": "user-agent",
"value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36"
}
],
"queryString": [],
"headersSize": 729,
"bodySize": 0
},
"response": {
"status": 200,
"statusText": "",
"httpVersion": "HTTP/2.0",
"cookies": [
{
"name": "_chorus_geoip_continent",
"value": "NA"
},
{
"name": "vmidv1",
"value": "40d8fd14-5ac3-4757-9e9c-efb106e82d3a",
"expires": "2027-06-15T21:41:24.000Z",
"domain": "www.theverge.com",
"path": "/",
"sameSite": "Lax",
"secure": true
}
],
"headers": [
{
"name": "accept-ranges",
"value": "bytes"
},
{
"name": "age",
"value": "263"
},
{
"name": "cache-control",
"value": "max-age=0, public, must-revalidate"
},
{
"name": "content-encoding",
"value": "br"
},
{
"name": "content-length",
"value": "14"
},
{
"name": "content-security-policy",
"value": "default-src https: data: 'unsafe-inline' 'unsafe-eval'; child-src https: data: blob:; connect-src https: data: blob: ; font-src https: data:; img-src https: data: blob:; media-src https: data: blob:; object-src https:; script-src https: data: blob: 'unsafe-inline' 'unsafe-eval'; style-src https: 'unsafe-inline'; block-all-mixed-content; upgrade-insecure-requests"
},
{
"name": "content-type",
"value": "text/html; charset=utf-8"
},
{
"name": "date",
"value": "Thu, 16 Jun 2022 21:41:24 GMT"
},
{
"name": "etag",
"value": "W/\"d498ef668223d015000070a66a181e85\""
},
{
"name": "link",
"value": "<https://concertads-configs.vox-cdn.com/sbn/verge/config.json>; rel=preload; as=fetch; crossorigin"
},
{
"name": "referrer-policy",
"value": "strict-origin-when-cross-origin"
},
{
"name": "server",
"value": "nginx"
},
{
"name": "set-cookie",
"value": "_chorus_geoip_continent=NA; expires=Fri, 17 Jun 2022 21:41:24 GMT; path=/;"
},
{
"name": "set-cookie",
"value": "vmidv1=40d8fd14-5ac3-4757-9e9c-efb106e82d3a;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=www.theverge.com;Path=/;SameSite=Lax;Secure"
},
{
"name": "strict-transport-security",
"value": "max-age=31556952; preload"
},
{
"name": "vary",
"value": "Accept-Encoding, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region, Origin, X-Forwarded-Proto, Cookie, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region"
},
{
"name": "via",
"value": "1.1 varnish"
},
{
"name": "x-cache",
"value": "HIT"
},
{
"name": "x-cache-hits",
"value": "1"
},
{
"name": "x-content-type-options",
"value": "nosniff"
},
{
"name": "x-download-options",
"value": "noopen"
},
{
"name": "x-frame-options",
"value": "SAMEORIGIN"
},
{
"name": "x-permitted-cross-domain-policies",
"value": "none"
},
{
"name": "x-request-id",
"value": "97363ad70e272e63641c0bb784fa06a01b848dfd"
},
{
"name": "x-runtime",
"value": "0.257911"
},
{
"name": "x-served-by",
"value": "cache-pao17436-PAO"
},
{
"name": "x-timer",
"value": "S1655415684.075077,VS0,VE1"
},
{
"name": "x-xss-protection",
"value": "1; mode=block"
}
],
"content": {
"size": 14,
"mimeType": "text/html",
"compression": 0,
"text": "<h1>hello</h1>"
},
"headersSize": 1742,
"bodySize": 48716,
"redirectURL": "",
"_transferSize": 48716
},
"cache": {
"beforeRequest": null,
"afterRequest": null
},
"timings": {
"dns": 0.016,
"connect": 24.487,
"ssl": 17.406,
"send": 0,
"wait": 8.383,
"receive": -1
},
"pageref": "page@8f314969edc000996eb5c2ab22f0e6b3",
"serverIPAddress": "151.101.189.52",
"_serverPort": 443,
"_securityDetails": {
"protocol": "TLS 1.2",
"subjectName": "*.americanninjawarriornation.com",
"issuer": "GlobalSign Atlas R3 DV TLS CA 2022 Q1",
"validFrom": 1644853133,
"validTo": 1679153932
}
}
]
}
}
@@ -0,0 +1 @@
Hello, world
@@ -0,0 +1,95 @@
{
"log": {
"version": "1.2",
"creator": {
"name": "Playwright",
"version": "1.23.0-next"
},
"browser": {
"name": "chromium",
"version": "103.0.5060.33"
},
"pages": [
{
"startedDateTime": "2022-06-10T04:27:32.125Z",
"id": "page@b17b177f1c2e66459db3dcbe44636ffd",
"title": "Hey",
"pageTimings": {
"onContentLoad": 70,
"onLoad": 70
}
}
],
"entries": [
{
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
"_monotonicTime": 270572145.898,
"startedDateTime": "2022-06-10T04:27:32.146Z",
"time": 8.286,
"request": {
"method": "GET",
"url": "http://no.playwright/",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "Accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
},
{
"name": "Upgrade-Insecure-Requests",
"value": "1"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36"
}
],
"queryString": [],
"headersSize": 326,
"bodySize": 0
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "content-length",
"value": "12"
},
{
"name": "content-type",
"value": "text/html"
}
],
"content": {
"size": 12,
"mimeType": "text/html",
"compression": 0,
"_file": "har-sha1-main-response.txt"
},
"headersSize": 64,
"bodySize": 71,
"redirectURL": "",
"_transferSize": 71
},
"cache": {
"beforeRequest": null,
"afterRequest": null
},
"timings": {
"dns": -1,
"connect": -1,
"ssl": -1,
"send": 0,
"wait": 8.286,
"receive": -1
},
"pageref": "page@b17b177f1c2e66459db3dcbe44636ffd",
"_securityDetails": {}
}
]
}
}
+3
View File
@@ -0,0 +1,3 @@
<title>HAR Page</title>
<link rel='stylesheet' href='./one-style.css'>
<div>hello, world!</div>
+2 -2
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.21.0-SNAPSHOT</version>
<version>1.24.1</version>
<packaging>pom</packaging>
<name>Playwright Parent Project</name>
<description>Java library to automate Chromium, Firefox and WebKit with a single API.
@@ -43,7 +43,7 @@
<properties>
<compiler.version>1.8</compiler.version>
<gson.version>2.8.6</gson.version>
<gson.version>2.8.9</gson.version>
<junit.version>5.7.0</junit.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<websocket.version>1.5.1</websocket.version>
+1 -1
View File
@@ -1 +1 @@
1.21.0-beta-1649696820000
1.24.1
+1 -1
View File
@@ -33,7 +33,7 @@ fi
mkdir -p driver
cd driver
for PLATFORM in mac linux win32_x64
for PLATFORM in mac linux linux-arm64 win32_x64
do
FILE_NAME=$FILE_PREFIX-$PLATFORM.zip
if [[ -d $PLATFORM ]]; then
+2 -2
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>api-generator</artifactId>
<version>1.21.0-SNAPSHOT</version>
<version>1.24.1</version>
<name>Playwright - API Generator</name>
<description>
This is an internal module used to generate Java API from the upstream Playwright
@@ -14,7 +14,7 @@
</description>
<properties>
<compiler.version>1.8</compiler.version>
<gson.version>2.8.6</gson.version>
<gson.version>2.8.9</gson.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@@ -944,7 +944,7 @@ class Interface extends TypeDefinition {
if (asList("Page", "Frame", "BrowserContext", "WebSocket").contains(jsonName)) {
output.add("import java.util.function.Predicate;");
}
if (asList("Page", "Frame", "FrameLocator", "Locator", "BrowserContext", "PageAssertions", "LocatorAssertions").contains(jsonName)) {
if (asList("Page", "Frame", "FrameLocator", "Locator", "Browser", "BrowserType", "BrowserContext", "PageAssertions", "LocatorAssertions").contains(jsonName)) {
output.add("import java.util.regex.Pattern;");
}
if ("PlaywrightAssertions".equals(jsonName)) {
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-cli-version</artifactId>
<version>1.21.0-SNAPSHOT</version>
<version>1.24.1</version>
<name>Test Playwright Command Line Version</name>
<properties>
<compiler.version>1.8</compiler.version>
+2 -2
View File
@@ -4,12 +4,12 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-local-installation</artifactId>
<version>1.21.0-SNAPSHOT</version>
<version>1.24.1</version>
<name>Test local installation</name>
<description>Runs Playwright test suite (copied from playwright module) against locally cached Playwright</description>
<properties>
<compiler.version>1.8</compiler.version>
<gson.version>2.8.6</gson.version>
<gson.version>2.8.9</gson.version>
<junit.version>5.7.0</junit.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<websocket.version>1.5.1</websocket.version>
+1 -1
View File
@@ -9,7 +9,7 @@
</parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-spring-boot-starter</artifactId>
<version>1.21.0-SNAPSHOT</version>
<version>1.24.1</version>
<name>Test Playwright With Spring Boot</name>
<properties>
<spring.version>2.4.3</spring.version>

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