1
0
mirror of synced 2026-05-23 19:23:20 +00:00

Compare commits

..

16 Commits

Author SHA1 Message Date
Yury Semikhatsky 4e48d88e33 chore: mark v1.56.0 (#1852) 2025-10-17 12:42:01 -07:00
Yury Semikhatsky eceeea2070 cherry-pick(#1855): chore: roll 1.56.1 (#1856) 2025-10-17 11:18:50 -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
61 changed files with 795 additions and 364 deletions
+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@v5
- 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@v5
- 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@v5
- 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@v5
- 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@v5
- 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@v5
- 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@v5
- 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@v5
- 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 -->141.0.7390.37<!-- 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 -->142.0.1<!-- 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.54.0</version>
<version>1.56.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.54.0</version>
<version>1.56.0</version>
</parent>
<artifactId>driver</artifactId>
+2 -2
View File
@@ -6,11 +6,11 @@
<groupId>org.example</groupId>
<artifactId>examples</artifactId>
<version>1.54.0</version>
<version>1.56.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.56.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.54.0</version>
<version>1.56.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);
@@ -5729,6 +5729,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 +5826,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 +6067,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
@@ -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;
@@ -731,9 +730,12 @@ 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());
}
ConsoleMessageImpl message = new ConsoleMessageImpl(connection, params, page);
listeners.notify(BrowserContextImpl.EventType.CONSOLE, message);
PageImpl page = message.page();
if (page != null) {
page.listeners.notify(PageImpl.EventType.CONSOLE, message);
}
@@ -781,14 +783,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());
@@ -26,17 +26,12 @@ import java.util.List;
public class ConsoleMessageImpl implements ConsoleMessage {
private final Connection connection;
private PageImpl page;
private final PageImpl page;
private final JsonObject initializer;
public ConsoleMessageImpl(Connection connection, JsonObject initializer) {
public ConsoleMessageImpl(Connection connection, JsonObject initializer, PageImpl page) {
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.initializer = initializer;
}
@@ -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;
@@ -567,6 +568,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;
@@ -983,6 +995,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));
}
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 +1043,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 +1202,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 +1393,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 {
@@ -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());
}
}
@@ -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();
}
}
@@ -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<>();
@@ -186,6 +186,17 @@ 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"));
// 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\""),
});
});
}
}
@@ -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;
}
}
@@ -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
}
+9 -8
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.54.0</version>
<version>1.56.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.13.4</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,7 +118,7 @@
<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>
@@ -128,7 +128,7 @@
<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>
@@ -148,7 +148,7 @@
<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,7 +181,7 @@
<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>
+1 -1
View File
@@ -1 +1 @@
1.54.1
1.56.1
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>api-generator</artifactId>
<version>1.54.0</version>
<version>1.56.0</version>
<name>Playwright - API Generator</name>
<description>
This is an internal module used to generate Java API from the upstream Playwright
+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.54.0</version>
<version>1.56.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.54.0</version>
<version>1.56.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.54.0</version>
<version>1.56.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.54.0</version>
<version>1.56.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.54.0</version>
<version>1.56.0</version>
<name>Playwright - Update Version in Documentation</name>
<description>
This is an internal module used to update versions in the documentation based on