Compare commits

...

12 Commits

Author SHA1 Message Date
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
40 changed files with 478 additions and 302 deletions

View File

@ -13,7 +13,7 @@ jobs:
environment: Docker environment: Docker
if: github.repository == 'microsoft/playwright-java' if: github.repository == 'microsoft/playwright-java'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Azure login - name: Azure login
uses: azure/login@v2 uses: azure/login@v2
with: with:
@ -26,5 +26,5 @@ jobs:
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
with: with:
platforms: arm64 platforms: arm64
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- run: ./utils/docker/publish_docker.sh stable - run: ./utils/docker/publish_docker.sh stable

View File

@ -20,9 +20,9 @@ jobs:
browser: [chromium, firefox, webkit] browser: [chromium, firefox, webkit]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Set up JDK 1.8 - name: Set up JDK 1.8
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: zulu distribution: zulu
java-version: 8 java-version: 8
@ -65,13 +65,13 @@ jobs:
browser-channel: msedge browser-channel: msedge
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install Media Pack - name: Install Media Pack
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
shell: powershell shell: powershell
run: Install-WindowsFeature Server-Media-Foundation run: Install-WindowsFeature Server-Media-Foundation
- name: Set up JDK 1.8 - name: Set up JDK 1.8
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: zulu distribution: zulu
java-version: 8 java-version: 8
@ -100,9 +100,9 @@ jobs:
browser: [chromium, firefox, webkit] browser: [chromium, firefox, webkit]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: adopt distribution: adopt
java-version: 21 java-version: 21

View File

@ -13,7 +13,7 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Cache Maven packages - name: Cache Maven packages
uses: actions/cache@v4 uses: actions/cache@v4
with: with:

View File

@ -21,18 +21,32 @@ jobs:
name: Test name: Test
timeout-minutes: 120 timeout-minutes: 120
runs-on: ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }}
env:
PW_MAX_RETRIES: 3
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
flavor: [jammy, noble] flavor: [jammy, noble]
runs-on: [ubuntu-24.04, ubuntu-24.04-arm] runs-on: [ubuntu-24.04, ubuntu-24.04-arm]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Build Docker image - name: Build Docker image
run: | run: |
ARCH="${{ matrix.runs-on == 'ubuntu-24.04-arm' && 'arm64' || 'amd64' }}" ARCH="${{ matrix.runs-on == 'ubuntu-24.04-arm' && 'arm64' || 'amd64' }}"
bash utils/docker/build.sh --$ARCH ${{ matrix.flavor }} playwright-java:localbuild-${{ matrix.flavor }} bash utils/docker/build.sh --$ARCH ${{ matrix.flavor }} playwright-java:localbuild-${{ matrix.flavor }}
- name: Test - name: Start container
run: | 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)" 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)
docker exec "${CONTAINER_ID}" /root/playwright/tools/test-local-installation/create_project_and_run_tests.sh 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"

View File

@ -19,7 +19,7 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Download drivers - name: Download drivers
run: scripts/download_driver.sh run: scripts/download_driver.sh
- name: Regenerate APIs - name: Regenerate APIs

View File

@ -32,9 +32,9 @@ scripts/download_driver.sh
mvn compile mvn compile
mvn test mvn test
# Executing a single 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 # Executing a single test class
BROWSER=chromium mvn test --projects=playwright -Dtest=TestPageNetworkSizes BROWSER=chromium mvn test -Dtest=TestPageNetworkSizes
``` ```
### Generating API ### Generating API

View File

@ -10,9 +10,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| | Linux | macOS | Windows | | | 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 -->140.0.7339.16<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.0<!-- GEN:stop --> | ✅ | ✅ | ✅ | | 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 -->141.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
## Documentation ## Documentation

View File

@ -114,7 +114,7 @@ public class DriverJar extends Driver {
} }
public static URI getDriverResourceURI() throws URISyntaxException { public static URI getDriverResourceURI() throws URISyntaxException {
ClassLoader classloader = Thread.currentThread().getContextClassLoader(); ClassLoader classloader = DriverJar.class.getClassLoader();
return classloader.getResource("driver/" + platformDir()).toURI(); return classloader.getResource("driver/" + platformDir()).toURI();
} }

View File

@ -10,7 +10,7 @@
<name>Playwright Client Examples</name> <name>Playwright Client Examples</name>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<playwright.version>1.54.0</playwright.version> <playwright.version>1.55.0</playwright.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>

View File

@ -51,6 +51,10 @@ public interface APIRequest {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code * {@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. * 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 * <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}. * 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 * {@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. * 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 * <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}. * replacing {@code localhost} with {@code local.playwright}.
*/ */

View File

@ -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 * {@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. * 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 * <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}. * 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 * {@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. * 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 * <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}. * 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 * {@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. * 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 * <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}. * 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 * {@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. * 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 * <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}. * replacing {@code localhost} with {@code local.playwright}.
*/ */

View File

@ -491,6 +491,10 @@ public interface BrowserType {
* {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code * {@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. * 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 * <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}. * 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 * {@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. * 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 * <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}. * 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}. * 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> 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 * @since v1.8
*/ */
default BrowserContext launchPersistentContext(Path userDataDir) { default BrowserContext launchPersistentContext(Path userDataDir) {
@ -1406,6 +1419,11 @@ public interface BrowserType {
* the **parent** directory of the "Profile Path" seen at {@code chrome://version}. * 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> 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 * @since v1.8
*/ */
BrowserContext launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options); BrowserContext launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options);

View File

@ -5812,8 +5812,8 @@ public interface Page extends AutoCloseable {
*/ */
Page opener(); Page opener();
/** /**
* Pauses script execution. Playwright will stop executing the script and wait for the user to either press 'Resume' button * Pauses script execution. Playwright will stop executing the script and wait for the user to either press the 'Resume'
* in the page overlay or to call {@code playwright.resume()} in the DevTools console. * 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 * <p> User can inspect selectors or perform manual steps while paused. Resume will continue running the original script from
* the place it was paused. * the place it was paused.

View File

@ -1008,15 +1008,16 @@ public class PageImpl extends ChannelOwner implements Page {
@Override @Override
public void pause() { public void pause() {
Double defaultNavigationTimeout = browserContext.timeoutSettings.defaultNavigationTimeout(); TimeoutSettings settings = browserContext.timeoutSettings;
Double defaultTimeout = browserContext.timeoutSettings.defaultTimeout(); Double defaultNavigationTimeout = settings.defaultNavigationTimeout();
browserContext.setDefaultNavigationTimeout(0.0); Double defaultTimeout = settings.defaultTimeout();
browserContext.setDefaultTimeout(0.0); settings.setDefaultNavigationTimeout(0.0);
settings.setDefaultTimeout(0.0);
try { try {
runUntil(() -> {}, new WaitableRace<>(asList(context().pause(), (Waitable<JsonElement>) waitableClosedOrCrashed))); runUntil(() -> {}, new WaitableRace<>(asList(context().pause(), (Waitable<JsonElement>) waitableClosedOrCrashed)));
} finally { } finally {
browserContext.setDefaultNavigationTimeout(defaultNavigationTimeout); settings.setDefaultNavigationTimeout(defaultNavigationTimeout);
browserContext.setDefaultTimeout(defaultTimeout); settings.setDefaultTimeout(defaultTimeout);
} }
} }
@ -1166,18 +1167,8 @@ public class PageImpl extends ChannelOwner implements Page {
} }
} }
} }
List<Locator> mask = options.mask;
options.mask = null;
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
options.mask = mask;
params.remove("path"); 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(); JsonObject json = sendMessage("screenshot", params, timeoutSettings.timeout(options.timeout)).getAsJsonObject();
byte[] buffer = Base64.getDecoder().decode(json.get("binary").getAsString()); byte[] buffer = Base64.getDecoder().decode(json.get("binary").getAsString());
@ -1367,10 +1358,13 @@ public class PageImpl extends ChannelOwner implements Page {
@Override @Override
public void waitForLoadState(LoadState state, WaitForLoadStateOptions options) { public void waitForLoadState(LoadState state, WaitForLoadStateOptions options) {
final LoadState loadState = state == null ? LoadState.LOAD : state;
withTitle("Wait for load state \"" + loadState.toString().toLowerCase() + "\"", () -> {
withWaitLogging("Page.waitForLoadState", logger -> { withWaitLogging("Page.waitForLoadState", logger -> {
mainFrame.waitForLoadStateImpl(state, convertType(options, Frame.WaitForLoadStateOptions.class), logger); mainFrame.waitForLoadStateImpl(loadState, convertType(options, Frame.WaitForLoadStateOptions.class), logger);
return null; return null;
}); });
});
} }
@Override @Override

View File

@ -66,6 +66,7 @@ class Serialization {
.registerTypeHierarchyAdapter(JSHandleImpl.class, new HandleSerializer()) .registerTypeHierarchyAdapter(JSHandleImpl.class, new HandleSerializer())
.registerTypeAdapter((new TypeToken<Map<String, String>>(){}).getType(), new StringMapSerializer()) .registerTypeAdapter((new TypeToken<Map<String, String>>(){}).getType(), new StringMapSerializer())
.registerTypeAdapter((new TypeToken<Map<String, Object>>(){}).getType(), new FirefoxUserPrefsSerializer()) .registerTypeAdapter((new TypeToken<Map<String, Object>>(){}).getType(), new FirefoxUserPrefsSerializer())
.registerTypeAdapter(LocatorImpl.class, new LocatorImplSerializer())
.registerTypeHierarchyAdapter(Path.class, new PathSerializer()).create(); .registerTypeHierarchyAdapter(Path.class, new PathSerializer()).create();
static Gson gson() { static Gson gson() {
@ -490,6 +491,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> { private static class SameSiteAdapter extends TypeAdapter<SameSiteAttribute> {
@Override @Override
public void write(JsonWriter out, SameSiteAttribute value) throws IOException { public void write(JsonWriter out, SameSiteAttribute value) throws IOException {

View File

@ -23,6 +23,7 @@ import java.net.InetSocketAddress;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.zip.GZIPOutputStream; import java.util.zip.GZIPOutputStream;
import static com.microsoft.playwright.Utils.copy; 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, String> csp = Collections.synchronizedMap(new HashMap<>());
private final Map<String, HttpHandler> routes = Collections.synchronizedMap(new HashMap<>()); private final Map<String, HttpHandler> routes = Collections.synchronizedMap(new HashMap<>());
private final Set<String> gzipRoutes = Collections.synchronizedSet(new HashSet<>()); private final Set<String> gzipRoutes = Collections.synchronizedSet(new HashSet<>());
private Function<String, InputStream> resourceProvider;
private static class Auth { private static class Auth {
public final String user; public final String user;
@ -75,6 +77,8 @@ public class Server implements HttpHandler {
server.createContext("/", this); server.createContext("/", this);
server.setExecutor(null); // creates a default executor server.setExecutor(null); // creates a default executor
server.start(); 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() { public void stop() {
@ -93,6 +97,10 @@ public class Server implements HttpHandler {
gzipRoutes.add(path); gzipRoutes.add(path);
} }
void setResourceProvider(Function<String, InputStream> resourceProvider) {
this.resourceProvider = resourceProvider;
}
static class Request { static class Request {
public final String url; public final String url;
public final String method; public final String method;
@ -187,18 +195,16 @@ public class Server implements HttpHandler {
path = "/index.html"; path = "/index.html";
} }
// Resources from "src/test/resources/" are copied to "resources/" directory in the jar. InputStream resource = resourceProvider.apply(path);
String resourcePath = "resources" + path;
InputStream resource = getClass().getClassLoader().getResourceAsStream(resourcePath);
if (resource == null) { if (resource == null) {
exchange.getResponseHeaders().add("Content-Type", "text/plain"); exchange.getResponseHeaders().add("Content-Type", "text/plain");
exchange.sendResponseHeaders(404, 0); exchange.sendResponseHeaders(404, 0);
try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) { try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("File not found: " + resourcePath); writer.write("File not found: " + path);
} }
return; return;
} }
exchange.getResponseHeaders().add("Content-Type", mimeType(new File(resourcePath))); exchange.getResponseHeaders().add("Content-Type", mimeType(new File(path)));
ByteArrayOutputStream body = new ByteArrayOutputStream(); ByteArrayOutputStream body = new ByteArrayOutputStream();
OutputStream output = body; OutputStream output = body;
if (gzipRoutes.contains(path)) { if (gzipRoutes.contains(path)) {

View File

@ -45,12 +45,12 @@ public class TestBase {
static final boolean isMac = Utils.getOS() == Utils.OS.MAC; static final boolean isMac = Utils.getOS() == Utils.OS.MAC;
static final boolean isLinux = Utils.getOS() == Utils.OS.LINUX; static final boolean isLinux = Utils.getOS() == Utils.OS.LINUX;
static final boolean isWindows = Utils.getOS() == Utils.OS.WINDOWS; static final boolean isWindows = Utils.getOS() == Utils.OS.WINDOWS;
static final boolean headful; static final boolean headed;
static final SameSiteAttribute defaultSameSiteCookieValue; static final SameSiteAttribute defaultSameSiteCookieValue;
static { static {
String headfulEnv = System.getenv("HEADFUL"); String headedEnv = System.getenv("HEADED");
headful = headfulEnv != null && !"0".equals(headfulEnv) && !"false".equals(headfulEnv); headed = headedEnv != null && !"0".equals(headedEnv) && !"false".equals(headedEnv);
defaultSameSiteCookieValue = initSameSiteAttribute(); defaultSameSiteCookieValue = initSameSiteAttribute();
} }
@ -58,8 +58,8 @@ public class TestBase {
Page page; Page page;
BrowserContext context; BrowserContext context;
static boolean isHeadful() { static boolean isHeaded() {
return headful; return headed;
} }
static boolean isChromium() { static boolean isChromium() {
@ -81,7 +81,7 @@ public class TestBase {
static BrowserType.LaunchOptions createLaunchOptions() { static BrowserType.LaunchOptions createLaunchOptions() {
BrowserType.LaunchOptions options; BrowserType.LaunchOptions options;
options = new BrowserType.LaunchOptions(); options = new BrowserType.LaunchOptions();
options.headless = !headful; options.headless = !headed;
options.channel = getBrowserChannelFromEnv(); options.channel = getBrowserChannelFromEnv();
return options; return options;
} }

View File

@ -27,7 +27,7 @@ public class TestBrowserContextCredentials extends TestBase {
static boolean isChromiumHeadedLike() { static boolean isChromiumHeadedLike() {
// --headless=new, the default in all Chromium channels, is like headless. // --headless=new, the default in all Chromium channels, is like headless.
return isChromium() && (isHeadful() || getBrowserChannelFromEnv() != null); return isChromium() && (isHeaded() || getBrowserChannelFromEnv() != null);
} }
@Test @Test

View File

@ -129,12 +129,12 @@ public class TestBrowserContextProxy extends TestBase {
context.close(); context.close();
} }
static boolean isChromiumHeadful() { static boolean isChromiumHeaded() {
return isChromium() && isHeadful(); return isChromium() && isHeaded();
} }
@Test @Test
@DisabledIf(value="isChromiumHeadful", disabledReason="fixme") @DisabledIf(value="isChromiumHeaded", disabledReason="fixme")
void shouldExcludePatterns() { void shouldExcludePatterns() {
server.setRoute("/target.html", exchange -> { server.setRoute("/target.html", exchange -> {
exchange.sendResponseHeaders(200, 0); exchange.sendResponseHeaders(200, 0);

View File

@ -128,7 +128,7 @@ public class TestDefaultBrowserContext2 extends TestBase {
@Test @Test
void shouldSupportExtraHTTPHeadersOption() throws ExecutionException, InterruptedException { 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"))); Page page = launchPersistent(new BrowserType.LaunchPersistentContextOptions().setExtraHTTPHeaders(mapOf("foo", "bar")));
Future<Server.Request> request = server.futureRequest("/empty.html"); Future<Server.Request> request = server.futureRequest("/empty.html");
page.navigate(server.EMPTY_PAGE); page.navigate(server.EMPTY_PAGE);

View File

@ -93,18 +93,12 @@ public class TestDownload extends TestBase {
assertTrue(Files.exists(path)); assertTrue(Files.exists(path));
byte[] bytes = readAllBytes(path); byte[] bytes = readAllBytes(path);
assertEquals("Hello world", new String(bytes, UTF_8)); 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")); assertTrue(error[0].getMessage().contains("Download is starting"));
} }
if (!isFirefox())
assertEquals("about:blank", page.url());
page.close(); page.close();
} }
@ -347,14 +341,14 @@ public class TestDownload extends TestBase {
} }
static boolean isChromiumHeadful() { static boolean isChromiumHeaded() {
return isChromium() && isHeadful(); return isChromium() && isHeaded();
} }
@Test @Test
@DisabledIf(value="isChromiumHeadful", disabledReason="fixme") @DisabledIf(value="isChromiumHeaded", disabledReason="fixme")
void shouldReportNewWindowDownloads() throws IOException { 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. // with the session before download completed event arrives.
// - WebKit doesn't close the popup page // - WebKit doesn't close the popup page
Page page = browser.newPage(new Browser.NewPageOptions().setAcceptDownloads(true)); Page page = browser.newPage(new Browser.NewPageOptions().setAcceptDownloads(true));

View File

@ -26,12 +26,12 @@ import static org.junit.jupiter.api.Assertions.*;
public class TestElementHandleBoundingBox extends TestBase { public class TestElementHandleBoundingBox extends TestBase {
static boolean isFirefoxHeadful() { static boolean isFirefoxHeaded() {
return isFirefox() && isHeadful(); return isFirefox() && isHeaded();
} }
@Test @Test
@DisabledIf(value="isFirefoxHeadful", disabledReason="fail") @DisabledIf(value="isFirefoxHeaded", disabledReason="fail")
void shouldWork() { void shouldWork() {
page.setViewportSize(500, 500); page.setViewportSize(500, 500);
page.navigate(server.PREFIX + "/grid.html"); page.navigate(server.PREFIX + "/grid.html");

View File

@ -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());
}
}

View File

@ -35,13 +35,13 @@ public class TestOptionsFactories {
public static BrowserType.LaunchOptions createLaunchOptions() { public static BrowserType.LaunchOptions createLaunchOptions() {
BrowserType.LaunchOptions options; BrowserType.LaunchOptions options;
options = new BrowserType.LaunchOptions(); options = new BrowserType.LaunchOptions();
options.headless = !getHeadful(); options.headless = !getHeaded();
return options; return options;
} }
private static boolean getHeadful() { private static boolean getHeaded() {
String headfulEnv = System.getenv("HEADFUL"); String headedEnv = System.getenv("HEADED");
return headfulEnv != null && !"0".equals(headfulEnv) && !"false".equals(headfulEnv); return headedEnv != null && !"0".equals(headedEnv) && !"false".equals(headedEnv);
} }
public static String getBrowserName() { public static String getBrowserName() {

View File

@ -356,4 +356,10 @@ public class TestPageBasic extends TestBase {
assertTrue(e.getMessage().contains("Can't add a null listener")); assertTrue(e.getMessage().contains("Can't add a null listener"));
} }
@Test
void pagePauseShouldNotThrow() {
page.pause();
}
} }

View File

@ -186,6 +186,13 @@ public class TestPageInterception extends TestBase {
assertTrue(urlMatches("http://playwright.dev", "http://playwright.dev/?x=y", "?x=y")); 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")); 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"));
// This is not supported, we treat ? as a query separator. // 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://localhost:8080/Simple/path.js", "http://localhost:8080/?imple/path.js"));
assertFalse(urlMatches(null, "http://playwright.dev/", "http://playwright.?ev")); assertFalse(urlMatches(null, "http://playwright.dev/", "http://playwright.?ev"));

View File

@ -40,7 +40,7 @@ import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertArrayEquals; 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 { public class TestPageScreenshot extends TestBase {
@Test @Test
void shouldWork() throws IOException { void shouldWork() throws IOException {
@ -136,7 +136,7 @@ public class TestPageScreenshot extends TestBase {
} }
@Test @Test
void maskShouldWork() { void maskShouldWorkForPage() {
page.setViewportSize(500, 500); page.setViewportSize(500, 500);
page.navigate(server.PREFIX + "/grid.html"); page.navigate(server.PREFIX + "/grid.html");
byte[] screenshot = page.screenshot(new Page.ScreenshotOptions() byte[] screenshot = page.screenshot(new Page.ScreenshotOptions()
@ -146,6 +146,17 @@ public class TestPageScreenshot extends TestBase {
assertThrows(AssertionFailedError.class, () -> assertArrayEquals(screenshot, originalScreenshot)); 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 @Test
void shouldWorkWithDeviceScaleFactorAndClip() { void shouldWorkWithDeviceScaleFactorAndClip() {
try (BrowserContext context = browser.newContext(new Browser.NewContextOptions() try (BrowserContext context = browser.newContext(new Browser.NewContextOptions()

View File

@ -451,7 +451,7 @@ public class TestPageSetInputFiles extends TestBase {
List<String> relativePathsSorted = new ArrayList<>(webkitRelativePaths); List<String> relativePathsSorted = new ArrayList<>(webkitRelativePaths);
relativePathsSorted.sort(String::compareTo); relativePathsSorted.sort(String::compareTo);
// https://issues.chromium.org/issues/345393164 // 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); assertEquals(asList("file-upload-test/file1.txt", "file-upload-test/file2"), relativePathsSorted);
} else { } else {
assertEquals(asList("file-upload-test/file1.txt", "file-upload-test/file2", "file-upload-test/sub-dir/really.txt"), relativePathsSorted); assertEquals(asList("file-upload-test/file1.txt", "file-upload-test/file2", "file-upload-test/sub-dir/really.txt"), relativePathsSorted);

View File

@ -55,12 +55,12 @@ public class TestRequestFulfill extends TestBase {
assertEquals("Yo, page!", page.evaluate("document.body.textContent")); assertEquals("Yo, page!", page.evaluate("document.body.textContent"));
} }
static boolean isFirefoxHeadful() { static boolean isFirefoxHeaded() {
return isFirefox() && isHeadful(); return isFirefox() && isHeaded();
} }
@Test @Test
@DisabledIf(value="isFirefoxHeadful", disabledReason="skip") @DisabledIf(value="isFirefoxHeaded", disabledReason="skip")
void shouldAllowMockingBinaryResponses() { void shouldAllowMockingBinaryResponses() {
page.route("**/*", route -> { page.route("**/*", route -> {
byte[] imageBuffer; byte[] imageBuffer;
@ -85,9 +85,9 @@ public class TestRequestFulfill extends TestBase {
} }
@Test @Test
@DisabledIf(value="isFirefoxHeadful", disabledReason="skip") @DisabledIf(value="isFirefoxHeaded", disabledReason="skip")
void shouldAllowMockingSvgWithCharset() { void shouldAllowMockingSvgWithCharset() {
// Firefox headful produces a different image. // Firefox headed produces a different image.
page.route("**/*", route -> { page.route("**/*", route -> {
route.fulfill(new Route.FulfillOptions() route.fulfill(new Route.FulfillOptions()
.setContentType("image/svg+xml ; charset=utf-8") .setContentType("image/svg+xml ; charset=utf-8")

View File

@ -16,8 +16,6 @@
package com.microsoft.playwright; 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.AriaRole;
import com.microsoft.playwright.options.Location; import com.microsoft.playwright.options.Location;
import com.microsoft.playwright.options.MouseButton; 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.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.util.regex.Pattern;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static java.nio.charset.StandardCharsets.UTF_8; import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
public class TestTracing extends TestBase { public class TestTracing extends TestBase {
@ -57,7 +49,7 @@ public class TestTracing extends TestBase {
} }
@Test @Test
void shouldCollectTrace1(@TempDir Path tempDir) { void shouldCollectTrace1(@TempDir Path tempDir) throws Exception {
context.tracing().start(new Tracing.StartOptions().setName("test") context.tracing().start(new Tracing.StartOptions().setName("test")
.setScreenshots(true).setSnapshots(true)); .setScreenshots(true).setSnapshots(true));
page.navigate(server.EMPTY_PAGE); page.navigate(server.EMPTY_PAGE);
@ -68,10 +60,18 @@ public class TestTracing extends TestBase {
context.tracing().stop(new Tracing.StopOptions().setPath(traceFile)); context.tracing().stop(new Tracing.StopOptions().setPath(traceFile));
assertTrue(Files.exists(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 @Test
void shouldCollectTwoTraces(@TempDir Path tempDir) { void shouldCollectTwoTraces(@TempDir Path tempDir) throws Exception {
context.tracing().start(new Tracing.StartOptions().setName("test1") context.tracing().start(new Tracing.StartOptions().setName("test1")
.setScreenshots(true).setSnapshots(true)); .setScreenshots(true).setSnapshots(true));
page.navigate(server.EMPTY_PAGE); page.navigate(server.EMPTY_PAGE);
@ -89,10 +89,25 @@ public class TestTracing extends TestBase {
assertTrue(Files.exists(traceFile1)); assertTrue(Files.exists(traceFile1));
assertTrue(Files.exists(traceFile2)); 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 @Test
void shouldWorkWithMultipleChunks(@TempDir Path tempDir) { void shouldWorkWithMultipleChunks(@TempDir Path tempDir) throws Exception {
context.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true)); context.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true));
page.navigate(server.PREFIX + "/frames/frame.html"); page.navigate(server.PREFIX + "/frames/frame.html");
@ -109,28 +124,60 @@ public class TestTracing extends TestBase {
assertTrue(Files.exists(traceFile1)); assertTrue(Files.exists(traceFile1));
assertTrue(Files.exists(traceFile2)); 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 @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."); 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)); context.tracing().start(new Tracing.StartOptions().setSources(true));
page.navigate(server.EMPTY_PAGE); page.navigate(server.EMPTY_PAGE);
page.setContent("<button>Click</button>"); page.setContent("<button>Click</button>");
page.click("'Click'"); myMethodOuter();
Path trace = tmpDir.resolve("trace1.zip"); Path trace = tmpDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(trace)); context.tracing().stop(new Tracing.StopOptions().setPath(trace));
Map<String, byte[]> entries = Utils.parseZip(trace); TraceViewerPage.showTraceViewer(this.browserType, trace, traceViewer -> {
Map<String, byte[]> sources = entries.entrySet().stream().filter(e -> e.getKey().endsWith(".txt")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
assertEquals(1, sources.size()); 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); private void myMethodOuter() {
String[] srcRoots = System.getenv("PLAYWRIGHT_JAVA_SRC").split(File.pathSeparator); myMethodInner();
// Resolve in the last specified source dir. }
Path sourceFile = Paths.get(srcRoots[srcRoots.length - 1], path + ".java");
byte[] thisFile = Files.readAllBytes(sourceFile); private void myMethodInner() {
assertEquals(new String(thisFile, UTF_8), new String(sources.values().iterator().next(), UTF_8)); page.getByText("Click").click();
} }
@Test @Test
@ -140,7 +187,7 @@ public class TestTracing extends TestBase {
} }
@Test @Test
void shouldRespectTracesDirAndName(@TempDir Path tempDir) { void shouldRespectTracesDirAndName(@TempDir Path tempDir) throws Exception {
Path tracesDir = tempDir.resolve("trace-dir"); Path tracesDir = tempDir.resolve("trace-dir");
BrowserType.LaunchOptions options = createLaunchOptions(); BrowserType.LaunchOptions options = createLaunchOptions();
options.setTracesDir(tracesDir); options.setTracesDir(tracesDir);
@ -159,6 +206,24 @@ public class TestTracing extends TestBase {
context.tracing().stop(new Tracing.StopOptions().setPath(tempDir.resolve("trace2.zip"))); context.tracing().stop(new Tracing.StopOptions().setPath(tempDir.resolve("trace2.zip")));
assertTrue(Files.exists(tracesDir.resolve("name2.trace"))); assertTrue(Files.exists(tracesDir.resolve("name2.trace")));
assertTrue(Files.exists(tracesDir.resolve("name2.network"))); 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();
context.tracing().groupEnd(); context.tracing().groupEnd();
List<TraceEvent> events = parseTraceEvents(traceFile1); TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
List<TraceEvent> groups = events.stream().filter(e -> "tracingGroup".equals(e.method)).collect(Collectors.toList()); assertThat(traceViewer.actionTitles()).containsText(new String[] {"actual", "Navigate to \"/empty.html\""});
assertEquals(1, groups.size()); });
assertEquals("actual", groups.get(0).title);
} }
@Test @Test
@ -202,9 +265,16 @@ public class TestTracing extends TestBase {
Path traceFile1 = tempDir.resolve("trace1.zip"); Path traceFile1 = tempDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1)); context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1));
List<TraceEvent> events = parseTraceEvents(traceFile1); TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
List<String> calls = events.stream().filter(e -> e.renderedTitle() != null).map(e -> e.renderedTitle()).collect(Collectors.toList()); traceViewer.expandAction("inner group 1");
assertEquals(asList("outer group", "Frame.goto", "inner group 1", "Frame.click", "inner group 2", "Frame.isVisible"), calls); 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 @Test
@ -240,37 +310,36 @@ public class TestTracing extends TestBase {
Path traceFile1 = tempDir.resolve("trace1.zip"); Path traceFile1 = tempDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1)); context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1));
List<TraceEvent> events = parseTraceEvents(traceFile1); TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
List<String> calls = events.stream().filter(e -> e.renderedTitle() != null).map(e -> e.renderedTitle()) assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
.collect(Collectors.toList()); Pattern.compile("Install clock"),
assertEquals(asList( Pattern.compile("Set content"),
"BrowserContext.clockInstall", Pattern.compile("Click"),
"Frame.setContent", Pattern.compile("Click"),
"Frame.click", Pattern.compile("Type"),
"Frame.click", Pattern.compile("Press"),
"Page.keyboardType", Pattern.compile("Key down"),
"Page.keyboardPress", Pattern.compile("Insert"),
"Page.keyboardDown", Pattern.compile("Key up"),
"Page.keyboardInsertText", Pattern.compile("Mouse move"),
"Page.keyboardUp", Pattern.compile("Mouse down"),
"Page.mouseMove", Pattern.compile("Mouse move"),
"Page.mouseDown", Pattern.compile("Mouse wheel"),
"Page.mouseMove", Pattern.compile("Mouse up"),
"Page.mouseWheel", Pattern.compile("Fast forward clock"),
"Page.mouseUp", Pattern.compile("Fast forward clock"),
"BrowserContext.clockFastForward", Pattern.compile("Pause clock"),
"BrowserContext.clockFastForward", Pattern.compile("Run clock"),
"BrowserContext.clockPauseAt", Pattern.compile("Set fixed time"),
"BrowserContext.clockRunFor", Pattern.compile("Set system time"),
"BrowserContext.clockSetFixedTime", Pattern.compile("Resume clock"),
"BrowserContext.clockSetSystemTime", Pattern.compile("Click")
"BrowserContext.clockResume", });
"Frame.click"), });
calls);
} }
@Test @Test
public void shouldNotRecordNetworkActions(@TempDir Path tempDir) throws IOException { public void shouldNotRecordNetworkActions(@TempDir Path tempDir) throws Exception {
context.tracing().start(new Tracing.StartOptions()); context.tracing().start(new Tracing.StartOptions());
page.onRequest(request -> { page.onRequest(request -> {
@ -284,41 +353,30 @@ public class TestTracing extends TestBase {
Path traceFile1 = tempDir.resolve("trace1.zip"); Path traceFile1 = tempDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1)); context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1));
List<TraceEvent> events = parseTraceEvents(traceFile1); TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
List<String> calls = events.stream().filter(e -> e.renderedTitle() != null).map(e -> e.renderedTitle()) assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
.collect(Collectors.toList()); Pattern.compile("Navigate to \"/empty.html\"")
assertEquals(asList("Frame.goto"), calls); });
});
} }
private static class TraceEvent { @Test
String type; public void shouldShowWaitForLoadState(@TempDir Path tempDir) throws Exception {
String name; // https://github.com/microsoft/playwright/issues/37297
String title;
@SerializedName("class")
String clazz;
String method;
Double startTime;
Double endTime;
String callId;
String renderedTitle() { context.tracing().start(new Tracing.StartOptions());
if (title != null) {
return title;
}
if (clazz != null && method != null) {
return clazz + "." + method;
}
return null;
}
}
private static List<TraceEvent> parseTraceEvents(Path traceFile) throws IOException { page.navigate(server.EMPTY_PAGE);
Map<String, byte[]> files = Utils.parseZip(traceFile); page.waitForLoadState();
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")); Path traceFile1 = tempDir.resolve("trace1.zip");
return Arrays.stream(new String(traces.get("trace.trace"), UTF_8) context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1));
.split("\n"))
.map(s -> new Gson().fromJson(s, TraceEvent.class)) TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
.collect(Collectors.toList()); assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
Pattern.compile("Navigate to \"/empty.html\""),
Pattern.compile("Wait for load state \"load\""),
});
});
} }
} }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1,3 +0,0 @@
console.log('hey from the content-script');
self.thisIsTheContentScript = true;

View File

@ -1,2 +0,0 @@
// Mock script for background extension
window.MAGIC = 42;

View File

@ -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
}

11
pom.xml
View File

@ -44,8 +44,8 @@
<maven.compiler.source>8</maven.compiler.source> <maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target> <maven.compiler.target>8</maven.compiler.target>
<maven.compiler.parameters>true</maven.compiler.parameters> <maven.compiler.parameters>true</maven.compiler.parameters>
<gson.version>2.12.1</gson.version> <gson.version>2.13.1</gson.version>
<junit.version>5.12.1</junit.version> <junit.version>5.13.4</junit.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<websocket.version>1.6.0</websocket.version> <websocket.version>1.6.0</websocket.version>
<slf4j.version>2.0.17</slf4j.version> <slf4j.version>2.0.17</slf4j.version>
@ -118,7 +118,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId> <artifactId>maven-clean-plugin</artifactId>
<version>3.4.1</version> <version>3.5.0</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
@ -148,7 +148,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId> <artifactId>maven-javadoc-plugin</artifactId>
<version>3.11.2</version> <version>3.11.3</version>
<configuration> <configuration>
<additionalOptions>--allow-script-in-comments</additionalOptions> <additionalOptions>--allow-script-in-comments</additionalOptions>
<failOnError>false</failOnError> <failOnError>false</failOnError>
@ -170,6 +170,7 @@
junit.jupiter.execution.parallel.config.dynamic.factor=0.5 junit.jupiter.execution.parallel.config.dynamic.factor=0.5
</configurationParameters> </configurationParameters>
</properties> </properties>
<failIfNoSpecifiedTests>false</failIfNoSpecifiedTests>
<failIfNoTests>false</failIfNoTests> <failIfNoTests>false</failIfNoTests>
<rerunFailingTestsCount>${env.PW_MAX_RETRIES}</rerunFailingTestsCount> <rerunFailingTestsCount>${env.PW_MAX_RETRIES}</rerunFailingTestsCount>
<!-- Activate the use of TCP to transmit events to the plugin and avoid <!-- Activate the use of TCP to transmit events to the plugin and avoid
@ -180,7 +181,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId> <artifactId>maven-gpg-plugin</artifactId>
<version>3.2.7</version> <version>3.2.8</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@ -1 +1 @@
1.54.1 1.55.0

View File

@ -64,7 +64,15 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <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> </plugin>
</plugins> </plugins>
</build> </build>

View File

@ -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

View File

@ -5,6 +5,9 @@ import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
@SpringBootApplication @SpringBootApplication
public class TestApp implements CommandLineRunner { public class TestApp implements CommandLineRunner {
@ -14,6 +17,19 @@ public class TestApp implements CommandLineRunner {
@Override @Override
public void run(String... args) { 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()) { try (Playwright playwright = Playwright.create()) {
BrowserType browserType = getBrowserTypeFromEnv(playwright); BrowserType browserType = getBrowserTypeFromEnv(playwright);
System.out.println("Running test with " + browserType.name()); System.out.println("Running test with " + browserType.name());