1
0
mirror of synced 2026-07-13 03:50:01 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Yury Semikhatsky c433c1d6e5 chore: mark 1.59.0 (#1909) 2026-04-09 14:14:15 -07:00
114 changed files with 561 additions and 3332 deletions
@@ -1,76 +0,0 @@
---
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.
+41 -39
View File
@@ -7,45 +7,10 @@ Help the user roll to a new version of Playwright.
ROLLING.md contains general instructions and scripts.
Start with running ./scripts/roll_driver.sh to update the version and generate the API to see the state of things.
Afterwards, walk through the upstream changes that affect the Java client and port the relevant ones.
## Determining what to port
List the upstream commits that touched a client-relevant path since the last release. The paths cover everything that can change the public Java surface or the wire protocol:
- `docs/src/api/` — the source of truth for `api.json`. Method/option additions, removals, and `langs:` filter changes flow from here.
- `packages/playwright-core/src/client/` — the JS client implementation that the Java client mirrors.
- `packages/isomorphic/` — selector engines, locator generation/parsing, and aria-snapshot logic shared between client and server. Changes here can affect client-side helpers like `getByRoleSelector`.
- `packages/playwright/src/matchers/matchers.ts` — assertion-method definitions. Changes here usually correspond to new options on `LocatorAssertions` / `PageAssertions`.
- `packages/protocol/src/protocol.yml` — the wire protocol schema. Method/event additions, parameter renames, and result-shape changes affect what the Java `*Impl` classes need to send/receive.
```bash
cd ~/playwright
PREV_TAG=$(git tag | grep -E '^v1\.[0-9]+\.[0-9]+$' | sort -V | tail -1) # e.g. v1.59.1
git log "$PREV_TAG"..HEAD --oneline -- \
'docs/src/api/' \
'packages/playwright-core/src/client/' \
'packages/isomorphic/' \
'packages/playwright/src/matchers/matchers.ts' \
'packages/protocol/src/protocol.yml'
```
Walk that list top-to-bottom (oldest-first is easier — newest is at top, so reverse). For each commit:
1. Read the commit (`git show <sha>`) to see what client/protocol/docs changed.
2. If it's JS-internal (bundling, dispatcher conventions, electron, mcp, dashboard, trace-viewer, test-runner) — skip.
3. If it touches `docs/src/api/` or types, check `langs:` annotations — features marked `langs: js`/`langs: js, python` don't apply to Java.
4. If it adds/changes a public API method or option that applies to Java, port it. The api.json regenerated by `roll_driver.sh` already contains the new types/options, so the generated Java interfaces usually pick them up automatically — what's typically missing is the `*Impl` wiring.
5. Watch for follow-up reverts — a "feat: X" commit might be undone by a later "Revert X". Check whether the change still exists in HEAD before porting.
6. Maintain a running notes file (e.g. `/tmp/roll-notes.md`) listing each upstream PR as ported / skipped / verified-already-supported, with a one-line reason. This file becomes the body of the eventual PR.
## What to include in the rolling PR
- Driver version bump
- Generated interface diffs from `roll_driver.sh`
- `*Impl` wiring for each ported feature
- Generator updates (import lists, special-cases) if new types appeared
- A small test per new public API surface — listener for new events, basic call for new methods, regression for changed return types
- PR description: list each upstream PR ported, each skipped (with reason), and each verified-already-supported
Afterwards, work through the list of changes that need to be backported.
You can find a list of pull requests that might need to be taking into account in the issue titled "Backport changes".
Work through them one-by-one and check off the items that you have handled.
Not all of them will be relevant, some might have partially been reverted, etc. - so feel free to check with the upstream release branch.
Rolling includes:
- updating client implementation to match changes in the upstream JS implementation (see ../playwright/packages/playwright-core/src/client)
@@ -164,3 +129,40 @@ 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 (`../`).
- When updating checkboxes, store the issue content into /tmp and edit it there, then update the issue based on the file
- use the "gh" cli to interact with GitHub
-8
View File
@@ -18,14 +18,6 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
browser: [chromium, firefox, webkit]
exclude:
# macos-latest is the free M1 runner (3 vCPU / 7 GB); WebKit needs more headroom.
# Upstream's webkit matrix runs on macos-15-xlarge for the same reason.
- os: macos-latest
browser: webkit
include:
- os: macos-15-xlarge
browser: webkit
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
+7 -32
View File
@@ -26,51 +26,26 @@ jobs:
strategy:
fail-fast: false
matrix:
flavor: [jammy, noble, resolute]
flavor: [jammy, noble]
runs-on: [ubuntu-24.04, ubuntu-24.04-arm]
include:
- runs-on: ubuntu-24.04
arch: amd64
- runs-on: ubuntu-24.04-arm
arch: arm64
steps:
- uses: actions/checkout@v6
- name: Build Docker image
run: |
bash utils/docker/build.sh --${{ matrix.arch }} ${{ matrix.flavor }} playwright-java:localbuild-${{ matrix.flavor }}
ARCH="${{ matrix.runs-on == 'ubuntu-24.04-arm' && 'arm64' || 'amd64' }}"
bash utils/docker/build.sh --$ARCH ${{ matrix.flavor }} playwright-java:localbuild-${{ matrix.flavor }}
- name: Start container
run: |
CONTAINER_ID=$(docker run \
--rm \
--name playwright-docker-test \
--platform linux/${{ matrix.arch }} \
--user=pwuser \
--workdir /home/pwuser \
--shm-size=2g \
-e CI \
-e PW_MAX_RETRIES \
-d -t \
playwright-java:localbuild-${{ matrix.flavor }} /bin/bash)
CONTAINER_ID=$(docker run --rm -e CI -e PW_MAX_RETRIES --ipc=host -v "$(pwd)":/root/playwright --name playwright-docker-test -d -t playwright-java:localbuild-${{ matrix.flavor }} /bin/bash)
echo "CONTAINER_ID=$CONTAINER_ID" >> $GITHUB_ENV
- name: Copy repository inside docker container
- name: Run test in container
run: |
docker cp . "$CONTAINER_ID":/home/pwuser/playwright
# /root/.m2 was populated as root during image build; move it to
# pwuser so the locally-installed SNAPSHOT artifacts resolve.
docker exec --user root "$CONTAINER_ID" bash -c '
chown -R pwuser /home/pwuser/playwright
mv /root/.m2 /home/pwuser/.m2
chown -R pwuser /home/pwuser/.m2
'
- name: Run smoke tests in container
run: |
docker exec "$CONTAINER_ID" /home/pwuser/playwright/tools/test-local-installation/create_project_and_run_tests.sh -Dgroups=smoke
docker exec "$CONTAINER_ID" /root/playwright/tools/test-local-installation/create_project_and_run_tests.sh
- name: Test ClassLoader
run: |
docker exec "${CONTAINER_ID}" /home/pwuser/playwright/tools/test-spring-boot-starter/package_and_run_async_test.sh
docker exec "${CONTAINER_ID}" /root/playwright/tools/test-spring-boot-starter/package_and_run_async_test.sh
- name: Stop container
run: |
-48
View File
@@ -1,48 +0,0 @@
# 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.
+3 -4
View File
@@ -20,14 +20,12 @@ git clone https://github.com/microsoft/playwright-java
cd playwright-java
```
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).
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).
```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
@@ -41,9 +39,10 @@ BROWSER=chromium mvn test -Dtest=TestPageNetworkSizes
### Generating API
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:
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:
```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 -->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: |
| Chromium <!-- GEN:chromium-version -->147.0.7727.15<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->148.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
## Documentation
+16 -17
View File
@@ -6,27 +6,26 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.61.0</version>
<version>1.59.0</version>
</parent>
<artifactId>driver-bundle</artifactId>
<name>Playwright - Node.js For All Platforms</name>
<name>Playwright - Drivers For All Platforms</name>
<description>
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).
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.
</description>
<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>
<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>
</project>
@@ -30,11 +30,17 @@ 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 {
this(createTempDriverDir(), true);
// 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();
String nodePath = System.getProperty("playwright.nodejs.path");
if (nodePath != null) {
preinstalledNodePath = Paths.get(nodePath);
@@ -45,32 +51,6 @@ 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)) {
@@ -139,21 +119,7 @@ public class DriverJar extends Driver {
}
void extractDriverToTempDir() throws URISyntaxException, IOException {
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 originalUri = getDriverResourceURI();
URI uri = maybeExtractNestedJar(originalUri);
// Create zip filesystem if loading from jar.
@@ -165,8 +131,14 @@ 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 = destDir.resolve(relative.toString());
Path toPath = driverTempDir.resolve(relative.toString());
try {
if (Files.isDirectory(fromPath)) {
Files.createDirectories(toPath);
@@ -176,9 +148,7 @@ public class DriverJar extends Driver {
toPath.toFile().setExecutable(true, true);
}
}
if (deleteOnExit) {
toPath.toFile().deleteOnExit();
}
toPath.toFile().deleteOnExit();
} catch (IOException e) {
throw new RuntimeException("Failed to extract driver from " + uri + ", full uri: " + originalUri, e);
}
@@ -201,9 +171,7 @@ public class DriverJar extends Driver {
Path fromPath = Paths.get(jarUri);
Path toPath = driverTempDir.resolve(fromPath.getFileName().toString());
Files.copy(fromPath, toPath);
if (deleteOnExit) {
toPath.toFile().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);
@@ -132,30 +132,6 @@ 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();
+2 -17
View File
@@ -6,14 +6,13 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.61.0</version>
<version>1.59.0</version>
</parent>
<artifactId>driver</artifactId>
<name>Playwright - Driver</name>
<description>
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.
This module provides API for discovery and launching of Playwright driver.
</description>
<dependencies>
@@ -22,18 +21,4 @@
<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,14 +25,12 @@ 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 classpath: the platform-independent driver code ships in the
* driver module and the Node.js binary in the optional driver-bundle module.
* 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.
*/
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;
@@ -109,12 +107,9 @@ public abstract class Driver {
}
private static Driver newInstance() throws Exception {
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 pathFromProperty = System.getProperty("playwright.cli.dir");
if (pathFromProperty != null) {
return new PreinstalledDriver(Paths.get(pathFromProperty));
}
String driverImpl =
-2
View File
@@ -1,2 +0,0 @@
driver/
local-driver/
+2 -2
View File
@@ -6,11 +6,11 @@
<groupId>org.example</groupId>
<artifactId>examples</artifactId>
<version>1.61.0</version>
<version>1.59.0</version>
<name>Playwright Client Examples</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<playwright.version>1.61.0</playwright.version>
<playwright.version>1.59.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.61.0</version>
<version>1.59.0</version>
</parent>
<artifactId>playwright</artifactId>
@@ -23,23 +23,24 @@ import java.nio.file.Path;
* This API is used for the Web API testing. You can use it to trigger API endpoints, configure micro-services, prepare
* environment or the service to your e2e test.
*
* <p> Each Playwright browser context has an associated {@code APIRequestContext}, accessible via {@link
* com.microsoft.playwright.BrowserContext#request BrowserContext.request()} or {@link
* com.microsoft.playwright.Page#request Page.request()} (these return the
*
* <p> **same instance** — {@code page.request} is a shortcut for {@code page.context().request}). You can also create a
* standalone, isolated instance with {@link com.microsoft.playwright.APIRequest#newContext APIRequest.newContext()}.
* <p> Each Playwright browser context has associated with it {@code APIRequestContext} instance which shares cookie storage
* with the browser context and can be accessed via {@link com.microsoft.playwright.BrowserContext#request
* BrowserContext.request()} or {@link com.microsoft.playwright.Page#request Page.request()}. It is also possible to create
* a new APIRequestContext instance manually by calling {@link com.microsoft.playwright.APIRequest#newContext
* APIRequest.newContext()}.
*
* <p> <strong>Cookie management</strong>
*
* <p> The {@code APIRequestContext} returned by {@link com.microsoft.playwright.BrowserContext#request
* BrowserContext.request()} and
* <p> {@code APIRequestContext} returned by {@link com.microsoft.playwright.BrowserContext#request BrowserContext.request()}
* and {@link com.microsoft.playwright.Page#request Page.request()} shares cookie storage with the corresponding {@code
* BrowserContext}. Each API request will have {@code Cookie} header populated with the values from the browser context. If
* the API response contains {@code Set-Cookie} header it will automatically update {@code BrowserContext} cookies and
* requests made from the page will pick them up. This means that if you log in using this API, your e2e test will be
* logged in and vice versa.
*
* <p> {@link com.microsoft.playwright.Page#request Page.request()} uses the same cookie jar as its {@code BrowserContext}:
*
* <p> If you want API requests that do **not** share cookies with the browser, create an isolated context via {@link
* com.microsoft.playwright.APIRequest#newContext APIRequest.newContext()}. Such {@code APIRequestContext} object will have
* its own isolated cookie storage.
* <p> If you want API requests to not interfere with the browser cookies you should create a new {@code APIRequestContext} by
* calling {@link com.microsoft.playwright.APIRequest#newContext APIRequest.newContext()}. Such {@code APIRequestContext}
* object will have its own isolated cookie storage.
*/
public interface APIRequestContext {
class DisposeOptions {
@@ -483,11 +484,5 @@ public interface APIRequestContext {
* @since v1.16
*/
String storageState(StorageStateOptions options);
/**
*
*
* @since v1.60
*/
Tracing tracing();
}
@@ -55,20 +55,6 @@ 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).
*
@@ -43,15 +43,6 @@ import java.util.regex.Pattern;
*/
public interface Browser extends AutoCloseable {
/**
* Emitted when a new browser context is created.
*/
void onContext(Consumer<BrowserContext> handler);
/**
* Removes handler that was previously added with {@link #onContext onContext(handler)}.
*/
void offContext(Consumer<BrowserContext> handler);
/**
* Emitted when Browser gets disconnected from the browser application. This might happen because of one of the following:
* <ul>
@@ -114,48 +114,6 @@ public interface BrowserContext extends AutoCloseable {
*/
void offDialog(Consumer<Dialog> handler);
/**
* Emitted when attachment download started in any page belonging to this context. User can access basic file operations on
* downloaded content via the passed {@code Download} instance. See also {@link com.microsoft.playwright.Page#onDownload
* Page.onDownload()} to receive events about a specific page.
*/
void onDownload(Consumer<Download> handler);
/**
* Removes handler that was previously added with {@link #onDownload onDownload(handler)}.
*/
void offDownload(Consumer<Download> handler);
/**
* Emitted when a frame is attached in any page belonging to this context. See also {@link
* com.microsoft.playwright.Page#onFrameAttached Page.onFrameAttached()} to receive events about a specific page.
*/
void onFrameAttached(Consumer<Frame> handler);
/**
* Removes handler that was previously added with {@link #onFrameAttached onFrameAttached(handler)}.
*/
void offFrameAttached(Consumer<Frame> handler);
/**
* Emitted when a frame is detached in any page belonging to this context. See also {@link
* com.microsoft.playwright.Page#onFrameDetached Page.onFrameDetached()} to receive events about a specific page.
*/
void onFrameDetached(Consumer<Frame> handler);
/**
* Removes handler that was previously added with {@link #onFrameDetached onFrameDetached(handler)}.
*/
void offFrameDetached(Consumer<Frame> handler);
/**
* Emitted when a frame is navigated to a new url in any page belonging to this context. See also {@link
* com.microsoft.playwright.Page#onFrameNavigated Page.onFrameNavigated()} to receive events about navigations in a
* specific page.
*/
void onFrameNavigated(Consumer<Frame> handler);
/**
* Removes handler that was previously added with {@link #onFrameNavigated onFrameNavigated(handler)}.
*/
void offFrameNavigated(Consumer<Frame> handler);
/**
* The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event will
* also fire for popup pages. See also {@link com.microsoft.playwright.Page#onPopup Page.onPopup()} to receive events about
@@ -183,27 +141,6 @@ public interface BrowserContext extends AutoCloseable {
*/
void offPage(Consumer<Page> handler);
/**
* Emitted when a page in this context is closed. See also {@link com.microsoft.playwright.Page#onClose Page.onClose()} to
* receive events about a specific page.
*/
void onPageClose(Consumer<Page> handler);
/**
* Removes handler that was previously added with {@link #onPageClose onPageClose(handler)}.
*/
void offPageClose(Consumer<Page> handler);
/**
* Emitted when the JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/Events/load">{@code load}</a> event is
* dispatched in any page belonging to this context. See also {@link com.microsoft.playwright.Page#onLoad Page.onLoad()} to
* receive events about a specific page.
*/
void onPageLoad(Consumer<Page> handler);
/**
* Removes handler that was previously added with {@link #onPageLoad onPageLoad(handler)}.
*/
void offPageLoad(Consumer<Page> handler);
/**
* Emitted when exception is unhandled in any of the pages in this context. To listen for errors from a particular page,
* use {@link com.microsoft.playwright.Page#onPageError Page.onPageError()} instead.
@@ -334,6 +271,20 @@ public interface BrowserContext extends AutoCloseable {
return this;
}
}
class ExposeBindingOptions {
/**
* @deprecated This option will be removed in the future.
*/
public Boolean handle;
/**
* @deprecated This option will be removed in the future.
*/
public ExposeBindingOptions setHandle(boolean handle) {
this.handle = handle;
return this;
}
}
class GrantPermissionsOptions {
/**
* The [origin] to grant permissions to, e.g. "https://example.com".
@@ -563,13 +514,6 @@ 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.
*
@@ -792,7 +736,54 @@ public interface BrowserContext extends AutoCloseable {
* @param callback Callback function that will be called in the Playwright's context.
* @since v1.8
*/
AutoCloseable exposeBinding(String name, BindingCallback callback);
default AutoCloseable exposeBinding(String name, BindingCallback callback) {
return exposeBinding(name, callback, null);
}
/**
* The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context.
* When called, the function executes {@code callback} and returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a> which
* resolves to the return value of {@code callback}. If the {@code callback} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, it will be
* awaited.
*
* <p> The first argument of the {@code callback} function contains information about the caller: {@code { browserContext:
* BrowserContext, page: Page, frame: Frame }}.
*
* <p> See {@link com.microsoft.playwright.Page#exposeBinding Page.exposeBinding()} for page-only version.
*
* <p> <strong>Usage</strong>
*
* <p> An example of exposing page URL to all frames in all pages in the context:
* <pre>{@code
* import com.microsoft.playwright.*;
*
* public class Example {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType webkit = playwright.webkit();
* Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
* BrowserContext context = browser.newContext();
* context.exposeBinding("pageURL", (source, args) -> source.page().url());
* Page page = context.newPage();
* page.setContent("<script>\n" +
* " async function onClick() {\n" +
* " document.querySelector('div').textContent = await window.pageURL();\n" +
* " }\n" +
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>");
* page.getByRole(AriaRole.BUTTON).click();
* }
* }
* }
* }</pre>
*
* @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context.
* @since v1.8
*/
AutoCloseable exposeBinding(String name, BindingCallback callback, ExposeBindingOptions options);
/**
* The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context.
* When called, the function executes {@code callback} and returns a <a
@@ -124,10 +124,6 @@ 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.
*/
@@ -137,15 +133,6 @@ public interface BrowserType {
* the file system being the same between Playwright and the Browser.
*/
public Boolean isLocal;
/**
* When true, Playwright will not apply its default overrides to the existing default browser context. Specifically, {@code
* acceptDownloads} is left at the browser's setting, focus emulation is not enabled, and media emulation options (such as
* {@code colorScheme}, {@code reducedMotion}, {@code forcedColors}, and {@code contrast}) are not applied. Useful when
* attaching to a user's daily-driver browser where these overrides would interfere with existing browser state. New
* contexts created via {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} are not affected. Defaults
* to {@code false}.
*/
public Boolean noDefaults;
/**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0.
@@ -157,13 +144,6 @@ 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.
*/
@@ -179,18 +159,6 @@ public interface BrowserType {
this.isLocal = isLocal;
return this;
}
/**
* When true, Playwright will not apply its default overrides to the existing default browser context. Specifically, {@code
* acceptDownloads} is left at the browser's setting, focus emulation is not enabled, and media emulation options (such as
* {@code colorScheme}, {@code reducedMotion}, {@code forcedColors}, and {@code contrast}) are not applied. Useful when
* attaching to a user's daily-driver browser where these overrides would interfere with existing browser state. New
* contexts created via {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} are not affected. Defaults
* to {@code false}.
*/
public ConnectOverCDPOptions setNoDefaults(boolean noDefaults) {
this.noDefaults = noDefaults;
return this;
}
/**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0.
@@ -17,12 +17,9 @@
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;
@@ -31,13 +28,7 @@ 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, 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;
}
public static void main(String[] args) throws IOException, InterruptedException {
Driver driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
ProcessBuilder pb = driver.createProcessBuilder();
pb.command().addAll(asList(args));
@@ -49,17 +40,4 @@ 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());
}
}
@@ -1,238 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import 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);
}
@@ -867,13 +867,6 @@ public interface Frame {
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>.
*/
public Boolean checked;
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public Object description;
/**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
*
@@ -882,8 +875,8 @@ public interface Frame {
*/
public Boolean disabled;
/**
* Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* Ignored when the value is a regular expression. Note that exact match still trims whitespace.
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name}
* is a regular expression. Note that exact match still trims whitespace.
*/
public Boolean exact;
/**
@@ -935,26 +928,6 @@ public interface Frame {
this.checked = checked;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(String description) {
this.description = description;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(Pattern description) {
this.description = description;
return this;
}
/**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
*
@@ -966,8 +939,8 @@ public interface Frame {
return this;
}
/**
* Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* Ignored when the value is a regular expression. Note that exact match still trims whitespace.
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name}
* is a regular expression. Note that exact match still trims whitespace.
*/
public GetByRoleOptions setExact(boolean exact) {
this.exact = exact;
@@ -2965,7 +2938,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.evaluateHandle("document.body");
* ElementHandle bodyHandle = frame.evaluate("document.body");
* String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -3005,7 +2978,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.evaluateHandle("document.body");
* ElementHandle bodyHandle = frame.evaluate("document.body");
* String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -107,13 +107,6 @@ public interface FrameLocator {
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>.
*/
public Boolean checked;
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public Object description;
/**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
*
@@ -122,8 +115,8 @@ public interface FrameLocator {
*/
public Boolean disabled;
/**
* Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* Ignored when the value is a regular expression. Note that exact match still trims whitespace.
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name}
* is a regular expression. Note that exact match still trims whitespace.
*/
public Boolean exact;
/**
@@ -175,26 +168,6 @@ public interface FrameLocator {
this.checked = checked;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(String description) {
this.description = description;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(Pattern description) {
this.description = description;
return this;
}
/**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
*
@@ -206,8 +179,8 @@ public interface FrameLocator {
return this;
}
/**
* Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* Ignored when the value is a regular expression. Note that exact match still trims whitespace.
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name}
* is a regular expression. Note that exact match still trims whitespace.
*/
public GetByRoleOptions setExact(boolean exact) {
this.exact = exact;
@@ -30,13 +30,6 @@ import java.util.regex.Pattern;
*/
public interface Locator {
class AriaSnapshotOptions {
/**
* When {@code true}, appends each element's bounding box as {@code [box=x,y,width,height]} to the snapshot. Coordinates
* are relative to the viewport, in CSS pixels, as returned by <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">{@code
* Element.getBoundingClientRect()}</a>. Defaults to {@code false}.
*/
public Boolean boxes;
/**
* When specified, limits the depth of the snapshot.
*/
@@ -54,16 +47,6 @@ public interface Locator {
*/
public Double timeout;
/**
* When {@code true}, appends each element's bounding box as {@code [box=x,y,width,height]} to the snapshot. Coordinates
* are relative to the viewport, in CSS pixels, as returned by <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">{@code
* Element.getBoundingClientRect()}</a>. Defaults to {@code false}.
*/
public AriaSnapshotOptions setBoxes(boolean boxes) {
this.boxes = boxes;
return this;
}
/**
* When specified, limits the depth of the snapshot.
*/
@@ -662,46 +645,6 @@ public interface Locator {
return this;
}
}
class DropOptions {
/**
* A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the
* element.
*/
public Position position;
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
* BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()}
* methods.
*/
public Double timeout;
/**
* A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the
* element.
*/
public DropOptions setPosition(double x, double y) {
return setPosition(new Position(x, y));
}
/**
* A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the
* element.
*/
public DropOptions setPosition(Position position) {
this.position = position;
return this;
}
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
* value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout
* BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()}
* methods.
*/
public DropOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class ElementHandleOptions {
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
@@ -1000,13 +943,6 @@ public interface Locator {
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>.
*/
public Boolean checked;
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public Object description;
/**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
*
@@ -1015,8 +951,8 @@ public interface Locator {
*/
public Boolean disabled;
/**
* Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* Ignored when the value is a regular expression. Note that exact match still trims whitespace.
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name}
* is a regular expression. Note that exact match still trims whitespace.
*/
public Boolean exact;
/**
@@ -1068,26 +1004,6 @@ public interface Locator {
this.checked = checked;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(String description) {
this.description = description;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(Pattern description) {
this.description = description;
return this;
}
/**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
*
@@ -1099,8 +1015,8 @@ public interface Locator {
return this;
}
/**
* Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* Ignored when the value is a regular expression. Note that exact match still trims whitespace.
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name}
* is a regular expression. Note that exact match still trims whitespace.
*/
public GetByRoleOptions setExact(boolean exact) {
this.exact = exact;
@@ -1206,20 +1122,6 @@ public interface Locator {
return this;
}
}
class HighlightOptions {
/**
* Additional inline CSS applied to the highlight overlay, e.g. {@code "outline: 2px dashed red"}.
*/
public String style;
/**
* Additional inline CSS applied to the highlight overlay, e.g. {@code "outline: 2px dashed red"}.
*/
public HighlightOptions setStyle(String style) {
this.style = style;
return this;
}
}
class HoverOptions {
/**
* Whether to bypass the <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks. Defaults to
@@ -2998,54 +2900,6 @@ public interface Locator {
* @since v1.18
*/
void dragTo(Locator target, DragToOptions options);
/**
* Simulate an external drag-and-drop of files or clipboard-like data onto this locator.
*
* <p> <strong>Details</strong>
*
* <p> Dispatches the native {@code dragenter}, {@code dragover}, and {@code drop} events at the center of the target element
* with a synthetic [DataTransfer] carrying the provided files and/or data entries. Works cross-browser by constructing the
* [DataTransfer] in the page context.
*
* <p> If the target element's {@code dragover} listener does not call {@code preventDefault()}, the target is considered to
* have rejected the drop: Playwright dispatches {@code dragleave} and this method throws.
*
* <p> <strong>Usage</strong>
*
* <p> Drop a file buffer onto an upload area:
*
* <p> Drop plain text and a URL together:
*
* @param payload Data to drop onto the target. Provide {@code files} (file paths or in-memory buffers), {@code data} (a mime-type
* string map for clipboard-like content such as {@code text/plain}, {@code text/html}, {@code text/uri-list}), or both.
* @since v1.60
*/
default void drop(DropPayload payload) {
drop(payload, null);
}
/**
* Simulate an external drag-and-drop of files or clipboard-like data onto this locator.
*
* <p> <strong>Details</strong>
*
* <p> Dispatches the native {@code dragenter}, {@code dragover}, and {@code drop} events at the center of the target element
* with a synthetic [DataTransfer] carrying the provided files and/or data entries. Works cross-browser by constructing the
* [DataTransfer] in the page context.
*
* <p> If the target element's {@code dragover} listener does not call {@code preventDefault()}, the target is considered to
* have rejected the drop: Playwright dispatches {@code dragleave} and this method throws.
*
* <p> <strong>Usage</strong>
*
* <p> Drop a file buffer onto an upload area:
*
* <p> Drop plain text and a URL together:
*
* @param payload Data to drop onto the target. Provide {@code files} (file paths or in-memory buffers), {@code data} (a mime-type
* string map for clipboard-like content such as {@code text/plain}, {@code text/html}, {@code text/uri-list}), or both.
* @since v1.60
*/
void drop(DropPayload payload, DropOptions options);
/**
* Resolves given locator to the first matching DOM element. If there are no matching elements, waits for one. If multiple
* elements match the locator, throws.
@@ -4025,28 +3879,13 @@ public interface Locator {
* @since v1.27
*/
Locator getByTitle(Pattern text, GetByTitleOptions options);
/**
* Hides the element highlight previously added by {@link com.microsoft.playwright.Locator#highlight Locator.highlight()}.
*
* @since v1.60
*/
void hideHighlight();
/**
* Highlight the corresponding element(s) on the screen. Useful for debugging, don't commit the code that uses {@link
* com.microsoft.playwright.Locator#highlight Locator.highlight()}.
*
* @since v1.20
*/
default AutoCloseable highlight() {
return highlight(null);
}
/**
* Highlight the corresponding element(s) on the screen. Useful for debugging, don't commit the code that uses {@link
* com.microsoft.playwright.Locator#highlight Locator.highlight()}.
*
* @since v1.20
*/
AutoCloseable highlight(HighlightOptions options);
void highlight();
/**
* Hover over the matching element.
*
@@ -1058,6 +1058,20 @@ public interface Page extends AutoCloseable {
return this;
}
}
class ExposeBindingOptions {
/**
* @deprecated This option will be removed in the future.
*/
public Boolean handle;
/**
* @deprecated This option will be removed in the future.
*/
public ExposeBindingOptions setHandle(boolean handle) {
this.handle = handle;
return this;
}
}
class FillOptions {
/**
* Whether to bypass the <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks. Defaults to
@@ -1236,13 +1250,6 @@ public interface Page extends AutoCloseable {
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>.
*/
public Boolean checked;
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public Object description;
/**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
*
@@ -1251,8 +1258,8 @@ public interface Page extends AutoCloseable {
*/
public Boolean disabled;
/**
* Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* Ignored when the value is a regular expression. Note that exact match still trims whitespace.
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name}
* is a regular expression. Note that exact match still trims whitespace.
*/
public Boolean exact;
/**
@@ -1304,26 +1311,6 @@ public interface Page extends AutoCloseable {
this.checked = checked;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(String description) {
this.description = description;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(Pattern description) {
this.description = description;
return this;
}
/**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
*
@@ -1335,8 +1322,8 @@ public interface Page extends AutoCloseable {
return this;
}
/**
* Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* Ignored when the value is a regular expression. Note that exact match still trims whitespace.
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name}
* is a regular expression. Note that exact match still trims whitespace.
*/
public GetByRoleOptions setExact(boolean exact) {
this.exact = exact;
@@ -2994,13 +2981,6 @@ public interface Page extends AutoCloseable {
}
}
class AriaSnapshotOptions {
/**
* When {@code true}, appends each element's bounding box as {@code [box=x,y,width,height]} to the snapshot. Coordinates
* are relative to the viewport, in CSS pixels, as returned by <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">{@code
* Element.getBoundingClientRect()}</a>. Defaults to {@code false}.
*/
public Boolean boxes;
/**
* When specified, limits the depth of the snapshot.
*/
@@ -3018,16 +2998,6 @@ public interface Page extends AutoCloseable {
*/
public Double timeout;
/**
* When {@code true}, appends each element's bounding box as {@code [box=x,y,width,height]} to the snapshot. Coordinates
* are relative to the viewport, in CSS pixels, as returned by <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">{@code
* Element.getBoundingClientRect()}</a>. Defaults to {@code false}.
*/
public AriaSnapshotOptions setBoxes(boolean boxes) {
this.boxes = boxes;
return this;
}
/**
* When specified, limits the depth of the snapshot.
*/
@@ -4529,7 +4499,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.evaluateHandle("document.body");
* ElementHandle bodyHandle = page.evaluate("document.body");
* String html = (String) page.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -4571,7 +4541,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.evaluateHandle("document.body");
* ElementHandle bodyHandle = page.evaluate("document.body");
* String html = (String) page.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -4706,7 +4676,57 @@ public interface Page extends AutoCloseable {
* @param callback Callback function that will be called in the Playwright's context.
* @since v1.8
*/
AutoCloseable exposeBinding(String name, BindingCallback callback);
default AutoCloseable exposeBinding(String name, BindingCallback callback) {
return exposeBinding(name, callback, null);
}
/**
* The method adds a function called {@code name} on the {@code window} object of every frame in this page. When called,
* the function executes {@code callback} and returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a> which
* resolves to the return value of {@code callback}. If the {@code callback} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, it will be
* awaited.
*
* <p> The first argument of the {@code callback} function contains information about the caller: {@code { browserContext:
* BrowserContext, page: Page, frame: Frame }}.
*
* <p> See {@link com.microsoft.playwright.BrowserContext#exposeBinding BrowserContext.exposeBinding()} for the context-wide
* version.
*
* <p> <strong>NOTE:</strong> Functions installed via {@link com.microsoft.playwright.Page#exposeBinding Page.exposeBinding()} survive navigations.
*
* <p> <strong>Usage</strong>
*
* <p> An example of exposing page URL to all frames in a page:
* <pre>{@code
* import com.microsoft.playwright.*;
*
* public class Example {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType webkit = playwright.webkit();
* Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
* BrowserContext context = browser.newContext();
* Page page = context.newPage();
* page.exposeBinding("pageURL", (source, args) -> source.page().url());
* page.setContent("<script>\n" +
* " async function onClick() {\n" +
* " document.querySelector('div').textContent = await window.pageURL();\n" +
* " }\n" +
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>");
* page.click("button");
* }
* }
* }
* }</pre>
*
* @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context.
* @since v1.8
*/
AutoCloseable exposeBinding(String name, BindingCallback callback, ExposeBindingOptions options);
/**
* The method adds a function called {@code name} on the {@code window} object of every frame in the page. When called, the
* function executes {@code callback} and returns a <a
@@ -5577,13 +5597,6 @@ public interface Page extends AutoCloseable {
* @since v1.8
*/
Response navigate(String url, NavigateOptions options);
/**
* Hide all locator highlight overlays previously added by {@link com.microsoft.playwright.Locator#highlight
* Locator.highlight()} on this page.
*
* @since v1.60
*/
void hideHighlight();
/**
* This method hovers over an element matching {@code selector} by performing the following steps:
* <ol>
@@ -5808,18 +5821,6 @@ 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.
@@ -7564,8 +7565,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()} will throw if the {@code hasTouch} option of the browser context is
* false.
* <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.
*
* @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
@@ -7587,8 +7588,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()} will throw if the {@code hasTouch} option of the browser context is
* false.
* <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.
*
* @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
@@ -27,7 +27,7 @@ import java.util.function.Consumer;
public interface Screencast {
class StartOptions {
/**
* Callback that receives JPEG-encoded frame data along with the page viewport size at the time of capture.
* Callback that receives JPEG-encoded frame data.
*/
public Consumer<ScreencastFrame> onFrame;
/**
@@ -38,16 +38,9 @@ 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.
* Callback that receives JPEG-encoded frame data.
*/
public StartOptions setOnFrame(Consumer<ScreencastFrame> onFrame) {
this.onFrame = onFrame;
@@ -67,25 +60,6 @@ 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 {
/**
@@ -129,11 +103,6 @@ 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}.
*/
@@ -147,14 +116,6 @@ 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}.
*/
@@ -1,39 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
public interface ScreencastFrame {
/**
* JPEG-encoded frame data.
*/
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.
*/
int viewportWidth();
/**
* Height of the page viewport at the time the frame was captured.
*/
int viewportHeight();
}
@@ -201,8 +201,7 @@ 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. To match elements with any of several attributes, pass them as a comma-separated list, e.g.
* {@code "data-pw,data-ti"}.
* @param attributeName Test id attribute name.
* @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.Touchscreen#tap Touchscreen.tap()} will throw if the {@code hasTouch} option of the
* browser context is false.
* <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.
*
* @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.
@@ -18,7 +18,6 @@ package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.nio.file.Path;
import java.util.regex.Pattern;
/**
* API for collecting and saving Playwright traces. Playwright traces can be opened in <a
@@ -166,59 +165,6 @@ public interface Tracing {
return this;
}
}
class StartHarOptions {
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If
* {@code attach} is specified, resources are persisted as separate files or entries in the ZIP archive. If {@code embed}
* is specified, content is stored inline the HAR file as per HAR specification. Defaults to {@code attach} for {@code
* .zip} output files and to {@code embed} for all other file extensions.
*/
public HarContentPolicy content;
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page,
* cookies, security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code
* full}.
*/
public HarMode mode;
/**
* A glob or regex pattern to filter requests that are stored in the HAR. Defaults to none.
*/
public Object urlFilter;
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If
* {@code attach} is specified, resources are persisted as separate files or entries in the ZIP archive. If {@code embed}
* is specified, content is stored inline the HAR file as per HAR specification. Defaults to {@code attach} for {@code
* .zip} output files and to {@code embed} for all other file extensions.
*/
public StartHarOptions setContent(HarContentPolicy content) {
this.content = content;
return this;
}
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page,
* cookies, security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code
* full}.
*/
public StartHarOptions setMode(HarMode mode) {
this.mode = mode;
return this;
}
/**
* A glob or regex pattern to filter requests that are stored in the HAR. Defaults to none.
*/
public StartHarOptions setUrlFilter(String urlFilter) {
this.urlFilter = urlFilter;
return this;
}
/**
* A glob or regex pattern to filter requests that are stored in the HAR. Defaults to none.
*/
public StartHarOptions setUrlFilter(Pattern urlFilter) {
this.urlFilter = urlFilter;
return this;
}
}
class GroupOptions {
/**
* Specifies a custom location for the group to be shown in the trace viewer. Defaults to the location of the {@link
@@ -382,48 +328,6 @@ public interface Tracing {
* @since v1.15
*/
void startChunk(StartChunkOptions options);
/**
* Start recording a HAR (HTTP Archive) of network activity in this context. The HAR file is written to disk when {@link
* com.microsoft.playwright.Tracing#stopHar Tracing.stopHar()} is called, or when the returned {@code Disposable} is
* disposed.
*
* <p> Only one HAR recording can be active at a time per {@code BrowserContext}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* context.tracing().startHar(Paths.get("trace.har"));
* Page page = context.newPage();
* page.navigate("https://playwright.dev");
* context.tracing().stopHar();
* }</pre>
*
* @param path Path on the filesystem to write the HAR file to. If the file name ends with {@code .zip}, the HAR is saved as a zip
* archive with response bodies attached as separate files.
* @since v1.60
*/
default AutoCloseable startHar(Path path) {
return startHar(path, null);
}
/**
* Start recording a HAR (HTTP Archive) of network activity in this context. The HAR file is written to disk when {@link
* com.microsoft.playwright.Tracing#stopHar Tracing.stopHar()} is called, or when the returned {@code Disposable} is
* disposed.
*
* <p> Only one HAR recording can be active at a time per {@code BrowserContext}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* context.tracing().startHar(Paths.get("trace.har"));
* Page page = context.newPage();
* page.navigate("https://playwright.dev");
* context.tracing().stopHar();
* }</pre>
*
* @param path Path on the filesystem to write the HAR file to. If the file name ends with {@code .zip}, the HAR is saved as a zip
* archive with response bodies attached as separate files.
* @since v1.60
*/
AutoCloseable startHar(Path path, StartHarOptions options);
/**
* <strong>NOTE:</strong> Use {@code test.step} instead when available.
*
@@ -504,12 +408,5 @@ public interface Tracing {
* @since v1.15
*/
void stopChunk(StopChunkOptions options);
/**
* Stop HAR recording and save the HAR file to the path given to {@link com.microsoft.playwright.Tracing#startHar
* Tracing.startHar()}.
*
* @since v1.60
*/
void stopHar();
}
@@ -16,7 +16,6 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
/**
* {@code WebError} class represents an unhandled exception thrown in the page. It is dispatched via the {@link
@@ -44,11 +43,5 @@ public interface WebError {
* @since v1.38
*/
String error();
/**
*
*
* @since v1.60
*/
WebErrorLocation location();
}
@@ -16,7 +16,6 @@
package com.microsoft.playwright;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@@ -214,27 +213,6 @@ public interface WebSocketRoute {
* @since v1.48
*/
void send(byte[] message);
/**
* The list of WebSocket subprotocols requested by the page, as passed via the second argument to the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket">{@code WebSocket} constructor</a>.
* Corresponds to the {@code Sec-WebSocket-Protocol} request header.
*
* <p> Returns an empty array if no protocols were specified.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.routeWebSocket("wss://example.com/ws", ws -> {
* if (ws.protocols().contains("chat.v2")) {
* ws.onMessage(frame -> ws.send("v2:" + frame.text()));
* } else {
* ws.close(1002, "Unsupported protocol");
* }
* });
* }</pre>
*
* @since v1.60
*/
List<String> protocols();
/**
* URL of the WebSocket created in the page.
*
@@ -1,73 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import 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();
}
@@ -19,7 +19,6 @@ package com.microsoft.playwright.assertions;
import java.util.*;
import java.util.regex.Pattern;
import com.microsoft.playwright.options.AriaRole;
import com.microsoft.playwright.options.PseudoElement;
/**
* The {@code LocatorAssertions} class provides assertion methods that can be used to make assertions about the {@code
@@ -428,22 +427,11 @@ public interface LocatorAssertions {
}
}
class HasCSSOptions {
/**
* Pseudo-element to read computed styles from.
*/
public PseudoElement pseudo;
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public Double timeout;
/**
* Pseudo-element to read computed styles from.
*/
public HasCSSOptions setPseudo(PseudoElement pseudo) {
this.pseudo = pseudo;
return this;
}
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
@@ -37,20 +37,6 @@ import java.util.regex.Pattern;
* }</pre>
*/
public interface PageAssertions {
class MatchesAriaSnapshotOptions {
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public Double timeout;
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public MatchesAriaSnapshotOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class HasTitleOptions {
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
@@ -105,40 +91,6 @@ public interface PageAssertions {
* @since v1.20
*/
PageAssertions not();
/**
* Asserts that the page body matches the given <a href="https://playwright.dev/java/docs/aria-snapshots">accessibility
* snapshot</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.navigate("https://demo.playwright.dev/todomvc/");
* assertThat(page).matchesAriaSnapshot("""
* - heading "todos"
* - textbox "What needs to be done?"
* """);
* }</pre>
*
* @since v1.60
*/
default void matchesAriaSnapshot(String expected) {
matchesAriaSnapshot(expected, null);
}
/**
* Asserts that the page body matches the given <a href="https://playwright.dev/java/docs/aria-snapshots">accessibility
* snapshot</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.navigate("https://demo.playwright.dev/todomvc/");
* assertThat(page).matchesAriaSnapshot("""
* - heading "todos"
* - textbox "What needs to be done?"
* """);
* }</pre>
*
* @since v1.60
*/
void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions options);
/**
* Ensures the page has the given title.
*
@@ -46,11 +46,6 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
this.tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString());
}
@Override
public com.microsoft.playwright.Tracing tracing() {
return tracing;
}
@Override
public APIResponse delete(String url, RequestOptions options) {
return fetch(url, ensureOptions(options, "DELETE"));
@@ -22,8 +22,6 @@ 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;
@@ -87,22 +85,6 @@ 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();
@@ -57,14 +57,7 @@ abstract class AssertionsBase {
}
FrameExpectResult result = doExpect(expression, expectOptions, title);
if (result.matches == isNot) {
Object actual;
if (result.received == null) {
actual = null;
} else if (result.received.value != null) {
actual = Serialization.deserialize(result.received.value);
} else {
actual = result.received.ariaSnapshot;
}
Object actual = result.received == null ? null : Serialization.deserialize(result.received);
String log = (result.log == null) ? "" : String.join("\n", result.log);
if (!log.isEmpty()) {
log = "\nCall log:\n" + log;
@@ -46,7 +46,6 @@ 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();
@@ -69,18 +68,23 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
private final ListenerCollection<EventType> listeners = new ListenerCollection<>(eventSubscriptions(), this);
final TimeoutSettings timeoutSettings = new TimeoutSettings();
final Map<String, HarRecorder> harRecorders = new HashMap<>();
static class HarRecorder {
final Path path;
final HarContentPolicy contentPolicy;
HarRecorder(Path har, HarContentPolicy policy) {
path = har;
contentPolicy = policy;
}
}
enum EventType {
CLOSE,
CONSOLE,
DIALOG,
DOWNLOAD,
FRAMEATTACHED,
FRAMEDETACHED,
FRAMENAVIGATED,
PAGE,
PAGECLOSE,
PAGELOAD,
WEBERROR,
REQUEST,
REQUESTFAILED,
@@ -95,7 +99,6 @@ 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);
}
@@ -136,20 +139,6 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
public void offBackgroundPage(Consumer<Page> handler) {
}
@Override
public void onDownload(Consumer<Download> handler) {
listeners.add(EventType.DOWNLOAD, handler);
}
@Override
public void offDownload(Consumer<Download> handler) {
listeners.remove(EventType.DOWNLOAD, handler);
}
void notifyDownload(Download download) {
listeners.notify(EventType.DOWNLOAD, download);
}
@Override
public void onClose(Consumer<BrowserContext> handler) {
listeners.add(EventType.CLOSE, handler);
@@ -190,76 +179,6 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
listeners.remove(EventType.PAGE, handler);
}
@Override
public void onFrameAttached(Consumer<Frame> handler) {
listeners.add(EventType.FRAMEATTACHED, handler);
}
@Override
public void offFrameAttached(Consumer<Frame> handler) {
listeners.remove(EventType.FRAMEATTACHED, handler);
}
void notifyFrameAttached(FrameImpl frame) {
listeners.notify(EventType.FRAMEATTACHED, frame);
}
@Override
public void onFrameDetached(Consumer<Frame> handler) {
listeners.add(EventType.FRAMEDETACHED, handler);
}
@Override
public void offFrameDetached(Consumer<Frame> handler) {
listeners.remove(EventType.FRAMEDETACHED, handler);
}
void notifyFrameDetached(FrameImpl frame) {
listeners.notify(EventType.FRAMEDETACHED, frame);
}
@Override
public void onFrameNavigated(Consumer<Frame> handler) {
listeners.add(EventType.FRAMENAVIGATED, handler);
}
@Override
public void offFrameNavigated(Consumer<Frame> handler) {
listeners.remove(EventType.FRAMENAVIGATED, handler);
}
void notifyFrameNavigated(FrameImpl frame) {
listeners.notify(EventType.FRAMENAVIGATED, frame);
}
@Override
public void onPageClose(Consumer<Page> handler) {
listeners.add(EventType.PAGECLOSE, handler);
}
@Override
public void offPageClose(Consumer<Page> handler) {
listeners.remove(EventType.PAGECLOSE, handler);
}
void notifyPageClose(PageImpl page) {
listeners.notify(EventType.PAGECLOSE, page);
}
@Override
public void onPageLoad(Consumer<Page> handler) {
listeners.add(EventType.PAGELOAD, handler);
}
@Override
public void offPageLoad(Consumer<Page> handler) {
listeners.remove(EventType.PAGELOAD, handler);
}
void notifyPageLoad(PageImpl page) {
listeners.notify(EventType.PAGELOAD, page);
}
@Override
public void onWebError(Consumer<WebError> handler) {
listeners.add(EventType.WEBERROR, handler);
@@ -315,11 +234,6 @@ 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));
@@ -370,7 +284,27 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
closeReason = options.reason;
request.dispose(convertType(options, APIRequestContext.DisposeOptions.class));
tracing.exportAllHars();
for (Map.Entry<String, HarRecorder> entry : harRecorders.entrySet()) {
JsonObject params = new JsonObject();
params.addProperty("harId", entry.getKey());
JsonObject json = sendMessage("harExport", params, NO_TIMEOUT).getAsJsonObject();
ArtifactImpl artifact = connection.getExistingObject(json.getAsJsonObject("artifact").get("guid").getAsString());
// Server side will compress artifact if content is attach or if file is .zip.
HarRecorder harParams = entry.getValue();
boolean isCompressed = harParams.contentPolicy == HarContentPolicy.ATTACH || harParams.path.toString().endsWith(".zip");
boolean needCompressed = harParams.path.toString().endsWith(".zip");
if (isCompressed && !needCompressed) {
String tmpPath = harParams.path + ".tmp";
artifact.saveAs(Paths.get(tmpPath));
JsonObject unzipParams = new JsonObject();
unzipParams.addProperty("zipFile", tmpPath);
unzipParams.addProperty("harFile", harParams.path.toString());
connection.localUtils.sendMessage("harUnzip", unzipParams, NO_TIMEOUT);
} else {
artifact.saveAs(harParams.path);
}
artifact.delete();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("close", params, NO_TIMEOUT);
}
@@ -458,11 +392,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding) {
return exposeBindingImpl(name, playwrightBinding);
public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
return exposeBindingImpl(name, playwrightBinding, options);
}
private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding) {
private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
if (bindings.containsKey(name)) {
throw new PlaywrightException("Function \"" + name + "\" has been already registered");
}
@@ -475,13 +409,16 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
JsonObject params = new JsonObject();
params.addProperty("name", name);
if (options != null && options.handle != null && options.handle) {
params.addProperty("needsHandle", true);
}
JsonObject result = sendMessage("exposeBinding", params, NO_TIMEOUT).getAsJsonObject();
return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString());
}
@Override
public AutoCloseable exposeFunction(String name, FunctionCallback playwrightFunction) {
return exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args));
return exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args), null);
}
@Override
@@ -578,7 +515,24 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
if (contentPolicy == null) {
contentPolicy = Utils.convertType(options.updateContent, HarContentPolicy.class);
}
tracing.recordIntoHar(page, har, options.url, contentPolicy, options.updateMode, null);
if (contentPolicy == null) {
contentPolicy = HarContentPolicy.ATTACH;
}
JsonObject params = new JsonObject();
if (page != null) {
params.add("page", page.toProtocolRef());
}
JsonObject recordHarArgs = new JsonObject();
recordHarArgs.addProperty("zip", har.toString().endsWith(".zip"));
recordHarArgs.addProperty("content", contentPolicy.name().toLowerCase());
recordHarArgs.addProperty("mode", (options.updateMode == null ? HarMode.MINIMAL : options.updateMode).name().toLowerCase());
addHarUrlFilter(recordHarArgs, options.url);
params.add("options", recordHarArgs);
JsonObject json = sendMessage("harStart", params, NO_TIMEOUT).getAsJsonObject();
String harId = json.get("harId").getAsString();
harRecorders.put(harId, new HarRecorder(har, contentPolicy));
}
@Override
@@ -864,11 +818,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} catch (PlaywrightException e) {
page = null;
}
WebErrorLocation location = null;
if (params.has("location")) {
location = gson().fromJson(params.getAsJsonObject("location"), WebErrorLocation.class);
}
listeners.notify(BrowserContextImpl.EventType.WEBERROR, new WebErrorImpl(page, errorStr, location));
listeners.notify(BrowserContextImpl.EventType.WEBERROR, new WebErrorImpl(page, errorStr));
if (page != null) {
page.listeners.notify(PageImpl.EventType.PAGEERROR, errorStr);
}
@@ -43,7 +43,6 @@ class BrowserImpl extends ChannelOwner implements Browser {
String closeReason;
enum EventType {
CONTEXT,
DISCONNECTED,
}
@@ -51,16 +50,6 @@ class BrowserImpl extends ChannelOwner implements Browser {
super(parent, type, guid, initializer);
}
@Override
public void onContext(Consumer<BrowserContext> handler) {
listeners.add(EventType.CONTEXT, handler);
}
@Override
public void offContext(Consumer<BrowserContext> handler) {
listeners.remove(EventType.CONTEXT, handler);
}
@Override
public void onDisconnected(Consumer<Browser> handler) {
listeners.add(EventType.DISCONNECTED, handler);
@@ -313,7 +302,6 @@ class BrowserImpl extends ChannelOwner implements Browser {
context.tracing().setTracesDir(tracePath);
browserType.playwright.selectors.contextsForSelectors.add(context);
}
listeners.notify(EventType.CONTEXT, context);
}
private void didClose() {
@@ -112,8 +112,8 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
@Override
public Browser connectOverCDP(String endpointURL, ConnectOverCDPOptions options) {
if (!"chromium".equals(name()) && !"webkit".equals(name())) {
throw new PlaywrightException("Connecting over CDP is only supported in Chromium and WebKit.");
if (!"chromium".equals(name())) {
throw new PlaywrightException("Connecting over CDP is only supported in Chromium.");
}
if (options == null) {
options = new ConnectOverCDPOptions();
@@ -110,14 +110,6 @@ 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,7 +39,6 @@ class Message {
JsonObject params;
JsonElement result;
SerializedError error;
JsonObject errorDetails;
JsonArray log;
@Override
@@ -133,20 +132,13 @@ public class Connection {
}
public WaitableResult<JsonElement> sendMessageAsync(String guid, String method, JsonObject params) {
return internalSendMessage(guid, method, params, true, true);
return internalSendMessage(guid, method, params, true);
}
// 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) {
private WaitableResult<JsonElement> internalSendMessage(String guid, String method, JsonObject params, boolean sendStack) {
int id = ++lastId;
WaitableResult<JsonElement> result = new WaitableResult<>();
if (expectsReply) {
callbacks.put(id, result);
}
callbacks.put(id, result);
JsonObject message = new JsonObject();
message.addProperty("id", id);
message.addProperty("guid", guid);
@@ -183,7 +175,7 @@ public class Connection {
callData.add("stack", stack);
JsonObject stackParams = new JsonObject();
stackParams.add("callData", callData);
internalSendMessage(localUtils.guid,"addStackToTracingNoReply", stackParams, false, true);
internalSendMessage(localUtils.guid,"addStackToTracingNoReply", stackParams, false);
}
return result;
}
@@ -259,20 +251,16 @@ public class Connection {
callback.complete(message.result);
} else {
String callLog = formatCallLog(message.log);
PlaywrightException exception;
if (message.error.error == null) {
exception = new PlaywrightException(message.error + callLog);
callback.completeExceptionally(new PlaywrightException(message.error + callLog));
} else if ("TimeoutError".equals(message.error.error.name)) {
exception = new TimeoutError(message.error.error + callLog);
callback.completeExceptionally(new TimeoutError(message.error.error + callLog));
} else if ("TargetClosedError".equals(message.error.error.name)) {
exception = new TargetClosedError(message.error.error + callLog);
callback.completeExceptionally(new TargetClosedError(message.error.error + callLog));
} else {
exception = new DriverException(message.error.error + callLog);
callback.completeExceptionally(new DriverException(message.error.error + callLog));
}
if (message.errorDetails != null) {
exception = new ServerErrorWithDetails(exception, message.errorDetails, message.log);
}
callback.completeExceptionally(exception);
}
return;
}
@@ -1,62 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.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));
}
}
@@ -1051,58 +1051,12 @@ public class FrameImpl extends ChannelOwner implements Frame {
return result.get("value").getAsInt();
}
void dropImpl(String selector, DropPayload payload, com.microsoft.playwright.Locator.DropOptions options) {
if (options == null) {
options = new com.microsoft.playwright.Locator.DropOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
params.addProperty("strict", true);
if (payload != null) {
if (payload.files != null) {
if (payload.files instanceof Path) {
addFilePathUploadParams(new Path[] { (Path) payload.files }, params, page.context());
} else if (payload.files instanceof Path[]) {
addFilePathUploadParams((Path[]) payload.files, params, page.context());
} else if (payload.files instanceof com.microsoft.playwright.options.FilePayload) {
checkFilePayloadSize(new com.microsoft.playwright.options.FilePayload[] { (com.microsoft.playwright.options.FilePayload) payload.files });
params.add("payloads", toJsonArray(new com.microsoft.playwright.options.FilePayload[] { (com.microsoft.playwright.options.FilePayload) payload.files }));
} else if (payload.files instanceof com.microsoft.playwright.options.FilePayload[]) {
checkFilePayloadSize((com.microsoft.playwright.options.FilePayload[]) payload.files);
params.add("payloads", toJsonArray((com.microsoft.playwright.options.FilePayload[]) payload.files));
} else {
throw new com.microsoft.playwright.PlaywrightException("Unsupported files type: " + payload.files.getClass());
}
}
if (payload.data != null) {
com.google.gson.JsonArray dataArray = new com.google.gson.JsonArray();
for (java.util.Map.Entry<String, String> entry : payload.data.entrySet()) {
JsonObject e = new JsonObject();
e.addProperty("mimeType", entry.getKey());
e.addProperty("value", entry.getValue());
dataArray.add(e);
}
params.add("data", dataArray);
}
}
sendMessage("drop", params, timeout(options.timeout));
}
void highlightImpl(String selector, String style) {
void highlightImpl(String selector) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
if (style != null) {
params.addProperty("style", style);
}
sendMessage("highlight", params, NO_TIMEOUT);
}
void hideHighlightImpl(String selector) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
sendMessage("hideHighlight", params, NO_TIMEOUT);
}
protected void handleEvent(String event, JsonObject params) {
if ("loadstate".equals(event)) {
JsonElement add = params.get("add");
@@ -1112,7 +1066,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
if (parentFrame == null && page != null) {
if (state == LOAD) {
page.listeners.notify(PageImpl.EventType.LOAD, page);
page.browserContext.notifyPageLoad(page);
} else if (state == DOMCONTENTLOADED) {
page.listeners.notify(PageImpl.EventType.DOMCONTENTLOADED, page);
}
@@ -1154,17 +1107,8 @@ public class FrameImpl extends ChannelOwner implements Frame {
FrameExpectResult expect(String expression, FrameExpectOptions options) {
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("expression", expression);
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();
}
JsonElement json = sendMessage("expect", params, options.timeout);
FrameExpectResult result = gson().fromJson(json, FrameExpectResult.class);
return result;
}
}
@@ -371,20 +371,8 @@ class LocatorImpl implements Locator {
}
@Override
public void drop(DropPayload payload, DropOptions options) {
frame.dropImpl(selector, payload, options);
}
@Override
public AutoCloseable highlight(HighlightOptions options) {
String style = options == null ? null : options.style;
frame.highlightImpl(selector, style);
return new DisposableStub(this::hideHighlight);
}
@Override
public void hideHighlight() {
frame.hideHighlightImpl(selector);
public void highlight() {
frame.highlightImpl(selector);
}
@Override
@@ -43,14 +43,8 @@ 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) {
String attributeName = encodeTestIdAttributeName(playwright.selectors.testIdAttributeName);
return "internal:testid=[" + attributeName + "=" + escapeForAttributeSelector(testId, true) + "]";
return getByAttributeTextSelector(playwright.selectors.testIdAttributeName, testId, true);
}
static String getByAltTextSelector(Object text, Locator.GetByAltTextOptions options) {
@@ -92,10 +86,6 @@ public class LocatorUtils {
String name = escapeForAttributeSelector(options.name, options.exact != null && options.exact);
addAttr(result, "name", name);
}
if (options.description != null) {
String description = escapeForAttributeSelector(options.description, options.exact != null && options.exact);
addAttr(result, "description", description);
}
if (options.pressed != null)
addAttr(result, "pressed", options.pressed.toString());
}
@@ -73,16 +73,6 @@ public class PageAssertionsImpl extends AssertionsBase implements PageAssertions
expectImpl("to.have.url", expected, pattern, "Page URL expected to match regex", convertType(options, FrameExpectOptions.class), "Assert \"hasURL\"");
}
@Override
public void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions snapshotOptions) {
if (snapshotOptions == null) {
snapshotOptions = new MatchesAriaSnapshotOptions();
}
FrameExpectOptions options = convertType(snapshotOptions, FrameExpectOptions.class);
options.expectedValue = Serialization.serializeArgument(expected);
expectImpl("to.match.aria", options, expected, "Page expected to match Aria snapshot", "Assert \"matchesAriaSnapshot\"");
}
@Override
public PageAssertions not() {
return new PageAssertionsImpl(actualPage, !isNot);
@@ -41,14 +41,12 @@ import static java.util.Arrays.asList;
public class PageImpl extends ChannelOwner implements Page {
final BrowserContextImpl browserContext;
private final BrowserContextImpl browserContext;
private final FrameImpl mainFrame;
private final KeyboardImpl keyboard;
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();
@@ -139,8 +137,6 @@ 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();
@@ -175,7 +171,6 @@ public class PageImpl extends ChannelOwner implements Page {
ArtifactImpl artifact = connection.getExistingObject(artifactGuid);
DownloadImpl download = new DownloadImpl(this, artifact, params);
listeners.notify(EventType.DOWNLOAD, download);
browserContext.notifyDownload(download);
} else if ("fileChooser".equals(event)) {
String guid = params.getAsJsonObject("element").get("guid").getAsString();
ElementHandleImpl elementHandle = connection.getExistingObject(guid);
@@ -206,7 +201,6 @@ public class PageImpl extends ChannelOwner implements Page {
frame.parentFrame.childFrames.add(frame);
}
listeners.notify(EventType.FRAMEATTACHED, frame);
browserContext.notifyFrameAttached(frame);
} else if ("frameDetached".equals(event)) {
String guid = params.getAsJsonObject("frame").get("guid").getAsString();
FrameImpl frame = connection.getExistingObject(guid);
@@ -216,7 +210,6 @@ public class PageImpl extends ChannelOwner implements Page {
frame.parentFrame.childFrames.remove(frame);
}
listeners.notify(EventType.FRAMEDETACHED, frame);
browserContext.notifyFrameDetached(frame);
} else if ("locatorHandlerTriggered".equals(event)) {
int uid = params.get("uid").getAsInt();
onLocatorHandlerTriggered(uid);
@@ -252,7 +245,6 @@ public class PageImpl extends ChannelOwner implements Page {
isClosed = true;
browserContext.pages.remove(this);
listeners.notify(EventType.CLOSE, this);
browserContext.notifyPageClose(this);
}
private String effectiveCloseReason() {
@@ -559,13 +551,8 @@ 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 = new JsonObject();
if (options.reason != null) {
params.addProperty("reason", options.reason);
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("close", params, NO_TIMEOUT);
}
} catch (PlaywrightException exception) {
@@ -766,11 +753,11 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding) {
return exposeBindingImpl(name, playwrightBinding);
public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
return exposeBindingImpl(name, playwrightBinding, options);
}
private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding) {
private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) {
if (bindings.containsKey(name)) {
throw new PlaywrightException("Function \"" + name + "\" has been already registered");
}
@@ -781,13 +768,16 @@ public class PageImpl extends ChannelOwner implements Page {
JsonObject params = new JsonObject();
params.addProperty("name", name);
if (options != null && options.handle != null && options.handle) {
params.addProperty("needsHandle", true);
}
JsonObject result = sendMessage("exposeBinding", params, NO_TIMEOUT).getAsJsonObject();
return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString());
}
@Override
public AutoCloseable exposeFunction(String name, FunctionCallback playwrightFunction) {
return exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args));
return exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args), null);
}
@Override
@@ -1070,11 +1060,6 @@ public class PageImpl extends ChannelOwner implements Page {
return mainFrame;
}
@Override
public void hideHighlight() {
sendMessage("hideHighlight", new JsonObject(), NO_TIMEOUT);
}
@Override
public Mouse mouse() {
return mouse;
@@ -1371,16 +1356,6 @@ 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));
@@ -1482,7 +1457,6 @@ public class PageImpl extends ChannelOwner implements Page {
void frameNavigated(FrameImpl frame) {
listeners.notify(EventType.FRAMENAVIGATED, frame);
browserContext.notifyFrameNavigated(frame);
}
private class WaitableFrameDetach extends WaitableEvent<EventType, Frame> {
@@ -112,20 +112,10 @@ class FrameExpectOptions {
}
class FrameExpectResult {
static class Received {
SerializedValue value;
String ariaSnapshot;
}
boolean matches;
Received received;
SerializedValue received;
String errorMessage;
List<String> log;
}
class FrameExpectErrorDetails {
FrameExpectResult.Received received;
Boolean timedOut;
String customErrorMessage;
}
@@ -1,53 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
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, double timestamp, int viewportWidth, int viewportHeight) {
this.data = data;
this.timestamp = timestamp;
this.viewportWidth = viewportWidth;
this.viewportHeight = viewportHeight;
}
@Override
public byte[] data() {
return data;
}
@Override
public double timestamp() {
return timestamp;
}
@Override
public int viewportWidth() {
return viewportWidth;
}
@Override
public int viewportHeight() {
return viewportHeight;
}
}
@@ -19,7 +19,7 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Screencast;
import com.microsoft.playwright.ScreencastFrame;
import com.microsoft.playwright.options.ScreencastFrame;
import java.nio.file.Path;
import java.util.function.Consumer;
@@ -44,10 +44,7 @@ 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, timestamp, viewportWidth, viewportHeight));
onFrame.accept(new ScreencastFrame(data));
}
@Override
@@ -77,7 +77,6 @@ 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();
@@ -572,15 +571,6 @@ 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) {
@@ -1,57 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.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;
}
}
@@ -18,21 +18,14 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Tracing;
import com.microsoft.playwright.options.HarContentPolicy;
import com.microsoft.playwright.options.HarMode;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter;
import static com.microsoft.playwright.impl.Serialization.gson;
class TracingImpl extends ChannelOwner implements Tracing {
@@ -41,17 +34,6 @@ class TracingImpl extends ChannelOwner implements Tracing {
private boolean isTracing;
private String stacksId;
private final Set<String> additionalSources = new HashSet<>();
final Map<String, HarRecorder> harRecorders = new HashMap<>();
static class HarRecorder {
final Path path;
final HarContentPolicy contentPolicy;
HarRecorder(Path har, HarContentPolicy policy) {
this.path = har;
this.contentPolicy = policy;
}
}
TracingImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -179,110 +161,6 @@ class TracingImpl extends ChannelOwner implements Tracing {
stopChunkImpl(options == null ? null : options.path);
}
private String currentHarId;
@Override
public AutoCloseable startHar(Path path, StartHarOptions options) {
if (currentHarId != null) {
throw new PlaywrightException("HAR recording has already been started");
}
if (options == null) {
options = new StartHarOptions();
}
boolean isZip = path.toString().endsWith(".zip");
HarContentPolicy contentPolicy = options.content != null
? options.content
: (isZip ? HarContentPolicy.ATTACH : HarContentPolicy.EMBED);
HarMode mode = options.mode != null ? options.mode : HarMode.FULL;
currentHarId = recordIntoHar(null, path, options.urlFilter, contentPolicy, mode, null);
return new DisposableStub(this::stopHar);
}
@Override
public void stopHar() {
if (currentHarId == null) {
throw new PlaywrightException("HAR recording has not been started");
}
String harId = currentHarId;
currentHarId = null;
exportHar(harId);
}
String recordIntoHar(PageImpl page, Path har, Object urlFilter, HarContentPolicy contentPolicy, HarMode mode, Path resourcesDir) {
if (contentPolicy == null) {
contentPolicy = HarContentPolicy.ATTACH;
}
if (mode == null) {
mode = HarMode.MINIMAL;
}
JsonObject params = new JsonObject();
if (page != null) {
params.add("page", page.toProtocolRef());
}
JsonObject recordHarArgs = new JsonObject();
recordHarArgs.addProperty("zip", har.toString().endsWith(".zip"));
recordHarArgs.addProperty("content", contentPolicy.name().toLowerCase());
recordHarArgs.addProperty("mode", mode.name().toLowerCase());
addHarUrlFilter(recordHarArgs, urlFilter);
if (resourcesDir != null) {
recordHarArgs.addProperty("resourcesDir", resourcesDir.toString());
}
if (!har.toString().endsWith(".zip")) {
recordHarArgs.addProperty("harPath", har.toString());
}
params.add("options", recordHarArgs);
JsonObject json = sendMessage("harStart", params, NO_TIMEOUT).getAsJsonObject();
String harId = json.get("harId").getAsString();
harRecorders.put(harId, new HarRecorder(har, contentPolicy));
return harId;
}
void exportHar(String harId) {
HarRecorder harParams = harRecorders.remove(harId);
if (harParams == null) {
return;
}
boolean isLocal = !connection.isRemote;
boolean isZip = harParams.path.toString().endsWith(".zip");
JsonObject params = new JsonObject();
params.addProperty("harId", harId);
if (isLocal) {
params.addProperty("mode", "entries");
JsonObject json = sendMessage("harExport", params, NO_TIMEOUT).getAsJsonObject();
if (!isZip) {
return;
}
JsonArray entries = json.getAsJsonArray("entries");
connection.localUtils.zip(harParams.path, entries, null, false, false, java.util.Collections.emptyList());
return;
}
params.addProperty("mode", "archive");
JsonObject json = sendMessage("harExport", params, NO_TIMEOUT).getAsJsonObject();
ArtifactImpl artifact = connection.getExistingObject(json.getAsJsonObject("artifact").get("guid").getAsString());
if (isZip) {
artifact.saveAs(harParams.path);
artifact.delete();
return;
}
String tmpPath = harParams.path + ".tmp";
artifact.saveAs(Paths.get(tmpPath));
JsonObject unzipParams = new JsonObject();
unzipParams.addProperty("zipFile", tmpPath);
unzipParams.addProperty("harFile", harParams.path.toString());
connection.localUtils.sendMessage("harUnzip", unzipParams, NO_TIMEOUT);
artifact.delete();
}
void exportAllHars() {
for (String harId : new ArrayList<>(harRecorders.keySet())) {
exportHar(harId);
}
}
void setTracesDir(Path tracesDir) {
this.tracesDir = tracesDir;
}
@@ -41,8 +41,7 @@ public class WaitForEventLogger<T> implements Supplier<T>, Logger {
{
JsonObject info = new JsonObject();
info.addProperty("phase", "before");
info.addProperty("event", "");
sendWaitInfo(info);
sendWaitForEventInfo(info);
}
JsonObject info = new JsonObject();
info.addProperty("phase", "after");
@@ -52,7 +51,7 @@ public class WaitForEventLogger<T> implements Supplier<T>, Logger {
info.addProperty("error", e.getMessage());
throw e;
} finally {
sendWaitInfo(info);
sendWaitForEventInfo(info);
}
}
@@ -62,15 +61,14 @@ public class WaitForEventLogger<T> implements Supplier<T>, Logger {
JsonObject info = new JsonObject();
info.addProperty("phase", "log");
info.addProperty("message", message);
sendWaitInfo(info);
sendWaitForEventInfo(info);
}
private void sendWaitInfo(JsonObject info) {
private void sendWaitForEventInfo(JsonObject info) {
info.addProperty("event", "");
info.addProperty("waitId", waitId);
try {
channel.sendMessageNoReply("__waitInfo__", info);
} catch (RuntimeException e) {
// Fire-and-forget: never throw to the caller.
}
JsonObject params = new JsonObject();
params.add("info", info);
channel.sendMessageAsync("waitForEventInfo", params);
}
}
@@ -52,8 +52,6 @@ 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);
}
@@ -17,17 +17,14 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.WebError;
import com.microsoft.playwright.options.WebErrorLocation;
public class WebErrorImpl implements WebError {
private final PageImpl page;
private final String error;
private final WebErrorLocation location;
WebErrorImpl(PageImpl page, String error, WebErrorLocation location) {
WebErrorImpl(PageImpl page, String error) {
this.page = page;
this.error = error;
this.location = location;
}
@Override
@@ -39,9 +36,4 @@ public class WebErrorImpl implements WebError {
public String error() {
return error;
}
@Override
public WebErrorLocation location() {
return location;
}
}
@@ -1,14 +1,11 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.WebSocketFrame;
import com.microsoft.playwright.WebSocketRoute;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@@ -68,11 +65,6 @@ class WebSocketRouteImpl extends ChannelOwner implements WebSocketRoute {
public String url() {
return initializer.get("url").getAsString();
}
@Override
public List<String> protocols() {
return readProtocols();
}
};
WebSocketRouteImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -131,22 +123,6 @@ class WebSocketRouteImpl extends ChannelOwner implements WebSocketRoute {
return initializer.get("url").getAsString();
}
@Override
public List<String> protocols() {
return readProtocols();
}
private List<String> readProtocols() {
List<String> result = new ArrayList<>();
if (!initializer.has("protocols")) {
return result;
}
for (JsonElement element : initializer.getAsJsonArray("protocols")) {
result.add(element.getAsString());
}
return result;
}
void afterHandle() {
if (this.connected) {
return;
@@ -1,77 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.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);
}
}
@@ -1,22 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public enum PseudoElement {
BEFORE,
AFTER
}
@@ -1,22 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public enum ScreencastCursor {
NONE,
POINTER
}
@@ -16,18 +16,17 @@
package com.microsoft.playwright.options;
import java.util.Map;
/**
* A single screencast frame delivered to {@link com.microsoft.playwright.Screencast#start Screencast.start()}'s
* {@code onFrame} callback.
*/
public class ScreencastFrame {
/**
* JPEG-encoded frame data.
*/
public byte[] data;
public class DropPayload {
public Object files;
public Map<String, String> data;
public DropPayload setFiles(Object files) {
this.files = files;
return this;
}
public DropPayload setData(Map<String, String> data) {
public ScreencastFrame(byte[] data) {
this.data = data;
return this;
}
}
}
@@ -18,11 +18,11 @@ package com.microsoft.playwright.options;
public class Size {
/**
* Max frame width in pixels.
* Video frame width.
*/
public int width;
/**
* Max frame height in pixels.
* Video frame height.
*/
public int height;
@@ -1,41 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.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;
}
@@ -1,33 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class WebErrorLocation {
/**
* URL of the resource.
*/
public String url;
/**
* 0-based line number in the resource.
*/
public int line;
/**
* 0-based column number in the resource.
*/
public int column;
}
@@ -1,23 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class WebStorageItem {
public String name;
public String value;
}
@@ -20,7 +20,6 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.junit.FixtureTest;
import com.microsoft.playwright.junit.UsePlaywright;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
@@ -31,7 +30,6 @@ import static org.junit.jupiter.api.Assertions.*;
@FixtureTest
@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class)
@Tag("smoke")
public class TestBrowser1 {
@Test
@@ -115,13 +113,4 @@ public class TestBrowser1 {
assertTrue(e.getMessage().contains("The reason."), e.getMessage());
}
@Test
void shouldFireContextEvent(Browser browser) {
BrowserContext[] contextEvent = { null };
browser.onContext(c -> contextEvent[0] = c);
BrowserContext context = browser.newContext();
assertEquals(context, contextEvent[0]);
context.close();
}
}
@@ -18,7 +18,6 @@ package com.microsoft.playwright;
import com.microsoft.playwright.junit.FixtureTest;
import com.microsoft.playwright.junit.UsePlaywright;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.io.OutputStreamWriter;
@@ -33,7 +32,6 @@ import static org.junit.jupiter.api.Assertions.*;
@FixtureTest
@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class)
@Tag("smoke")
public class TestBrowserContextBasic {
@Test
void shouldCreateNewContext(Browser browser) {
@@ -131,9 +131,8 @@ public class TestBrowserContextCDPSession extends TestBase {
CDPSession session = page.context().newCDPSession(page);
page.close();
// 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);
PlaywrightException exception = assertThrows(PlaywrightException.class, session::detach);
assertTrue(exception.getMessage().contains("Target page, context or browser has been closed"), exception.getMessage());
context.close();
}
@@ -186,90 +186,4 @@ public class TestBrowserContextEvents extends TestBase {
assertTrue(webError[0].error().contains("boom"), webError[0].error());
}
@Test
void weberrorEventShouldIncludeLocation() {
server.setRoute("/error.js", exchange -> {
exchange.getResponseHeaders().add("content-type", "application/javascript");
exchange.sendResponseHeaders(200, 0);
try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("\nfunction foo() {\n throw new Error('boom');\n}\nfoo();\n");
}
});
server.setRoute("/error.html", exchange -> {
exchange.getResponseHeaders().add("content-type", "text/html");
exchange.sendResponseHeaders(200, 0);
try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("<script src=\"/error.js\"></script>");
}
});
WebError[] webError = { null };
context.onWebError(e -> webError[0] = e);
page.navigate(server.PREFIX + "/error.html");
waitForCondition(() -> webError[0] != null);
com.microsoft.playwright.options.WebErrorLocation location = webError[0].location();
assertEquals(server.PREFIX + "/error.js", location.url);
assertEquals(2, location.line);
assertTrue(location.column > 0, "expected column > 0, got " + location.column);
}
@Test
void pageLoadEventShouldWork() {
Page[] loaded = { null };
context.onPageLoad(p -> loaded[0] = p);
page.navigate(server.EMPTY_PAGE);
waitForCondition(() -> loaded[0] != null);
assertEquals(page, loaded[0]);
}
@Test
void frameNavigatedEventShouldWork() {
Frame[] navigated = { null };
context.onFrameNavigated(f -> navigated[0] = f);
page.navigate(server.EMPTY_PAGE);
waitForCondition(() -> navigated[0] != null);
assertEquals(page.mainFrame(), navigated[0]);
assertEquals(server.EMPTY_PAGE, navigated[0].url());
}
@Test
void pageCloseEventShouldWork() {
Page newPage = context.newPage();
Page[] closed = { null };
context.onPageClose(p -> closed[0] = p);
newPage.close();
waitForCondition(() -> closed[0] != null);
assertEquals(newPage, closed[0]);
}
@Test
void frameAttachedEventShouldWork() {
page.navigate(server.EMPTY_PAGE);
Frame[] attached = { null };
context.onFrameAttached(f -> attached[0] = f);
page.evaluate("() => {\n" +
" const iframe = document.createElement('iframe');\n" +
" iframe.src = 'about:blank';\n" +
" document.body.appendChild(iframe);\n" +
"}");
waitForCondition(() -> attached[0] != null);
assertEquals(page.mainFrame(), attached[0].parentFrame());
}
@Test
void frameDetachedEventShouldWork() {
page.navigate(server.EMPTY_PAGE);
page.evaluate("() => {\n" +
" const iframe = document.createElement('iframe');\n" +
" iframe.id = 'x';\n" +
" iframe.src = 'about:blank';\n" +
" document.body.appendChild(iframe);\n" +
"}");
page.waitForSelector("iframe");
Frame[] detached = { null };
context.onFrameDetached(f -> detached[0] = f);
page.evaluate("() => document.getElementById('x').remove()");
waitForCondition(() -> detached[0] != null);
assertEquals(page.mainFrame(), detached[0].parentFrame());
}
}
@@ -84,4 +84,19 @@ public class TestBrowserContextExposeFunction extends TestBase {
assertEquals(asList("context", "page"), actualArgs);
}
@Test
void exposeBindingHandleShouldWork() {
JSHandle[] target = { null };
context.exposeBinding("logme", (source, args) -> {
target[0] = (JSHandle) args[0];
return 17;
}, new BrowserContext.ExposeBindingOptions().setHandle(true));
Page page = context.newPage();
Object result = page.evaluate("async function() {\n" +
" return window['logme']({ foo: 42 });\n" +
"}");
assertNotNull(target[0]);
assertEquals(42, target[0].evaluate("x => x.foo"));
assertEquals(17, result);
}
}
@@ -28,7 +28,6 @@ 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;
@@ -534,23 +533,6 @@ 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");
@@ -759,12 +741,10 @@ 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("Request context disposed") ||
e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
assertTrue(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("Request context disposed") ||
e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
}
@Test
@@ -1,179 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import 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,14 +40,10 @@ public class TestBrowserTypeBasic extends TestBase {
assertEquals(getBrowserNameFromEnv(), browserType.name());
}
static boolean isChromiumOrWebKit() {
return isChromium() || isWebKit();
}
@Test
@DisabledIf(value="isChromiumOrWebKit", disabledReason="Connecting over CDP is supported in Chromium and WebKit")
@DisabledIf(value="com.microsoft.playwright.TestBase#isChromium", disabledReason="Non-chromium behavior")
void shouldThrowWhenTryingToConnectWithNotChromium() {
PlaywrightException e = assertThrows(PlaywrightException.class, () -> browserType.connectOverCDP("foo"));
assertTrue(e.getMessage().contains("Connecting over CDP is only supported in Chromium and WebKit."));
assertTrue(e.getMessage().contains("Connecting over CDP is only supported in Chromium."));
}
}
@@ -259,11 +259,7 @@ public class TestBrowserTypeConnect extends TestBase {
}
assertFalse(browser.isConnected());
PlaywrightException e = assertThrows(PlaywrightException.class, () -> page.waitForNavigation(() -> {}));
// 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());
assertTrue(e.getMessage().contains("Browser closed") || e.getMessage().contains("Page closed") || e.getMessage().contains("Browser has been closed"), e.getMessage());
}
@Test
@@ -5,7 +5,6 @@ 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;
@@ -224,9 +223,7 @@ public class TestClientCertificates extends TestBase {
@Test
@DisabledIf(value="com.microsoft.playwright.TestClientCertificates#isWebKitMacOS", disabledReason="The network connection was lost.")
// 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) {
public void shouldWorkWithBrowserLaunchPersistentContext(@TempDir Path tmpDir) {
BrowserType.LaunchPersistentContextOptions options = new BrowserType.LaunchPersistentContextOptions()
.setIgnoreHTTPSErrors(true) // TODO: remove once we can pass a custom CA.
.setClientCertificates(asList(
@@ -22,7 +22,6 @@ 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;
@@ -47,9 +46,7 @@ public class TestDefaultBrowserContext2 extends TestBase {
private BrowserContext persistentContext;
// 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;
@TempDir Path tempDir;
@AfterEach
void closePersistentContext() {
@@ -22,7 +22,6 @@ 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;
@@ -253,25 +252,6 @@ 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));
@@ -17,14 +17,12 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.KeyboardModifier;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Tag("smoke")
public class TestLocatorClick extends TestBase {
@Test
@@ -36,18 +36,4 @@ public class TestLocatorHighlight extends TestBase {
BoundingBox box2 = page.locator("x-pw-highlight").boundingBox();
assertEquals(new Gson().toJson(box2), new Gson().toJson(box1));
}
@Test
void highlightAndHideHighlightShouldNotThrow() {
page.setContent("<input type='text' />");
AutoCloseable disposable = page.locator("input").highlight(new Locator.HighlightOptions().setStyle("outline: 2px dashed red"));
try {
disposable.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
page.locator("input").highlight();
page.locator("input").hideHighlight();
page.hideHighlight();
}
}
@@ -341,14 +341,4 @@ public class TestPageAriaSnapshot {
" - /placeholder: Placeholder");
}
@Test
void pageMatchesAriaSnapshot(Page page) {
page.setContent("<h1>hello</h1>");
assertThat(page).matchesAriaSnapshot("- heading \"hello\" [level=1]");
AssertionFailedError e = assertThrows(AssertionFailedError.class,
() -> assertThat(page).matchesAriaSnapshot("- heading \"world\"",
new com.microsoft.playwright.assertions.PageAssertions.MatchesAriaSnapshotOptions().setTimeout(1000)));
org.junit.jupiter.api.Assertions.assertTrue(e.getMessage().contains("Page expected to match Aria snapshot"), e.getMessage());
}
}
@@ -16,7 +16,6 @@
package com.microsoft.playwright;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
@@ -31,7 +30,6 @@ import static com.microsoft.playwright.options.LoadState.LOAD;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
@Tag("smoke")
public class TestPageBasic extends TestBase {
@Test
@@ -1,125 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.FilePayload;
import com.microsoft.playwright.options.DropPayload;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestPageDrop extends TestBase {
private void setupDropzone() {
page.setContent("<style>#dropzone { width: 300px; height: 200px; border: 2px dashed #888; }</style>\n" +
"<div id=\"dropzone\"></div>\n" +
"<script>\n" +
" window.__dropInfo = null;\n" +
" const zone = document.getElementById('dropzone');\n" +
" zone.addEventListener('dragenter', e => e.preventDefault());\n" +
" zone.addEventListener('dragover', e => e.preventDefault());\n" +
" zone.addEventListener('drop', async e => {\n" +
" e.preventDefault();\n" +
" const files = [];\n" +
" for (const file of e.dataTransfer.files)\n" +
" files.push({ name: file.name, type: file.type, size: file.size, text: await file.text() });\n" +
" const data = {};\n" +
" for (const t of e.dataTransfer.types) {\n" +
" if (t !== 'Files')\n" +
" data[t] = e.dataTransfer.getData(t);\n" +
" }\n" +
" window.__dropInfo = { files, data };\n" +
" });\n" +
"</script>");
}
@SuppressWarnings("unchecked")
private Map<String, Object> waitForDropInfo() {
page.waitForCondition(() -> page.evaluate("window.__dropInfo") != null);
return (Map<String, Object>) page.evaluate("window.__dropInfo");
}
@Test
void shouldDropFilePayload() {
setupDropzone();
page.locator("#dropzone").drop(new DropPayload().setFiles(new FilePayload("note.txt", "text/plain", "hello".getBytes(StandardCharsets.UTF_8))));
Map<String, Object> info = waitForDropInfo();
@SuppressWarnings("unchecked")
List<Map<String, Object>> files = (List<Map<String, Object>>) info.get("files");
assertEquals(1, files.size());
assertEquals("note.txt", files.get(0).get("name"));
assertEquals("text/plain", files.get(0).get("type"));
assertEquals("hello", files.get(0).get("text"));
}
@Test
void shouldDropMultipleFilePayloads() {
setupDropzone();
page.locator("#dropzone").drop(new DropPayload().setFiles(new FilePayload[] {
new FilePayload("a.txt", "text/plain", "AAA".getBytes(StandardCharsets.UTF_8)),
new FilePayload("b.txt", "text/plain", "BB".getBytes(StandardCharsets.UTF_8)),
}));
Map<String, Object> info = waitForDropInfo();
@SuppressWarnings("unchecked")
List<Map<String, Object>> files = (List<Map<String, Object>>) info.get("files");
assertEquals(2, files.size());
assertEquals("a.txt", files.get(0).get("name"));
assertEquals("AAA", files.get(0).get("text"));
assertEquals("b.txt", files.get(1).get("name"));
assertEquals("BB", files.get(1).get("text"));
}
@Test
void shouldDropClipboardLikeData() {
setupDropzone();
Map<String, String> data = new HashMap<>();
data.put("text/plain", "hello world");
data.put("text/uri-list", "https://example.com");
page.locator("#dropzone").drop(new DropPayload().setData(data));
Map<String, Object> info = waitForDropInfo();
@SuppressWarnings("unchecked")
List<?> files = (List<?>) info.get("files");
assertTrue(files.isEmpty(), "expected no files");
@SuppressWarnings("unchecked")
Map<String, String> droppedData = (Map<String, String>) info.get("data");
assertEquals("hello world", droppedData.get("text/plain"));
assertEquals("https://example.com", droppedData.get("text/uri-list"));
}
@Test
void shouldDropFileByLocalPath(@org.junit.jupiter.api.io.TempDir Path dir) throws Exception {
setupDropzone();
Path filePath = dir.resolve("hello.txt");
Files.write(filePath, "path-content".getBytes(StandardCharsets.UTF_8));
page.locator("#dropzone").drop(new DropPayload().setFiles(filePath));
Map<String, Object> info = waitForDropInfo();
@SuppressWarnings("unchecked")
List<Map<String, Object>> files = (List<Map<String, Object>>) info.get("files");
assertEquals(1, files.size());
assertEquals("hello.txt", files.get(0).get("name"));
assertEquals("path-content", files.get(0).get("text"));
}
}
@@ -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, 2000));\n" +
" await new Promise(f => window.setTimeout(f, 100));\n" +
" }");
List<String> errors = page.pageErrors();
@@ -164,6 +164,35 @@ public class TestPageExposeFunction extends TestBase {
assertEquals( 7, ((Map) result).get("x"));
}
@Test
void exposeBindingHandleShouldWork() {
JSHandle[] target = { null };
page.exposeBinding("logme", (source, args) -> {
target[0] = (JSHandle) args[0];
return 17;
}, new Page.ExposeBindingOptions().setHandle(true));
Object result = page.evaluate("async function() {\n" +
" return window['logme']({ foo: 42 });\n" +
"}");
assertEquals(42, target[0].evaluate("x => x.foo"));
assertEquals(17, result);
}
@Test
void exposeBindingHandleShouldNotThrowDuringNavigation() {
page.exposeBinding("logme", (source, args) -> {
return 17;
}, new Page.ExposeBindingOptions().setHandle(true));
page.navigate(server.EMPTY_PAGE);
page.waitForNavigation(new Page.WaitForNavigationOptions().setWaitUntil(LOAD), () -> {
page.evaluate("async url => {\n" +
" window['logme']({ foo: 42 });\n" +
" window.location.href = url;\n" +
"}", server.PREFIX + "/one-style.html");
});
}
@Test
void shouldThrowForDuplicateRegistrations() {
page.exposeFunction("foo", args -> null);
@@ -173,6 +202,28 @@ public class TestPageExposeFunction extends TestBase {
assertTrue(e.getMessage().contains("Function \"foo\" has been already registered"));
}
@Test
void exposeBindingHandleShouldThrowForMultipleArguments() {
page.exposeBinding("logme", (source, args) -> {
return 17;
}, new Page.ExposeBindingOptions().setHandle(true));
assertEquals(17, page.evaluate("async function() {\n" +
" return window['logme']({ foo: 42 });\n" +
"}"));
assertEquals(17, page.evaluate("async function() {\n" +
" return window['logme']({ foo: 42 }, undefined, undefined);\n" +
"}"));
assertEquals(17, page.evaluate("async function() {\n" +
" return window['logme'](undefined, undefined, undefined);\n" +
"}"));
PlaywrightException e = assertThrows(PlaywrightException.class, () -> {
page.evaluate("async function() {\n" +
" return window['logme'](1, 2);\n" +
"}");
});
assertTrue(e.getMessage().contains("exposeBindingHandle supports a single argument, 2 received"));
}
@Test
void shouldSerializeCycles() {
Object[] object = { null };
@@ -45,13 +45,13 @@ public class TestRouteWebSocket {
}
private void setupWS(Frame target, Server server, int port, String binaryType) {
target.navigate(server.EMPTY_PAGE);
// No 'error' listener: WebKit fires a spurious 'error' before 'close' on non-normal closures (e.g. 1008).
target.evaluate("({ port, binaryType }) => {\n" +
" window.log = [];\n" +
" window.ws = new WebSocket('ws://localhost:' + port + '/ws');\n" +
" window.ws.binaryType = binaryType;\n" +
" window.ws.addEventListener('open', () => window.log.push('open'));\n" +
" window.ws.addEventListener('close', event => window.log.push(`close code=${event.code} reason=${event.reason}`));\n" +
" window.ws.addEventListener('error', event => window.log.push(`error`));\n" +
" window.ws.addEventListener('message', async event => {\n" +
" let data;\n" +
" if (typeof event.data === 'string')\n" +
@@ -391,29 +391,4 @@ public class TestRouteWebSocket {
});
assertEquals(asList("response"), page.evaluate("window.log"));
}
@Test
public void shouldExposeProtocolsToTheRouteHandler(Page page, Server server) {
List<com.microsoft.playwright.WebSocketRoute> routes = new ArrayList<>();
page.routeWebSocket(Pattern.compile(".*"), ws -> routes.add(ws));
page.navigate(server.EMPTY_PAGE);
int port = webSocketServer.getPort();
page.evaluate("({ port }) => {\n" +
" window.wsNone = new WebSocket('ws://localhost:' + port + '/ws-none');\n" +
" window.wsString = new WebSocket('ws://localhost:' + port + '/ws-string', 'chat.v1');\n" +
" window.wsArray = new WebSocket('ws://localhost:' + port + '/ws-array', ['chat.v2', 'chat.v1']);\n" +
"}", mapOf("port", port));
page.waitForCondition(() -> routes.size() == 3);
java.util.Map<String, com.microsoft.playwright.WebSocketRoute> byUrl = new java.util.HashMap<>();
for (com.microsoft.playwright.WebSocketRoute r : routes) {
String path = java.net.URI.create(r.url()).getPath();
byUrl.put(path, r);
}
assertEquals(asList(), byUrl.get("/ws-none").protocols());
assertEquals(asList("chat.v1"), byUrl.get("/ws-string").protocols());
assertEquals(asList("chat.v2", "chat.v1"), byUrl.get("/ws-array").protocols());
}
}
@@ -16,6 +16,7 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.ScreencastFrame;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -112,31 +113,9 @@ public class TestScreencast extends TestBase {
assertFalse(frames.isEmpty(), "expected at least one frame");
// JPEG-encoded frames start with FF D8.
for (ScreencastFrame frame : frames) {
assertNotNull(frame.data());
assertEquals((byte) 0xFF, frame.data()[0]);
assertEquals((byte) 0xD8, frame.data()[1]);
}
} finally {
context.close();
}
}
@Test
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).setSize(500, 400));
page.navigate(server.EMPTY_PAGE);
page.evaluate("() => document.body.style.backgroundColor = 'red'");
page.waitForTimeout(500);
page.screencast().stop();
assertFalse(frames.isEmpty(), "expected at least one frame");
for (ScreencastFrame frame : frames) {
assertEquals(1000, frame.viewportWidth());
assertEquals(400, frame.viewportHeight());
assertTrue(frame.timestamp() > 0, "expected a positive timestamp, got " + frame.timestamp());
assertNotNull(frame.data);
assertEquals((byte) 0xFF, frame.data[0]);
assertEquals((byte) 0xD8, frame.data[1]);
}
} finally {
context.close();
@@ -16,7 +16,6 @@
package com.microsoft.playwright;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
@@ -25,7 +24,6 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@Tag("smoke")
public class TestSelectorsCss extends TestBase {
@Test
@@ -51,20 +51,6 @@ 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");
@@ -448,7 +448,7 @@ public class TestSelectorsRole extends TestBase {
assertTrue(e0.getMessage().contains("Role must not be empty"), e0.getMessage());
PlaywrightException e1 = assertThrows(PlaywrightException.class, () -> page.querySelector("role=foo[sElected]"));
assertTrue(e1.getMessage().contains("Unknown attribute \"sElected\", must be one of \"checked\", \"description\", \"disabled\", \"expanded\", \"include-hidden\", \"level\", \"name\", \"pressed\", \"selected\""), e1.getMessage());
assertTrue(e1.getMessage().contains("Unknown attribute \"sElected\", must be one of \"checked\", \"disabled\", \"expanded\", \"include-hidden\", \"level\", \"name\", \"pressed\", \"selected\""), e1.getMessage());
PlaywrightException e2 = assertThrows(PlaywrightException.class, () -> page.querySelector("role=foo[bar . qux=true]"));
assertTrue(e2.getMessage().contains("Unknown attribute \"bar.qux\""), e2.getMessage());
@@ -158,7 +158,6 @@ public class TestTracing extends TestBase {
Pattern.compile("Set content"),
Pattern.compile("Click")
});
traceViewer.selectAction("Click");
traceViewer.showSourceTab();
assertThat(traceViewer.stackFrames()).containsText(new Pattern[] {
Pattern.compile("myMethodInner"),
@@ -380,15 +379,4 @@ public class TestTracing extends TestBase {
});
});
}
@Test
public void shouldRecordHarWithStartHarStopHar(@TempDir Path tempDir) throws Exception {
Path harPath = tempDir.resolve("tracing.har");
context.tracing().startHar(harPath, new Tracing.StartHarOptions().setMode(com.microsoft.playwright.options.HarMode.MINIMAL));
page.navigate(server.PREFIX + "/one-style.html");
context.tracing().stopHar();
String content = new String(Files.readAllBytes(harPath));
assertTrue(content.contains("\"log\""), content);
assertTrue(content.contains("/one-style.html"), content);
}
}
@@ -1,132 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import 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"));
}
}
@@ -43,7 +43,7 @@ class TraceViewerPage {
}
Locator stackFrames() {
return this.page.getByRole(AriaRole.LISTBOX, new Page.GetByRoleOptions().setName("stack trace")).getByRole(AriaRole.OPTION);
return this.page.getByRole(AriaRole.LIST, new Page.GetByRoleOptions().setName("stack trace")).getByRole(AriaRole.LISTITEM);
}
void selectAction(String title, int ordinal) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 45 KiB

+4 -4
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.61.0</version>
<version>1.59.0</version>
<packaging>pom</packaging>
<name>Playwright Parent Project</name>
<description>Java library to automate Chromium, Firefox and WebKit with a single API.
@@ -44,11 +44,11 @@
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<maven.compiler.parameters>true</maven.compiler.parameters>
<gson.version>2.14.0</gson.version>
<gson.version>2.13.2</gson.version>
<junit.version>5.14.1</junit.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<websocket.version>1.6.0</websocket.version>
<slf4j.version>2.0.18</slf4j.version>
<slf4j.version>2.0.17</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.6</version>
<version>3.5.5</version>
<configuration>
<properties>
<configurationParameters>
+1 -1
View File
@@ -1 +1 @@
1.61.1
1.59.1-beta-1775762078000
+32 -80
View File
@@ -8,12 +8,8 @@ cd "$(dirname $0)"
if [[ ($1 == '-h') || ($1 == '--help') ]]; then
echo ""
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 "This script for downloading playwright driver for all platforms."
echo "The downloaded files will be put under 'driver-bundle/src/main/resources/driver'."
echo ""
echo "Usage: scripts/download_driver.sh [option]"
echo ""
@@ -23,89 +19,45 @@ 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
# 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
cd ../driver-bundle/src/main/resources
if [[ -d 'driver' ]]; then
echo "Deleting existing drivers from $(pwd)"
rm -rf driver
fi
# 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
mkdir -p driver
cd driver
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"
for PLATFORM in mac mac-arm64 linux linux-arm64 win32_x64
do
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"
FILE_NAME=$FILE_PREFIX-$PLATFORM.zip
mkdir $PLATFORM
cd $PLATFORM
echo "Downloading driver for $PLATFORM to $(pwd)"
# 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
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"
URL=https://cdn.playwright.dev/builds/driver
if [[ "$DRIVER_VERSION" == *-alpha* || "$DRIVER_VERSION" == *-beta* || "$DRIVER_VERSION" == *-next* ]]; then
URL=$URL/next
fi
rm -f "$NODE_ARCHIVE"
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
else
curl -O $URL
fi
unzip $FILE_NAME -d .
rm $FILE_NAME
cd -
done
echo ""
echo "All drivers have been successfully assembled."
echo "All drivers have been successfully downloaded."
echo ""

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