1
0
mirror of synced 2026-07-13 11:55:09 +00:00

Compare commits

...

13 Commits

Author SHA1 Message Date
Yury Semikhatsky 1c805748ad cherry-pick(#1938): feat(docker): pre-extract the driver in images to avoid /tmp unpacking 2026-06-29 11:32:07 -07:00
Yury Semikhatsky e8869de065 chore: mark 1.61.0 (#1942) 2026-06-29 11:21:16 -07:00
Yury Semikhatsky 20e4edd73c chore: roll driver to 1.61.1 (#1941) 2026-06-29 11:06:07 -07:00
Yury Semikhatsky d2d29d446d feat(driver): bundle playwright-core in driver, keep only Node.js in driver-bundle (#1936) 2026-06-19 09:54:36 -07:00
Yury Semikhatsky 43d2601be8 fix(fetch): serialize LocalDate in post data (#1934) 2026-06-18 10:20:37 -07:00
Yury Semikhatsky ace7a1241f fix(driver-bundle): exclude driver binaries from sources JAR (#1933) 2026-06-16 09:06:07 -07:00
Yury Semikhatsky fddd7c3708 feat(docker): add Ubuntu 26.04 (Resolute Raccoon) image (#1932) 2026-06-15 14:50:36 -07:00
Yury Semikhatsky 423cbf4cc7 chore: roll driver to 1.61.0-beta-1781285686000 (#1929) 2026-06-14 10:53:00 -07:00
Yury Semikhatsky e3f2f6fa5b fix(docker): assemble driver on the host for docker builds (#1930) 2026-06-12 14:09:31 -07:00
Yury Semikhatsky 1b7d164873 chore: extract common skill conventions into CLAUDE.md (#1928) 2026-06-12 11:14:19 -07:00
Yury Semikhatsky 7a59a68828 feat: assemble driver from npm instead of CDN (#1927)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:09:55 -07:00
dependabot[bot] 07b50c85e3 chore(deps-dev): bump the all group with 2 updates (#1925) 2026-06-12 11:04:05 -07:00
Yury Semikhatsky c949d8398d chore: add playwright-java-release skill (#1924) 2026-05-21 08:38:33 +01:00
78 changed files with 1720 additions and 208 deletions
@@ -0,0 +1,76 @@
---
name: playwright-java-release
description: Prepare a Playwright Java release after the rolling PR has merged — cut the release branch, mark the Maven version, draft the GitHub release, and tick the Java boxes in the internal checklist.
---
Use this skill once the `chore: roll driver to 1.X.0` PR has merged into `main` and the upstream JS `v1.X.0` is published. The rolling work itself is covered by the [[playwright-roll]] skill.
Throughout this doc, replace `X` with the minor version (e.g. `60` for `1.60.0`) and `<user>` with the fork owner (`gh api user --jq .login`).
The full release checklist lives in the private `microsoft/playwright-internal` repo as the `v1.X checklist` issue. Find its number once:
```bash
unset GITHUB_TOKEN
ISSUE=$(gh search issues --repo microsoft/playwright-internal "v1.X checklist" --json number --jq '.[0].number')
```
Tick each Java box incrementally (one PATCH per item) so the issue reflects accurate state if the flow is interrupted:
```bash
gh api repos/microsoft/playwright-internal/issues/$ISSUE --jq '.body' > /tmp/body.md
# edit /tmp/body.md to flip "- [ ]" → "- [x]" on the relevant Java item
gh api repos/microsoft/playwright-internal/issues/$ISSUE -X PATCH --field body=@/tmp/body.md
```
## 1. Cut the release branch
Push `release-1.X` from current `upstream/main` (which now contains the merged roll commit):
```bash
git fetch upstream main
git push upstream upstream/main:refs/heads/release-1.X
```
## 2. Draft the GitHub release
Generate the release notes from the upstream docs:
```bash
cd ~/playwright
node utils/render_release_notes.mjs java 1.X > /tmp/v1.X.0-release-notes.md
```
The renderer leaves JS-isms that need fixing for Java. Apply these substitutions — the list is not exhaustive, eyeball the diff before publishing:
- `toMatchAriaSnapshot()``matchesAriaSnapshot()`
- `toHaveCSS()``hasCSS()` (and other `toHaveX` matchers → `hasX`)
- `browser.on('context')``browser.onContext()`
- `browserContext.on('download' | 'frameattached' | ...)``browserContext.onDownload()` / `onFrameAttached()` / …
Create the draft directly against `release-1.X` — drafting against `main` and retargeting later is fragile because every `gh release edit` rotates the `untagged-<hash>` ID:
```bash
gh release create v1.X.0 --repo microsoft/playwright-java --draft \
--title "v1.X.0" --notes-file /tmp/v1.X.0-release-notes.md --target release-1.X
```
## 3. Bump the Maven version on the release branch
Cut `mark-v-1.X.0` off `upstream/release-1.X`, run `set_maven_version.sh`, and PR back to the release branch:
```bash
git checkout -b mark-v-1.X.0 upstream/release-1.X
./scripts/set_maven_version.sh 1.X.0
git add -u
git commit -m "chore: mark 1.X.0"
git push -u origin mark-v-1.X.0
gh pr create --repo microsoft/playwright-java --head <user>:mark-v-1.X.0 --base release-1.X \
--title "chore: mark 1.X.0" \
--body "Updates Maven version in all modules to \`1.X.0\` for the v1.X release."
```
`set_maven_version.sh` only invokes `mvn versions:set` on `pom.xml`, `tools/*/pom.xml`, and `examples/pom.xml`, but the root invocation cascades through the reactor, so the expected diff is 11 poms: root + `driver/` + `driver-bundle/` + `playwright/` (from the reactor cascade) + 6 under `tools/` + `examples/`, all flipping `1.<prev>.0-SNAPSHOT``1.X.0`. Any other file in the diff is a red flag.
## 4. Publish
The user publishes the draft release manually once the `mark-v-1.X.0` PR is merged. After publishing, CI pushes the artifacts to Maven Central and runs the Docker workflow automatically: https://github.com/microsoft/playwright-java/actions.
-36
View File
@@ -164,39 +164,3 @@ When you've identified a hanging test:
1. Run it in isolation: `mvn -f playwright/pom.xml test -Dtest='TestClass#testMethod'`. If it passes alone, it's a parallel-load flake — note it but move on.
2. If it still hangs in isolation, look for a recent fix in the upstream repo for the *same* test name. Use `git log --oneline tests/library/<spec>.spec.ts` in `~/playwright`. Upstream fixes for client-side hangs are often small and portable (e.g. `about:blank``server.EMPTY_PAGE` from microsoft/playwright#39840 fixed `route-web-socket.spec.ts` arraybuffer hangs — apparently some browser changed the WebSocket origin policy on `about:blank`).
3. When porting an upstream fix, mirror the helper signature change rather than hard-coding workarounds. E.g. if upstream added a `server` parameter to `setupWS`, do the same in Java by injecting `Server server` via the JUnit fixture (`@FixtureTest` already wires up `ServerLifecycle`, so adding `Server server` to the test method signature is enough — no class-level boilerplate). Watch for local-variable shadowing when you add a `Server server` parameter to a method that already has a `WebSocketRoute server` local; rename the local.
## Commit Convention
Semantic commit messages: `label(scope): description`
Labels: `fix`, `feat`, `chore`, `docs`, `test`, `devops`
```bash
git checkout -b fix-39562
# ... make changes ...
git add <changed-files>
git commit -m "$(cat <<'EOF'
fix(proxy): handle SOCKS proxy authentication
Fixes: https://github.com/microsoft/playwright-java/issues/39562
EOF
)"
git push origin fix-39562
gh pr create --repo microsoft/playwright-java --head username:fix-39562 \
--title "fix(proxy): handle SOCKS proxy authentication" \
--body "$(cat <<'EOF'
## Summary
- <describe the change very! briefly>
Fixes https://github.com/microsoft/playwright-java/issues/39562
EOF
)"
```
Never add Co-Authored-By agents in commit message.
Never add "Generated with" in commit message.
Branch naming for issue fixes: `fix-<issue-number>`
## Tips & Tricks
- Project checkouts are in the parent directory (`../`).
- use the "gh" cli to interact with GitHub
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
strategy:
fail-fast: false
matrix:
flavor: [jammy, noble]
flavor: [jammy, noble, resolute]
runs-on: [ubuntu-24.04, ubuntu-24.04-arm]
include:
- runs-on: ubuntu-24.04
+48
View File
@@ -0,0 +1,48 @@
# Playwright Java
The Java client is a port of the JavaScript client in `../playwright/packages/playwright-core/src/client/`. When implementing or changing a method, read the corresponding JS file first and mirror its logic.
Project checkouts (including the upstream `playwright` repo) live in the parent directory (`../`). Use the `gh` cli to interact with GitHub.
## Commit Convention
Semantic commit messages: `label(scope): description`
Labels: `fix`, `feat`, `chore`, `docs`, `test`, `devops`
```bash
git checkout -b fix-39562
# ... make changes ...
git add <changed-files>
git commit -m "$(cat <<'EOF'
fix(proxy): handle SOCKS proxy authentication
Fixes: https://github.com/microsoft/playwright-java/issues/39562
EOF
)"
# **Never `git push` without an explicit instruction to push.**
git push origin fix-39562
gh pr create --repo microsoft/playwright-java --head <user>:fix-39562 \
--title "fix(proxy): handle SOCKS proxy authentication" \
--body "$(cat <<'EOF'
## Summary
- <describe the change very! briefly>
Fixes https://github.com/microsoft/playwright-java/issues/39562
EOF
)"
```
Never add Co-Authored-By agents in commit message.
Never add "Generated with" in commit message.
Never add test plan to PR description. Keep PR description short — a few bullet points at most.
Branch naming for issue fixes: `fix-<issue-number>`.
**Never amend commits.** Always create a new commit for follow-up changes, even when iterating on an open PR. Amending rewrites history and forces a force-push, losing the incremental review trail. Only amend if the user explicitly says so.
**Never `git push` without an explicit instruction to push.** Applies even when a PR is already open for the branch — additional commits are immediately visible to reviewers. Commit locally, report what was committed, and wait. Only push when the user's message contains "push", "upload", "create PR", "ship it", or equivalent.
## Skills
- **playwright-roll** (`.claude/skills/playwright-roll/SKILL.md`) — roll Playwright Java to a new upstream version: bump the driver, regenerate the API, and port relevant upstream changes.
- **playwright-java-release** (`.claude/skills/playwright-java-release/SKILL.md`) — prepare a release after the rolling PR merges: cut the release branch, mark the Maven version, and draft the GitHub release.
+4 -3
View File
@@ -20,12 +20,14 @@ git clone https://github.com/microsoft/playwright-java
cd playwright-java
```
2. Run the following script to download Playwright driver for all platforms into `driver-bundle/src/main/resources/driver/` directory (browser binaries for Chromium, Firefox and WebKit will be automatically downloaded later on first Playwright run).
2. Run the following script to download and assemble the Playwright driver. The platform-independent `playwright-core` package is assembled once into `driver/src/main/resources/driver/package/`, and the Node.js binary for each platform into `driver-bundle/src/main/resources/driver/<platform>/` (browser binaries for Chromium, Firefox and WebKit will be automatically downloaded later on first Playwright run).
```bash
scripts/download_driver.sh
```
Each driver is assembled from the [`playwright-core`](https://www.npmjs.com/package/playwright-core) npm package (version pinned in [scripts/DRIVER_VERSION](scripts/DRIVER_VERSION)) and the matching Node.js binary from https://nodejs.org, the same way the upstream Playwright build does it.
### Building and running the tests with Maven
```bash
@@ -39,10 +41,9 @@ BROWSER=chromium mvn test -Dtest=TestPageNetworkSizes
### Generating API
Public Java API is generated from api.json which is produced by `print-api-json` command of playwright CLI. To regenerate Java interfaces for the current driver run the following commands:
Public Java API is generated from api.json, which is generated from the upstream Playwright source at the exact commit that produced the driver version in [scripts/DRIVER_VERSION](scripts/DRIVER_VERSION) (resolved via `npm view playwright@<version> gitHead`). `scripts/generate_api.sh` fetches a minimal upstream checkout automatically; set `PW_SRC_DIR` to reuse an existing `microsoft/playwright` checkout instead. To regenerate Java interfaces for the current driver run:
```bash
./scripts/download_driver.sh
./scripts/generate_api.sh
```
+3 -3
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 -->148.0.7778.96<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->150.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->149.0.7827.55<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.5<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->151.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
## Documentation
+17 -16
View File
@@ -6,26 +6,27 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.61.0</version>
</parent>
<artifactId>driver-bundle</artifactId>
<name>Playwright - Drivers For All Platforms</name>
<name>Playwright - Node.js For All Platforms</name>
<description>
This module includes Playwright driver and related utilities for all supported platforms.
It is intended to be used on the systems where Playwright driver is not preinstalled.
Node.js binaries for the Playwright driver on every supported platform. Can be excluded when
Node.js is preinstalled on the host (see PLAYWRIGHT_NODEJS_PATH).
</description>
<dependencies>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>driver</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- The Node.js binaries for all platforms live in src/main/resources and must not
be packaged into the sources JAR (see issue #1913). -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<configuration>
<excludeResources>true</excludeResources>
</configuration>
</plugin>
</plugins>
</build>
</project>
+17 -2
View File
@@ -6,13 +6,14 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.61.0</version>
</parent>
<artifactId>driver</artifactId>
<name>Playwright - Driver</name>
<description>
This module provides API for discovery and launching of Playwright driver.
API for launching the Playwright driver. Bundles the platform-independent playwright-core
package; the Node.js binary comes from the driver-bundle module or a preinstalled Node.js.
</description>
<dependencies>
@@ -21,4 +22,18 @@
<artifactId>junit-jupiter-engine</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- The playwright-core package lives in src/main/resources and must not be packaged
into the sources JAR (see issue #1913). -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<configuration>
<excludeResources>true</excludeResources>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -25,12 +25,14 @@ import static com.microsoft.playwright.impl.driver.DriverLogging.logWithTimestam
/**
* This class provides access to playwright-cli. It can be either preinstalled
* in the host system and its path is passed as a system property or it can be
* loaded from the driver-bundle module if that module is in the classpath.
* in the host system and its path is passed as a system property, or it can be
* loaded from the classpath: the platform-independent driver code ships in the
* driver module and the Node.js binary in the optional driver-bundle module.
*/
public abstract class Driver {
protected final Map<String, String> env = new LinkedHashMap<>(System.getenv());
public static final String PLAYWRIGHT_NODEJS_PATH = "PLAYWRIGHT_NODEJS_PATH";
public static final String PLAYWRIGHT_DRIVER_DIR = "PLAYWRIGHT_DRIVER_DIR";
private static Driver instance;
@@ -107,9 +109,12 @@ public abstract class Driver {
}
private static Driver newInstance() throws Exception {
String pathFromProperty = System.getProperty("playwright.cli.dir");
if (pathFromProperty != null) {
return new PreinstalledDriver(Paths.get(pathFromProperty));
String driverDir = System.getProperty("playwright.cli.dir");
if (driverDir == null) {
driverDir = System.getenv(PLAYWRIGHT_DRIVER_DIR);
}
if (driverDir != null) {
return new PreinstalledDriver(Paths.get(driverDir));
}
String driverImpl =
@@ -30,17 +30,11 @@ public class DriverJar extends Driver {
private static final String PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD";
private static final String SELENIUM_REMOTE_URL = "SELENIUM_REMOTE_URL";
private final Path driverTempDir;
private final boolean deleteOnExit;
private Path preinstalledNodePath;
public DriverJar() throws IOException {
// Allow specifying custom path for the driver installation
// See https://github.com/microsoft/playwright-java/issues/728
String alternativeTmpdir = System.getProperty("playwright.driver.tmpdir");
String prefix = "playwright-java-";
driverTempDir = alternativeTmpdir == null
? Files.createTempDirectory(prefix)
: Files.createTempDirectory(Paths.get(alternativeTmpdir), prefix);
driverTempDir.toFile().deleteOnExit();
this(createTempDriverDir(), true);
String nodePath = System.getProperty("playwright.nodejs.path");
if (nodePath != null) {
preinstalledNodePath = Paths.get(nodePath);
@@ -51,6 +45,32 @@ public class DriverJar extends Driver {
logMessage("created DriverJar: " + driverTempDir);
}
private DriverJar(Path driverDir, boolean deleteOnExit) {
this.driverTempDir = driverDir;
this.deleteOnExit = deleteOnExit;
if (deleteOnExit) {
driverTempDir.toFile().deleteOnExit();
}
}
private static Path createTempDriverDir() throws IOException {
// Allow specifying custom path for the driver installation
// See https://github.com/microsoft/playwright-java/issues/728
String alternativeTmpdir = System.getProperty("playwright.driver.tmpdir");
String prefix = "playwright-java-";
return alternativeTmpdir == null
? Files.createTempDirectory(prefix)
: Files.createTempDirectory(Paths.get(alternativeTmpdir), prefix);
}
// Extracts the driver (playwright-core package and the Node.js binary for the current platform)
// into the given directory, persistently. Point playwright.cli.dir / PLAYWRIGHT_DRIVER_DIR at it
// to run without extracting to a temp directory on every launch. See issue #1268.
public static void installDriverTo(Path driverDir) throws IOException, URISyntaxException {
Files.createDirectories(driverDir);
new DriverJar(driverDir, false).extractDriverToTempDir();
}
@Override
protected void initialize(Boolean installBrowsers) throws Exception {
if (preinstalledNodePath == null && env.containsKey(PLAYWRIGHT_NODEJS_PATH)) {
@@ -119,7 +139,21 @@ public class DriverJar extends Driver {
}
void extractDriverToTempDir() throws URISyntaxException, IOException {
URI originalUri = getDriverResourceURI();
extractResourceToDir("driver/package", driverTempDir.resolve("package"));
if (preinstalledNodePath == null) {
String platformResource = "driver/" + platformDir();
if (DriverJar.class.getClassLoader().getResource(platformResource) == null) {
throw new RuntimeException("Failed to find the bundled Node.js for platform '" + platformDir()
+ "'. Add the com.microsoft.playwright:driver-bundle dependency, or set the "
+ PLAYWRIGHT_NODEJS_PATH + " environment variable (or the playwright.nodejs.path system "
+ "property) to point at a preinstalled Node.js.");
}
extractResourceToDir(platformResource, driverTempDir);
}
}
private void extractResourceToDir(String resourcePath, Path destDir) throws URISyntaxException, IOException {
URI originalUri = DriverJar.class.getClassLoader().getResource(resourcePath).toURI();
URI uri = maybeExtractNestedJar(originalUri);
// Create zip filesystem if loading from jar.
@@ -131,14 +165,8 @@ public class DriverJar extends Driver {
// See https://github.com/microsoft/playwright-java/issues/306
Path srcRootDefaultFs = Paths.get(srcRoot.toString());
Files.walk(srcRoot).forEach(fromPath -> {
if (preinstalledNodePath != null) {
String fileName = fromPath.getFileName().toString();
if ("node.exe".equals(fileName) || "node".equals(fileName)) {
return;
}
}
Path relative = srcRootDefaultFs.relativize(Paths.get(fromPath.toString()));
Path toPath = driverTempDir.resolve(relative.toString());
Path toPath = destDir.resolve(relative.toString());
try {
if (Files.isDirectory(fromPath)) {
Files.createDirectories(toPath);
@@ -148,7 +176,9 @@ public class DriverJar extends Driver {
toPath.toFile().setExecutable(true, true);
}
}
toPath.toFile().deleteOnExit();
if (deleteOnExit) {
toPath.toFile().deleteOnExit();
}
} catch (IOException e) {
throw new RuntimeException("Failed to extract driver from " + uri + ", full uri: " + originalUri, e);
}
@@ -171,7 +201,9 @@ public class DriverJar extends Driver {
Path fromPath = Paths.get(jarUri);
Path toPath = driverTempDir.resolve(fromPath.getFileName().toString());
Files.copy(fromPath, toPath);
toPath.toFile().deleteOnExit();
if (deleteOnExit) {
toPath.toFile().deleteOnExit();
}
return new URI("jar:" + toPath.toUri() + JAR_URL_SEPARATOR + parts[2]);
} catch (IOException e) {
throw new RuntimeException("Failed to extract driver's nested .jar from " + jarUri + "; full uri: " + uri, e);
+2
View File
@@ -0,0 +1,2 @@
driver/
local-driver/
+2 -2
View File
@@ -6,11 +6,11 @@
<groupId>org.example</groupId>
<artifactId>examples</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.61.0</version>
<name>Playwright Client Examples</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<playwright.version>1.60.0</playwright.version>
<playwright.version>1.61.0</playwright.version>
</properties>
<dependencies>
<dependency>
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.61.0</version>
</parent>
<artifactId>playwright</artifactId>
@@ -55,6 +55,20 @@ public interface APIResponse {
* @since v1.16
*/
boolean ok();
/**
* Returns SSL and other security information. Resolves to {@code null} for non-HTTPS responses. For redirected requests,
* returns the information for the last request in the redirect chain.
*
* @since v1.61
*/
SecurityDetails securityDetails();
/**
* Returns the IP address and port of the server. Resolves to {@code null} if the server address is not available. For
* redirected requests, returns the information for the last request in the redirect chain.
*
* @since v1.61
*/
ServerAddr serverAddr();
/**
* Contains the status code of the response (e.g., 200 for a success).
*
@@ -563,6 +563,13 @@ public interface BrowserContext extends AutoCloseable {
* @since v1.45
*/
Clock clock();
/**
* Virtual WebAuthn authenticator for this context. Lets tests seed credentials and intercept {@code
* navigator.credentials.create()} / {@code navigator.credentials.get()} ceremonies.
*
* @since v1.61
*/
Credentials credentials();
/**
* Debugger allows to pause and resume the execution.
*
@@ -124,6 +124,10 @@ public interface BrowserType {
}
}
class ConnectOverCDPOptions {
/**
* If specified, browser artifacts (such as traces and downloads) are saved into this directory.
*/
public Path artifactsDir;
/**
* Additional HTTP headers to be sent with connect request. Optional.
*/
@@ -153,6 +157,13 @@ public interface BrowserType {
*/
public Double timeout;
/**
* If specified, browser artifacts (such as traces and downloads) are saved into this directory.
*/
public ConnectOverCDPOptions setArtifactsDir(Path artifactsDir) {
this.artifactsDir = artifactsDir;
return this;
}
/**
* Additional HTTP headers to be sent with connect request. Optional.
*/
@@ -17,9 +17,12 @@
package com.microsoft.playwright;
import com.microsoft.playwright.impl.driver.Driver;
import com.microsoft.playwright.impl.driver.jar.DriverJar;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import static java.util.Arrays.asList;
@@ -28,7 +31,13 @@ import static java.util.Arrays.asList;
* Use this class to launch playwright cli.
*/
public class CLI {
public static void main(String[] args) throws IOException, InterruptedException {
public static void main(String[] args) throws IOException, InterruptedException, URISyntaxException {
// Extract the driver into a fixed directory instead of running the playwright CLI. This is
// handled in Java because it must not require an already-extracted driver. See issue #1268.
if (args.length > 0 && "install-driver".equals(args[0])) {
installDriver(args);
return;
}
Driver driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
ProcessBuilder pb = driver.createProcessBuilder();
pb.command().addAll(asList(args));
@@ -40,4 +49,17 @@ public class CLI {
Process process = pb.start();
System.exit(process.waitFor());
}
private static void installDriver(String[] args) throws IOException, URISyntaxException {
String dir = args.length > 1 ? args[1] : System.getenv(Driver.PLAYWRIGHT_DRIVER_DIR);
if (dir == null) {
System.err.println("Usage: install-driver <dir> (or set the " + Driver.PLAYWRIGHT_DRIVER_DIR
+ " environment variable)");
System.exit(1);
return;
}
Path driverDir = Paths.get(dir);
DriverJar.installDriverTo(driverDir);
System.out.println("Installed Playwright driver into " + driverDir.toAbsolutePath());
}
}
@@ -0,0 +1,238 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* {@code Credentials} is a virtual WebAuthn authenticator scoped to a {@code BrowserContext}. It lets tests register
* passkeys and answer {@code navigator.credentials.create()} / {@code navigator.credentials.get()} ceremonies in the page,
* without a real authenticator or hardware security key.
*
* <p> There are two common ways to use it:
*
* <p> <strong>Usage: seed a known credential</strong>
* <pre>{@code
* BrowserContext context = browser.newContext();
*
* // A passkey your backend already provisioned for a test user.
* context.credentials().create("example.com", new Credentials.CreateOptions()
* .setId(knownCredentialId) // base64url
* .setUserHandle(knownUserHandle) // base64url
* .setPrivateKey(knownPrivateKey) // base64url PKCS#8 (DER)
* .setPublicKey(knownPublicKey)); // base64url SPKI (DER)
* context.credentials().install();
*
* Page page = context.newPage();
* page.navigate("https://example.com/login");
* // The page's navigator.credentials.get() is answered with the seeded passkey.
* }</pre>
*
* <p> <strong>Usage: capture a passkey, then reuse it</strong>
* <pre>{@code
* // setup test: let the app register a passkey, then save it.
* BrowserContext context = browser.newContext();
* context.credentials().install();
*
* Page page = context.newPage();
* page.navigate("https://example.com/register");
* page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Create a passkey")).click();
*
* // Read back the passkey the page registered — it includes the private key.
* VirtualCredential credential = context.credentials().get(
* new Credentials.GetOptions().setRpId("example.com")).get(0);
* Files.writeString(Paths.get("playwright/.auth/passkey.json"), new Gson().toJson(credential));
* }</pre>
* <pre>{@code
* // later test: seed the captured passkey so the app starts already enrolled.
* VirtualCredential credential = new Gson().fromJson(
* Files.readString(Paths.get("playwright/.auth/passkey.json")), VirtualCredential.class);
* BrowserContext context = browser.newContext();
* context.credentials().create(credential.rpId, new Credentials.CreateOptions()
* .setId(credential.id)
* .setUserHandle(credential.userHandle)
* .setPrivateKey(credential.privateKey)
* .setPublicKey(credential.publicKey));
* context.credentials().install();
*
* Page page = context.newPage();
* page.navigate("https://example.com/login");
* // navigator.credentials.get() resolves the captured passkey — already signed in.
* }</pre>
*
* <p> <strong>Defaults</strong>
*/
public interface Credentials {
class CreateOptions {
/**
* Base64url-encoded credential id. Auto-generated if omitted.
*/
public String id;
/**
* Base64url-encoded PKCS#8 (DER) private key. Auto-generated if omitted.
*/
public String privateKey;
/**
* Base64url-encoded SPKI (DER) public key. Auto-generated if omitted.
*/
public String publicKey;
/**
* Base64url-encoded user handle. Auto-generated if omitted.
*/
public String userHandle;
/**
* Base64url-encoded credential id. Auto-generated if omitted.
*/
public CreateOptions setId(String id) {
this.id = id;
return this;
}
/**
* Base64url-encoded PKCS#8 (DER) private key. Auto-generated if omitted.
*/
public CreateOptions setPrivateKey(String privateKey) {
this.privateKey = privateKey;
return this;
}
/**
* Base64url-encoded SPKI (DER) public key. Auto-generated if omitted.
*/
public CreateOptions setPublicKey(String publicKey) {
this.publicKey = publicKey;
return this;
}
/**
* Base64url-encoded user handle. Auto-generated if omitted.
*/
public CreateOptions setUserHandle(String userHandle) {
this.userHandle = userHandle;
return this;
}
}
class GetOptions {
/**
* Only return the credential with this base64url-encoded id.
*/
public String id;
/**
* Only return credentials for this relying party id.
*/
public String rpId;
/**
* Only return the credential with this base64url-encoded id.
*/
public GetOptions setId(String id) {
this.id = id;
return this;
}
/**
* Only return credentials for this relying party id.
*/
public GetOptions setRpId(String rpId) {
this.rpId = rpId;
return this;
}
}
/**
* Installs the virtual WebAuthn authenticator into the context, overriding {@code navigator.credentials.create()} and
* {@code navigator.credentials.get()} in all current and future pages. Call this before the page first touches {@code
* navigator.credentials}.
*
* <p> Required: until {@link com.microsoft.playwright.Credentials#install Credentials.install()} is called, no interception is
* in place and the page sees the platform's native (or absent) WebAuthn behaviour. Seeding credentials with {@link
* com.microsoft.playwright.Credentials#create Credentials.create()} without installing populates the authenticator, but
* the page will never see those credentials.
*
* @since v1.61
*/
void install();
/**
* Seeds a virtual WebAuthn credential and returns it.
*
* <p> With only {@code rpId}, generates a fresh **ECDSA P-256** keypair, credential id and user handle. The seeded credential
* is discoverable (resident), so the page can resolve it from both username-then-passkey and usernameless passkey flows.
* The returned object carries the private and public keys, so it can be persisted to disk and re-seeded in a later test.
*
* <p> To **import a known credential**, supply all four of {@code id}, {@code userHandle}, {@code privateKey} and {@code
* publicKey} together.
*
* <p> Call {@link com.microsoft.playwright.Credentials#install Credentials.install()} before navigating to a page that uses
* WebAuthn.
*
* @param rpId Relying party id (typically the site's effective domain).
* @since v1.61
*/
default VirtualCredential create(String rpId) {
return create(rpId, null);
}
/**
* Seeds a virtual WebAuthn credential and returns it.
*
* <p> With only {@code rpId}, generates a fresh **ECDSA P-256** keypair, credential id and user handle. The seeded credential
* is discoverable (resident), so the page can resolve it from both username-then-passkey and usernameless passkey flows.
* The returned object carries the private and public keys, so it can be persisted to disk and re-seeded in a later test.
*
* <p> To **import a known credential**, supply all four of {@code id}, {@code userHandle}, {@code privateKey} and {@code
* publicKey} together.
*
* <p> Call {@link com.microsoft.playwright.Credentials#install Credentials.install()} before navigating to a page that uses
* WebAuthn.
*
* @param rpId Relying party id (typically the site's effective domain).
* @since v1.61
*/
VirtualCredential create(String rpId, CreateOptions options);
/**
* Removes a credential from the authenticator by its id. Works for any credential currently held — both those seeded with
* {@link com.microsoft.playwright.Credentials#create Credentials.create()} and those the page registered itself by calling
* {@code navigator.credentials.create()}.
*
* @param id Base64url-encoded credential id.
* @since v1.61
*/
void delete(String id);
/**
* Returns every credential currently held by the authenticator, optionally filtered by {@code rpId} or {@code id}. This
* includes both credentials seeded with {@link com.microsoft.playwright.Credentials#create Credentials.create()} and
* credentials the page registered itself by calling {@code navigator.credentials.create()}.
*
* <p> Each returned credential includes its private and public keys, so a passkey the app just registered can be saved and
* re-seeded into a later test with {@link com.microsoft.playwright.Credentials#create Credentials.create()} — see the
* second example in the class overview.
*
* @since v1.61
*/
default List<VirtualCredential> get() {
return get(null);
}
/**
* Returns every credential currently held by the authenticator, optionally filtered by {@code rpId} or {@code id}. This
* includes both credentials seeded with {@link com.microsoft.playwright.Credentials#create Credentials.create()} and
* credentials the page registered itself by calling {@code navigator.credentials.create()}.
*
* <p> Each returned credential includes its private and public keys, so a passkey the app just registered can be saved and
* re-seeded into a later test with {@link com.microsoft.playwright.Credentials#create Credentials.create()} — see the
* second example in the class overview.
*
* @since v1.61
*/
List<VirtualCredential> get(GetOptions options);
}
@@ -2965,7 +2965,7 @@ public interface Frame {
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Frame#evaluate
* Frame.evaluate()}:
* <pre>{@code
* ElementHandle bodyHandle = frame.evaluate("document.body");
* ElementHandle bodyHandle = frame.evaluateHandle("document.body");
* String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -3005,7 +3005,7 @@ public interface Frame {
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Frame#evaluate
* Frame.evaluate()}:
* <pre>{@code
* ElementHandle bodyHandle = frame.evaluate("document.body");
* ElementHandle bodyHandle = frame.evaluateHandle("document.body");
* String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -4529,7 +4529,7 @@ public interface Page extends AutoCloseable {
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Page#evaluate
* Page.evaluate()}:
* <pre>{@code
* ElementHandle bodyHandle = page.evaluate("document.body");
* ElementHandle bodyHandle = page.evaluateHandle("document.body");
* String html = (String) page.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -4571,7 +4571,7 @@ public interface Page extends AutoCloseable {
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Page#evaluate
* Page.evaluate()}:
* <pre>{@code
* ElementHandle bodyHandle = page.evaluate("document.body");
* ElementHandle bodyHandle = page.evaluateHandle("document.body");
* String html = (String) page.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -5808,6 +5808,18 @@ public interface Page extends AutoCloseable {
* @since v1.59
*/
void clearPageErrors();
/**
* Provides access to the page's {@code localStorage} for the current origin. See {@code WebStorage}.
*
* @since v1.61
*/
WebStorage localStorage();
/**
* Provides access to the page's {@code sessionStorage} for the current origin. See {@code WebStorage}.
*
* @since v1.61
*/
WebStorage sessionStorage();
/**
* Returns up to (currently) 200 last console messages from this page. See {@link
* com.microsoft.playwright.Page#onConsoleMessage Page.onConsoleMessage()} for more details.
@@ -7552,8 +7564,8 @@ public interface Page extends AutoCloseable {
* <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code
* TimeoutError}. Passing zero timeout disables this.
*
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Page#tap Page.tap()} the method will throw if {@code hasTouch} option of the browser
* context is false.
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Page#tap Page.tap()} will throw if the {@code hasTouch} option of the browser context is
* false.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used.
* @since v1.8
@@ -7575,8 +7587,8 @@ public interface Page extends AutoCloseable {
* <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code
* TimeoutError}. Passing zero timeout disables this.
*
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Page#tap Page.tap()} the method will throw if {@code hasTouch} option of the browser
* context is false.
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Page#tap Page.tap()} will throw if the {@code hasTouch} option of the browser context is
* false.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used.
* @since v1.8
@@ -38,6 +38,13 @@ public interface Screencast {
* The quality of the image, between 0-100.
*/
public Integer quality;
/**
* Specifies the dimensions of screencast frames. The actual frame is scaled to preserve the page's aspect ratio and may be
* smaller than these bounds. If a screencast is already active (e.g. started by tracing or video recording), the existing
* configuration takes precedence and the frame size may exceed these bounds or this option may be ignored. If not
* specified the size will be equal to page viewport scaled down to fit into 800×800.
*/
public Size size;
/**
* Callback that receives JPEG-encoded frame data along with the page viewport size at the time of capture.
@@ -60,6 +67,25 @@ public interface Screencast {
this.quality = quality;
return this;
}
/**
* Specifies the dimensions of screencast frames. The actual frame is scaled to preserve the page's aspect ratio and may be
* smaller than these bounds. If a screencast is already active (e.g. started by tracing or video recording), the existing
* configuration takes precedence and the frame size may exceed these bounds or this option may be ignored. If not
* specified the size will be equal to page viewport scaled down to fit into 800×800.
*/
public StartOptions setSize(int width, int height) {
return setSize(new Size(width, height));
}
/**
* Specifies the dimensions of screencast frames. The actual frame is scaled to preserve the page's aspect ratio and may be
* smaller than these bounds. If a screencast is already active (e.g. started by tracing or video recording), the existing
* configuration takes precedence and the frame size may exceed these bounds or this option may be ignored. If not
* specified the size will be equal to page viewport scaled down to fit into 800×800.
*/
public StartOptions setSize(Size size) {
this.size = size;
return this;
}
}
class ShowOverlayOptions {
/**
@@ -103,6 +129,11 @@ public interface Screencast {
}
}
class ShowActionsOptions {
/**
* Cursor decoration shown for pointer actions. {@code "pointer"} (the default) renders a mouse pointer that animates from
* the previous action point to the next one. {@code "none"} disables the cursor decoration.
*/
public ScreencastCursor cursor;
/**
* How long each annotation is displayed in milliseconds. Defaults to {@code 500}.
*/
@@ -116,6 +147,14 @@ public interface Screencast {
*/
public AnnotatePosition position;
/**
* Cursor decoration shown for pointer actions. {@code "pointer"} (the default) renders a mouse pointer that animates from
* the previous action point to the next one. {@code "none"} disables the cursor decoration.
*/
public ShowActionsOptions setCursor(ScreencastCursor cursor) {
this.cursor = cursor;
return this;
}
/**
* How long each annotation is displayed in milliseconds. Defaults to {@code 500}.
*/
@@ -22,6 +22,11 @@ public interface ScreencastFrame {
*/
byte[] data();
/**
* The timestamp of when the frame was presented by the browser, in milliseconds since the Unix epoch.
*/
double timestamp();
/**
* Width of the page viewport at the time the frame was captured.
*/
@@ -201,7 +201,8 @@ public interface Selectors {
* Defines custom attribute name to be used in {@link com.microsoft.playwright.Page#getByTestId Page.getByTestId()}. {@code
* data-testid} is used by default.
*
* @param attributeName Test id attribute name.
* @param attributeName Test id attribute name. To match elements with any of several attributes, pass them as a comma-separated list, e.g.
* {@code "data-pw,data-ti"}.
* @since v1.27
*/
void setTestIdAttribute(String attributeName);
@@ -28,8 +28,8 @@ public interface Touchscreen {
/**
* Dispatches a {@code touchstart} and {@code touchend} event with a single touch at the position ({@code x},{@code y}).
*
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Page#tap Page.tap()} the method will throw if {@code hasTouch} option of the browser
* context is false.
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Touchscreen#tap Touchscreen.tap()} will throw if the {@code hasTouch} option of the
* browser context is false.
*
* @param x X coordinate relative to the main frame's viewport in CSS pixels.
* @param y Y coordinate relative to the main frame's viewport in CSS pixels.
@@ -0,0 +1,73 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* WebStorage exposes the page's {@code localStorage} or {@code sessionStorage} for the current origin via an async, <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Storage">browser-consistent</a> API.
*
* <p> Instances are accessed through {@link com.microsoft.playwright.Page#localStorage Page.localStorage()} and {@link
* com.microsoft.playwright.Page#sessionStorage Page.sessionStorage()}.
* <pre>{@code
* page.navigate("https://example.com");
* page.localStorage().setItem("token", "abc");
* String token = page.localStorage().getItem("token");
* List<WebStorageItem> all = page.localStorage().items();
* page.localStorage().removeItem("token");
* page.localStorage().clear();
* }</pre>
*/
public interface WebStorage {
/**
* Returns all items in the storage as name/value pairs.
*
* @since v1.61
*/
List<WebStorageItem> items();
/**
* Returns the value for the given {@code name} if present.
*
* @param name Name of the item to retrieve.
* @since v1.61
*/
String getItem(String name);
/**
* Sets the value for the given {@code name}. Overwrites any existing value for that name.
*
* @param name Name of the item to set.
* @param value New value for the item.
* @since v1.61
*/
void setItem(String name, String value);
/**
* Removes the item with the given {@code name}. No-op if the item is absent.
*
* @param name Name of the item to remove.
* @since v1.61
*/
void removeItem(String name);
/**
* Removes all items from the storage.
*
* @since v1.61
*/
void clear();
}
@@ -22,6 +22,8 @@ import com.google.gson.reflect.TypeToken;
import com.microsoft.playwright.APIResponse;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.HttpHeader;
import com.microsoft.playwright.options.SecurityDetails;
import com.microsoft.playwright.options.ServerAddr;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@@ -85,6 +87,22 @@ class APIResponseImpl implements APIResponse {
return status == 0 || (status >= 200 && status <= 299);
}
@Override
public SecurityDetails securityDetails() {
if (!initializer.has("securityDetails")) {
return null;
}
return gson().fromJson(initializer.get("securityDetails"), SecurityDetails.class);
}
@Override
public ServerAddr serverAddr() {
if (!initializer.has("serverAddr")) {
return null;
}
return gson().fromJson(initializer.get("serverAddr"), ServerAddr.class);
}
@Override
public int status() {
return initializer.get("status").getAsInt();
@@ -46,6 +46,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
private final DebuggerImpl debugger;
private final APIRequestContextImpl request;
private final ClockImpl clock;
private final CredentialsImpl credentials;
final List<PageImpl> pages = new ArrayList<>();
final Router routes = new Router();
@@ -94,6 +95,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
request = connection.getExistingObject(initializer.getAsJsonObject("requestContext").get("guid").getAsString());
request.timeoutSettings = timeoutSettings;
clock = new ClockImpl(this);
credentials = new CredentialsImpl(this);
closePromise = new WaitableEvent<>(listeners, EventType.CLOSE);
}
@@ -313,6 +315,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
return clock;
}
@Override
public Credentials credentials() {
return credentials;
}
private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Predicate<T> predicate, Double timeout) {
List<Waitable<T>> waitables = new ArrayList<>();
waitables.add(new WaitableEvent<>(listeners, eventType, predicate));
@@ -112,8 +112,8 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
@Override
public Browser connectOverCDP(String endpointURL, ConnectOverCDPOptions options) {
if (!"chromium".equals(name())) {
throw new PlaywrightException("Connecting over CDP is only supported in Chromium.");
if (!"chromium".equals(name()) && !"webkit".equals(name())) {
throw new PlaywrightException("Connecting over CDP is only supported in Chromium and WebKit.");
}
if (options == null) {
options = new ConnectOverCDPOptions();
@@ -110,6 +110,14 @@ class ChannelOwner extends LoggingSupport {
return connection.sendMessageAsync(guid, method, params);
}
// Fire-and-forget: silently drop if the object was collected.
void sendMessageNoReply(String method, JsonObject params) {
if (wasCollected) {
return;
}
connection.sendMessageNoReply(guid, method, params);
}
JsonElement sendMessage(String method) {
return sendMessage(method, new JsonObject(), NO_TIMEOUT);
}
@@ -39,6 +39,7 @@ class Message {
JsonObject params;
JsonElement result;
SerializedError error;
JsonObject errorDetails;
JsonArray log;
@Override
@@ -132,13 +133,20 @@ public class Connection {
}
public WaitableResult<JsonElement> sendMessageAsync(String guid, String method, JsonObject params) {
return internalSendMessage(guid, method, params, true);
return internalSendMessage(guid, method, params, true, true);
}
private WaitableResult<JsonElement> internalSendMessage(String guid, String method, JsonObject params, boolean sendStack) {
// Fire-and-forget: the server never replies.
public void sendMessageNoReply(String guid, String method, JsonObject params) {
internalSendMessage(guid, method, params, false, false);
}
private WaitableResult<JsonElement> internalSendMessage(String guid, String method, JsonObject params, boolean sendStack, boolean expectsReply) {
int id = ++lastId;
WaitableResult<JsonElement> result = new WaitableResult<>();
callbacks.put(id, result);
if (expectsReply) {
callbacks.put(id, result);
}
JsonObject message = new JsonObject();
message.addProperty("id", id);
message.addProperty("guid", guid);
@@ -175,7 +183,7 @@ public class Connection {
callData.add("stack", stack);
JsonObject stackParams = new JsonObject();
stackParams.add("callData", callData);
internalSendMessage(localUtils.guid,"addStackToTracingNoReply", stackParams, false);
internalSendMessage(localUtils.guid,"addStackToTracingNoReply", stackParams, false, true);
}
return result;
}
@@ -251,16 +259,20 @@ public class Connection {
callback.complete(message.result);
} else {
String callLog = formatCallLog(message.log);
PlaywrightException exception;
if (message.error.error == null) {
callback.completeExceptionally(new PlaywrightException(message.error + callLog));
exception = new PlaywrightException(message.error + callLog);
} else if ("TimeoutError".equals(message.error.error.name)) {
callback.completeExceptionally(new TimeoutError(message.error.error + callLog));
exception = new TimeoutError(message.error.error + callLog);
} else if ("TargetClosedError".equals(message.error.error.name)) {
callback.completeExceptionally(new TargetClosedError(message.error.error + callLog));
exception = new TargetClosedError(message.error.error + callLog);
} else {
callback.completeExceptionally(new DriverException(message.error.error + callLog));
exception = new DriverException(message.error.error + callLog);
}
if (message.errorDetails != null) {
exception = new ServerErrorWithDetails(exception, message.errorDetails, message.log);
}
callback.completeExceptionally(exception);
}
return;
}
@@ -0,0 +1,62 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Credentials;
import com.microsoft.playwright.options.VirtualCredential;
import java.util.List;
import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT;
import static com.microsoft.playwright.impl.Serialization.gson;
import static java.util.Arrays.asList;
class CredentialsImpl implements Credentials {
private final BrowserContextImpl context;
CredentialsImpl(BrowserContextImpl context) {
this.context = context;
}
@Override
public void install() {
context.sendMessage("credentialsInstall", new JsonObject(), NO_TIMEOUT);
}
@Override
public VirtualCredential create(String rpId, CreateOptions options) {
JsonObject params = options == null ? new JsonObject() : gson().toJsonTree(options).getAsJsonObject();
params.addProperty("rpId", rpId);
JsonObject json = context.sendMessage("credentialsCreate", params, NO_TIMEOUT).getAsJsonObject();
return gson().fromJson(json.get("credential"), VirtualCredential.class);
}
@Override
public void delete(String id) {
JsonObject params = new JsonObject();
params.addProperty("id", id);
context.sendMessage("credentialsDelete", params, NO_TIMEOUT);
}
@Override
public List<VirtualCredential> get(GetOptions options) {
JsonObject params = options == null ? new JsonObject() : gson().toJsonTree(options).getAsJsonObject();
JsonObject json = context.sendMessage("credentialsGet", params, NO_TIMEOUT).getAsJsonObject();
return asList(gson().fromJson(json.getAsJsonArray("credentials"), VirtualCredential[].class));
}
}
@@ -1154,8 +1154,17 @@ public class FrameImpl extends ChannelOwner implements Frame {
FrameExpectResult expect(String expression, FrameExpectOptions options) {
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("expression", expression);
JsonElement json = sendMessage("expect", params, options.timeout);
FrameExpectResult result = gson().fromJson(json, FrameExpectResult.class);
FrameExpectResult result = new FrameExpectResult();
try {
sendMessage("expect", params, options.timeout);
result.matches = !options.isNot;
} catch (ServerErrorWithDetails e) {
FrameExpectErrorDetails details = gson().fromJson(e.errorDetails(), FrameExpectErrorDetails.class);
result.matches = options.isNot;
result.received = details.received;
result.errorMessage = details.customErrorMessage == null ? null : "Error: " + details.customErrorMessage;
result.log = e.log();
}
return result;
}
}
@@ -43,8 +43,14 @@ public class LocatorUtils {
return "internal:describe=" + gson().toJson(description);
}
// Multiple test id attribute names can be joined with a comma. Attribute names cannot contain commas.
private static String encodeTestIdAttributeName(String testIdAttributeName) {
return testIdAttributeName.contains(",") ? gson().toJson(testIdAttributeName) : testIdAttributeName;
}
static String getByTestIdSelector(Object testId, PlaywrightImpl playwright) {
return getByAttributeTextSelector(playwright.selectors.testIdAttributeName, testId, true);
String attributeName = encodeTestIdAttributeName(playwright.selectors.testIdAttributeName);
return "internal:testid=[" + attributeName + "=" + escapeForAttributeSelector(testId, true) + "]";
}
static String getByAltTextSelector(Object text, Locator.GetByAltTextOptions options) {
@@ -47,6 +47,8 @@ public class PageImpl extends ChannelOwner implements Page {
private final MouseImpl mouse;
private final TouchscreenImpl touchscreen;
private final ScreencastImpl screencast;
private final WebStorageImpl localStorage;
private final WebStorageImpl sessionStorage;
final Waitable<?> waitableClosedOrCrashed;
private ViewportSize viewport;
private final Router routes = new Router();
@@ -137,6 +139,8 @@ public class PageImpl extends ChannelOwner implements Page {
mouse = new MouseImpl(this);
touchscreen = new TouchscreenImpl(this);
screencast = new ScreencastImpl(this);
localStorage = new WebStorageImpl(this, "local");
sessionStorage = new WebStorageImpl(this, "session");
frames.add(mainFrame);
timeoutSettings = new TimeoutSettings(browserContext.timeoutSettings);
waitableClosedOrCrashed = createWaitForCloseHelper();
@@ -555,8 +559,13 @@ public class PageImpl extends ChannelOwner implements Page {
try {
if (ownedContext != null) {
ownedContext.close();
} else if (options.runBeforeUnload != null && options.runBeforeUnload) {
sendMessage("runBeforeUnload", new JsonObject(), NO_TIMEOUT);
} else {
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
JsonObject params = new JsonObject();
if (options.reason != null) {
params.addProperty("reason", options.reason);
}
sendMessage("close", params, NO_TIMEOUT);
}
} catch (PlaywrightException exception) {
@@ -1362,6 +1371,16 @@ public class PageImpl extends ChannelOwner implements Page {
return screencast;
}
@Override
public WebStorage localStorage() {
return localStorage;
}
@Override
public WebStorage sessionStorage() {
return sessionStorage;
}
@Override
public void type(String selector, String text, TypeOptions options) {
mainFrame.type(selector, text, convertType(options, Frame.TypeOptions.class));
@@ -122,4 +122,10 @@ class FrameExpectResult {
List<String> log;
}
class FrameExpectErrorDetails {
FrameExpectResult.Received received;
Boolean timedOut;
String customErrorMessage;
}
@@ -20,11 +20,13 @@ import com.microsoft.playwright.ScreencastFrame;
class ScreencastFrameImpl implements ScreencastFrame {
private final byte[] data;
private final double timestamp;
private final int viewportWidth;
private final int viewportHeight;
ScreencastFrameImpl(byte[] data, int viewportWidth, int viewportHeight) {
ScreencastFrameImpl(byte[] data, double timestamp, int viewportWidth, int viewportHeight) {
this.data = data;
this.timestamp = timestamp;
this.viewportWidth = viewportWidth;
this.viewportHeight = viewportHeight;
}
@@ -34,6 +36,11 @@ class ScreencastFrameImpl implements ScreencastFrame {
return data;
}
@Override
public double timestamp() {
return timestamp;
}
@Override
public int viewportWidth() {
return viewportWidth;
@@ -44,9 +44,10 @@ class ScreencastImpl implements Screencast {
}
String dataBase64 = params.get("data").getAsString();
byte[] data = java.util.Base64.getDecoder().decode(dataBase64);
double timestamp = params.get("timestamp").getAsDouble();
int viewportWidth = params.get("viewportWidth").getAsInt();
int viewportHeight = params.get("viewportHeight").getAsInt();
onFrame.accept(new ScreencastFrameImpl(data, viewportWidth, viewportHeight));
onFrame.accept(new ScreencastFrameImpl(data, timestamp, viewportWidth, viewportHeight));
}
@Override
@@ -77,6 +77,7 @@ class Serialization {
static final Gson jsonDataSerializer = new GsonBuilder().disableHtmlEscaping()
.registerTypeAdapter(Date.class, new DateSerializer())
.registerTypeAdapter(LocalDate.class, new LocalDateSerializer())
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeSerializer())
.registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeSerializer())
.serializeNulls().create();
@@ -571,6 +572,15 @@ class Serialization {
}
}
private static class LocalDateSerializer implements JsonSerializer<LocalDate> {
@Override
public JsonElement serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context) {
// LocalDate has no time or zone, so emit the ISO-8601 date (yyyy-MM-dd) as-is to
// avoid shifting the calendar date when converting through a time zone.
return new JsonPrimitive(src.toString());
}
}
private static class LocalDateTimeSerializer implements JsonSerializer<LocalDateTime> {
@Override
public JsonElement serialize(LocalDateTime src, Type typeOfSrc, JsonSerializationContext context) {
@@ -0,0 +1,57 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import java.util.ArrayList;
import java.util.List;
class ServerErrorWithDetails extends PlaywrightException {
private final JsonObject errorDetails;
private final JsonArray log;
ServerErrorWithDetails(PlaywrightException cause, JsonObject errorDetails, JsonArray log) {
super(cause.getMessage(), cause);
this.errorDetails = errorDetails;
this.log = log;
}
// Rethrown with the calling thread's stack trace, see WaitableResult.get().
ServerErrorWithDetails(ServerErrorWithDetails cause) {
super(cause.getMessage(), cause);
this.errorDetails = cause.errorDetails;
this.log = cause.log;
}
JsonObject errorDetails() {
return errorDetails;
}
List<String> log() {
List<String> result = new ArrayList<>();
if (log != null) {
for (JsonElement e : log) {
result.add(e.getAsString());
}
}
return result;
}
}
@@ -41,7 +41,8 @@ public class WaitForEventLogger<T> implements Supplier<T>, Logger {
{
JsonObject info = new JsonObject();
info.addProperty("phase", "before");
sendWaitForEventInfo(info);
info.addProperty("event", "");
sendWaitInfo(info);
}
JsonObject info = new JsonObject();
info.addProperty("phase", "after");
@@ -51,7 +52,7 @@ public class WaitForEventLogger<T> implements Supplier<T>, Logger {
info.addProperty("error", e.getMessage());
throw e;
} finally {
sendWaitForEventInfo(info);
sendWaitInfo(info);
}
}
@@ -61,14 +62,15 @@ public class WaitForEventLogger<T> implements Supplier<T>, Logger {
JsonObject info = new JsonObject();
info.addProperty("phase", "log");
info.addProperty("message", message);
sendWaitForEventInfo(info);
sendWaitInfo(info);
}
private void sendWaitForEventInfo(JsonObject info) {
info.addProperty("event", "");
private void sendWaitInfo(JsonObject info) {
info.addProperty("waitId", waitId);
JsonObject params = new JsonObject();
params.add("info", info);
channel.sendMessageAsync("waitForEventInfo", params);
try {
channel.sendMessageNoReply("__waitInfo__", info);
} catch (RuntimeException e) {
// Fire-and-forget: never throw to the caller.
}
}
}
@@ -52,6 +52,8 @@ class WaitableResult<T> implements Waitable<T> {
throw new TimeoutError(exception.getMessage(), exception);
} if (exception instanceof TargetClosedError) {
throw new TargetClosedError(exception.getMessage(), exception);
} if (exception instanceof ServerErrorWithDetails) {
throw new ServerErrorWithDetails((ServerErrorWithDetails) exception);
}
throw new PlaywrightException(exception.getMessage(), exception);
}
@@ -0,0 +1,77 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.WebStorage;
import com.microsoft.playwright.options.WebStorageItem;
import java.util.List;
import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT;
import static com.microsoft.playwright.impl.Serialization.gson;
import static java.util.Arrays.asList;
class WebStorageImpl implements WebStorage {
private final PageImpl page;
private final String kind;
WebStorageImpl(PageImpl page, String kind) {
this.page = page;
this.kind = kind;
}
private JsonObject createParams() {
JsonObject params = new JsonObject();
params.addProperty("kind", kind);
return params;
}
@Override
public List<WebStorageItem> items() {
JsonObject json = page.sendMessage("webStorageItems", createParams(), NO_TIMEOUT).getAsJsonObject();
return asList(gson().fromJson(json.getAsJsonArray("items"), WebStorageItem[].class));
}
@Override
public String getItem(String name) {
JsonObject params = createParams();
params.addProperty("name", name);
JsonObject json = page.sendMessage("webStorageGetItem", params, NO_TIMEOUT).getAsJsonObject();
return json.has("value") ? json.get("value").getAsString() : null;
}
@Override
public void setItem(String name, String value) {
JsonObject params = createParams();
params.addProperty("name", name);
params.addProperty("value", value);
page.sendMessage("webStorageSetItem", params, NO_TIMEOUT);
}
@Override
public void removeItem(String name) {
JsonObject params = createParams();
params.addProperty("name", name);
page.sendMessage("webStorageRemoveItem", params, NO_TIMEOUT);
}
@Override
public void clear() {
page.sendMessage("webStorageClear", createParams(), NO_TIMEOUT);
}
}
@@ -0,0 +1,22 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public enum ScreencastCursor {
NONE,
POINTER
}
@@ -18,11 +18,11 @@ package com.microsoft.playwright.options;
public class Size {
/**
* Video frame width.
* Max frame width in pixels.
*/
public int width;
/**
* Video frame height.
* Max frame height in pixels.
*/
public int height;
@@ -0,0 +1,41 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class VirtualCredential {
/**
* Base64url-encoded credential id.
*/
public String id;
/**
* Relying party id.
*/
public String rpId;
/**
* Base64url-encoded user handle.
*/
public String userHandle;
/**
* Base64url-encoded PKCS#8 (DER) private key.
*/
public String privateKey;
/**
* Base64url-encoded SPKI (DER) public key.
*/
public String publicKey;
}
@@ -0,0 +1,23 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class WebStorageItem {
public String name;
public String value;
}
@@ -131,8 +131,9 @@ public class TestBrowserContextCDPSession extends TestBase {
CDPSession session = page.context().newCDPSession(page);
page.close();
PlaywrightException exception = assertThrows(PlaywrightException.class, session::detach);
assertTrue(exception.getMessage().contains("Target page, context or browser has been closed"), exception.getMessage());
// Like the upstream test, only check that detach fails the error depends on
// whether the session detached before or after the page closed.
assertThrows(PlaywrightException.class, session::detach);
context.close();
}
@@ -28,6 +28,7 @@ import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
@@ -533,6 +534,23 @@ public class TestBrowserContextFetch extends TestBase {
assertEquals("{\"date\":\"2024-07-10T18:15:30.000Z\"}", new String(body));
}
public static class LocalDateData {
public String name;
public LocalDate date;
}
@Test
void shouldSupportLocalDateInData() throws ExecutionException, InterruptedException {
APIRequestContext request = playwright.request().newContext();
LocalDateData testData = new LocalDateData();
testData.name = "foo";
testData.date = LocalDate.of(2022, 12, 23);
Future<Server.Request> serverRequest = server.futureRequest("/empty.html");
request.post(server.EMPTY_PAGE, RequestOptions.create().setData(testData));
byte[] body = serverRequest.get().postBody;
assertEquals("{\"name\":\"foo\",\"date\":\"2022-12-23\"}", new String(body));
}
@Test
void shouldSupportApplicationXWwwFormUrlencoded() throws ExecutionException, InterruptedException {
Future<Server.Request> req = server.futureRequest("/empty.html");
@@ -741,10 +759,12 @@ public class TestBrowserContextFetch extends TestBase {
});
page.evaluate("() => setTimeout(closeContext, 1000);");
PlaywrightException e = assertThrows(PlaywrightException.class, () -> context.request().get(server.EMPTY_PAGE));
assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
assertTrue(e.getMessage().contains("Request context disposed") ||
e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
e = assertThrows(PlaywrightException.class, () -> context.request().post(server.EMPTY_PAGE));
assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
assertTrue(e.getMessage().contains("Request context disposed") ||
e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
}
@Test
@@ -0,0 +1,179 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.VirtualCredential;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static com.microsoft.playwright.Utils.mapOf;
import static org.junit.jupiter.api.Assertions.*;
public class TestBrowserContextWebAuthn extends TestBase {
private static final String B64URL_TO_BYTES_JS =
" const b64UrlToBytes = s => {\n" +
" let str = s.replace(/-/g, '+').replace(/_/g, '/');\n" +
" while (str.length % 4)\n" +
" str += '=';\n" +
" const bin = atob(str);\n" +
" const u8 = new Uint8Array(bin.length);\n" +
" for (let i = 0; i < bin.length; i++)\n" +
" u8[i] = bin.charCodeAt(i);\n" +
" return u8;\n" +
" };\n";
@Test
void shouldNotInterceptNavigatorCredentialsWithoutInstall() {
// Seed a credential, but do not install the interceptor.
context.credentials().create("localhost");
page.navigate(server.EMPTY_PAGE);
Object intercepted = page.evaluate("() => globalThis.__pwWebAuthnInstalled === true");
assertEquals(false, intercepted);
}
@Test
void shouldSeedKnownCredentialAndAuthenticate() {
// This is the easiest way to create credentials. In practice, this
// probably comes from environment.
VirtualCredential known;
try (BrowserContext source = browser.newContext()) {
known = source.credentials().create("localhost");
}
// A fresh context imports the known credential and signs in with it.
context.credentials().create(known.rpId, new Credentials.CreateOptions()
.setId(known.id)
.setUserHandle(known.userHandle)
.setPrivateKey(known.privateKey)
.setPublicKey(known.publicKey));
context.credentials().install();
page.navigate(server.EMPTY_PAGE);
Map<String, Object> result = (Map<String, Object>) page.evaluate(
"async ({ rpId, credentialId }) => {\n" +
B64URL_TO_BYTES_JS +
" const challenge = crypto.getRandomValues(new Uint8Array(32));\n" +
" const cred = await navigator.credentials.get({\n" +
" publicKey: {\n" +
" challenge,\n" +
" rpId,\n" +
" allowCredentials: [{ type: 'public-key', id: b64UrlToBytes(credentialId) }],\n" +
" userVerification: 'preferred',\n" +
" },\n" +
" });\n" +
" const resp = cred.response;\n" +
" return {\n" +
" id: cred.id,\n" +
" type: cred.type,\n" +
" hasClientData: resp.clientDataJSON.byteLength > 0,\n" +
" hasAuthData: resp.authenticatorData.byteLength > 0,\n" +
" hasSignature: resp.signature.byteLength > 0,\n" +
" authDataFlags: new Uint8Array(resp.authenticatorData)[32],\n" +
" };\n" +
"}", mapOf("rpId", "localhost", "credentialId", known.id));
assertEquals(known.id, result.get("id"));
assertEquals("public-key", result.get("type"));
assertEquals(true, result.get("hasClientData"));
assertEquals(true, result.get("hasAuthData"));
assertEquals(true, result.get("hasSignature"));
// UP (0x01) | UV (0x04) = 0x05
assertEquals(0x05, ((Number) result.get("authDataFlags")).intValue() & 0x05);
// After the credential is deleted, the page can no longer authenticate with it.
context.credentials().delete(known.id);
assertEquals(0, context.credentials().get().size());
Object error = page.evaluate(
"async ({ rpId, credentialId }) => {\n" +
B64URL_TO_BYTES_JS +
" const challenge = crypto.getRandomValues(new Uint8Array(32));\n" +
" try {\n" +
" await navigator.credentials.get({\n" +
" publicKey: {\n" +
" challenge,\n" +
" rpId,\n" +
" allowCredentials: [{ type: 'public-key', id: b64UrlToBytes(credentialId) }],\n" +
" },\n" +
" });\n" +
" return 'no-error';\n" +
" } catch (e) {\n" +
" return e.name;\n" +
" }\n" +
"}", mapOf("rpId", "localhost", "credentialId", known.id));
assertEquals("NotAllowedError", error);
}
@Test
void shouldCapturePageCreatedCredentialAndReuseItInAnotherContext() {
// Setup context: the app registers a passkey via navigator.credentials.create().
String createdId;
VirtualCredential captured;
try (BrowserContext setupContext = browser.newContext()) {
setupContext.credentials().install();
Page setupPage = setupContext.newPage();
setupPage.navigate(server.EMPTY_PAGE);
createdId = (String) setupPage.evaluate(
"async ({ rpId }) => {\n" +
" const challenge = crypto.getRandomValues(new Uint8Array(32));\n" +
" const created = await navigator.credentials.create({\n" +
" publicKey: {\n" +
" challenge,\n" +
" rp: { id: rpId, name: 'Test RP' },\n" +
" user: { id: new Uint8Array([1, 2, 3, 4]), name: 'u', displayName: 'User' },\n" +
" pubKeyCredParams: [{ type: 'public-key', alg: -7 }],\n" +
" authenticatorSelection: { residentKey: 'required', userVerification: 'preferred' },\n" +
" },\n" +
" });\n" +
" return created.id;\n" +
"}", mapOf("rpId", "localhost"));
List<VirtualCredential> credentials = setupContext.credentials().get(
new Credentials.GetOptions().setRpId("localhost"));
assertEquals(1, credentials.size());
captured = credentials.get(0);
assertEquals(createdId, captured.id);
assertTrue(captured.privateKey.matches("^[A-Za-z0-9_-]+$"), captured.privateKey);
assertTrue(captured.publicKey.matches("^[A-Za-z0-9_-]+$"), captured.publicKey);
}
// Reuse the captured passkey in a fresh context and sign in with it.
context.credentials().create(captured.rpId, new Credentials.CreateOptions()
.setId(captured.id)
.setUserHandle(captured.userHandle)
.setPrivateKey(captured.privateKey)
.setPublicKey(captured.publicKey));
context.credentials().install();
page.navigate(server.EMPTY_PAGE);
Object gotId = page.evaluate(
"async ({ rpId }) => {\n" +
" const challenge = crypto.getRandomValues(new Uint8Array(32));\n" +
" // No allowCredentials — relies on the re-seeded credential being discoverable.\n" +
" const cred = await navigator.credentials.get({\n" +
" publicKey: { challenge, rpId, userVerification: 'preferred' },\n" +
" });\n" +
" return cred.id;\n" +
"}", mapOf("rpId", "localhost"));
assertEquals(createdId, gotId);
}
}
@@ -40,10 +40,14 @@ public class TestBrowserTypeBasic extends TestBase {
assertEquals(getBrowserNameFromEnv(), browserType.name());
}
static boolean isChromiumOrWebKit() {
return isChromium() || isWebKit();
}
@Test
@DisabledIf(value="com.microsoft.playwright.TestBase#isChromium", disabledReason="Non-chromium behavior")
@DisabledIf(value="isChromiumOrWebKit", disabledReason="Connecting over CDP is supported in Chromium and WebKit")
void shouldThrowWhenTryingToConnectWithNotChromium() {
PlaywrightException e = assertThrows(PlaywrightException.class, () -> browserType.connectOverCDP("foo"));
assertTrue(e.getMessage().contains("Connecting over CDP is only supported in Chromium."));
assertTrue(e.getMessage().contains("Connecting over CDP is only supported in Chromium and WebKit."));
}
}
@@ -259,7 +259,11 @@ public class TestBrowserTypeConnect extends TestBase {
}
assertFalse(browser.isConnected());
PlaywrightException e = assertThrows(PlaywrightException.class, () -> page.waitForNavigation(() -> {}));
assertTrue(e.getMessage().contains("Browser closed") || e.getMessage().contains("Page closed") || e.getMessage().contains("Browser has been closed"), e.getMessage());
// The surfaced message depends on which call hits the closed connection first.
assertTrue(e.getMessage().contains("Browser closed") ||
e.getMessage().contains("Page closed") ||
e.getMessage().contains("Browser has been closed") ||
e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
}
@Test
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
import org.junit.jupiter.api.io.CleanupMode;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
@@ -223,7 +224,9 @@ public class TestClientCertificates extends TestBase {
@Test
@DisabledIf(value="com.microsoft.playwright.TestClientCertificates#isWebKitMacOS", disabledReason="The network connection was lost.")
public void shouldWorkWithBrowserLaunchPersistentContext(@TempDir Path tmpDir) {
// No cleanup: on Windows Chromium may keep chrome_debug.log in the user data dir
// locked briefly after close, failing the deletion.
public void shouldWorkWithBrowserLaunchPersistentContext(@TempDir(cleanup = CleanupMode.NEVER) Path tmpDir) {
BrowserType.LaunchPersistentContextOptions options = new BrowserType.LaunchPersistentContextOptions()
.setIgnoreHTTPSErrors(true) // TODO: remove once we can pass a custom CA.
.setClientCertificates(asList(
@@ -22,6 +22,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
import org.junit.jupiter.api.io.CleanupMode;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
@@ -46,7 +47,9 @@ public class TestDefaultBrowserContext2 extends TestBase {
private BrowserContext persistentContext;
@TempDir Path tempDir;
// No cleanup: on Windows Chromium may keep chrome_debug.log in the user data dir
// locked briefly after close, failing the deletion.
@TempDir(cleanup = CleanupMode.NEVER) Path tempDir;
@AfterEach
void closePersistentContext() {
@@ -22,6 +22,7 @@ import com.microsoft.playwright.options.HttpCredentials;
import com.microsoft.playwright.options.HttpCredentialsSend;
import com.microsoft.playwright.options.HttpHeader;
import com.microsoft.playwright.options.RequestOptions;
import com.microsoft.playwright.options.ServerAddr;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -252,6 +253,25 @@ public class TestGlobalFetch extends TestBase {
assertEquals(200, response.status());
}
@Test
void shouldReturnServerAddressFromResponse() {
APIRequestContext request = playwright.request().newContext();
APIResponse response = request.get(server.EMPTY_PAGE);
ServerAddr address = response.serverAddr();
assertNotNull(address);
assertEquals(server.PORT, address.port);
assertTrue(asList("127.0.0.1", "::1").contains(address.ipAddress), address.ipAddress);
request.dispose();
}
@Test
void shouldReturnNullSecurityDetailsForHttpResponse() {
APIRequestContext request = playwright.request().newContext();
APIResponse response = request.get(server.EMPTY_PAGE);
assertNull(response.securityDetails());
request.dispose();
}
@Test
void shouldResolveUrlRelativeToGobalBaseURLOption() {
APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions().setBaseURL(server.PREFIX));
@@ -30,7 +30,7 @@ public class TestPageEventPageError extends TestBase {
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" +
" await new Promise(f => window.setTimeout(f, 2000));\n" +
" }");
List<String> errors = page.pageErrors();
@@ -122,12 +122,12 @@ public class TestScreencast extends TestBase {
}
@Test
void onFrameShouldReceiveViewportSize() {
void onFrameShouldReceiveViewportSizeAndTimestamp() {
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setViewportSize(1000, 400));
Page page = context.newPage();
try {
List<ScreencastFrame> frames = new ArrayList<>();
page.screencast().start(new Screencast.StartOptions().setOnFrame(frames::add));
page.screencast().start(new Screencast.StartOptions().setOnFrame(frames::add).setSize(500, 400));
page.navigate(server.EMPTY_PAGE);
page.evaluate("() => document.body.style.backgroundColor = 'red'");
page.waitForTimeout(500);
@@ -136,6 +136,7 @@ public class TestScreencast extends TestBase {
for (ScreencastFrame frame : frames) {
assertEquals(1000, frame.viewportWidth());
assertEquals(400, frame.viewportHeight());
assertTrue(frame.timestamp() > 0, "expected a positive timestamp, got " + frame.timestamp());
}
} finally {
context.close();
@@ -51,6 +51,20 @@ public class TestSelectorsGetBy extends TestBase {
assertThat(page.locator("div").getByTestId("Hello")).hasText("Hello world");
}
@Test
void getByTestIdWithCommaSeparatedTestIdAttributesShouldMatchAny() {
page.setContent("<section>\n" +
" <div data-pw='Hello'>first</div>\n" +
" <div data-ti='Hello'>second</div>\n" +
" <div data-testid='Hello'>third</div>\n" +
"</section>");
playwright.selectors().setTestIdAttribute("data-pw,data-ti");
assertThat(page.getByTestId("Hello")).hasCount(2);
assertThat(page.getByTestId("Hello")).hasText(new String[]{"first", "second"});
assertThat(page.mainFrame().getByTestId("Hello")).hasCount(2);
assertThat(page.locator("section").getByTestId("Hello")).hasCount(2);
}
@Test
void shouldUseDataTestidInStrictErrors() {
playwright.selectors().setTestIdAttribute("data-custom-id");
@@ -0,0 +1,132 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.WebStorageItem;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.microsoft.playwright.Utils.mapOf;
import static org.junit.jupiter.api.Assertions.*;
public class TestWebStorage extends TestBase {
private static Map<String, String> asMap(List<WebStorageItem> items) {
Map<String, String> map = new HashMap<>();
for (WebStorageItem item : items) {
map.put(item.name, item.value);
}
return map;
}
@Test
void localStorageItemsReturnsEmptyListOnFreshOrigin() {
page.navigate(server.EMPTY_PAGE);
assertEquals(0, page.localStorage().items().size());
}
@Test
void localStorageGetItemReturnsNullForMissingKey() {
page.navigate(server.EMPTY_PAGE);
assertNull(page.localStorage().getItem("absent"));
}
@Test
void localStorageSetItemPersistsAndSurfacesInItemsAndGetItem() {
page.navigate(server.EMPTY_PAGE);
page.localStorage().setItem("alpha", "1");
page.localStorage().setItem("beta", "2");
assertEquals(mapOf("alpha", "1", "beta", "2"), asMap(page.localStorage().items()));
assertEquals("1", page.localStorage().getItem("alpha"));
assertEquals("1", page.evaluate("() => localStorage.getItem('alpha')"));
}
@Test
void localStorageSetItemOverwritesExistingValue() {
page.navigate(server.EMPTY_PAGE);
page.localStorage().setItem("k", "first");
page.localStorage().setItem("k", "second");
assertEquals("second", page.localStorage().getItem("k"));
}
@Test
void localStorageRemoveItemRemovesSingleItem() {
page.navigate(server.EMPTY_PAGE);
page.localStorage().setItem("a", "1");
page.localStorage().setItem("b", "2");
page.localStorage().removeItem("a");
assertEquals(mapOf("b", "2"), asMap(page.localStorage().items()));
}
@Test
void localStorageClearEmptiesStorage() {
page.navigate(server.EMPTY_PAGE);
page.localStorage().setItem("a", "1");
page.localStorage().setItem("b", "2");
page.localStorage().clear();
assertEquals(0, page.localStorage().items().size());
}
@Test
void sessionStorageRoundTrip() {
page.navigate(server.EMPTY_PAGE);
assertEquals(0, page.sessionStorage().items().size());
page.sessionStorage().setItem("s1", "v1");
page.sessionStorage().setItem("s2", "v2");
assertEquals(mapOf("s1", "v1", "s2", "v2"), asMap(page.sessionStorage().items()));
assertEquals("v1", page.sessionStorage().getItem("s1"));
page.sessionStorage().removeItem("s1");
assertEquals(mapOf("s2", "v2"), asMap(page.sessionStorage().items()));
page.sessionStorage().clear();
assertEquals(0, page.sessionStorage().items().size());
}
@Test
void localStorageAndSessionStorageAreIndependent() {
page.navigate(server.EMPTY_PAGE);
page.localStorage().setItem("shared", "local");
page.sessionStorage().setItem("shared", "session");
assertEquals("local", page.localStorage().getItem("shared"));
assertEquals("session", page.sessionStorage().getItem("shared"));
page.localStorage().clear();
assertEquals(0, page.localStorage().items().size());
assertEquals("session", page.sessionStorage().getItem("shared"));
}
@Test
void storageMethodsAreScopedToTheCurrentOrigin() {
page.navigate(server.PREFIX + "/empty.html");
page.localStorage().setItem("k", "origin-1");
page.navigate(server.CROSS_PROCESS_PREFIX + "/empty.html");
assertEquals(0, page.localStorage().items().size());
page.localStorage().setItem("k", "origin-2");
page.navigate(server.PREFIX + "/empty.html");
assertEquals("origin-1", page.localStorage().getItem("k"));
}
}
@@ -132,6 +132,30 @@ public class TestInstall {
}
@Test
void canInstallDriverToDirectoryAndReuseIt(@TempDir Path tmpDir) throws Exception {
Path driverDir = tmpDir.resolve("driver");
DriverJar.installDriverTo(driverDir);
// The directory is self-contained: the playwright-core package and the Node.js binary.
assertTrue(Files.exists(driverDir.resolve("package").resolve("cli.js")));
assertTrue(Files.exists(driverDir.resolve(isWindows() ? "node.exe" : "node")));
// Pointing playwright.cli.dir at it must reuse it as-is, without extracting to a temp directory.
System.setProperty("playwright.cli.dir", driverDir.toString());
Driver driver = Driver.createAndInstall(Collections.emptyMap(), false);
assertEquals(driverDir, driver.driverDir());
ProcessBuilder pb = driver.createProcessBuilder();
pb.command().add("--version");
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
Path out = tmpDir.resolve("out.txt");
pb.redirectOutput(out.toFile());
Process p = pb.start();
assertTrue(p.waitFor(1, TimeUnit.MINUTES), "Timed out waiting for version to be printed");
String stdout = new String(Files.readAllBytes(out), StandardCharsets.UTF_8);
assertTrue(stdout.contains("Version "), stdout);
}
private static String extractNodeJsToTemp() throws URISyntaxException, IOException {
DriverJar auxDriver = new DriverJar();
auxDriver.extractDriverToTempDir();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 39 KiB

+3 -3
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.61.0</version>
<packaging>pom</packaging>
<name>Playwright Parent Project</name>
<description>Java library to automate Chromium, Firefox and WebKit with a single API.
@@ -48,7 +48,7 @@
<junit.version>5.14.1</junit.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<websocket.version>1.6.0</websocket.version>
<slf4j.version>2.0.17</slf4j.version>
<slf4j.version>2.0.18</slf4j.version>
<opentest4j.version>1.3.0</opentest4j.version>
</properties>
@@ -159,7 +159,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.5</version>
<version>3.5.6</version>
<configuration>
<properties>
<configurationParameters>
+1 -1
View File
@@ -1 +1 @@
1.60.0
1.61.1
+79 -31
View File
@@ -8,8 +8,12 @@ cd "$(dirname $0)"
if [[ ($1 == '-h') || ($1 == '--help') ]]; then
echo ""
echo "This script for downloading playwright driver for all platforms."
echo "The downloaded files will be put under 'driver-bundle/src/main/resources/driver'."
echo "This script downloads and assembles the Playwright driver for all platforms."
echo "The platform-independent 'playwright-core' npm package is assembled once into the driver"
echo "module ('driver/src/main/resources/driver/package'), and the matching Node.js binary from"
echo "https://nodejs.org for each platform goes into the driver-bundle module"
echo "('driver-bundle/src/main/resources/driver/<platform>'), the same way the upstream"
echo "Playwright build does it."
echo ""
echo "Usage: scripts/download_driver.sh [option]"
echo ""
@@ -19,45 +23,89 @@ if [[ ($1 == '-h') || ($1 == '--help') ]]; then
exit 0
fi
# Ubuntu 24.04-arm64 emulated via qemu has a bug, so we prefer wget over curl.
# See https://github.com/microsoft/playwright-java/issues/1678.
download() {
local url=$1
local out=$2
echo "Downloading $url"
if command -v wget &> /dev/null; then
wget -q -O "$out" "$url"
else
curl --retry 5 --retry-delay 2 -fL -o "$out" "$url"
fi
}
DRIVER_VERSION=$(head -1 ./DRIVER_VERSION)
FILE_PREFIX=playwright-$DRIVER_VERSION
cd ../driver-bundle/src/main/resources
if [[ -d 'driver' ]]; then
echo "Deleting existing drivers from $(pwd)"
rm -rf driver
# Resolve the exact upstream commit that produced this driver version, so that the
# bundled Node.js version matches the driver exactly.
GIT_HEAD=$(npm view playwright@"$DRIVER_VERSION" gitHead)
if [[ -z "$GIT_HEAD" ]]; then
echo "Failed to resolve upstream commit (gitHead) for playwright@$DRIVER_VERSION"
exit 1
fi
mkdir -p driver
cd driver
# The Node.js version is kept in sync with the driver version in the upstream build script.
NODE_VERSION=$(curl -fsSL "https://raw.githubusercontent.com/microsoft/playwright/$GIT_HEAD/utils/build/build-playwright-driver.sh" \
| sed -n 's/^NODE_VERSION="\([^"]*\)".*/\1/p')
if [[ -z "$NODE_VERSION" ]]; then
echo "Failed to determine Node.js version for playwright@$DRIVER_VERSION ($GIT_HEAD)"
exit 1
fi
for PLATFORM in mac mac-arm64 linux linux-arm64 win32_x64
echo "Driver version: $DRIVER_VERSION"
echo "Upstream commit: $GIT_HEAD"
echo "Node.js version: $NODE_VERSION"
# The platform-independent driver code (playwright-core) is assembled once into the driver module;
# the Node.js binary for each platform is assembled into the driver-bundle module. See issue #1196.
ROOT="$(cd .. && pwd)"
CORE_DEST="$ROOT/driver/src/main/resources/driver"
NODE_DEST="$ROOT/driver-bundle/src/main/resources/driver"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
# 1. playwright-core package -> driver module (once, shared by every platform).
echo "Assembling playwright-core package to $CORE_DEST/package"
rm -rf "$CORE_DEST/package"
mkdir -p "$CORE_DEST"
CORE_TGZ="$TMP_DIR/playwright-core-$DRIVER_VERSION.tgz"
download "https://registry.npmjs.org/playwright-core/-/playwright-core-$DRIVER_VERSION.tgz" "$CORE_TGZ"
# The npm tarball has a top-level package/ directory, so this creates $CORE_DEST/package.
tar -xzf "$CORE_TGZ" -C "$CORE_DEST"
rm -f "$CORE_TGZ"
# 2. Node.js binary for each platform -> driver-bundle module.
# <java platform dir>:<nodejs platform suffix>:<archive extension>
for ENTRY in \
"mac:darwin-x64:tar.gz" \
"mac-arm64:darwin-arm64:tar.gz" \
"linux:linux-x64:tar.gz" \
"linux-arm64:linux-arm64:tar.gz" \
"win32_x64:win-x64:zip"
do
FILE_NAME=$FILE_PREFIX-$PLATFORM.zip
mkdir $PLATFORM
cd $PLATFORM
echo "Downloading driver for $PLATFORM to $(pwd)"
IFS=':' read -r PLATFORM NODE_SUFFIX ARCHIVE <<< "$ENTRY"
DEST="$NODE_DEST/$PLATFORM"
echo "Assembling Node.js for $PLATFORM to $DEST"
rm -rf "$DEST"
mkdir -p "$DEST"
URL=https://cdn.playwright.dev/builds/driver
if [[ "$DRIVER_VERSION" == *-alpha* || "$DRIVER_VERSION" == *-beta* || "$DRIVER_VERSION" == *-next* ]]; then
URL=$URL/next
fi
URL=$URL/$FILE_NAME
echo "Using url: $URL"
# Ubuntu 24.04-arm64 emulated via qemu has a bug, so we prefer wget over curl.
# See https://github.com/microsoft/playwright-java/issues/1678.
if command -v wget &> /dev/null; then
wget $URL
# Node.js binary and its license from the official Node.js distribution.
NODE_DIR="node-v$NODE_VERSION-$NODE_SUFFIX"
NODE_ARCHIVE="$TMP_DIR/$NODE_DIR.$ARCHIVE"
download "https://nodejs.org/dist/v$NODE_VERSION/$NODE_DIR.$ARCHIVE" "$NODE_ARCHIVE"
if [[ $ARCHIVE == "zip" ]]; then
unzip -joq "$NODE_ARCHIVE" "$NODE_DIR/node.exe" -d "$DEST"
unzip -joq "$NODE_ARCHIVE" "$NODE_DIR/LICENSE" -d "$DEST"
else
curl --retry 5 --retry-delay 2 -fL -O $URL
tar -xzf "$NODE_ARCHIVE" -C "$DEST" --strip-components=2 "$NODE_DIR/bin/node"
tar -xzf "$NODE_ARCHIVE" -C "$DEST" --strip-components=1 "$NODE_DIR/LICENSE"
fi
unzip $FILE_NAME -d .
rm $FILE_NAME
cd -
rm -f "$NODE_ARCHIVE"
done
echo ""
echo "All drivers have been successfully downloaded."
echo "All drivers have been successfully assembled."
echo ""
+31 -18
View File
@@ -6,26 +6,39 @@ set +x
trap 'cd $(pwd -P)' EXIT
cd "$(dirname "$0")/.."
PLAYWRIGHT_CLI="unknown"
case $(uname) in
Darwin)
PLAYWRIGHT_CLI=./driver-bundle/src/main/resources/driver/mac/package/cli.js
;;
Linux)
PLAYWRIGHT_CLI=./driver-bundle/src/main/resources/driver/linux/package/cli.js
;;
MINGW64*)
PLAYWRIGHT_CLI=./driver-bundle/src/main/resources/driver/win32_x64/package/cli.js
;;
*)
echo "Unknown platform '$(uname)'"
exit 1;
;;
esac
DRIVER_VERSION=$(head -1 ./scripts/DRIVER_VERSION)
echo "Updating api.json from $($PLAYWRIGHT_CLI --version)"
# api.json is generated from the upstream Playwright source at the exact commit
# that produced this driver version. Set PW_SRC_DIR to reuse an existing upstream
# checkout, otherwise a minimal one is fetched into a temporary directory.
GIT_HEAD=$(npm view playwright@"$DRIVER_VERSION" gitHead)
if [[ -z "$GIT_HEAD" ]]; then
echo "Failed to resolve upstream commit (gitHead) for playwright@$DRIVER_VERSION"
exit 1
fi
node $PLAYWRIGHT_CLI print-api-json > ./tools/api-generator/src/main/resources/api.json
CLONED_UPSTREAM=""
if [[ -n "$PW_SRC_DIR" ]]; then
UPSTREAM_DIR="$PW_SRC_DIR"
echo "Using upstream Playwright checkout at $UPSTREAM_DIR (PW_SRC_DIR)"
else
UPSTREAM_DIR=$(mktemp -d)
CLONED_UPSTREAM="$UPSTREAM_DIR"
echo "Fetching upstream Playwright source at $GIT_HEAD"
# generateApiJson.js only needs utils/ and docs/, so fetch just those.
git clone --quiet --filter=blob:none --no-checkout https://github.com/microsoft/playwright.git "$UPSTREAM_DIR"
git -C "$UPSTREAM_DIR" sparse-checkout init --cone
git -C "$UPSTREAM_DIR" sparse-checkout set utils docs
git -C "$UPSTREAM_DIR" checkout --quiet "$GIT_HEAD"
fi
echo "Updating api.json from upstream playwright@$DRIVER_VERSION ($GIT_HEAD)"
API_JSON_MODE=1 node "$UPSTREAM_DIR/utils/doclint/generateApiJson.js" \
> ./tools/api-generator/src/main/resources/api.json
if [[ -n "$CLONED_UPSTREAM" ]]; then
rm -rf "$CLONED_UPSTREAM"
fi
mvn compile -f ./tools/api-generator --no-transfer-progress
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>api-generator</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.61.0</version>
<name>Playwright - API Generator</name>
<description>
This is an internal module used to generate Java API from the upstream Playwright
@@ -997,10 +997,11 @@ class Interface extends TypeDefinition {
}
void writeTo(List<String> output, String offset) {
if (methods.stream().anyMatch(m -> "create".equals(m.jsonName))) {
// Interfaces with a static factory method, see Method.writeTo.
if (asList("Playwright", "FormData", "RequestOptions").contains(jsonName) && methods.stream().anyMatch(m -> "create".equals(m.jsonName))) {
output.add("import com.microsoft.playwright.impl." + jsonName + "Impl;");
}
if (asList("Page", "Request", "Response", "APIRequestContext", "APIRequest", "APIResponse", "FileChooser", "Frame", "FrameLocator", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard", "Tracing", "Video", "Debugger", "Screencast", "WebError").contains(jsonName)) {
if (asList("Page", "Request", "Response", "APIRequestContext", "APIRequest", "APIResponse", "FileChooser", "Frame", "FrameLocator", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard", "Tracing", "Video", "Debugger", "Screencast", "WebError", "Credentials", "WebStorage").contains(jsonName)) {
output.add("import com.microsoft.playwright.options.*;");
}
if ("Download".equals(jsonName)) {
@@ -1012,7 +1013,7 @@ class Interface extends TypeDefinition {
if ("Clock".equals(jsonName)) {
output.add("import java.util.Date;");
}
if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright", "Debugger", "Screencast", "WebSocketRoute").contains(jsonName)) {
if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright", "Debugger", "Screencast", "WebSocketRoute", "Credentials", "WebStorage").contains(jsonName)) {
output.add("import java.util.*;");
}
if (asList("WebSocketRoute").contains(jsonName)) {
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-cli-fatjar</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.61.0</version>
<name>Test Playwright Command Line FatJar</name>
<properties>
<compiler.version>1.8</compiler.version>
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-cli-version</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.61.0</version>
<name>Test Playwright Command Line Version</name>
<properties>
<compiler.version>1.8</compiler.version>
@@ -10,7 +10,6 @@ cd "$(dirname $0)"
PROJECT_DIR=$(mktemp -d)
echo "Creating project in $PROJECT_DIR"
cp -R . $PROJECT_DIR
cp -R ../../driver-bundle/src/test/ $PROJECT_DIR/src/
cp -R ../../playwright/src/test/ $PROJECT_DIR/src/
cd $PROJECT_DIR
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-local-installation</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.61.0</version>
<name>Test local installation</name>
<description>Runs Playwright test suite (copied from playwright module) against locally cached Playwright</description>
<properties>
+1 -1
View File
@@ -9,7 +9,7 @@
</parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-spring-boot-starter</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.61.0</version>
<name>Test Playwright With Spring Boot</name>
<properties>
<spring.version>2.4.3</spring.version>
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>update-version</artifactId>
<version>1.50.0-SNAPSHOT</version>
<version>1.61.0</version>
<name>Playwright - Update Version in Documentation</name>
<description>
This is an internal module used to update versions in the documentation based on
+8 -2
View File
@@ -38,14 +38,19 @@ ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH}
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# Extract the Playwright driver into the image once so the library reuses it instead of unpacking
# it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268.
ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver
RUN mkdir /ms-playwright && \
mkdir /tmp/pw-java
COPY . /tmp/pw-java
RUN cd /tmp/pw-java && \
./scripts/download_driver.sh && \
mvn install -D skipTests --no-transfer-progress && \
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="install-driver" -f playwright/pom.xml --no-transfer-progress && \
DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
@@ -62,4 +67,5 @@ RUN cd /tmp/pw-java && \
else \
rm /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstwebrtc.so; \
fi && \
chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH
chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \
chmod -R 777 $PLAYWRIGHT_DRIVER_DIR
+8 -2
View File
@@ -38,14 +38,19 @@ ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH}
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# Extract the Playwright driver into the image once so the library reuses it instead of unpacking
# it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268.
ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver
RUN mkdir /ms-playwright && \
mkdir /tmp/pw-java
COPY . /tmp/pw-java
RUN cd /tmp/pw-java && \
./scripts/download_driver.sh && \
mvn install -D skipTests --no-transfer-progress && \
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="install-driver" -f playwright/pom.xml --no-transfer-progress && \
DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
@@ -53,4 +58,5 @@ RUN cd /tmp/pw-java && \
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="mark-docker-image '${DOCKER_IMAGE_NAME_TEMPLATE}'" -f playwright/pom.xml --no-transfer-progress && \
rm -rf /tmp/pw-java && \
chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH
chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \
chmod -R 777 $PLAYWRIGHT_DRIVER_DIR
+62
View File
@@ -0,0 +1,62 @@
FROM ubuntu:resolute
ARG DEBIAN_FRONTEND=noninteractive
ARG TZ=America/Los_Angeles
ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/java:v%version%-resolute"
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
# === INSTALL JDK and Maven ===
RUN apt-get update && \
apt-get install -y --no-install-recommends openjdk-25-jdk \
# Install utilities required for downloading browsers
wget \
# Install utilities required for downloading driver
unzip \
# For the MSEdge install script
gpg && \
rm -rf /var/lib/apt/lists/* && \
# Create the pwuser
useradd -m -s /bin/bash pwuser
# Ubuntu 22.04 and earlier come with Maven 3.6.3 which fails with
# Java 25, so we install latest Maven from Apache instead.
RUN VERSION=3.9.12 && \
wget -O - https://archive.apache.org/dist/maven/maven-3/$VERSION/binaries/apache-maven-$VERSION-bin.tar.gz | tar zxfv - -C /opt/ && \
ln -s /opt/apache-maven-$VERSION/bin/mvn /usr/local/bin/
ARG PW_TARGET_ARCH
ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH}
# === BAKE BROWSERS INTO IMAGE ===
# Browsers will remain downloaded in `/ms-playwright`.
# Note: make sure to set 777 to the registry so that any user can access
# registry.
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# Extract the Playwright driver into the image once so the library reuses it instead of unpacking
# it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268.
ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver
RUN mkdir /ms-playwright && \
mkdir /tmp/pw-java
COPY . /tmp/pw-java
RUN cd /tmp/pw-java && \
mvn install -D skipTests --no-transfer-progress && \
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="install-driver" -f playwright/pom.xml --no-transfer-progress && \
DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="install" -f playwright/pom.xml --no-transfer-progress && \
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="mark-docker-image '${DOCKER_IMAGE_NAME_TEMPLATE}'" -f playwright/pom.xml --no-transfer-progress && \
rm -rf /tmp/pw-java && \
chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \
chmod -R 777 $PLAYWRIGHT_DRIVER_DIR
+5 -1
View File
@@ -3,7 +3,7 @@ set -e
set +x
if [[ ($1 == '--help') || ($1 == '-h') || ($1 == '') || ($2 == '') ]]; then
echo "usage: $(basename $0) {--arm64,--amd64} {jammy,noble} playwright:localbuild-noble"
echo "usage: $(basename $0) {--arm64,--amd64} {jammy,noble,resolute} playwright:localbuild-noble"
echo
echo "Build Playwright docker image and tag it as 'playwright:localbuild-noble'."
echo "Once image is built, you can run it with"
@@ -34,4 +34,8 @@ fi
PW_TARGET_ARCH=$(echo $1 | cut -c3-)
# Assemble the driver on the host where npm is available; the Dockerfile picks
# it up via `COPY . /tmp/pw-java`.
../../scripts/download_driver.sh
docker build --platform "${PLATFORM}" --build-arg "PW_TARGET_ARCH=${PW_TARGET_ARCH}" -t "$3" -f "Dockerfile.$2" ../../
+15 -2
View File
@@ -38,6 +38,11 @@ NOBLE_TAGS=(
"v${PW_VERSION}-noble"
)
# Ubuntu 26.04
RESOLUTE_TAGS=(
"v${PW_VERSION}-resolute"
)
tag_and_push() {
local source="$1"
local target="$2"
@@ -74,8 +79,10 @@ publish_docker_images_with_arch_suffix() {
TAGS=("${JAMMY_TAGS[@]}")
elif [[ "$FLAVOR" == "noble" ]]; then
TAGS=("${NOBLE_TAGS[@]}")
elif [[ "$FLAVOR" == "resolute" ]]; then
TAGS=("${RESOLUTE_TAGS[@]}")
else
echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', or 'noble'"
echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', 'noble', or 'resolute'"
exit 1
fi
local ARCH="$2"
@@ -100,8 +107,10 @@ publish_docker_manifest () {
TAGS=("${JAMMY_TAGS[@]}")
elif [[ "$FLAVOR" == "noble" ]]; then
TAGS=("${NOBLE_TAGS[@]}")
elif [[ "$FLAVOR" == "resolute" ]]; then
TAGS=("${RESOLUTE_TAGS[@]}")
else
echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', 'noble'"
echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', 'noble', or 'resolute'"
exit 1
fi
@@ -127,3 +136,7 @@ publish_docker_manifest jammy amd64 arm64
publish_docker_images_with_arch_suffix noble amd64
publish_docker_images_with_arch_suffix noble arm64
publish_docker_manifest noble amd64 arm64
publish_docker_images_with_arch_suffix resolute amd64
publish_docker_images_with_arch_suffix resolute arm64
publish_docker_manifest resolute amd64 arm64