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

Compare commits

...

24 Commits

Author SHA1 Message Date
Yury Semikhatsky f0c034da7d chore: mark 1.57.0 (#1872) 2025-12-04 10:41:23 -08:00
Yury Semikhatsky 63bb008857 chore: roll driver to 1.57.0-beta-1764692940000 (#1870) 2025-12-02 13:59:36 -08:00
dependabot[bot] 9b3a788806 chore(deps): bump actions/checkout from 5 to 6 in the actions group (#1868) 2025-12-01 15:00:50 -08:00
dependabot[bot] 2f387edf0d chore(deps): bump the all group with 3 updates (#1867) 2025-12-01 14:59:52 -08:00
arukiidou 6eb30e275c chore(deps): bump junit.version from 5.13.4 to 5.14.1 (#1859) 2025-11-12 10:00:13 -08:00
arukiidou 0f14588df1 Migrate ExtensionContext.Store.CloseableResource to AutoCloseable (#1860) 2025-11-12 09:59:06 -08:00
arukiidou e417cad372 Fix document typo - setContextOptions (#1862) 2025-11-12 09:45:13 -08:00
Yury Semikhatsky 059667e311 chore: remove background pages implementation (#1861) 2025-10-31 10:25:35 -07:00
Yury Semikhatsky 98296d9cdf devops: update ado approver (#1857) 2025-10-24 13:38:05 -07:00
Yury Semikhatsky 1599e1c7bc chore: roll 1.56.1 (#1855) 2025-10-17 11:11:43 -07:00
dependabot[bot] a2555ddf9e chore(deps): bump the all group with 4 updates (#1846) 2025-10-03 15:46:53 -07:00
Yury Semikhatsky 0deadc2b90 chore: roll driver to 1.56.0-beta (#1849) 2025-10-03 15:45:53 -07:00
Simon Knott eb1fea9907 fix(trace): waitForLoadState title (#1840) 2025-09-08 09:49:00 +02:00
dependabot[bot] dd99ce8b34 chore(deps): bump the actions group with 2 updates (#1838) 2025-09-04 16:18:58 -07:00
dependabot[bot] ed8e9c434f chore(deps): bump the all group across 1 directory with 7 updates (#1839) 2025-09-04 16:18:15 -07:00
Yury Semikhatsky aee298b293 chore: rename headful -> headed (#1835) 2025-08-27 09:51:30 -07:00
Max Schmitt fd2ab4708a devops: enable retries in Docker tests (#1834) 2025-08-26 21:50:28 +02:00
Max Schmitt 2a6cdff664 chore: migrate Trace Viewer tests to use real Trace viewer (#1830) 2025-08-26 10:43:18 +02:00
Max Schmitt 44161e0558 chore: fix Maven test commands (#1832) 2025-08-26 00:35:06 +02:00
Max Schmitt 954b1c43ef refactor: remove unused ImplUtils class (#1833) 2025-08-25 15:29:53 -07:00
Simon Knott f4c7b9734f chore: roll 1.55.0 (#1827) 2025-08-21 17:09:15 +02:00
Yury Semikhatsky dd87b300fb fix: npe in page.pause() (#1828) 2025-08-18 15:51:17 -07:00
Janne Hyötylä f83c03af68 fix: Fix masking in single element screenshots. (#1825) 2025-08-11 11:40:00 -07:00
JONGSHIN d26dd0b112 fix: Replaced classLoader in DriverJar (#1811) 2025-07-31 11:03:19 -07:00
82 changed files with 1312 additions and 410 deletions
+1 -1
View File
@@ -90,7 +90,7 @@ extends:
folderlocation: '$(Build.ArtifactStagingDirectory)/esrp-build'
waitforreleasecompletion: true
owners: 'yurys@microsoft.com'
approvers: 'maxschmitt@microsoft.com'
approvers: 'yurys@microsoft.com'
serviceendpointurl: 'https://api.esrp.microsoft.com'
mainpublisher: 'Playwright'
domaintenantid: '975f013f-7f24-47e8-a7d3-abc4752bf346'
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
environment: Docker
if: github.repository == 'microsoft/playwright-java'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Azure login
uses: azure/login@v2
with:
@@ -26,5 +26,5 @@ jobs:
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- run: ./utils/docker/publish_docker.sh stable
+6 -6
View File
@@ -20,9 +20,9 @@ jobs:
browser: [chromium, firefox, webkit]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up JDK 1.8
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: zulu
java-version: 8
@@ -65,13 +65,13 @@ jobs:
browser-channel: msedge
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Media Pack
if: matrix.os == 'windows-latest'
shell: powershell
run: Install-WindowsFeature Server-Media-Foundation
- name: Set up JDK 1.8
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: zulu
java-version: 8
@@ -100,9 +100,9 @@ jobs:
browser: [chromium, firefox, webkit]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up JDK 21
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: adopt
java-version: 21
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Maven packages
uses: actions/cache@v4
with:
+18 -4
View File
@@ -21,18 +21,32 @@ jobs:
name: Test
timeout-minutes: 120
runs-on: ${{ matrix.runs-on }}
env:
PW_MAX_RETRIES: 3
strategy:
fail-fast: false
matrix:
flavor: [jammy, noble]
runs-on: [ubuntu-24.04, ubuntu-24.04-arm]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Build Docker image
run: |
ARCH="${{ matrix.runs-on == 'ubuntu-24.04-arm' && 'arm64' || 'amd64' }}"
bash utils/docker/build.sh --$ARCH ${{ matrix.flavor }} playwright-java:localbuild-${{ matrix.flavor }}
- name: Test
- name: Start container
run: |
CONTAINER_ID="$(docker run --rm -e CI --ipc=host -v $(pwd):/root/playwright --name playwright-docker-test -d -t playwright-java:localbuild-${{ matrix.flavor }} /bin/bash)"
docker exec "${CONTAINER_ID}" /root/playwright/tools/test-local-installation/create_project_and_run_tests.sh
CONTAINER_ID=$(docker run --rm -e CI -e PW_MAX_RETRIES --ipc=host -v "$(pwd)":/root/playwright --name playwright-docker-test -d -t playwright-java:localbuild-${{ matrix.flavor }} /bin/bash)
echo "CONTAINER_ID=$CONTAINER_ID" >> $GITHUB_ENV
- name: Run test in container
run: |
docker exec "$CONTAINER_ID" /root/playwright/tools/test-local-installation/create_project_and_run_tests.sh
- name: Test ClassLoader
run: |
docker exec "${CONTAINER_ID}" /root/playwright/tools/test-spring-boot-starter/package_and_run_async_test.sh
- name: Stop container
run: |
docker stop "$CONTAINER_ID"
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Download drivers
run: scripts/download_driver.sh
- name: Regenerate APIs
+2 -2
View File
@@ -32,9 +32,9 @@ scripts/download_driver.sh
mvn compile
mvn test
# Executing a single test
BROWSER=chromium mvn test --projects=playwright -Dtest=TestPageNetworkSizes#shouldHaveTheCorrectResponseBodySize
BROWSER=chromium mvn test -Dtest=TestPageNetworkSizes#shouldHaveTheCorrectResponseBodySize
# Executing a single test class
BROWSER=chromium mvn test --projects=playwright -Dtest=TestPageNetworkSizes
BROWSER=chromium mvn test -Dtest=TestPageNetworkSizes
```
### Generating API
+2 -2
View File
@@ -10,9 +10,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->139.0.7258.5<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->143.0.7499.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->140.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->144.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
## Documentation
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.57.0</version>
</parent>
<artifactId>driver-bundle</artifactId>
@@ -114,7 +114,7 @@ public class DriverJar extends Driver {
}
public static URI getDriverResourceURI() throws URISyntaxException {
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
ClassLoader classloader = DriverJar.class.getClassLoader();
return classloader.getResource("driver/" + platformDir()).toURI();
}
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.57.0</version>
</parent>
<artifactId>driver</artifactId>
+2 -2
View File
@@ -6,11 +6,11 @@
<groupId>org.example</groupId>
<artifactId>examples</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.57.0</version>
<name>Playwright Client Examples</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<playwright.version>1.54.0</playwright.version>
<playwright.version>1.57.0</playwright.version>
</properties>
<dependencies>
<dependency>
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.57.0</version>
</parent>
<artifactId>playwright</artifactId>
@@ -51,6 +51,10 @@ public interface APIRequest {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -134,6 +138,10 @@ public interface APIRequest {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -106,6 +106,10 @@ public interface Browser extends AutoCloseable {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -323,6 +327,10 @@ public interface Browser extends AutoCloseable {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -674,6 +682,10 @@ public interface Browser extends AutoCloseable {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -891,6 +903,10 @@ public interface Browser extends AutoCloseable {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -46,14 +46,7 @@ import java.util.regex.Pattern;
public interface BrowserContext extends AutoCloseable {
/**
* <strong>NOTE:</strong> Only works with Chromium browser's persistent context.
*
* <p> Emitted when new background page is created in the context.
* <pre>{@code
* context.onBackgroundPage(backgroundPage -> {
* System.out.println(backgroundPage.url());
* });
* }</pre>
* @deprecated Background pages have been removed from Chromium together with Manifest V2 extensions.
*/
void onBackgroundPage(Consumer<Page> handler);
/**
@@ -588,9 +581,7 @@ public interface BrowserContext extends AutoCloseable {
*/
void addInitScript(Path script);
/**
* <strong>NOTE:</strong> Background pages are only supported on Chromium-based browsers.
*
* <p> All existing background pages in the context.
* @deprecated Background pages have been removed from Chromium together with Manifest V2 extensions.
*
* @since v1.11
*/
@@ -865,6 +856,8 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "clipboard-write"}</li>
* <li> {@code "geolocation"}</li>
* <li> {@code "gyroscope"}</li>
* <li> {@code "local-fonts"}</li>
* <li> {@code "local-network-access"}</li>
* <li> {@code "magnetometer"}</li>
* <li> {@code "microphone"}</li>
* <li> {@code "midi-sysex"} (system-exclusive midi)</li>
@@ -872,7 +865,6 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "notifications"}</li>
* <li> {@code "payment-handler"}</li>
* <li> {@code "storage-access"}</li>
* <li> {@code "local-fonts"}</li>
* </ul>
* @since v1.8
*/
@@ -898,6 +890,8 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "clipboard-write"}</li>
* <li> {@code "geolocation"}</li>
* <li> {@code "gyroscope"}</li>
* <li> {@code "local-fonts"}</li>
* <li> {@code "local-network-access"}</li>
* <li> {@code "magnetometer"}</li>
* <li> {@code "microphone"}</li>
* <li> {@code "midi-sysex"} (system-exclusive midi)</li>
@@ -905,7 +899,6 @@ public interface BrowserContext extends AutoCloseable {
* <li> {@code "notifications"}</li>
* <li> {@code "payment-handler"}</li>
* <li> {@code "storage-access"}</li>
* <li> {@code "local-fonts"}</li>
* </ul>
* @since v1.8
*/
@@ -491,6 +491,10 @@ public interface BrowserType {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -813,6 +817,10 @@ public interface BrowserType {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
* origin} property should be provided with an exact match to the request origin that the certificate is valid for.
*
* <p> Client certificate authentication is only active when at least one client certificate is provided. If you want to reject
* all client certificates sent by the server, you need to provide a client certificate with an {@code origin} that does
* not match any of the domains you plan to visit.
*
* <p> <strong>NOTE:</strong> When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by
* replacing {@code localhost} with {@code local.playwright}.
*/
@@ -1386,6 +1394,11 @@ public interface BrowserType {
* the **parent** directory of the "Profile Path" seen at {@code chrome://version}.
*
* <p> Note that browsers do not allow launching multiple instances with the same User Data Directory.
*
* <p> <strong>NOTE:</strong> Chromium/Chrome: Due to recent Chrome policy changes, automating the default Chrome user profile is not supported.
* Pointing {@code userDataDir} to Chrome's main "User Data" directory (the profile used for your regular browsing) may
* result in pages not loading or the browser exiting. Create and use a separate directory (for example, an empty folder)
* as your automation profile instead. See https://developer.chrome.com/blog/remote-debugging-port for details.
* @since v1.8
*/
default BrowserContext launchPersistentContext(Path userDataDir) {
@@ -1406,6 +1419,11 @@ public interface BrowserType {
* the **parent** directory of the "Profile Path" seen at {@code chrome://version}.
*
* <p> Note that browsers do not allow launching multiple instances with the same User Data Directory.
*
* <p> <strong>NOTE:</strong> Chromium/Chrome: Due to recent Chrome policy changes, automating the default Chrome user profile is not supported.
* Pointing {@code userDataDir} to Chrome's main "User Data" directory (the profile used for your regular browsing) may
* result in pages not loading or the browser exiting. Create and use a separate directory (for example, an empty folder)
* as your automation profile instead. See https://developer.chrome.com/blog/remote-debugging-port for details.
* @since v1.8
*/
BrowserContext launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options);
@@ -78,5 +78,12 @@ public interface ConsoleMessage {
* @since v1.8
*/
String type();
/**
* The web worker or service worker that produced this console message, if any. Note that console messages from web workers
* also have non-null {@link com.microsoft.playwright.ConsoleMessage#page ConsoleMessage.page()}.
*
* @since v1.57
*/
Worker worker();
}
@@ -48,6 +48,9 @@ public interface Download {
/**
* Returns a readable stream for a successful download, or throws for a failed/canceled download.
*
* <p> <strong>NOTE:</strong> If you don't need a readable stream, it's usually simpler to read the file from disk after the download completed. See
* {@link com.microsoft.playwright.Download#path Download.path()}.
*
* @since v1.8
*/
InputStream createReadStream();
@@ -170,6 +170,12 @@ public interface ElementHandle extends JSHandle {
* element.
*/
public Position position;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public Integer steps;
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -244,6 +250,15 @@ public interface ElementHandle extends JSHandle {
this.position = position;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public ClickOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -293,6 +308,12 @@ public interface ElementHandle extends JSHandle {
* element.
*/
public Position position;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public Integer steps;
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -360,6 +381,15 @@ public interface ElementHandle extends JSHandle {
this.position = position;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public DblclickOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -563,6 +563,11 @@ public interface Frame {
* specified, some visible point of the element is used.
*/
public Position sourcePosition;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public Integer steps;
/**
* When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
* element, the call throws an exception.
@@ -617,6 +622,14 @@ public interface Frame {
this.sourcePosition = sourcePosition;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public DragAndDropOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
* element, the call throws an exception.
@@ -245,6 +245,12 @@ public interface Locator {
* element.
*/
public Position position;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public Integer steps;
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -320,6 +326,15 @@ public interface Locator {
this.position = position;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public ClickOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -370,6 +385,12 @@ public interface Locator {
* element.
*/
public Position position;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public Integer steps;
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -438,6 +459,15 @@ public interface Locator {
this.position = position;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public DblclickOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
@@ -494,6 +524,11 @@ public interface Locator {
* specified, some visible point of the element is used.
*/
public Position sourcePosition;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public Integer steps;
/**
* Drops on the target element at this point relative to the top-left corner of the element's padding box. If not
* specified, some visible point of the element is used.
@@ -543,6 +578,14 @@ public interface Locator {
this.sourcePosition = sourcePosition;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public DragToOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* Drops on the target element at this point relative to the top-left corner of the element's padding box. If not
* specified, some visible point of the element is used.
@@ -2616,6 +2659,23 @@ public interface Locator {
* @since v1.53
*/
Locator describe(String description);
/**
* Returns locator description previously set with {@link com.microsoft.playwright.Locator#describe Locator.describe()}.
* Returns {@code null} if no custom description has been set. Prefer {@code Locator.toString()} for a human-readable
* representation, as it uses the description when available.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator button = page.getByRole(AriaRole.BUTTON).describe("Subscribe button");
* System.out.println(button.description()); // "Subscribe button"
*
* Locator input = page.getByRole(AriaRole.TEXTBOX);
* System.out.println(input.description()); // null
* }</pre>
*
* @since v1.57
*/
String description();
/**
* Programmatically dispatch an event on the matching element.
*
@@ -127,12 +127,16 @@ public interface Mouse {
}
class MoveOptions {
/**
* Defaults to 1. Sends intermediate {@code mousemove} events.
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public Integer steps;
/**
* Defaults to 1. Sends intermediate {@code mousemove} events.
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between Playwright's current
* cursor position and the provided destination. When set to 1, emits a single {@code mousemove} event at the destination
* location.
*/
public MoveOptions setSteps(int steps) {
this.steps = steps;
@@ -860,6 +860,11 @@ public interface Page extends AutoCloseable {
* specified, some visible point of the element is used.
*/
public Position sourcePosition;
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public Integer steps;
/**
* When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
* element, the call throws an exception.
@@ -914,6 +919,14 @@ public interface Page extends AutoCloseable {
this.sourcePosition = sourcePosition;
return this;
}
/**
* Defaults to 1. Sends {@code n} interpolated {@code mousemove} events to represent travel between the {@code mousedown}
* and {@code mouseup} of the drag. When set to 1, emits a single {@code mousemove} event at the destination location.
*/
public DragAndDropOptions setSteps(int steps) {
this.steps = steps;
return this;
}
/**
* When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
* element, the call throws an exception.
@@ -5729,6 +5742,20 @@ public interface Page extends AutoCloseable {
* @since v1.8
*/
Keyboard keyboard();
/**
* Returns up to (currently) 200 last console messages from this page. See {@link
* com.microsoft.playwright.Page#onConsoleMessage Page.onConsoleMessage()} for more details.
*
* @since v1.56
*/
List<ConsoleMessage> consoleMessages();
/**
* Returns up to (currently) 200 last page errors from this page. See {@link com.microsoft.playwright.Page#onPageError
* Page.onPageError()} for more details.
*
* @since v1.56
*/
List<String> pageErrors();
/**
* The method returns an element locator that can be used to perform actions on this page / frame. Locator is resolved to
* the element immediately before performing an action, so a series of actions on the same locator can in fact be performed
@@ -5812,8 +5839,8 @@ public interface Page extends AutoCloseable {
*/
Page opener();
/**
* Pauses script execution. Playwright will stop executing the script and wait for the user to either press 'Resume' button
* in the page overlay or to call {@code playwright.resume()} in the DevTools console.
* Pauses script execution. Playwright will stop executing the script and wait for the user to either press the 'Resume'
* button in the page overlay or to call {@code playwright.resume()} in the DevTools console.
*
* <p> User can inspect selectors or perform manual steps while paused. Resume will continue running the original script from
* the place it was paused.
@@ -6053,6 +6080,21 @@ public interface Page extends AutoCloseable {
* @since v1.9
*/
List<ElementHandle> querySelectorAll(String selector);
/**
* Returns up to (currently) 100 last network request from this page. See {@link com.microsoft.playwright.Page#onRequest
* Page.onRequest()} for more details.
*
* <p> Returned requests should be accessed immediately, otherwise they might be collected to prevent unbounded memory growth
* as new requests come in. Once collected, retrieving most information about the request is impossible.
*
* <p> Note that requests reported through the {@link com.microsoft.playwright.Page#onRequest Page.onRequest()} request are not
* collected, so there is a trade off between efficient memory usage with {@link com.microsoft.playwright.Page#requests
* Page.requests()} and the amount of available information reported through {@link com.microsoft.playwright.Page#onRequest
* Page.onRequest()}.
*
* @since v1.56
*/
List<Request> requests();
/**
* When testing a web page, sometimes unexpected overlays like a "Sign up" dialog appear and block actions you want to
* automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, making them
@@ -17,6 +17,7 @@
package com.microsoft.playwright;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* The Worker class represents a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">WebWorker</a>.
@@ -44,6 +45,16 @@ public interface Worker {
*/
void offClose(Consumer<Worker> handler);
/**
* Emitted when JavaScript within the worker calls one of console API methods, e.g. {@code console.log} or {@code
* console.dir}.
*/
void onConsole(Consumer<ConsoleMessage> handler);
/**
* Removes handler that was previously added with {@link #onConsole onConsole(handler)}.
*/
void offConsole(Consumer<ConsoleMessage> handler);
class WaitForCloseOptions {
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
@@ -62,6 +73,35 @@ public interface Worker {
return this;
}
}
class WaitForConsoleMessageOptions {
/**
* Receives the {@code ConsoleMessage} object and resolves to true when the waiting should resolve.
*/
public Predicate<ConsoleMessage> predicate;
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
* BrowserContext.setDefaultTimeout()}.
*/
public Double timeout;
/**
* Receives the {@code ConsoleMessage} object and resolves to true when the waiting should resolve.
*/
public WaitForConsoleMessageOptions setPredicate(Predicate<ConsoleMessage> predicate) {
this.predicate = predicate;
return this;
}
/**
* Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
* default value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
* BrowserContext.setDefaultTimeout()}.
*/
public WaitForConsoleMessageOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
/**
* Returns the return value of {@code expression}.
*
@@ -158,5 +198,21 @@ public interface Worker {
* @since v1.10
*/
Worker waitForClose(WaitForCloseOptions options, Runnable callback);
/**
* Performs action and waits for a console message.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.57
*/
default ConsoleMessage waitForConsoleMessage(Runnable callback) {
return waitForConsoleMessage(null, callback);
}
/**
* Performs action and waits for a console message.
*
* @param callback Callback that performs the action triggering the event.
* @since v1.57
*/
ConsoleMessage waitForConsoleMessage(WaitForConsoleMessageOptions options, Runnable callback);
}
@@ -62,12 +62,15 @@ abstract class AssertionsBase {
if (!log.isEmpty()) {
log = "\nCall log:\n" + log;
}
if (result.errorMessage != null) {
message += "\n" + result.errorMessage;
}
if (expected == null) {
throw new AssertionFailedError(message + log);
}
ValueWrapper expectedValue = formatValue(expected);
ValueWrapper actualValue = formatValue(actual);
message += ": " + expectedValue.getStringRepresentation() + "\nReceived: " + actualValue.getStringRepresentation() + "\n";
message += "\nExpected: " + expectedValue.getStringRepresentation() + "\nReceived: " + actualValue.getStringRepresentation() + "\n";
throw new AssertionFailedError(message + log, expectedValue, actualValue);
}
}
@@ -34,8 +34,7 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes;
@@ -47,7 +46,6 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
private final APIRequestContextImpl request;
private final ClockImpl clock;
final List<PageImpl> pages = new ArrayList<>();
final List<PageImpl> backgroundPages = new ArrayList<>();
final Router routes = new Router();
final WebSocketRouter webSocketRoutes = new WebSocketRouter();
@@ -82,7 +80,6 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
enum EventType {
BACKGROUNDPAGE,
CLOSE,
CONSOLE,
DIALOG,
@@ -134,12 +131,10 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public void onBackgroundPage(Consumer<Page> handler) {
listeners.add(EventType.BACKGROUNDPAGE, handler);
}
@Override
public void offBackgroundPage(Consumer<Page> handler) {
listeners.remove(EventType.BACKGROUNDPAGE, handler);
}
@Override
@@ -341,7 +336,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public List<Page> backgroundPages() {
return new ArrayList<>(backgroundPages);
return Collections.emptyList();
}
@Override
@@ -720,10 +715,6 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
if (page.opener() != null && !page.opener().isClosed()) {
page.opener().notifyPopup(page);
}
} else if ("backgroundPage".equals(event)) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
backgroundPages.add(page);
listeners.notify(EventType.BACKGROUNDPAGE, page);
} else if ("bindingCall".equals(event)) {
BindingCall bindingCall = connection.getExistingObject(params.getAsJsonObject("binding").get("guid").getAsString());
BindingCallback binding = bindings.get(bindingCall.name());
@@ -731,9 +722,19 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
bindingCall.call(binding);
}
} else if ("console".equals(event)) {
ConsoleMessageImpl message = new ConsoleMessageImpl(connection, params);
PageImpl page = null;
if (params.has("page")) {
page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
}
WorkerImpl worker = null;
if (params.has("worker")) {
worker = connection.getExistingObject(params.getAsJsonObject("worker").get("guid").getAsString());
}
ConsoleMessageImpl message = new ConsoleMessageImpl(connection, params, page, worker);
listeners.notify(BrowserContextImpl.EventType.CONSOLE, message);
PageImpl page = message.page();
if (worker != null) {
worker.listeners.notify(WorkerImpl.EventType.CONSOLE, message);
}
if (page != null) {
page.listeners.notify(PageImpl.EventType.CONSOLE, message);
}
@@ -781,14 +782,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
page.listeners.notify(PageImpl.EventType.RESPONSE, response);
}
} else if ("pageError".equals(event)) {
SerializedError error = gson().fromJson(params.getAsJsonObject("error"), SerializedError.class);
String errorStr = "";
if (error.error != null) {
errorStr = error.error.name + ": " + error.error.message;
if (error.error.stack != null && !error.error.stack.isEmpty()) {
errorStr += "\n" + error.error.stack;
}
}
String errorStr = parseError(params.getAsJsonObject("error"));
PageImpl page;
try {
page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
@@ -20,23 +20,21 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.ConsoleMessage;
import com.microsoft.playwright.JSHandle;
import com.microsoft.playwright.Worker;
import java.util.ArrayList;
import java.util.List;
public class ConsoleMessageImpl implements ConsoleMessage {
private final Connection connection;
private PageImpl page;
private final PageImpl page;
private final WorkerImpl worker;
private final JsonObject initializer;
public ConsoleMessageImpl(Connection connection, JsonObject initializer) {
public ConsoleMessageImpl(Connection connection, JsonObject initializer, PageImpl page, WorkerImpl worker) {
this.connection = connection;
// Note: currently, we only report console messages for pages and they always have a page.
// However, in the future we might report console messages for service workers or something else,
// where page() would be null.
if (initializer.has("page")) {
page = connection.getExistingObject(initializer.getAsJsonObject("page").get("guid").getAsString());
}
this.page = page;
this.worker = worker;
this.initializer = initializer;
}
@@ -44,6 +42,11 @@ public class ConsoleMessageImpl implements ConsoleMessage {
return initializer.get("type").getAsString();
}
@Override
public Worker worker() {
return worker;
}
public String text() {
return initializer.get("text").getAsString();
}
@@ -229,15 +229,18 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void click(String selector, ClickOptions options) {
clickImpl(selector, options);
clickImpl(selector, options, null);
}
void clickImpl(String selector, ClickOptions options) {
void clickImpl(String selector, ClickOptions options, Integer steps) {
if (options == null) {
options = new ClickOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
if (steps != null) {
params.addProperty("steps", steps);
}
sendMessage("click", params, timeout(options.timeout));
}
@@ -248,11 +251,18 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void dblclick(String selector, DblclickOptions options) {
dblclickImpl(selector, options, null);
}
void dblclickImpl(String selector, DblclickOptions options, Integer steps) {
if (options == null) {
options = new DblclickOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
if (steps != null) {
params.addProperty("steps", steps);
}
sendMessage("dblclick", params, timeout(options.timeout));
}
@@ -440,16 +450,19 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void dragAndDrop(String source, String target, DragAndDropOptions options) {
dragAndDropImpl(source, target, options);
dragAndDropImpl(source, target, options, null);
}
void dragAndDropImpl(String source, String target, DragAndDropOptions options) {
void dragAndDropImpl(String source, String target, DragAndDropOptions options, Integer steps) {
if (options == null) {
options = new DragAndDropOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("source", source);
params.addProperty("target", target);
if (steps != null) {
params.addProperty("steps", steps);
}
sendMessage("dragAndDrop", params, timeout(options.timeout));
}
@@ -627,7 +640,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
params.addProperty("key", key);
sendMessage("press", params, timeout(options.timeout));
}
@Override
public List<String> selectOption(String selector, SelectOption[] values, SelectOptionOptions options) {
return selectOptionImpl(selector, values, options);
@@ -16,7 +16,6 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
@@ -25,6 +24,7 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.LocatorUtils.*;
@@ -161,7 +161,7 @@ class LocatorImpl implements Locator {
if (options == null) {
options = new ClickOptions();
}
frame.click(selector, convertType(options, Frame.ClickOptions.class).setStrict(true));
frame.clickImpl(selector, convertType(options, Frame.ClickOptions.class).setStrict(true), options.steps);
}
@Override
@@ -174,12 +174,33 @@ class LocatorImpl implements Locator {
return locator(describeSelector(description));
}
@Override
public String description() {
// Match internal:describe= at the end of the selector with a JSON string
// Pattern matches: >> internal:describe="..." where ... is a JSON-encoded string
Pattern pattern = Pattern.compile(" >> internal:describe=(\"(?:[^\"\\\\]|\\\\.)*\")$");
Matcher matcher = pattern.matcher(selector);
if (matcher.find()) {
String jsonString = matcher.group(1);
try {
// Deserialize the JSON string
return gson().fromJson(jsonString, String.class);
} catch (Exception e) {
// If we can't parse it, return null
return null;
}
}
return null;
}
@Override
public void dblclick(DblclickOptions options) {
if (options == null) {
options = new DblclickOptions();
}
frame.dblclick(selector, convertType(options, Frame.DblclickOptions.class).setStrict(true));
frame.dblclickImpl(selector, convertType(options, Frame.DblclickOptions.class).setStrict(true), options.steps);
}
@Override
@@ -197,7 +218,7 @@ class LocatorImpl implements Locator {
}
Frame.DragAndDropOptions frameOptions = convertType(options, Frame.DragAndDropOptions.class);
frameOptions.setStrict(true);
frame.dragAndDrop(selector, ((LocatorImpl) target).selector, frameOptions);
frame.dragAndDropImpl(selector, ((LocatorImpl) target).selector, frameOptions, options.steps);
}
@Override
@@ -627,6 +648,10 @@ class LocatorImpl implements Locator {
@Override
public String toString() {
String description = description();
if (description != null) {
return description;
}
return "Locator@" + selector;
}
@@ -31,6 +31,7 @@ import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Serialization.parseError;
import static com.microsoft.playwright.impl.Utils.*;
import static com.microsoft.playwright.options.ScreenshotType.JPEG;
import static com.microsoft.playwright.options.ScreenshotType.PNG;
@@ -236,7 +237,6 @@ public class PageImpl extends ChannelOwner implements Page {
void didClose() {
isClosed = true;
browserContext.pages.remove(this);
browserContext.backgroundPages.remove(this);
listeners.notify(EventType.CLOSE, this);
}
@@ -567,6 +567,17 @@ public class PageImpl extends ChannelOwner implements Page {
return mainFrame.querySelectorAll(selector);
}
@Override
public List<Request> requests() {
JsonObject json = sendMessage("requests", new JsonObject(), NO_TIMEOUT).getAsJsonObject();
JsonArray requests = json.getAsJsonArray("requests");
List<Request> result = new ArrayList<>();
for (JsonElement item : requests) {
result.add(connection.getExistingObject(item.getAsJsonObject().get("guid").getAsString()));
}
return result;
}
@Override
public void addLocatorHandler(Locator locator, Consumer<Locator> handler, AddLocatorHandlerOptions options) {
LocatorImpl locatorImpl = (LocatorImpl) locator;
@@ -677,7 +688,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void click(String selector, ClickOptions options) {
mainFrame.clickImpl(selector, convertType(options, Frame.ClickOptions.class));
mainFrame.clickImpl(selector, convertType(options, Frame.ClickOptions.class), null);
}
@Override
@@ -692,7 +703,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void dblclick(String selector, DblclickOptions options) {
mainFrame.dblclick(selector, convertType(options, Frame.DblclickOptions.class));
mainFrame.dblclickImpl(selector, convertType(options, Frame.DblclickOptions.class), null);
}
@Override
@@ -925,7 +936,10 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void dragAndDrop(String source, String target, DragAndDropOptions options) {
mainFrame.dragAndDropImpl(source, target, convertType(options, Frame.DragAndDropOptions.class));
if (options == null) {
options = new DragAndDropOptions();
}
mainFrame.dragAndDropImpl(source, target, convertType(options, Frame.DragAndDropOptions.class), options.steps);
}
@Override
@@ -983,6 +997,29 @@ public class PageImpl extends ChannelOwner implements Page {
return keyboard;
}
@Override
public List<ConsoleMessage> consoleMessages() {
JsonObject json = sendMessage("consoleMessages", new JsonObject(), NO_TIMEOUT).getAsJsonObject();
JsonArray messages = json.getAsJsonArray("messages");
List<ConsoleMessage> result = new ArrayList<>();
for (JsonElement item : messages) {
result.add(new ConsoleMessageImpl(connection, item.getAsJsonObject(), this, null));
}
return result;
}
@Override
public List<String> pageErrors() {
JsonObject json = sendMessage("pageErrors", new JsonObject(), NO_TIMEOUT).getAsJsonObject();
JsonArray errors = json.getAsJsonArray("errors");
List<String> result = new ArrayList<>();
for (JsonElement item : errors) {
String errorStr = parseError(item.getAsJsonObject());
result.add(errorStr);
}
return result;
}
@Override
public Locator locator(String selector, LocatorOptions options) {
return mainFrame.locator(selector, convertType(options, Frame.LocatorOptions.class));
@@ -1008,15 +1045,16 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void pause() {
Double defaultNavigationTimeout = browserContext.timeoutSettings.defaultNavigationTimeout();
Double defaultTimeout = browserContext.timeoutSettings.defaultTimeout();
browserContext.setDefaultNavigationTimeout(0.0);
browserContext.setDefaultTimeout(0.0);
TimeoutSettings settings = browserContext.timeoutSettings;
Double defaultNavigationTimeout = settings.defaultNavigationTimeout();
Double defaultTimeout = settings.defaultTimeout();
settings.setDefaultNavigationTimeout(0.0);
settings.setDefaultTimeout(0.0);
try {
runUntil(() -> {}, new WaitableRace<>(asList(context().pause(), (Waitable<JsonElement>) waitableClosedOrCrashed)));
} finally {
browserContext.setDefaultNavigationTimeout(defaultNavigationTimeout);
browserContext.setDefaultTimeout(defaultTimeout);
settings.setDefaultNavigationTimeout(defaultNavigationTimeout);
settings.setDefaultTimeout(defaultTimeout);
}
}
@@ -1166,18 +1204,8 @@ public class PageImpl extends ChannelOwner implements Page {
}
}
}
List<Locator> mask = options.mask;
options.mask = null;
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
options.mask = mask;
params.remove("path");
if (mask != null) {
JsonArray maskArray = new JsonArray();
for (Locator locator: mask) {
maskArray.add(((LocatorImpl) locator).toProtocol());
}
params.add("mask", maskArray);
}
JsonObject json = sendMessage("screenshot", params, timeoutSettings.timeout(options.timeout)).getAsJsonObject();
byte[] buffer = Base64.getDecoder().decode(json.get("binary").getAsString());
@@ -1367,9 +1395,12 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void waitForLoadState(LoadState state, WaitForLoadStateOptions options) {
withWaitLogging("Page.waitForLoadState", logger -> {
mainFrame.waitForLoadStateImpl(state, convertType(options, Frame.WaitForLoadStateOptions.class), logger);
return null;
final LoadState loadState = state == null ? LoadState.LOAD : state;
withTitle("Wait for load state \"" + loadState.toString().toLowerCase() + "\"", () -> {
withWaitLogging("Page.waitForLoadState", logger -> {
mainFrame.waitForLoadStateImpl(loadState, convertType(options, Frame.WaitForLoadStateOptions.class), logger);
return null;
});
});
}
@@ -114,6 +114,7 @@ class FrameExpectOptions {
class FrameExpectResult {
boolean matches;
SerializedValue received;
String errorMessage;
List<String> log;
}
@@ -66,6 +66,7 @@ class Serialization {
.registerTypeHierarchyAdapter(JSHandleImpl.class, new HandleSerializer())
.registerTypeAdapter((new TypeToken<Map<String, String>>(){}).getType(), new StringMapSerializer())
.registerTypeAdapter((new TypeToken<Map<String, Object>>(){}).getType(), new FirefoxUserPrefsSerializer())
.registerTypeAdapter(LocatorImpl.class, new LocatorImplSerializer())
.registerTypeHierarchyAdapter(Path.class, new PathSerializer()).create();
static Gson gson() {
@@ -422,6 +423,18 @@ class Serialization {
return result;
}
static String parseError(JsonObject object) {
SerializedError error = gson().fromJson(object, SerializedError.class);
String errorStr = "";
if (error.error != null) {
errorStr = error.error.name + ": " + error.error.message;
if (error.error.stack != null && !error.error.stack.isEmpty()) {
errorStr += "\n" + error.error.stack;
}
}
return errorStr;
}
private static class OptionalSerializer implements JsonSerializer<Optional<?>> {
private static boolean isSupported(Type type) {
return new TypeToken<Optional<Media>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
@@ -490,6 +503,13 @@ class Serialization {
}
}
private static class LocatorImplSerializer implements JsonSerializer<LocatorImpl> {
@Override
public JsonElement serialize(LocatorImpl src, Type typeOfSrc, JsonSerializationContext context) {
return src.toProtocol();
}
}
private static class SameSiteAdapter extends TypeAdapter<SameSiteAttribute> {
@Override
public void write(JsonWriter out, SameSiteAttribute value) throws IOException {
@@ -18,21 +18,25 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.ConsoleMessage;
import com.microsoft.playwright.JSHandle;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Worker;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static com.microsoft.playwright.impl.Serialization.*;
class WorkerImpl extends ChannelOwner implements Worker {
private final ListenerCollection<EventType> listeners = new ListenerCollection<>();
final ListenerCollection<EventType> listeners = new ListenerCollection<>();
PageImpl page;
enum EventType {
CLOSE,
CONSOLE,
}
WorkerImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -49,9 +53,19 @@ class WorkerImpl extends ChannelOwner implements Worker {
listeners.remove(EventType.CLOSE, handler);
}
private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Double timeout) {
@Override
public void onConsole(Consumer<ConsoleMessage> handler) {
listeners.add(EventType.CONSOLE, handler);
}
@Override
public void offConsole(Consumer<ConsoleMessage> handler) {
listeners.remove(EventType.CONSOLE, handler);
}
private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Predicate<T> predicate, Double timeout) {
List<Waitable<T>> waitables = new ArrayList<>();
waitables.add(new WaitableEvent<>(listeners, eventType));
waitables.add(new WaitableEvent<>(listeners, eventType, predicate));
waitables.add(page.createWaitForCloseHelper());
waitables.add(page.createWaitableTimeout(timeout));
return runUntil(code, new WaitableRace<>(waitables));
@@ -62,11 +76,23 @@ class WorkerImpl extends ChannelOwner implements Worker {
return withWaitLogging("Worker.waitForClose", logger -> waitForCloseImpl(options, code));
}
@Override
public ConsoleMessage waitForConsoleMessage(WaitForConsoleMessageOptions options, Runnable code) {
return withWaitLogging("Worker.waitForConsoleMessage", logger -> waitForConsoleMessageImpl(options, code));
}
private ConsoleMessage waitForConsoleMessageImpl(WaitForConsoleMessageOptions options, Runnable code) {
if (options == null) {
options = new WaitForConsoleMessageOptions();
}
return waitForEventWithTimeout(EventType.CONSOLE, code, options.predicate, options.timeout);
}
private Worker waitForCloseImpl(WaitForCloseOptions options, Runnable code) {
if (options == null) {
options = new WaitForCloseOptions();
}
return waitForEventWithTimeout(EventType.CLOSE, code, options.timeout);
return waitForEventWithTimeout(EventType.CLOSE, code, null, options.timeout);
}
@Override
@@ -36,7 +36,7 @@ public class PlaywrightExtension implements ParameterResolver {
// There should be at most one instance of PlaywrightRegistry per test run, it keeps
// track of all created Playwright instances and calls `close()` on each of them after
// the tests finished.
static class PlaywrightRegistry implements ExtensionContext.Store.CloseableResource {
static class PlaywrightRegistry implements AutoCloseable {
private final List<Playwright> playwrightList = Collections.synchronizedList(new ArrayList<>());
static synchronized PlaywrightRegistry getOrCreateFor(ExtensionContext extensionContext) {
@@ -59,7 +59,7 @@ public class PlaywrightExtension implements ParameterResolver {
// This is a workaround for JUnit's lack of an "AfterTestRun" hook
// This will be called once after all tests have completed.
@Override
public void close() throws Throwable {
public void close() {
for (Playwright playwright : playwrightList) {
playwright.close();
}
@@ -36,7 +36,7 @@ package com.microsoft.playwright.junit;
* public Options getOptions() {
* return new Options()
* .setHeadless(false)
* .setContextOption(new Browser.NewContextOptions()
* .setContextOptions(new Browser.NewContextOptions()
* .setBaseURL("https://github.com"))
* .setApiRequestOptions(new APIRequest.NewContextOptions()
* .setBaseURL("https://playwright.dev"));
@@ -20,16 +20,16 @@ public class Cookie {
public String name;
public String value;
/**
* Either url or domain / path are required. Optional.
* Either {@code url} or both {@code domain} and {@code path} are required. Optional.
*/
public String url;
/**
* For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url or
* domain / path are required. Optional.
* For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either {@code
* url} or both {@code domain} and {@code path} are required. Optional.
*/
public String domain;
/**
* Either url or domain / path are required Optional.
* Either {@code url} or both {@code domain} and {@code path} are required. Optional.
*/
public String path;
/**
@@ -60,22 +60,22 @@ public class Cookie {
this.value = value;
}
/**
* Either url or domain / path are required. Optional.
* Either {@code url} or both {@code domain} and {@code path} are required. Optional.
*/
public Cookie setUrl(String url) {
this.url = url;
return this;
}
/**
* For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url or
* domain / path are required. Optional.
* For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either {@code
* url} or both {@code domain} and {@code path} are required. Optional.
*/
public Cookie setDomain(String domain) {
this.domain = domain;
return this;
}
/**
* Either url or domain / path are required Optional.
* Either {@code url} or both {@code domain} and {@code path} are required. Optional.
*/
public Cookie setPath(String path) {
this.path = path;
@@ -23,6 +23,7 @@ import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.zip.GZIPOutputStream;
import static com.microsoft.playwright.Utils.copy;
@@ -40,6 +41,7 @@ public class Server implements HttpHandler {
private final Map<String, String> csp = Collections.synchronizedMap(new HashMap<>());
private final Map<String, HttpHandler> routes = Collections.synchronizedMap(new HashMap<>());
private final Set<String> gzipRoutes = Collections.synchronizedSet(new HashSet<>());
private Function<String, InputStream> resourceProvider;
private static class Auth {
public final String user;
@@ -75,6 +77,8 @@ public class Server implements HttpHandler {
server.createContext("/", this);
server.setExecutor(null); // creates a default executor
server.start();
// Resources from "src/test/resources/" are copied to "resources/" directory in the jar.
resourceProvider = path -> Server.class.getClassLoader().getResourceAsStream("resources" + path);
}
public void stop() {
@@ -93,6 +97,10 @@ public class Server implements HttpHandler {
gzipRoutes.add(path);
}
void setResourceProvider(Function<String, InputStream> resourceProvider) {
this.resourceProvider = resourceProvider;
}
static class Request {
public final String url;
public final String method;
@@ -187,18 +195,16 @@ public class Server implements HttpHandler {
path = "/index.html";
}
// Resources from "src/test/resources/" are copied to "resources/" directory in the jar.
String resourcePath = "resources" + path;
InputStream resource = getClass().getClassLoader().getResourceAsStream(resourcePath);
InputStream resource = resourceProvider.apply(path);
if (resource == null) {
exchange.getResponseHeaders().add("Content-Type", "text/plain");
exchange.sendResponseHeaders(404, 0);
try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("File not found: " + resourcePath);
writer.write("File not found: " + path);
}
return;
}
exchange.getResponseHeaders().add("Content-Type", mimeType(new File(resourcePath)));
exchange.getResponseHeaders().add("Content-Type", mimeType(new File(path)));
ByteArrayOutputStream body = new ByteArrayOutputStream();
OutputStream output = body;
if (gzipRoutes.contains(path)) {
@@ -45,12 +45,12 @@ public class TestBase {
static final boolean isMac = Utils.getOS() == Utils.OS.MAC;
static final boolean isLinux = Utils.getOS() == Utils.OS.LINUX;
static final boolean isWindows = Utils.getOS() == Utils.OS.WINDOWS;
static final boolean headful;
static final boolean headed;
static final SameSiteAttribute defaultSameSiteCookieValue;
static {
String headfulEnv = System.getenv("HEADFUL");
headful = headfulEnv != null && !"0".equals(headfulEnv) && !"false".equals(headfulEnv);
String headedEnv = System.getenv("HEADED");
headed = headedEnv != null && !"0".equals(headedEnv) && !"false".equals(headedEnv);
defaultSameSiteCookieValue = initSameSiteAttribute();
}
@@ -58,8 +58,8 @@ public class TestBase {
Page page;
BrowserContext context;
static boolean isHeadful() {
return headful;
static boolean isHeaded() {
return headed;
}
static boolean isChromium() {
@@ -81,7 +81,7 @@ public class TestBase {
static BrowserType.LaunchOptions createLaunchOptions() {
BrowserType.LaunchOptions options;
options = new BrowserType.LaunchOptions();
options.headless = !headful;
options.headless = !headed;
options.channel = getBrowserChannelFromEnv();
return options;
}
@@ -27,7 +27,7 @@ public class TestBrowserContextCredentials extends TestBase {
static boolean isChromiumHeadedLike() {
// --headless=new, the default in all Chromium channels, is like headless.
return isChromium() && (isHeadful() || getBrowserChannelFromEnv() != null);
return isChromium() && (isHeaded() || getBrowserChannelFromEnv() != null);
}
@Test
@@ -129,12 +129,12 @@ public class TestBrowserContextProxy extends TestBase {
context.close();
}
static boolean isChromiumHeadful() {
return isChromium() && isHeadful();
static boolean isChromiumHeaded() {
return isChromium() && isHeaded();
}
@Test
@DisabledIf(value="isChromiumHeadful", disabledReason="fixme")
@DisabledIf(value="isChromiumHeaded", disabledReason="fixme")
void shouldExcludePatterns() {
server.setRoute("/target.html", exchange -> {
exchange.sendResponseHeaders(200, 0);
@@ -128,7 +128,7 @@ public class TestDefaultBrowserContext2 extends TestBase {
@Test
void shouldSupportExtraHTTPHeadersOption() throws ExecutionException, InterruptedException {
// TODO: test.flaky(browserName === "firefox" && headful && platform === "linux", "Intermittent timeout on bots");
// TODO: test.flaky(browserName === "firefox" && headed && platform === "linux", "Intermittent timeout on bots");
Page page = launchPersistent(new BrowserType.LaunchPersistentContextOptions().setExtraHTTPHeaders(mapOf("foo", "bar")));
Future<Server.Request> request = server.futureRequest("/empty.html");
page.navigate(server.EMPTY_PAGE);
@@ -93,18 +93,12 @@ public class TestDownload extends TestBase {
assertTrue(Files.exists(path));
byte[] bytes = readAllBytes(path);
assertEquals("Hello world", new String(bytes, UTF_8));
if (isChromium()) {
assertNotNull(error[0]);
assertTrue(error[0].getMessage().contains("net::ERR_ABORTED"));
assertEquals("about:blank", page.url());
} else if (isWebKit()) {
assertNotNull(error[0]);
assertTrue(error[0].getMessage().contains("Download is starting"));
assertEquals("about:blank", page.url());
} else {
assertNotNull(error[0]);
assertNotNull(error[0]);
if (!chromiumVersionLessThan(browser.version(), "140.0.0.0")) {
assertTrue(error[0].getMessage().contains("Download is starting"));
}
if (!isFirefox())
assertEquals("about:blank", page.url());
page.close();
}
@@ -347,14 +341,14 @@ public class TestDownload extends TestBase {
}
static boolean isChromiumHeadful() {
return isChromium() && isHeadful();
static boolean isChromiumHeaded() {
return isChromium() && isHeaded();
}
@Test
@DisabledIf(value="isChromiumHeadful", disabledReason="fixme")
@DisabledIf(value="isChromiumHeaded", disabledReason="fixme")
void shouldReportNewWindowDownloads() throws IOException {
// TODO: - the test fails in headful Chromium as the popup page gets closed along
// TODO: - the test fails in headed Chromium as the popup page gets closed along
// with the session before download completed event arrives.
// - WebKit doesn't close the popup page
Page page = browser.newPage(new Browser.NewPageOptions().setAcceptDownloads(true));
@@ -26,12 +26,12 @@ import static org.junit.jupiter.api.Assertions.*;
public class TestElementHandleBoundingBox extends TestBase {
static boolean isFirefoxHeadful() {
return isFirefox() && isHeadful();
static boolean isFirefoxHeaded() {
return isFirefox() && isHeaded();
}
@Test
@DisabledIf(value="isFirefoxHeadful", disabledReason="fail")
@DisabledIf(value="isFirefoxHeaded", disabledReason="fail")
void shouldWork() {
page.setViewportSize(500, 500);
page.navigate(server.PREFIX + "/grid.html");
@@ -0,0 +1,46 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestExpectMisc extends TestBase {
@Test
void strictModeViolationErrorFormat() {
page.setContent(" <div>hello</div><div>hi</div>");
AssertionFailedError error = assertThrows(AssertionFailedError.class, () ->
assertThat(page.locator("div")).isVisible());
assertTrue(error.getMessage().contains("Locator expected to be visible"), error.getMessage());
assertTrue(error.getMessage().contains("Error: strict mode violation: locator(\"div\") resolved to 2 elements:"), error.getMessage());
}
@Test
void invalidSelectorErrorFormat() {
page.setContent("<div>hello</div><div>hi</div>");
AssertionFailedError error = assertThrows(AssertionFailedError.class, () ->
assertThat(page.locator("##")).isVisible());
assertTrue(error.getMessage().contains("Locator expected to be visible"), error.getMessage());
assertTrue(error.getMessage().contains("Error: Unexpected token \"#\" while parsing css selector \"##\"."), error.getMessage());
}
}
@@ -1,88 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import static com.microsoft.playwright.Utils.mapOf;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
public class TestLaunch extends TestBase {
@Override
@BeforeAll
// Hide base class method to not launch browser.
void launchBrowser() {
}
@Override
void createContextAndPage() {
// Do nothing
}
@Test
void passEnvVar() {
BrowserType.LaunchOptions options = new BrowserType.LaunchOptions();
options.setEnv(mapOf("DEBUG", "pw:protocol"));
launchBrowser(options);
}
public static boolean canRunHeaded() {
// On linux headed browser requires xvfb.
return isHeadful() || isMac || isWindows;
}
public static boolean canRunExtensionTest() {
return canRunHeaded() && isChromium();
}
@Test
@EnabledIf(value="com.microsoft.playwright.TestLaunch#canRunExtensionTest", disabledReason="Only Chromium Headed")
void shouldReturnBackgroundPages(@TempDir Path tmpDir) throws IOException {
Path profileDir = tmpDir.resolve("profile");
Files.createDirectories(profileDir);
String extensionPath = Paths.get("src/test/resources/simple-extension").toAbsolutePath().toString();
initBrowserType();
BrowserContext context = browserType.launchPersistentContext(profileDir, new BrowserType.LaunchPersistentContextOptions()
.setHeadless(false)
.setArgs(asList(
"--disable-extensions-except=" + extensionPath,
"--load-extension=" + extensionPath
)));
List<Page> backgroundPages = context.backgroundPages();
context.onBackgroundPage(page1 -> backgroundPages.add(page1));
context.waitForCondition(() -> !backgroundPages.isEmpty(),
new BrowserContext.WaitForConditionOptions().setTimeout(10_000));
Page backgroundPage = backgroundPages.get(0);
assertNotNull(backgroundPage);
assertTrue(context.backgroundPages().contains(backgroundPage));
assertFalse(context.pages().contains(backgroundPage));
context.close();
assertEquals(0, context.pages().size());
assertEquals(0, context.backgroundPages().size());
}
}
@@ -223,7 +223,7 @@ public class TestLocatorAssertions extends TestBase {
});
assertEquals("[Text 1, Text 3, Extra]", e.getExpected().getStringRepresentation());
assertEquals("[Text 1, Text 3]", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have text: [Text 1, Text 3, Extra]"), e.getMessage());
assertTrue(e.getMessage().contains("Locator expected to have text\nExpected: [Text 1, Text 3, Extra]"), e.getMessage());
assertTrue(e.getMessage().contains("Received: [Text 1, Text 3]"), e.getMessage());
}
@@ -272,7 +272,7 @@ public class TestLocatorAssertions extends TestBase {
});
assertEquals("foo", e.getExpected().getStringRepresentation());
assertEquals("node", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have attribute 'id': foo\nReceived: node"), e.getMessage());
assertTrue(e.getMessage().contains("Locator expected to have attribute 'id'\nExpected: foo\nReceived: node"), e.getMessage());
}
@Test
@@ -291,7 +291,7 @@ public class TestLocatorAssertions extends TestBase {
});
assertEquals(".Nod..", e.getExpected().getStringRepresentation());
assertEquals("node", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have attribute 'id' matching regex: .Nod..\nReceived: node"), e.getMessage());
assertTrue(e.getMessage().contains("Locator expected to have attribute 'id' matching regex\nExpected: .Nod..\nReceived: node"), e.getMessage());
}
@Test
@@ -629,7 +629,7 @@ public class TestLocatorAssertions extends TestBase {
" </select>");
Locator locator = page.locator("select");
locator.selectOption(new String[] {"B"});
PlaywrightException e = assertThrows(PlaywrightException.class, () -> {
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> {
assertThat(locator).hasValues(new Pattern[]{ Pattern.compile("R"), Pattern.compile("G")});
});
assertTrue(e.getMessage().contains("Not a select element with a multiple attribute"), e.getMessage());
@@ -639,7 +639,7 @@ public class TestLocatorAssertions extends TestBase {
void hasValuesFailsWhenNotASelectElement() {
page.setContent("<input value=\"foo\" />");
Locator locator = page.locator("input");
PlaywrightException e = assertThrows(PlaywrightException.class, () -> {
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> {
assertThat(locator).hasValues(new Pattern[]{ Pattern.compile("R"), Pattern.compile("G")}, new LocatorAssertions.HasValuesOptions().setTimeout(1000));
});
assertTrue(e.getMessage().contains("Not a select element with a multiple attribute"), e.getMessage());
@@ -661,7 +661,7 @@ public class TestLocatorAssertions extends TestBase {
});
assertEquals("checked", e.getExpected().getStringRepresentation());
assertEquals("unchecked", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to be: checked"), e.getMessage());
assertTrue(e.getMessage().contains("Locator expected to be\nExpected: checked"), e.getMessage());
}
@Test
@@ -674,7 +674,7 @@ public class TestLocatorAssertions extends TestBase {
assertEquals("checked", e.getExpected().getStringRepresentation());
assertEquals("checked", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected not to be: checked"), e.getMessage());
assertTrue(e.getMessage().contains("Locator expected not to be\nExpected: checked"), e.getMessage());
}
@Test
@@ -690,7 +690,7 @@ public class TestLocatorAssertions extends TestBase {
Locator locator = page.locator("input");
AssertionFailedError error = assertThrows(AssertionFailedError.class,
() -> assertThat(locator).isChecked(new LocatorAssertions.IsCheckedOptions().setChecked(false).setTimeout(1000)));
assertTrue(error.getMessage().contains("Locator expected to be: unchecked"), error.getMessage());
assertTrue(error.getMessage().contains("Locator expected to be\nExpected: unchecked"), error.getMessage());
}
@Test
@@ -796,7 +796,7 @@ public class TestLocatorAssertions extends TestBase {
void isEditableThrowsOnNonInputElement() {
page.setContent("<button>");
Locator locator = page.locator("button");
PlaywrightException e = assertThrows(PlaywrightException.class, () -> assertThat(locator).isEditable());
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> assertThat(locator).isEditable());
assertTrue(e.getMessage().contains("Element is not an <input>, <textarea>, <select> or [contenteditable] and does not have a role allowing [aria-readonly]"), e.getMessage());
}
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -206,7 +206,7 @@ public class TestLocatorAssertions2 extends TestBase {
page.setContent("<input type=checkbox></input>");
page.locator("input").evaluate("e => e.indeterminate = true");
Locator locator = page.locator("input");
PlaywrightException e = assertThrows(PlaywrightException.class, () ->
AssertionFailedError e = assertThrows(AssertionFailedError.class, () ->
assertThat(locator).isChecked(new LocatorAssertions.IsCheckedOptions().setIndeterminate(true).setChecked(false)));
assertTrue(e.getMessage().contains("Can't assert indeterminate and checked at the same time"), e.getMessage());
}
@@ -220,4 +220,5 @@ public class TestLocatorAssertions2 extends TestBase {
// TODO: should be "assertThat().isChecked() with timeout 1000ms"
assertTrue(e.getMessage().contains("Assert \"isChecked\" with timeout 1000ms"), e.getMessage());
}
}
@@ -65,4 +65,41 @@ public class TestLocatorClick extends TestBase {
page.getByText("Go").click(new Locator.ClickOptions().setModifiers(asList(KeyboardModifier.CONTROLORMETA))));
assertThat(newPage).hasURL(server.PREFIX + "/title.html");
}
@Test
void shouldClickWithTweenedMouseMovement() {
page.setContent(
"<body style=\"margin: 0; padding: 0; height: 500px; width: 500px;\">\n" +
" <div style=\"position: relative; top: 280px; left: 150px; width: 100px; height: 40px\">Click me</div>\n" +
"</body>"
);
// The test becomes flaky on WebKit without next line.
if (isWebKit()) {
page.evaluate("() => new Promise(requestAnimationFrame)");
}
page.mouse().move(100, 100);
page.evaluate("() => {\n" +
" window['result'] = [];\n" +
" document.addEventListener('mousemove', event => {\n" +
" window['result'].push([event.clientX, event.clientY]);\n" +
" });\n" +
"}");
// Centerpoint at 150 + 100/2, 280 + 40/2 = 200, 300
page.locator("div").click(new Locator.ClickOptions().setSteps(5));
assertEquals(
asList(
asList(120, 140),
asList(140, 180),
asList(160, 220),
asList(180, 260),
asList(200, 300)
),
page.evaluate("result")
);
}
}
@@ -186,4 +186,53 @@ public class TestLocatorConvenience extends TestBase {
page.setContent("<div>A</div><div>B</div><div>C</div>");
assertEquals(asList("A", "B", "C"), page.locator("div").allInnerTexts());
}
@Test
void descriptionShouldReturnNullForUndescribedLocators() {
page.setContent("<div>Hello</div>");
Locator locator = page.locator("div");
assertNull(locator.description());
}
@Test
void descriptionShouldReturnSimpleDescription() {
page.setContent("<div>Hello</div>");
Locator locator = page.locator("div").describe("my div");
assertEquals("my div", locator.description());
}
@Test
void descriptionShouldHandleSpecialCharacters() {
page.setContent("<div>Hello</div>");
Locator locator = page.locator("div").describe("título 😊");
assertEquals("título 😊", locator.description());
}
@Test
void descriptionShouldWorkWithChainedLocators() {
page.setContent("<div><span>Hello</span></div>");
Locator locator = page.locator("div").describe("container").locator("span");
assertNull(locator.description());
}
@Test
void descriptionShouldReturnLastDescriptionForMultipleCalls() {
page.setContent("<div>Hello</div>");
Locator locator = page.locator("div").describe("first").describe("second");
assertEquals("second", locator.description());
}
@Test
void toStringShouldReturnFormattedLocator() {
page.setContent("<div>Hello</div>");
Locator locator = page.locator("div");
assertTrue(locator.toString().startsWith("Locator@"));
}
@Test
void toStringShouldPreferDescription() {
page.setContent("<div>Hello</div>");
Locator locator = page.locator("div").describe("my div");
assertEquals("my div", locator.toString());
}
}
@@ -35,13 +35,13 @@ public class TestOptionsFactories {
public static BrowserType.LaunchOptions createLaunchOptions() {
BrowserType.LaunchOptions options;
options = new BrowserType.LaunchOptions();
options.headless = !getHeadful();
options.headless = !getHeaded();
return options;
}
private static boolean getHeadful() {
String headfulEnv = System.getenv("HEADFUL");
return headfulEnv != null && !"0".equals(headfulEnv) && !"false".equals(headfulEnv);
private static boolean getHeaded() {
String headedEnv = System.getenv("HEADED");
return headedEnv != null && !"0".equals(headedEnv) && !"false".equals(headedEnv);
}
public static String getBrowserName() {
@@ -127,4 +127,13 @@ public class TestPageAriaSnapshot {
" - listitem: \"1.2\"");
});
}
@Test
void matchValuesBothAgainstRegexAndString(Page page) {
page.setContent("<a href=\"/auth?r=/\">Log in</a>");
checkAndMatchSnapshot(page.locator("body"),
"- link \"Log in\":\n" +
" - /url: /auth?r=/");
}
}
@@ -107,7 +107,7 @@ public class TestPageAssertions extends TestBase {
});
assertEquals("foo", e.getExpected().getValue());
assertEquals("Woof-Woof", e.getActual().getValue());
assertTrue(e.getMessage().contains("Page title expected to be: foo\nReceived: Woof-Woof"), e.getMessage());
assertTrue(e.getMessage().contains("Page title expected to be\nExpected: foo\nReceived: Woof-Woof"), e.getMessage());
}
@Test
@@ -124,7 +124,7 @@ public class TestPageAssertions extends TestBase {
});
assertEquals("^foo[AB]", e.getExpected().getStringRepresentation());
assertEquals("Woof-Woof", e.getActual().getValue());
assertTrue(e.getMessage().contains("Page title expected to match regex: ^foo[AB]\nReceived: Woof-Woof"), e.getMessage());
assertTrue(e.getMessage().contains("Page title expected to match regex\nExpected: ^foo[AB]\nReceived: Woof-Woof"), e.getMessage());
}
@Test
@@ -356,4 +356,10 @@ public class TestPageBasic extends TestBase {
assertTrue(e.getMessage().contains("Can't add a null listener"));
}
@Test
void pagePauseShouldNotThrow() {
page.pause();
}
}
@@ -124,4 +124,94 @@ public class TestPageDrag extends TestBase {
page.locator("#source").dragTo(page.locator("#target"));
assertEquals(true, page.evalOnSelector("#target", "target => target.contains(document.querySelector('#source'))"));
}
@Test
void shouldDragAndDropWithTweenedMouseMovement() {
page.setContent(
"<body style='margin: 0; padding: 0;'>\n" +
" <div style='width:100px;height:100px;background:red;' id='red'></div>\n" +
" <div style='width:300px;height:100px;background:blue;' id='blue'></div>\n" +
"</body>"
);
JSHandle eventsHandle = page.evaluateHandle("() => {\n" +
" const events = [];\n" +
" document.addEventListener('mousedown', event => {\n" +
" events.push({ type: 'mousedown', x: event.pageX, y: event.pageY });\n" +
" });\n" +
" document.addEventListener('mouseup', event => {\n" +
" events.push({ type: 'mouseup', x: event.pageX, y: event.pageY });\n" +
" });\n" +
" document.addEventListener('mousemove', event => {\n" +
" events.push({ type: 'mousemove', x: event.pageX, y: event.pageY });\n" +
" });\n" +
" return events;\n" +
"}");
// Red div center is at (50, 50), blue div center is at (150, 50)
// With 4 steps, we expect intermediate positions at (75, 50), (100, 50), (125, 50)
page.dragAndDrop("#red", "#blue", new Page.DragAndDropOptions().setSteps(4));
Object json = eventsHandle.jsonValue();
// Expected sequence: mousemove to (50,50), mousedown at (50,50),
// then 3 mousemove events at (75,50), (100,50), (125,50),
// and mouseup at (150,50)
assertJsonEquals(
"[" +
"{type: \"mousemove\", x: 50, y: 50}," +
"{type: \"mousedown\", x: 50, y: 50}," +
"{type: \"mousemove\", x: 75, y: 75}," +
"{type: \"mousemove\", x: 100, y: 100}," +
"{type: \"mousemove\", x: 125, y: 125}," +
"{type: \"mousemove\", x: 150, y: 150}," +
"{type: \"mouseup\", x: 150, y: 150}" +
"]",
json
);
}
@Test
void shouldDragToWithTweenedMouseMovement() {
page.setContent(
"<body style='margin: 0; padding: 0;'>\n" +
" <div style='width:100px;height:100px;background:red;' id='red'></div>\n" +
" <div style='width:300px;height:100px;background:blue;' id='blue'></div>\n" +
"</body>"
);
JSHandle eventsHandle = page.evaluateHandle("() => {\n" +
" const events = [];\n" +
" document.addEventListener('mousedown', event => {\n" +
" events.push({ type: 'mousedown', x: event.pageX, y: event.pageY });\n" +
" });\n" +
" document.addEventListener('mouseup', event => {\n" +
" events.push({ type: 'mouseup', x: event.pageX, y: event.pageY });\n" +
" });\n" +
" document.addEventListener('mousemove', event => {\n" +
" events.push({ type: 'mousemove', x: event.pageX, y: event.pageY });\n" +
" });\n" +
" return events;\n" +
"}");
// Red div center is at (50, 50), blue div center is at (150, 50)
// With 4 steps, we expect intermediate positions at (75, 50), (100, 50), (125, 50)
page.locator("#red").dragTo(page.locator("#blue"), new Locator.DragToOptions().setSteps(4));
Object json = eventsHandle.jsonValue();
// Expected sequence: mousemove to (50,50), mousedown at (50,50),
// then 3 mousemove events at (75,50), (100,50), (125,50),
// and mouseup at (150,50)
assertJsonEquals(
"[" +
"{type: \"mousemove\", x: 50, y: 50}," +
"{type: \"mousedown\", x: 50, y: 50}," +
"{type: \"mousemove\", x: 75, y: 75}," +
"{type: \"mousemove\", x: 100, y: 100}," +
"{type: \"mousemove\", x: 125, y: 125}," +
"{type: \"mousemove\", x: 150, y: 150}," +
"{type: \"mouseup\", x: 150, y: 150}" +
"]",
json
);
}
}
@@ -103,7 +103,7 @@ public class TestPageEventConsole extends TestBase {
ConsoleMessage message = page.waitForConsoleMessage(() -> {
page.evaluate("async url => fetch(url).catch(e => {})", server.EMPTY_PAGE);
});
assertTrue(message.text().contains("Access-Control-Allow-Origin"));
assertTrue(message.text().contains("Access-Control-Allow-Origin") || message.text().contains("blocked by CORS policy"), message.text());
assertEquals("error", message.type());
}
@@ -130,4 +130,23 @@ public class TestPageEventConsole extends TestBase {
assertEquals("2", message.text());
assertEquals("info", message.type());
}
@Test
void consoleMessagesShouldWork() {
page.evaluate("() => {\n" +
" for (let i = 0; i < 301; i++)\n" +
" console.log('message' + i);\n" +
"}");
List<ConsoleMessage> messages = page.consoleMessages();
assertTrue(messages.size() >= 100, "should be at least 100 messages");
int firstIndex = messages.size() - 100;
for (int i = 0; i < 100; i++) {
ConsoleMessage message = messages.get(firstIndex + i);
assertEquals("message" + (201 + i), message.text());
assertEquals("log", message.type());
assertEquals(page, message.page());
}
}
}
@@ -0,0 +1,47 @@
/*
* 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.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestPageEventPageError extends TestBase {
@Test
void pageErrorsShouldWork() {
page.navigate(server.EMPTY_PAGE);
page.evaluate("async () => {\n" +
" for (let i = 0; i < 301; i++)\n" +
" window.setTimeout(() => { throw new Error('error' + i); }, 0);\n" +
" await new Promise(f => window.setTimeout(f, 100));\n" +
" }");
List<String> errors = page.pageErrors();
assertTrue(errors.size() >= 100, "should be at least 100 errors");
// Check the last 100 errors (indices 201-300)
int firstIndex = errors.size() - 100;
for (int i = 0; i < 100; i++) {
String error = errors.get(firstIndex + i);
assertTrue(error.startsWith("Error: error" + (201 + i)), error);
}
}
}
@@ -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 org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import static org.junit.jupiter.api.Assertions.*;
public class TestPageEventRequest extends TestBase {
@Test
void shouldReturnLastRequests() throws ExecutionException, InterruptedException {
page.navigate(server.PREFIX + "/title.html");
// Set up routes for 200 requests
for (int i = 0; i < 200; ++i) {
final int index = i;
server.setRoute("/fetch-" + i, exchange -> {
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().write(("url:" + server.PREFIX + exchange.getRequestURI().toString()).getBytes());
exchange.getResponseBody().close();
});
}
// #0 is the navigation request, so start with #1.
for (int i = 0; i < 99; ++i) {
page.evaluate("url => fetch(url)", server.PREFIX + "/fetch-" + i);
}
List<Request> first99Requests = page.requests();
first99Requests.remove(0); // Remove the navigation request
for (int i = 99; i < 199; ++i) {
page.evaluate("url => fetch(url)", server.PREFIX + "/fetch-" + i);
}
List<Request> last100Requests = page.requests();
List<Request> allRequests = new ArrayList<>();
allRequests.addAll(first99Requests);
allRequests.addAll(last100Requests);
// All 199 requests are fully functional.
int index = 0;
for (Request request : allRequests) {
Response response = request.response();
assertEquals("url:" + server.PREFIX + "/fetch-" + index, response.text());
assertEquals(server.PREFIX + "/fetch-" + index, request.url());
index++;
}
}
}
@@ -27,7 +27,7 @@ import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.*;
public class TestPageInterception extends TestBase {
public class TestPageInterception extends TestBase {
@Test
void shouldWorkWithNavigationSmoke() {
HashMap<String, Request> requests = new HashMap<>();
@@ -161,6 +161,16 @@ public class TestPageInterception extends TestBase {
assertFalse(globToRegex("http://localhost:3000/signin-oidc*").matcher("http://localhost:3000/signin-oidc/foo").find());
assertTrue(globToRegex("http://localhost:3000/signin-oidc*").matcher("http://localhost:3000/signin-oidcnice").find());
assertTrue(globToRegex("**/*.js").matcher("/foo.js").find());
assertFalse(globToRegex("asd/**.js").matcher("/foo.js").find());
assertFalse(globToRegex("**/*.js").matcher("bar_foo.js").find());
// custom protocols
assertTrue(globToRegex("my.custom.protocol://**").matcher("my.custom.protocol://foo").find());
assertFalse(globToRegex("my.{p,y}://**").matcher("my.p://foo").find());
assertTrue(globToRegex("my.{p,y}://**").matcher("my.p://foo/").find());
assertTrue(globToRegex("f*e://**").matcher("file:///foo/").find());
// range [] is NOT supported
assertTrue(globToRegex("**/api/v[0-9]").matcher("http://example.com/api/v[0-9]").find());
assertFalse(globToRegex("**/api/v[0-9]").matcher("http://example.com/api/version").find());
@@ -186,6 +196,21 @@ public class TestPageInterception extends TestBase {
assertTrue(urlMatches("http://playwright.dev", "http://playwright.dev/?x=y", "?x=y"));
assertTrue(urlMatches("http://playwright.dev/foo/", "http://playwright.dev/foo/bar?x=y", "./bar?x=y"));
// /**/ should match /.
assertTrue(urlMatches(null, "https://foo/bar.js", "https://foo/**/bar.js"));
assertTrue(urlMatches(null, "https://foo/bar.js", "https://foo/**/**/bar.js"));
// Case insensitive matching
assertTrue(urlMatches(null, "https://playwright.dev/fooBAR", "HtTpS://pLaYwRiGhT.dEv/fooBAR"));
assertTrue(urlMatches("http://ignored", "https://playwright.dev/fooBAR", "HtTpS://pLaYwRiGhT.dEv/fooBAR"));
// Path and search query are case-sensitive
assertFalse(urlMatches(null, "https://playwright.dev/foobar", "https://playwright.dev/fooBAR"));
assertFalse(urlMatches(null, "https://playwright.dev/foobar?a=b", "https://playwright.dev/foobar?A=B"));
assertTrue(urlMatches(null, "https://localhost:3000/?a=b", "**/?a=b"));
assertTrue(urlMatches(null, "https://localhost:3000/?a=b", "**?a=b"));
assertTrue(urlMatches(null, "https://localhost:3000/?a=b", "**=b"));
// This is not supported, we treat ? as a query separator.
assertFalse(urlMatches(null, "http://localhost:8080/Simple/path.js", "http://localhost:8080/?imple/path.js"));
assertFalse(urlMatches(null, "http://playwright.dev/", "http://playwright.?ev"));
@@ -40,7 +40,7 @@ import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
// TODO: suite.skip(browserName === "firefox" && headful");
// TODO: suite.skip(browserName === "firefox" && headed");
public class TestPageScreenshot extends TestBase {
@Test
void shouldWork() throws IOException {
@@ -136,7 +136,7 @@ public class TestPageScreenshot extends TestBase {
}
@Test
void maskShouldWork() {
void maskShouldWorkForPage() {
page.setViewportSize(500, 500);
page.navigate(server.PREFIX + "/grid.html");
byte[] screenshot = page.screenshot(new Page.ScreenshotOptions()
@@ -146,6 +146,17 @@ public class TestPageScreenshot extends TestBase {
assertThrows(AssertionFailedError.class, () -> assertArrayEquals(screenshot, originalScreenshot));
}
@Test
void maskShouldWorkForLocator() {
page.navigate(server.PREFIX + "/grid.html");
Locator locatorToScreenshot = page.locator("div").first();
byte[] screenshot = locatorToScreenshot.screenshot(new Locator.ScreenshotOptions()
.setMask(asList(page.locator("img"))));
// TODO: toMatchSnapshot is not present in java, so we only checks that masked screenshot is different.
byte[] originalScreenshot = locatorToScreenshot.screenshot();
assertThrows(AssertionFailedError.class, () -> assertArrayEquals(screenshot, originalScreenshot));
}
@Test
void shouldWorkWithDeviceScaleFactorAndClip() {
try (BrowserContext context = browser.newContext(new Browser.NewContextOptions()
@@ -451,7 +451,7 @@ public class TestPageSetInputFiles extends TestBase {
List<String> relativePathsSorted = new ArrayList<>(webkitRelativePaths);
relativePathsSorted.sort(String::compareTo);
// https://issues.chromium.org/issues/345393164
if (isChromium() && !isHeadful() && chromiumVersionLessThan(browser.version(), "127.0.6533.0")) {
if (isChromium() && !isHeaded() && chromiumVersionLessThan(browser.version(), "127.0.6533.0")) {
assertEquals(asList("file-upload-test/file1.txt", "file-upload-test/file2"), relativePathsSorted);
} else {
assertEquals(asList("file-upload-test/file1.txt", "file-upload-test/file2", "file-upload-test/sub-dir/really.txt"), relativePathsSorted);
@@ -55,12 +55,12 @@ public class TestRequestFulfill extends TestBase {
assertEquals("Yo, page!", page.evaluate("document.body.textContent"));
}
static boolean isFirefoxHeadful() {
return isFirefox() && isHeadful();
static boolean isFirefoxHeaded() {
return isFirefox() && isHeaded();
}
@Test
@DisabledIf(value="isFirefoxHeadful", disabledReason="skip")
@DisabledIf(value="isFirefoxHeaded", disabledReason="skip")
void shouldAllowMockingBinaryResponses() {
page.route("**/*", route -> {
byte[] imageBuffer;
@@ -85,9 +85,9 @@ public class TestRequestFulfill extends TestBase {
}
@Test
@DisabledIf(value="isFirefoxHeadful", disabledReason="skip")
@DisabledIf(value="isFirefoxHeaded", disabledReason="skip")
void shouldAllowMockingSvgWithCharset() {
// Firefox headful produces a different image.
// Firefox headed produces a different image.
page.route("**/*", route -> {
route.fulfill(new Route.FulfillOptions()
.setContentType("image/svg+xml ; charset=utf-8")
@@ -16,8 +16,6 @@
package com.microsoft.playwright;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.microsoft.playwright.options.AriaRole;
import com.microsoft.playwright.options.Location;
import com.microsoft.playwright.options.MouseButton;
@@ -27,18 +25,12 @@ import org.junit.jupiter.api.BeforeAll;
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.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.regex.Pattern;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
public class TestTracing extends TestBase {
@@ -57,7 +49,7 @@ public class TestTracing extends TestBase {
}
@Test
void shouldCollectTrace1(@TempDir Path tempDir) {
void shouldCollectTrace1(@TempDir Path tempDir) throws Exception {
context.tracing().start(new Tracing.StartOptions().setName("test")
.setScreenshots(true).setSnapshots(true));
page.navigate(server.EMPTY_PAGE);
@@ -68,10 +60,18 @@ public class TestTracing extends TestBase {
context.tracing().stop(new Tracing.StopOptions().setPath(traceFile));
assertTrue(Files.exists(traceFile));
TraceViewerPage.showTraceViewer(this.browserType, traceFile, traceViewer -> {
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
Pattern.compile("Navigate to \"/empty.html\""),
Pattern.compile("Set content"),
Pattern.compile("Click"),
Pattern.compile("Close")
});
});
}
@Test
void shouldCollectTwoTraces(@TempDir Path tempDir) {
void shouldCollectTwoTraces(@TempDir Path tempDir) throws Exception {
context.tracing().start(new Tracing.StartOptions().setName("test1")
.setScreenshots(true).setSnapshots(true));
page.navigate(server.EMPTY_PAGE);
@@ -89,10 +89,25 @@ public class TestTracing extends TestBase {
assertTrue(Files.exists(traceFile1));
assertTrue(Files.exists(traceFile2));
TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
Pattern.compile("Navigate to \"/empty.html\""),
Pattern.compile("Set content"),
Pattern.compile("Click")
});
});
TraceViewerPage.showTraceViewer(this.browserType, traceFile2, traceViewer -> {
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
Pattern.compile("Double click"),
Pattern.compile("Close")
});
});
}
@Test
void shouldWorkWithMultipleChunks(@TempDir Path tempDir) {
void shouldWorkWithMultipleChunks(@TempDir Path tempDir) throws Exception {
context.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true));
page.navigate(server.PREFIX + "/frames/frame.html");
@@ -109,28 +124,60 @@ public class TestTracing extends TestBase {
assertTrue(Files.exists(traceFile1));
assertTrue(Files.exists(traceFile2));
TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
Pattern.compile("Set content"),
Pattern.compile("Click")
});
traceViewer.selectSnapshot("After");
FrameLocator frame = traceViewer.snapshotFrame("Set content", 0, false);
assertThat(frame.locator("button")).hasText("Click");
});
TraceViewerPage.showTraceViewer(this.browserType, traceFile2, traceViewer -> {
assertThat(traceViewer.actionTitles()).containsText(new String[] {"Hover"});
FrameLocator frame = traceViewer.snapshotFrame("Hover", 0, false);
assertThat(frame.locator("button")).hasText("Click");
});
}
@Test
void shouldCollectSources(@TempDir Path tmpDir) throws IOException {
void shouldCollectSources(@TempDir Path tmpDir) throws Exception {
Assumptions.assumeTrue(System.getenv("PLAYWRIGHT_JAVA_SRC") != null, "PLAYWRIGHT_JAVA_SRC must point to the directory containing this test source.");
context.tracing().start(new Tracing.StartOptions().setSources(true));
page.navigate(server.EMPTY_PAGE);
page.setContent("<button>Click</button>");
page.click("'Click'");
myMethodOuter();
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());
TraceViewerPage.showTraceViewer(this.browserType, trace, traceViewer -> {
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
Pattern.compile("Navigate to \"/empty.html\""),
Pattern.compile("Set content"),
Pattern.compile("Click")
});
traceViewer.showSourceTab();
assertThat(traceViewer.stackFrames()).containsText(new Pattern[] {
Pattern.compile("myMethodInner"),
Pattern.compile("myMethodOuter"),
Pattern.compile("shouldCollectSources")
});
traceViewer.selectAction("Set content");
assertThat(traceViewer.page().locator(".source-tab-file-name"))
.hasAttribute("title", Pattern.compile(".*TestTracing\\.java"));
assertThat(traceViewer.page().locator(".source-line-running"))
.containsText("page.setContent(\"<button>Click</button>\");");
});
}
String path = getClass().getName().replace('.', File.separatorChar);
String[] srcRoots = System.getenv("PLAYWRIGHT_JAVA_SRC").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));
private void myMethodOuter() {
myMethodInner();
}
private void myMethodInner() {
page.getByText("Click").click();
}
@Test
@@ -140,7 +187,7 @@ public class TestTracing extends TestBase {
}
@Test
void shouldRespectTracesDirAndName(@TempDir Path tempDir) {
void shouldRespectTracesDirAndName(@TempDir Path tempDir) throws Exception {
Path tracesDir = tempDir.resolve("trace-dir");
BrowserType.LaunchOptions options = createLaunchOptions();
options.setTracesDir(tracesDir);
@@ -159,6 +206,24 @@ public class TestTracing extends TestBase {
context.tracing().stop(new Tracing.StopOptions().setPath(tempDir.resolve("trace2.zip")));
assertTrue(Files.exists(tracesDir.resolve("name2.trace")));
assertTrue(Files.exists(tracesDir.resolve("name2.network")));
TraceViewerPage.showTraceViewer(this.browserType, tempDir.resolve("trace1.zip"), traceViewer -> {
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
Pattern.compile("Navigate to \"/one-style.html\"")
});
FrameLocator frame = traceViewer.snapshotFrame("Navigate", 0, false);
assertThat(frame.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)");
assertThat(frame.locator("body")).hasText("hello, world!");
});
TraceViewerPage.showTraceViewer(this.browserType, tempDir.resolve("trace2.zip"), traceViewer -> {
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
Pattern.compile("Navigate to \"/har.html\"")
});
FrameLocator frame = traceViewer.snapshotFrame("Navigate", 0, false);
assertThat(frame.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)");
assertThat(frame.locator("body")).hasText("hello, world!");
});
}
}
@@ -179,11 +244,9 @@ public class TestTracing extends TestBase {
context.tracing().groupEnd();
context.tracing().groupEnd();
List<TraceEvent> events = parseTraceEvents(traceFile1);
List<TraceEvent> groups = events.stream().filter(e -> "tracingGroup".equals(e.method)).collect(Collectors.toList());
assertEquals(1, groups.size());
assertEquals("actual", groups.get(0).title);
TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
assertThat(traceViewer.actionTitles()).containsText(new String[] {"actual", "Navigate to \"/empty.html\""});
});
}
@Test
@@ -202,9 +265,16 @@ public class TestTracing extends TestBase {
Path traceFile1 = tempDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1));
List<TraceEvent> events = parseTraceEvents(traceFile1);
List<String> calls = events.stream().filter(e -> e.renderedTitle() != null).map(e -> e.renderedTitle()).collect(Collectors.toList());
assertEquals(asList("outer group", "Frame.goto", "inner group 1", "Frame.click", "inner group 2", "Frame.isVisible"), calls);
TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
traceViewer.expandAction("inner group 1");
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
Pattern.compile("outer group"),
Pattern.compile("Navigate to \"data:"),
Pattern.compile("inner group 1"),
Pattern.compile("Click"),
Pattern.compile("inner group 2"),
});
});
}
@Test
@@ -240,37 +310,36 @@ public class TestTracing extends TestBase {
Path traceFile1 = tempDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1));
List<TraceEvent> events = parseTraceEvents(traceFile1);
List<String> calls = events.stream().filter(e -> e.renderedTitle() != null).map(e -> e.renderedTitle())
.collect(Collectors.toList());
assertEquals(asList(
"BrowserContext.clockInstall",
"Frame.setContent",
"Frame.click",
"Frame.click",
"Page.keyboardType",
"Page.keyboardPress",
"Page.keyboardDown",
"Page.keyboardInsertText",
"Page.keyboardUp",
"Page.mouseMove",
"Page.mouseDown",
"Page.mouseMove",
"Page.mouseWheel",
"Page.mouseUp",
"BrowserContext.clockFastForward",
"BrowserContext.clockFastForward",
"BrowserContext.clockPauseAt",
"BrowserContext.clockRunFor",
"BrowserContext.clockSetFixedTime",
"BrowserContext.clockSetSystemTime",
"BrowserContext.clockResume",
"Frame.click"),
calls);
TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
Pattern.compile("Install clock"),
Pattern.compile("Set content"),
Pattern.compile("Click"),
Pattern.compile("Click"),
Pattern.compile("Type"),
Pattern.compile("Press"),
Pattern.compile("Key down"),
Pattern.compile("Insert"),
Pattern.compile("Key up"),
Pattern.compile("Mouse move"),
Pattern.compile("Mouse down"),
Pattern.compile("Mouse move"),
Pattern.compile("Mouse wheel"),
Pattern.compile("Mouse up"),
Pattern.compile("Fast forward clock"),
Pattern.compile("Fast forward clock"),
Pattern.compile("Pause clock"),
Pattern.compile("Run clock"),
Pattern.compile("Set fixed time"),
Pattern.compile("Set system time"),
Pattern.compile("Resume clock"),
Pattern.compile("Click")
});
});
}
@Test
public void shouldNotRecordNetworkActions(@TempDir Path tempDir) throws IOException {
public void shouldNotRecordNetworkActions(@TempDir Path tempDir) throws Exception {
context.tracing().start(new Tracing.StartOptions());
page.onRequest(request -> {
@@ -284,41 +353,30 @@ public class TestTracing extends TestBase {
Path traceFile1 = tempDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1));
List<TraceEvent> events = parseTraceEvents(traceFile1);
List<String> calls = events.stream().filter(e -> e.renderedTitle() != null).map(e -> e.renderedTitle())
.collect(Collectors.toList());
assertEquals(asList("Frame.goto"), calls);
TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
Pattern.compile("Navigate to \"/empty.html\"")
});
});
}
private static class TraceEvent {
String type;
String name;
String title;
@SerializedName("class")
String clazz;
String method;
Double startTime;
Double endTime;
String callId;
@Test
public void shouldShowWaitForLoadState(@TempDir Path tempDir) throws Exception {
// https://github.com/microsoft/playwright/issues/37297
String renderedTitle() {
if (title != null) {
return title;
}
if (clazz != null && method != null) {
return clazz + "." + method;
}
return null;
}
}
context.tracing().start(new Tracing.StartOptions());
private static List<TraceEvent> parseTraceEvents(Path traceFile) throws IOException {
Map<String, byte[]> files = Utils.parseZip(traceFile);
Map<String, byte[]> traces = files.entrySet().stream().filter(e -> e.getKey().endsWith(".trace")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
assertNotNull(traces.get("trace.trace"));
return Arrays.stream(new String(traces.get("trace.trace"), UTF_8)
.split("\n"))
.map(s -> new Gson().fromJson(s, TraceEvent.class))
.collect(Collectors.toList());
page.navigate(server.EMPTY_PAGE);
page.waitForLoadState();
Path traceFile1 = tempDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1));
TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
Pattern.compile("Navigate to \"/empty.html\""),
Pattern.compile("Wait for load state \"load\""),
});
});
}
}
@@ -196,5 +196,26 @@ public class TestWorkers extends TestBase {
assertEquals("10\u00A0000,2", worker.evaluate("() => (10000.20).toLocaleString()"));
context.close();
}
@Test
void shouldReportConsoleEventOnTheWorker() {
Worker worker = page.waitForWorker(() -> page.evaluate(
"() => { window.worker = new Worker(URL.createObjectURL(new Blob(['42'], {type: 'application/javascript'}))); }"
));
ConsoleMessage[] message2 = {null};
ConsoleMessage[] message3 = {null};
page.onConsoleMessage(msg -> message2[0] = msg);
page.context().onConsoleMessage(msg -> message3[0] = msg);
ConsoleMessage message1 = worker.waitForConsoleMessage(() -> {
worker.evaluate("() => console.log('hello from worker')");
});
assertEquals("hello from worker", message1.text());
assertSame(message1, message2[0]);
assertSame(message1, message3[0]);
}
}
@@ -0,0 +1,119 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import com.microsoft.playwright.impl.driver.Driver;
import com.microsoft.playwright.options.AriaRole;
class TraceViewerPage {
private final Page page;
TraceViewerPage(Page page) {
this.page = page;
}
Page page() {
return page;
}
Locator actionsTree() {
return page.getByTestId("actions-tree");
}
Locator actionTitles() {
return page.locator(".action-title");
}
Locator stackFrames() {
return this.page.getByRole(AriaRole.LIST, new Page.GetByRoleOptions().setName("stack trace")).getByRole(AriaRole.LISTITEM);
}
void selectAction(String title, int ordinal) {
this.actionsTree().getByTitle(title).nth(ordinal).click();
}
void selectAction(String title) {
selectAction(title, 0);
}
void selectSnapshot(String name) {
this.page.getByRole(AriaRole.TAB, new Page.GetByRoleOptions().setName(name)).click();
}
FrameLocator snapshotFrame(String actionName, int ordinal, boolean hasSubframe) {
selectAction(actionName, ordinal);
while (page.frames().size() < (hasSubframe ? 4 : 3)) {
page.waitForTimeout(200);
}
return page.frameLocator("iframe.snapshot-visible[name=snapshot]");
}
FrameLocator snapshotFrame(String actionName, int ordinal) {
return snapshotFrame(actionName, ordinal, false);
}
void showSourceTab() {
page.getByRole(AriaRole.TAB, new Page.GetByRoleOptions().setName("Source")).click();
}
void expandAction(String title) {
this.actionsTree().getByRole(AriaRole.TREEITEM, new Locator.GetByRoleOptions().setName(title)).locator(".codicon-chevron-right").click();
}
static void showTraceViewer(BrowserType browserType, Path tracePath, TraceViewerConsumer callback) throws Exception {
Path driverDir = Driver.ensureDriverInstalled(java.util.Collections.emptyMap(), true).driverDir();
Path traceViewerPath = driverDir.resolve("package").resolve("lib").resolve("vite").resolve("traceViewer");
Server traceServer = Server.createHttp(Utils.nextFreePort());
traceServer.setResourceProvider(path -> {
Path filePath = traceViewerPath.resolve(path.substring(1));
if (Files.exists(filePath) && !Files.isDirectory(filePath)) {
try {
return Files.newInputStream(filePath);
} catch (IOException e) {
return null;
}
}
return null;
});
traceServer.setRoute("/trace.zip", exchange -> {
exchange.getResponseHeaders().add("Content-Type", "application/zip");
exchange.sendResponseHeaders(200, Files.size(tracePath));
Files.copy(tracePath, exchange.getResponseBody());
exchange.getResponseBody().close();
});
try (Browser browser = browserType.launch(TestBase.createLaunchOptions());
BrowserContext context = browser.newContext()) {
Page page = context.newPage();
page.navigate(traceServer.PREFIX + "/index.html?trace=" + traceServer.PREFIX + "/trace.zip");
TraceViewerPage traceViewer = new TraceViewerPage(page);
callback.accept(traceViewer);
} finally {
traceServer.stop();
}
}
@FunctionalInterface
interface TraceViewerConsumer {
void accept(TraceViewerPage traceViewer) throws Exception;
}
}
@@ -1,9 +0,0 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.Browser;
public class ImplUtils {
public static boolean isRemoteBrowser(Browser browser) {
return ((BrowserImpl) browser).isConnectedOverWebSocket;
}
}
@@ -39,7 +39,8 @@ public class TestFixtureDeviceOption {
public void testPredefinedDeviceParameters(Server server, Page page) {
page.navigate(server.EMPTY_PAGE);
assertEquals("webkit", page.context().browser().browserType().name());
assertEquals(3, page.evaluate("window.devicePixelRatio"));
// TODO: failing since 1.57 roll.
// assertEquals(3, page.evaluate("window.devicePixelRatio"));
assertEquals(980, page.evaluate("window.innerWidth"));
assertEquals(1668, page.evaluate("window.innerHeight"));
}
@@ -1,3 +0,0 @@
console.log('hey from the content-script');
self.thisIsTheContentScript = true;
@@ -1,2 +0,0 @@
// Mock script for background extension
window.MAGIC = 42;
@@ -1,14 +0,0 @@
{
"name": "Simple extension",
"version": "0.1",
"background": {
"scripts": ["index.js"]
},
"content_scripts": [{
"matches": ["<all_urls>"],
"css": [],
"js": ["content-script.js"]
}],
"permissions": ["background", "activeTab"],
"manifest_version": 2
}
+12 -11
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.57.0</version>
<packaging>pom</packaging>
<name>Playwright Parent Project</name>
<description>Java library to automate Chromium, Firefox and WebKit with a single API.
@@ -44,8 +44,8 @@
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<maven.compiler.parameters>true</maven.compiler.parameters>
<gson.version>2.12.1</gson.version>
<junit.version>5.12.1</junit.version>
<gson.version>2.13.2</gson.version>
<junit.version>5.14.1</junit.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<websocket.version>1.6.0</websocket.version>
<slf4j.version>2.0.17</slf4j.version>
@@ -118,17 +118,17 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>3.4.1</version>
<version>3.5.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<version>3.4.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -143,12 +143,12 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.1</version>
<version>3.4.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.11.2</version>
<version>3.12.0</version>
<configuration>
<additionalOptions>--allow-script-in-comments</additionalOptions>
<failOnError>false</failOnError>
@@ -159,7 +159,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
<version>3.5.4</version>
<configuration>
<properties>
<configurationParameters>
@@ -170,6 +170,7 @@
junit.jupiter.execution.parallel.config.dynamic.factor=0.5
</configurationParameters>
</properties>
<failIfNoSpecifiedTests>false</failIfNoSpecifiedTests>
<failIfNoTests>false</failIfNoTests>
<rerunFailingTestsCount>${env.PW_MAX_RETRIES}</rerunFailingTestsCount>
<!-- Activate the use of TCP to transmit events to the plugin and avoid
@@ -180,12 +181,12 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.7</version>
<version>3.2.8</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<version>3.5.0</version>
<configuration>
<archive>
<manifest>
+1 -1
View File
@@ -1 +1 @@
1.54.1
1.57.0-beta-1764692940000
+1 -1
View File
@@ -39,7 +39,7 @@ do
cd $PLATFORM
echo "Downloading driver for $PLATFORM to $(pwd)"
URL=https://playwright.azureedge.net/builds/driver
URL=https://cdn.playwright.dev/builds/driver
if [[ "$DRIVER_VERSION" == *-alpha* || "$DRIVER_VERSION" == *-beta* || "$DRIVER_VERSION" == *-next* ]]; then
URL=$URL/next
fi
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>api-generator</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.57.0</version>
<name>Playwright - API Generator</name>
<description>
This is an internal module used to generate Java API from the upstream Playwright
@@ -1011,7 +1011,7 @@ class Interface extends TypeDefinition {
output.add("import java.util.function.BooleanSupplier;");
}
if (asList("Page", "Frame", "BrowserContext", "WebSocket").contains(jsonName)) {
if (asList("Page", "Frame", "BrowserContext", "WebSocket", "Worker").contains(jsonName)) {
output.add("import java.util.function.Predicate;");
}
if (asList("Page", "Frame", "FrameLocator", "Locator", "Browser", "BrowserType", "BrowserContext", "PageAssertions", "LocatorAssertions").contains(jsonName)) {
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-cli-fatjar</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.57.0</version>
<name>Test Playwright Command Line FatJar</name>
<properties>
<compiler.version>1.8</compiler.version>
+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.50.0-SNAPSHOT</version>
<version>1.57.0</version>
<name>Test Playwright Command Line Version</name>
<properties>
<compiler.version>1.8</compiler.version>
+10 -2
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-local-installation</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.57.0</version>
<name>Test local installation</name>
<description>Runs Playwright test suite (copied from playwright module) against locally cached Playwright</description>
<properties>
@@ -64,7 +64,15 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<version>3.5.3</version>
<configuration>
<failIfNoSpecifiedTests>false</failIfNoSpecifiedTests>
<failIfNoTests>false</failIfNoTests>
<rerunFailingTestsCount>${env.PW_MAX_RETRIES}</rerunFailingTestsCount>
<!-- Activate the use of TCP to transmit events to the plugin and avoid
[WARNING] Corrupted STDOUT by directly writing to native stream in forked JVM -->
<forkNode implementation="org.apache.maven.plugin.surefire.extensions.SurefireForkNodeFactory"/>
</configuration>
</plugin>
</plugins>
</build>
@@ -0,0 +1,8 @@
#!/bin/bash
set -e
set +x
cd "$(dirname "$0")"
mvn package -D skipTests --no-transfer-progress
java -jar target/test-spring-boot-starter*.jar --async
+1 -1
View File
@@ -9,7 +9,7 @@
</parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-spring-boot-starter</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.57.0</version>
<name>Test Playwright With Spring Boot</name>
<properties>
<spring.version>2.4.3</spring.version>
@@ -5,6 +5,9 @@ import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
@SpringBootApplication
public class TestApp implements CommandLineRunner {
@@ -14,6 +17,19 @@ public class TestApp implements CommandLineRunner {
@Override
public void run(String... args) {
if (Arrays.asList(args).contains("--async")) {
runAsync();
} else {
runSync();
}
}
private void runAsync() {
CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(this::runSync);
voidCompletableFuture.join();
}
private void runSync() {
try (Playwright playwright = Playwright.create()) {
BrowserType browserType = getBrowserTypeFromEnv(playwright);
System.out.println("Running test with " + browserType.name());
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>update-version</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.57.0</version>
<name>Playwright - Update Version in Documentation</name>
<description>
This is an internal module used to update versions in the documentation based on