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

Compare commits

...

40 Commits

Author SHA1 Message Date
Simon Knott 6b385a17b1 chore: mark 1.52.0 (#1783) 2025-05-02 08:51:06 +02:00
Simon Knott 8cca01851a chore: roll 1.52.0 driver, implement new features (#1780) 2025-04-29 09:13:50 -07:00
dependabot[bot] 478417bb56 chore(deps): bump org.apache.maven.plugins:maven-surefire-plugin from 3.5.2 to 3.5.3 in the all group (#1774) 2025-04-01 11:38:02 -07:00
Max Schmitt 739202fddf test: do not send Content-Length header for HEAD requests (#1771) 2025-03-20 18:41:01 +01:00
Max Schmitt 8593941005 test: fix 'SLF4J(W): No SLF4J providers were found.' warning (#1772) 2025-03-20 18:38:34 +01:00
Max Schmitt b2852f5d57 devops: migrate to GitHub App for automation (#1770) 2025-03-19 14:24:34 +01:00
Yury Semikhatsky 6c059e351f chore: roll driver to 1.51.1 (#1768) 2025-03-17 12:03:43 -07:00
Yury Semikhatsky e86911ed2a docs: remove obsolete toc from readme (#1767) 2025-03-17 19:37:04 +01:00
Yury Semikhatsky 17cc3b8297 docs: abridge readme, redirect to playwright.dev (#1766) 2025-03-17 11:17:28 -07:00
dependabot[bot] 43016df241 chore(deps): bump the all group with 3 updates (#1764) 2025-03-17 11:16:44 -07:00
dependabot[bot] ba4eb3ce7d chore(deps): bump the actions group with 2 updates (#1765) 2025-03-17 11:16:22 -07:00
Max Schmitt ba7dd3cd89 devops: update GitHub Action workflows via dependabot (#1763) 2025-03-17 19:05:26 +01:00
Max Schmitt 5e37b7f5ca devops: add DevContainer config (#1762) 2025-03-17 19:02:33 +01:00
Yury Semikhatsky 84eaf8f3cb devops: retry failures up to 3 times on GHA (#1758) 2025-03-10 11:50:10 -07:00
Yury Semikhatsky c090824c93 chore: roll 1.51.0 driver, implement new features (#1757) 2025-03-07 19:02:19 -08:00
dependabot[bot] add7f56117 chore(deps): bump the all group with 7 updates (#1754) 2025-03-04 11:25:31 -08:00
Max Schmitt 676f38d22c devops: use PME environment for ESRP publishing (#1755) 2025-03-03 16:52:51 +01:00
Max Schmitt bc82f2fa68 chore: auto update example pw version in pom (#1748) 2025-02-13 10:08:42 +01:00
stevenfuhr 995cf902fb fix: add pfxBase64 to jsonCert instead of params (#1746) 2025-02-07 12:33:20 -08:00
Yury Semikhatsky 87152ecc71 devops: make publish step type: releaseJob (#1744) 2025-02-06 11:28:50 -08:00
Yury Semikhatsky dbc0478e40 chore: roll 1.50.1-beta (#1739) 2025-02-03 09:22:37 -08:00
dependabot[bot] 9eb1db9034 chore(deps): bump com.google.code.gson:gson from 2.11.0 to 2.12.1 in the all group (#1738) 2025-02-03 08:27:25 -08:00
Max Schmitt 4820088457 fix(urlMatcher): normalize URLs to align with Node.js parser behavior (#1734) 2025-01-27 10:26:39 -08:00
Max Schmitt fcd0444c57 fix(webSocketRoute): resolve URL against baseURL (#1722) 2025-01-27 15:43:47 +01:00
Yury Semikhatsky 067e69f339 chore: roll 1.50.0 (#1733) 2025-01-23 12:14:30 -08:00
Yury Semikhatsky 015939b150 feat: roll driver to 1.50 alpha (#1729) 2025-01-17 09:21:19 -08:00
Adam Gastineau eb8cf62d74 fix(tracing): Properly log Clock calls (#1727) 2025-01-16 07:04:53 -08:00
dependabot[bot] 308b9913e7 chore(deps): bump the all group with 5 updates (#1723) 2025-01-02 10:36:48 -08:00
Max Schmitt 6b621ce6f7 chore: refactor UrlMatcher (#1720) 2024-12-28 10:47:01 +01:00
Yury Semikhatsky 42d0203b49 fix: waitForCondition should not call predicate after it returned true (#1721) 2024-12-19 12:25:42 -08:00
Max Schmitt c591a1470a test: do not create stray files when running tests (#1718) 2024-12-18 11:49:18 +01:00
dependabot[bot] eb08046e94 chore(deps): bump the all group with 2 updates (#1710) 2024-12-02 10:49:48 -08:00
Yury Semikhatsky 2ff37da5f5 chore: mark 1.50 snapshot (#1702) 2024-11-18 17:01:09 -08:00
Yury Semikhatsky ee99afc3a3 chore: print actual message for TestBrowserContextCDPSession.shouldDe… (#1701) 2024-11-18 16:25:10 -08:00
Yury Semikhatsky 34017a26a3 chore: roll 1.49.0 (#1700) 2024-11-18 15:25:34 -08:00
Yury Semikhatsky 29f58a5840 test: unflake TestPageClock (#1699) 2024-11-15 14:38:02 -08:00
Yury Semikhatsky d2d78a7299 chore: stop using microsoft/playwright-github-action@v1 (#1698) 2024-11-15 11:20:37 -08:00
Yury Semikhatsky 6e66ee7c35 chore: roll 1.49-beta (#1697) 2024-11-15 09:24:21 -08:00
dependabot[bot] 4bda800e11 chore(deps): bump the all group with 4 updates (#1695) 2024-11-14 08:54:00 -08:00
Max Schmitt 2cce9776be devops: stop publishing Ubuntu 20.04 (#1690) 2024-10-21 17:00:06 +02:00
104 changed files with 3599 additions and 954 deletions
+38 -22
View File
@@ -25,7 +25,12 @@ extends:
stages:
- stage: Stage
jobs:
- job: HostJob
- job: Build
templateContext:
outputs:
- output: pipelineArtifact
path: $(Build.ArtifactStagingDirectory)/esrp-build
artifact: esrp-build
steps:
- bash: |
if [[ ! "$CURRENT_BRANCH" =~ ^release-.* ]]; then
@@ -50,28 +55,39 @@ extends:
- bash: ./scripts/download_driver.sh
displayName: 'Download driver'
- bash: mvn -B deploy -D skipTests --no-transfer-progress --activate-profiles release -D gpg.passphrase=$GPG_PASSPHRASE -DaltDeploymentRepository=snapshot-repo::default::file:$(pwd)/local-build
- bash: mvn -B deploy -D skipTests --no-transfer-progress --activate-profiles release -D gpg.passphrase=$GPG_PASSPHRASE -DaltDeploymentRepository=snapshot-repo::default::file:$(Build.ArtifactStagingDirectory)/esrp-build
displayName: 'Build and deploy to a local directory'
env:
GPG_PASSPHRASE: $(GPG_PASSPHRASE) # secret variable has to be mapped to an env variable
- task: EsrpRelease@7
- job: Publish
dependsOn: Build
templateContext:
type: releaseJob
isProduction: true
inputs:
connectedservicename: 'Playwright-ESRP-Azure'
keyvaultname: 'pw-publishing-secrets'
authcertname: 'ESRP-Release-Auth'
signcertname: 'ESRP-Release-Sign'
clientid: '13434a40-7de4-4c23-81a3-d843dc81c2c5'
intent: 'PackageDistribution'
contenttype: 'Maven'
# Keeping it commented out as a workaround for:
# https://portal.microsofticm.com/imp/v3/incidents/incident/499972482/summary
# contentsource: 'folder'
folderlocation: './local-build'
waitforreleasecompletion: true
owners: 'yurys@microsoft.com'
approvers: 'maxschmitt@microsoft.com'
serviceendpointurl: 'https://api.esrp.microsoft.com'
mainpublisher: 'Playwright'
domaintenantid: '72f988bf-86f1-41af-91ab-2d7cd011db47'
displayName: 'ESRP Release to Maven'
- input: pipelineArtifact
artifactName: esrp-build
targetPath: $(Build.ArtifactStagingDirectory)/esrp-build
steps:
- checkout: none
- task: EsrpRelease@9
inputs:
connectedservicename: 'Playwright-ESRP-PME'
usemanagedidentity: true
keyvaultname: 'playwright-esrp-pme'
signcertname: 'ESRP-Release-Sign'
clientid: '13434a40-7de4-4c23-81a3-d843dc81c2c5'
intent: 'PackageDistribution'
contenttype: 'Maven'
# Keeping it commented out as a workaround for:
# https://portal.microsofticm.com/imp/v3/incidents/incident/499972482/summary
# contentsource: 'folder'
folderlocation: '$(Build.ArtifactStagingDirectory)/esrp-build'
waitforreleasecompletion: true
owners: 'yurys@microsoft.com'
approvers: 'maxschmitt@microsoft.com'
serviceendpointurl: 'https://api.esrp.microsoft.com'
mainpublisher: 'Playwright'
domaintenantid: '975f013f-7f24-47e8-a7d3-abc4752bf346'
displayName: 'ESRP Release to Maven'
+28
View File
@@ -0,0 +1,28 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/java
{
"name": "Java",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/java:1-21-bookworm",
"features": {
"ghcr.io/devcontainers/features/java:1": {
"version": "none",
"installGradle": "false",
"installMaven": "true"
},
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
}
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "java -version",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
+8
View File
@@ -17,3 +17,11 @@ updates:
allow:
- dependency-type: "direct" # Optional: Only update direct dependencies
- dependency-type: "indirect" # Optional: Only update indirect (transitive) dependencies
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
groups:
actions:
patterns:
- "*"
+14 -9
View File
@@ -8,6 +8,8 @@ on:
branches:
- main
- release-*
env:
PW_MAX_RETRIES: 3
jobs:
dev:
timeout-minutes: 30
@@ -18,10 +20,9 @@ jobs:
browser: [chromium, firefox, webkit]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1
- uses: actions/checkout@v4
- name: Set up JDK 1.8
uses: actions/setup-java@v2
uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 8
@@ -30,6 +31,8 @@ jobs:
run: scripts/download_driver.sh
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Install browsers
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps" -f playwright/pom.xml --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress --fail-at-end -D org.slf4j.simpleLogger.showDateTime=true -D org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss
env:
@@ -62,14 +65,13 @@ jobs:
browser-channel: msedge
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1
- uses: actions/checkout@v4
- name: Install Media Pack
if: matrix.os == 'windows-latest'
shell: powershell
run: Install-WindowsFeature Server-Media-Foundation
- name: Set up JDK 1.8
uses: actions/setup-java@v2
uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 8
@@ -78,6 +80,8 @@ jobs:
run: scripts/download_driver.sh
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Install browsers
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps" -f playwright/pom.xml --no-transfer-progress
- name: Install MS Edge
if: matrix.browser-channel == 'msedge' && matrix.os == 'macos-latest'
shell: bash
@@ -96,10 +100,9 @@ jobs:
browser: [chromium, firefox, webkit]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v2
uses: actions/setup-java@v4
with:
distribution: adopt
java-version: 21
@@ -108,6 +111,8 @@ jobs:
run: scripts/download_driver.sh
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Install browsers
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps" -f playwright/pom.xml --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress --fail-at-end
env:
+2 -2
View File
@@ -13,9 +13,9 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Cache Maven packages
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+2 -2
View File
@@ -24,9 +24,9 @@ jobs:
strategy:
fail-fast: false
matrix:
flavor: [focal, jammy, noble]
flavor: [jammy, noble]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Build Docker image
run: bash utils/docker/build.sh --amd64 ${{ matrix.flavor }} playwright-java:localbuild-${{ matrix.flavor }}
- name: Test
+9 -3
View File
@@ -9,13 +9,19 @@ on:
jobs:
trigger:
name: "trigger"
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.PLAYWRIGHT_APP_ID }}
private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }}
repositories: playwright-browsers
- run: |
curl -X POST \
curl -X POST --fail \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${GH_TOKEN}" \
--data "{\"event_type\": \"playwright_tests_java\", \"client_payload\": {\"ref\": \"${GITHUB_SHA}\"}}" \
https://api.github.com/repos/microsoft/playwright-browsers/dispatches
env:
GH_TOKEN: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
+5 -2
View File
@@ -19,12 +19,15 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: microsoft/playwright-github-action@v1
- uses: actions/checkout@v4
- name: Download drivers
run: scripts/download_driver.sh
- name: Regenerate APIs
run: scripts/generate_api.sh
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Install browsers
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps" -f playwright/pom.xml --no-transfer-progress
- name: Update browser versions in README
run: scripts/update_readme.sh
- name: Verify API is up to date
+13 -144
View File
@@ -10,59 +10,19 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->130.0.6723.31<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->131.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->136.0.7103.25<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->137.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all the browsers on all platforms. Check out [system requirements](https://playwright.dev/java/docs/intro#system-requirements) for details.
## Documentation
* [Usage](#usage)
- [Add Maven dependency](#add-maven-dependency)
- [Is Playwright thread-safe?](#is-playwright-thread-safe)
* [Examples](#examples)
- [Page screenshot](#page-screenshot)
- [Mobile and geolocation](#mobile-and-geolocation)
- [Evaluate JavaScript in browser](#evaluate-javascript-in-browser)
- [Intercept network requests](#intercept-network-requests)
* [Documentation](#documentation)
* [Contributing](#contributing)
* [Is Playwright for Java ready?](#is-playwright-for-java-ready)
[https://playwright.dev/java/docs/intro](https://playwright.dev/java/docs/intro)
## Usage
## API Reference
Playwright requires **Java 8** or newer.
[https://playwright.dev/java/docs/api/class-playwright](https://playwright.dev/java/docs/api/class-playwright)
#### Add Maven dependency
Playwright is distributed as a set of [Maven](https://maven.apache.org/what-is-maven.html) modules. The easiest way to use it is to add one dependency to your Maven `pom.xml` file as described below. If you're not familiar with Maven please refer to its [documentation](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html).
To run Playwright simply add following dependency to your Maven project:
```xml
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.41.0</version>
</dependency>
```
To run Playwright using Gradle add following dependency to your build.gradle file:
```gradle
dependencies {
implementation group: 'com.microsoft.playwright', name: 'playwright', version: '1.41.0'
}
```
#### Is Playwright thread-safe?
No, Playwright is not thread safe, i.e. all its methods as well as methods on all objects created by it (such as BrowserContext, Browser, Page etc.) are expected to be called on the same thread where Playwright object was created or proper synchronization should be implemented to ensure only one thread calls Playwright methods at any given time. Having said that it's okay to create multiple Playwright instances each on its own thread.
## Examples
You can find Maven project with the examples [here](./examples).
#### Page screenshot
## Example
This code snippet navigates to Playwright homepage in Chromium, Firefox and WebKit, and saves 3 screenshots.
@@ -94,100 +54,9 @@ public class PageScreenshot {
}
```
#### Mobile and geolocation
## Other languages
This snippet emulates Mobile Chromium on a device at a given geolocation, navigates to openstreetmap.org, performs action and takes a screenshot.
```java
import com.microsoft.playwright.options.*;
import com.microsoft.playwright.*;
import java.nio.file.Paths;
import static java.util.Arrays.asList;
public class MobileAndGeolocation {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setUserAgent("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36")
.setViewportSize(411, 731)
.setDeviceScaleFactor(2.625)
.setIsMobile(true)
.setHasTouch(true)
.setLocale("en-US")
.setGeolocation(41.889938, 12.492507)
.setPermissions(asList("geolocation")));
Page page = context.newPage();
page.navigate("https://www.openstreetmap.org/");
page.click("a[data-bs-original-title=\"Show My Location\"]");
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("colosseum-pixel2.png")));
}
}
}
```
#### Evaluate JavaScript in browser
This code snippet navigates to example.com in Firefox, and executes a script in the page context.
```java
import com.microsoft.playwright.*;
public class EvaluateInBrowserContext {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.firefox().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://www.example.com/");
Object dimensions = page.evaluate("() => {\n" +
" return {\n" +
" width: document.documentElement.clientWidth,\n" +
" height: document.documentElement.clientHeight,\n" +
" deviceScaleFactor: window.devicePixelRatio\n" +
" }\n" +
"}");
System.out.println(dimensions);
}
}
}
```
#### Intercept network requests
This code snippet sets up request routing for a WebKit page to log all network requests.
```java
import com.microsoft.playwright.*;
public class InterceptNetworkRequests {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.webkit().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.route("**", route -> {
System.out.println(route.request().url());
route.resume();
});
page.navigate("http://todomvc.com");
}
}
}
```
## Documentation
Check out our official [documentation site](https://playwright.dev/java).
You can also browse [javadoc online](https://www.javadoc.io/doc/com.microsoft.playwright/playwright/latest/index.html).
## Contributing
Follow [the instructions](https://github.com/microsoft/playwright-java/blob/main/CONTRIBUTING.md#getting-code) to build the project from source and install the driver.
## Is Playwright for Java ready?
Yes, Playwright for Java is ready. v1.10.0 is the first stable release. Going forward we will adhere to [semantic versioning](https://semver.org/) of the API.
More comfortable in another programming language? [Playwright](https://playwright.dev) is also available in
- [Node.js (JavaScript / TypeScript)](https://playwright.dev/docs/intro),
- [Python](https://playwright.dev/python/docs/intro).
- [.NET](https://playwright.dev/dotnet/docs/intro),
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.46.0-SNAPSHOT</version>
<version>1.52.0</version>
</parent>
<artifactId>driver-bundle</artifactId>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.46.0-SNAPSHOT</version>
<version>1.52.0</version>
</parent>
<artifactId>driver</artifactId>
+3 -2
View File
@@ -6,16 +6,17 @@
<groupId>org.example</groupId>
<artifactId>examples</artifactId>
<version>1.46.0-SNAPSHOT</version>
<version>1.52.0</version>
<name>Playwright Client Examples</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<playwright.version>1.51.0</playwright.version>
</properties>
<dependencies>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.41.0</version>
<version>${playwright.version}</version>
</dependency>
</dependencies>
<build>
+10 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.46.0-SNAPSHOT</version>
<version>1.52.0</version>
</parent>
<artifactId>playwright</artifactId>
@@ -57,6 +57,15 @@
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
</dependency>
<!--
The following slf4j-simple dependency resolves the warning:
'SLF4J(W): No SLF4J providers were found.'
This warning is produced by the org.java-websocket library.
-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
@@ -59,6 +59,10 @@ public interface APIRequest {
* An object containing additional HTTP headers to be sent with every request. Defaults to none.
*/
public Map<String, String> extraHTTPHeaders;
/**
* Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
*/
public Boolean failOnStatusCode;
/**
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
@@ -68,6 +72,12 @@ public interface APIRequest {
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
*/
public Boolean ignoreHTTPSErrors;
/**
* Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is
* exceeded. Defaults to {@code 20}. Pass {@code 0} to not follow redirects. This can be overwritten for each request
* individually.
*/
public Integer maxRedirects;
/**
* Network proxy settings.
*/
@@ -138,6 +148,13 @@ public interface APIRequest {
this.extraHTTPHeaders = extraHTTPHeaders;
return this;
}
/**
* Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
*/
public NewContextOptions setFailOnStatusCode(boolean failOnStatusCode) {
this.failOnStatusCode = failOnStatusCode;
return this;
}
/**
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
@@ -160,6 +177,15 @@ public interface APIRequest {
this.ignoreHTTPSErrors = ignoreHTTPSErrors;
return this;
}
/**
* Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is
* exceeded. Defaults to {@code 20}. Pass {@code 0} to not follow redirects. This can be overwritten for each request
* individually.
*/
public NewContextOptions setMaxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
return this;
}
/**
* Network proxy settings.
*/
@@ -58,12 +58,23 @@ public interface APIRequestContext {
}
}
class StorageStateOptions {
/**
* Set to {@code true} to include IndexedDB in the storage state snapshot.
*/
public Boolean indexedDB;
/**
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
*/
public Path path;
/**
* Set to {@code true} to include IndexedDB in the storage state snapshot.
*/
public StorageStateOptions setIndexedDB(boolean indexedDB) {
this.indexedDB = indexedDB;
return this;
}
/**
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
@@ -29,15 +29,15 @@ import java.util.regex.Pattern;
* import com.microsoft.playwright.*;
*
* public class Example {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType firefox = playwright.firefox()
* Browser browser = firefox.launch();
* Page page = browser.newPage();
* page.navigate('https://example.com');
* browser.close();
* }
* }
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType firefox = playwright.firefox();
* Browser browser = firefox.launch();
* Page page = browser.newPage();
* page.navigate("https://example.com");
* browser.close();
* }
* }
* }
* }</pre>
*/
@@ -111,11 +111,19 @@ public interface Browser extends AutoCloseable {
*/
public List<ClientCertificate> clientCertificates;
/**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code
* "no-preference"}. See {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing
* {@code null} resets emulation to system defaults. Defaults to {@code "light"}.
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. See {@link com.microsoft.playwright.Page#emulateMedia
* Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. Defaults to {@code
* "light"}.
*/
public Optional<ColorScheme> colorScheme;
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
* {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "no-preference"}.
*/
public Optional<Contrast> contrast;
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
@@ -323,14 +331,25 @@ public interface Browser extends AutoCloseable {
return this;
}
/**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code
* "no-preference"}. See {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing
* {@code null} resets emulation to system defaults. Defaults to {@code "light"}.
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. See {@link com.microsoft.playwright.Page#emulateMedia
* Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. Defaults to {@code
* "light"}.
*/
public NewContextOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = Optional.ofNullable(colorScheme);
return this;
}
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
* {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "no-preference"}.
*/
public NewContextOptions setContrast(Contrast contrast) {
this.contrast = Optional.ofNullable(contrast);
return this;
}
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
@@ -660,11 +679,19 @@ public interface Browser extends AutoCloseable {
*/
public List<ClientCertificate> clientCertificates;
/**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code
* "no-preference"}. See {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing
* {@code null} resets emulation to system defaults. Defaults to {@code "light"}.
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. See {@link com.microsoft.playwright.Page#emulateMedia
* Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. Defaults to {@code
* "light"}.
*/
public Optional<ColorScheme> colorScheme;
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
* {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "no-preference"}.
*/
public Optional<Contrast> contrast;
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
@@ -872,14 +899,25 @@ public interface Browser extends AutoCloseable {
return this;
}
/**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code
* "no-preference"}. See {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing
* {@code null} resets emulation to system defaults. Defaults to {@code "light"}.
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. See {@link com.microsoft.playwright.Page#emulateMedia
* Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. Defaults to {@code
* "light"}.
*/
public NewPageOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = Optional.ofNullable(colorScheme);
return this;
}
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
* {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "no-preference"}.
*/
public NewPageOptions setContrast(Contrast contrast) {
this.contrast = Optional.ofNullable(contrast);
return this;
}
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
@@ -1217,10 +1255,10 @@ public interface Browser extends AutoCloseable {
* <p> In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from the
* browser server.
*
* <p> <strong>NOTE:</strong> This is similar to force quitting the browser. Therefore, you should call {@link
* com.microsoft.playwright.BrowserContext#close BrowserContext.close()} on any {@code BrowserContext}'s you explicitly
* created earlier with {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} **before** calling {@link
* com.microsoft.playwright.Browser#close Browser.close()}.
* <p> <strong>NOTE:</strong> This is similar to force-quitting the browser. To close pages gracefully and ensure you receive page close events, call
* {@link com.microsoft.playwright.BrowserContext#close BrowserContext.close()} on any {@code BrowserContext} instances you
* explicitly created earlier using {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} **before**
* calling {@link com.microsoft.playwright.Browser#close Browser.close()}.
*
* <p> The {@code Browser} object itself is considered to be disposed and cannot be used anymore.
*
@@ -1236,10 +1274,10 @@ public interface Browser extends AutoCloseable {
* <p> In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from the
* browser server.
*
* <p> <strong>NOTE:</strong> This is similar to force quitting the browser. Therefore, you should call {@link
* com.microsoft.playwright.BrowserContext#close BrowserContext.close()} on any {@code BrowserContext}'s you explicitly
* created earlier with {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} **before** calling {@link
* com.microsoft.playwright.Browser#close Browser.close()}.
* <p> <strong>NOTE:</strong> This is similar to force-quitting the browser. To close pages gracefully and ensure you receive page close events, call
* {@link com.microsoft.playwright.BrowserContext#close BrowserContext.close()} on any {@code BrowserContext} instances you
* explicitly created earlier using {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} **before**
* calling {@link com.microsoft.playwright.Browser#close Browser.close()}.
*
* <p> The {@code Browser} object itself is considered to be disposed and cannot be used anymore.
*
@@ -1289,7 +1327,7 @@ public interface Browser extends AutoCloseable {
* BrowserContext context = browser.newContext();
* // Create a new page in a pristine context.
* Page page = context.newPage();
* page.navigate('https://example.com');
* page.navigate("https://example.com");
*
* // Graceful close up everything
* context.close();
@@ -1316,7 +1354,7 @@ public interface Browser extends AutoCloseable {
* BrowserContext context = browser.newContext();
* // Create a new page in a pristine context.
* Page page = context.newPage();
* page.navigate('https://example.com');
* page.navigate("https://example.com");
*
* // Graceful close up everything
* context.close();
@@ -1364,7 +1402,7 @@ public interface Browser extends AutoCloseable {
* <pre>{@code
* browser.startTracing(page, new Browser.StartTracingOptions()
* .setPath(Paths.get("trace.json")));
* page.goto('https://www.google.com');
* page.navigate("https://www.google.com");
* browser.stopTracing();
* }</pre>
*
@@ -1388,7 +1426,7 @@ public interface Browser extends AutoCloseable {
* <pre>{@code
* browser.startTracing(page, new Browser.StartTracingOptions()
* .setPath(Paths.get("trace.json")));
* page.goto('https://www.google.com');
* page.navigate("https://www.google.com");
* browser.stopTracing();
* }</pre>
*
@@ -1411,7 +1449,7 @@ public interface Browser extends AutoCloseable {
* <pre>{@code
* browser.startTracing(page, new Browser.StartTracingOptions()
* .setPath(Paths.get("trace.json")));
* page.goto('https://www.google.com');
* page.navigate("https://www.google.com");
* browser.stopTracing();
* }</pre>
*
@@ -407,12 +407,27 @@ public interface BrowserContext extends AutoCloseable {
}
}
class StorageStateOptions {
/**
* Set to {@code true} to include <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a> in
* the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase
* Authentication, enable this.
*/
public Boolean indexedDB;
/**
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
*/
public Path path;
/**
* Set to {@code true} to include <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a> in
* the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase
* Authentication, enable this.
*/
public StorageStateOptions setIndexedDB(boolean indexedDB) {
this.indexedDB = indexedDB;
return this;
}
/**
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
@@ -701,7 +716,7 @@ public interface BrowserContext extends AutoCloseable {
* public class Example {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType webkit = playwright.webkit()
* 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());
@@ -748,7 +763,7 @@ public interface BrowserContext extends AutoCloseable {
* public class Example {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType webkit = playwright.webkit()
* 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());
@@ -797,8 +812,9 @@ public interface BrowserContext extends AutoCloseable {
* public class Example {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType webkit = playwright.webkit()
* BrowserType webkit = playwright.webkit();
* Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
* BrowserContext context = browser.newContext();
* context.exposeFunction("sha256", args -> {
* String text = (String) args[0];
* MessageDigest crypto;
@@ -833,10 +849,14 @@ public interface BrowserContext extends AutoCloseable {
* Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if
* specified.
*
* @param permissions A permission or an array of permissions to grant. Permissions can be one of the following values:
* @param permissions A list of permissions to grant.
*
* <p> <strong>NOTE:</strong> Supported permissions differ between browsers, and even between different versions of the same browser. Any permission
* may stop working after an update.
*
* <p> Here are some permissions that may be supported by some browsers:
* <ul>
* <li> {@code "accelerometer"}</li>
* <li> {@code "accessibility-events"}</li>
* <li> {@code "ambient-light-sensor"}</li>
* <li> {@code "background-sync"}</li>
* <li> {@code "camera"}</li>
@@ -861,10 +881,14 @@ public interface BrowserContext extends AutoCloseable {
* Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if
* specified.
*
* @param permissions A permission or an array of permissions to grant. Permissions can be one of the following values:
* @param permissions A list of permissions to grant.
*
* <p> <strong>NOTE:</strong> Supported permissions differ between browsers, and even between different versions of the same browser. Any permission
* may stop working after an update.
*
* <p> Here are some permissions that may be supported by some browsers:
* <ul>
* <li> {@code "accelerometer"}</li>
* <li> {@code "accessibility-events"}</li>
* <li> {@code "ambient-light-sensor"}</li>
* <li> {@code "background-sync"}</li>
* <li> {@code "camera"}</li>
@@ -968,8 +992,8 @@ public interface BrowserContext extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
@@ -1024,8 +1048,8 @@ public interface BrowserContext extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
@@ -1078,8 +1102,8 @@ public interface BrowserContext extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
@@ -1134,8 +1158,8 @@ public interface BrowserContext extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
@@ -1188,8 +1212,8 @@ public interface BrowserContext extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
@@ -1244,8 +1268,8 @@ public interface BrowserContext extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
@@ -1387,7 +1411,7 @@ public interface BrowserContext extends AutoCloseable {
* com.microsoft.playwright.BrowserContext#setDefaultNavigationTimeout BrowserContext.setDefaultNavigationTimeout()} take
* priority over {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}.
*
* @param timeout Maximum time in milliseconds
* @param timeout Maximum time in milliseconds. Pass {@code 0} to disable timeout.
* @since v1.8
*/
void setDefaultTimeout(double timeout);
@@ -1426,7 +1450,7 @@ public interface BrowserContext extends AutoCloseable {
*/
void setOffline(boolean offline);
/**
* Returns storage state for this browser context, contains current cookies and local storage snapshot.
* Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
*
* @since v1.8
*/
@@ -1434,7 +1458,7 @@ public interface BrowserContext extends AutoCloseable {
return storageState(null);
}
/**
* Returns storage state for this browser context, contains current cookies and local storage snapshot.
* Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
*
* @since v1.8
*/
@@ -172,9 +172,14 @@ public interface BrowserType {
*/
public List<String> args;
/**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
* Browser distribution channel.
*
* <p> Use "chromium" to <a href="https://playwright.dev/java/docs/browsers#chromium-new-headless-mode">opt in to new headless
* mode</a>.
*
* <p> Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to
* use branded <a href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and
* Microsoft Edge</a>.
*/
public Object channel;
/**
@@ -221,8 +226,8 @@ public interface BrowserType {
/**
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true}
* unless the {@code devtools} option is {@code true}.
* href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true} unless
* the {@code devtools} option is {@code true}.
*/
public Boolean headless;
/**
@@ -265,18 +270,28 @@ public interface BrowserType {
}
@Deprecated
/**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
* Browser distribution channel.
*
* <p> Use "chromium" to <a href="https://playwright.dev/java/docs/browsers#chromium-new-headless-mode">opt in to new headless
* mode</a>.
*
* <p> Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to
* use branded <a href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and
* Microsoft Edge</a>.
*/
public LaunchOptions setChannel(BrowserChannel channel) {
this.channel = channel;
return this;
}
/**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
* Browser distribution channel.
*
* <p> Use "chromium" to <a href="https://playwright.dev/java/docs/browsers#chromium-new-headless-mode">opt in to new headless
* mode</a>.
*
* <p> Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to
* use branded <a href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and
* Microsoft Edge</a>.
*/
public LaunchOptions setChannel(String channel) {
this.channel = channel;
@@ -353,8 +368,8 @@ public interface BrowserType {
/**
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true}
* unless the {@code devtools} option is {@code true}.
* href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true} unless
* the {@code devtools} option is {@code true}.
*/
public LaunchOptions setHeadless(boolean headless) {
this.headless = headless;
@@ -446,9 +461,14 @@ public interface BrowserType {
*/
public Boolean bypassCSP;
/**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
* Browser distribution channel.
*
* <p> Use "chromium" to <a href="https://playwright.dev/java/docs/browsers#chromium-new-headless-mode">opt in to new headless
* mode</a>.
*
* <p> Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to
* use branded <a href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and
* Microsoft Edge</a>.
*/
public Object channel;
/**
@@ -470,11 +490,19 @@ public interface BrowserType {
*/
public List<ClientCertificate> clientCertificates;
/**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code
* "no-preference"}. See {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing
* {@code null} resets emulation to system defaults. Defaults to {@code "light"}.
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. See {@link com.microsoft.playwright.Page#emulateMedia
* Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. Defaults to {@code
* "light"}.
*/
public Optional<ColorScheme> colorScheme;
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
* {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "no-preference"}.
*/
public Optional<Contrast> contrast;
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
@@ -536,8 +564,8 @@ public interface BrowserType {
/**
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true}
* unless the {@code devtools} option is {@code true}.
* href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true} unless
* the {@code devtools} option is {@code true}.
*/
public Boolean headless;
/**
@@ -732,18 +760,28 @@ public interface BrowserType {
}
@Deprecated
/**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
* Browser distribution channel.
*
* <p> Use "chromium" to <a href="https://playwright.dev/java/docs/browsers#chromium-new-headless-mode">opt in to new headless
* mode</a>.
*
* <p> Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to
* use branded <a href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and
* Microsoft Edge</a>.
*/
public LaunchPersistentContextOptions setChannel(BrowserChannel channel) {
this.channel = channel;
return this;
}
/**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge",
* "msedge-beta", "msedge-dev", "msedge-canary". Read more about using <a
* href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and Microsoft Edge</a>.
* Browser distribution channel.
*
* <p> Use "chromium" to <a href="https://playwright.dev/java/docs/browsers#chromium-new-headless-mode">opt in to new headless
* mode</a>.
*
* <p> Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to
* use branded <a href="https://playwright.dev/java/docs/browsers#google-chrome--microsoft-edge">Google Chrome and
* Microsoft Edge</a>.
*/
public LaunchPersistentContextOptions setChannel(String channel) {
this.channel = channel;
@@ -774,14 +812,25 @@ public interface BrowserType {
return this;
}
/**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code
* "no-preference"}. See {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing
* {@code null} resets emulation to system defaults. Defaults to {@code "light"}.
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. See {@link com.microsoft.playwright.Page#emulateMedia
* Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. Defaults to {@code
* "light"}.
*/
public LaunchPersistentContextOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = Optional.ofNullable(colorScheme);
return this;
}
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
* {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
* emulation to system defaults. Defaults to {@code "no-preference"}.
*/
public LaunchPersistentContextOptions setContrast(Contrast contrast) {
this.contrast = Optional.ofNullable(contrast);
return this;
}
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
@@ -885,8 +934,8 @@ public interface BrowserType {
/**
* Whether to run browser in headless mode. More details for <a
* href="https://developers.google.com/web/updates/2017/04/headless-chrome">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode">Firefox</a>. Defaults to {@code true}
* unless the {@code devtools} option is {@code true}.
* href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox</a>. Defaults to {@code true} unless
* the {@code devtools} option is {@code true}.
*/
public LaunchPersistentContextOptions setHeadless(boolean headless) {
this.headless = headless;
@@ -1163,22 +1212,24 @@ public interface BrowserType {
}
}
/**
* This method attaches Playwright to an existing browser instance. When connecting to another browser launched via {@code
* BrowserType.launchServer} in Node.js, the major and minor version needs to match the client version (1.2.3 → is
* compatible with 1.2.x).
* This method attaches Playwright to an existing browser instance created via {@code BrowserType.launchServer} in Node.js.
*
* @param wsEndpoint A browser websocket endpoint to connect to.
* <p> <strong>NOTE:</strong> The major and minor version of the Playwright instance that connects needs to match the version of Playwright that
* launches the browser (1.2.3 → is compatible with 1.2.x).
*
* @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}.
* @since v1.8
*/
default Browser connect(String wsEndpoint) {
return connect(wsEndpoint, null);
}
/**
* This method attaches Playwright to an existing browser instance. When connecting to another browser launched via {@code
* BrowserType.launchServer} in Node.js, the major and minor version needs to match the client version (1.2.3 → is
* compatible with 1.2.x).
* This method attaches Playwright to an existing browser instance created via {@code BrowserType.launchServer} in Node.js.
*
* @param wsEndpoint A browser websocket endpoint to connect to.
* <p> <strong>NOTE:</strong> The major and minor version of the Playwright instance that connects needs to match the version of Playwright that
* launches the browser (1.2.3 → is compatible with 1.2.x).
*
* @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}.
* @since v1.8
*/
Browser connect(String wsEndpoint, ConnectOptions options);
@@ -1189,6 +1240,11 @@ public interface BrowserType {
*
* <p> <strong>NOTE:</strong> Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
*
* <p> <strong>NOTE:</strong> This connection is significantly lower fidelity than the Playwright protocol connection via {@link
* com.microsoft.playwright.BrowserType#connect BrowserType.connect()}. If you are experiencing issues or attempting to use
* advanced functionality, you probably want to use {@link com.microsoft.playwright.BrowserType#connect
* BrowserType.connect()}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Browser browser = playwright.chromium().connectOverCDP("http://localhost:9222");
@@ -1210,6 +1266,11 @@ public interface BrowserType {
*
* <p> <strong>NOTE:</strong> Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
*
* <p> <strong>NOTE:</strong> This connection is significantly lower fidelity than the Playwright protocol connection via {@link
* com.microsoft.playwright.BrowserType#connect BrowserType.connect()}. If you are experiencing issues or attempting to use
* advanced functionality, you probably want to use {@link com.microsoft.playwright.BrowserType#connect
* BrowserType.connect()}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Browser browser = playwright.chromium().connectOverCDP("http://localhost:9222");
@@ -1304,11 +1365,15 @@ public interface BrowserType {
* <p> Launches browser that uses persistent storage located at {@code userDataDir} and returns the only context. Closing this
* context will automatically close the browser.
*
* @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for <a
* @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. Pass an empty string to
* create a temporary directory.
*
* <p> More details for <a
* href="https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile">Firefox</a>. Note that
* Chromium's user data directory is the **parent** directory of the "Profile Path" seen at {@code chrome://version}. Pass
* an empty string to use a temporary directory instead.
* href="https://wiki.mozilla.org/Firefox/CommandLineOptions#User_profile">Firefox</a>. Chromium's user data directory is
* the **parent** directory of the "Profile Path" seen at {@code chrome://version}.
*
* <p> Note that browsers do not allow launching multiple instances with the same User Data Directory.
* @since v1.8
*/
default BrowserContext launchPersistentContext(Path userDataDir) {
@@ -1320,11 +1385,15 @@ public interface BrowserType {
* <p> Launches browser that uses persistent storage located at {@code userDataDir} and returns the only context. Closing this
* context will automatically close the browser.
*
* @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for <a
* @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. Pass an empty string to
* create a temporary directory.
*
* <p> More details for <a
* href="https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction">Chromium</a> and <a
* href="https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile">Firefox</a>. Note that
* Chromium's user data directory is the **parent** directory of the "Profile Path" seen at {@code chrome://version}. Pass
* an empty string to use a temporary directory instead.
* href="https://wiki.mozilla.org/Firefox/CommandLineOptions#User_profile">Firefox</a>. Chromium's user data directory is
* the **parent** directory of the "Profile Path" seen at {@code chrome://version}.
*
* <p> Note that browsers do not allow launching multiple instances with the same User Data Directory.
* @since v1.8
*/
BrowserContext launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options);
@@ -174,6 +174,19 @@ public interface Clock {
* page.clock().pauseAt("2020-02-02");
* }</pre>
*
* <p> For best results, install the clock before navigating the page and set it to a time slightly before the intended test
* time. This ensures that all timers run normally during page loading, preventing the page from getting stuck. Once the
* page has fully loaded, you can safely use {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} to pause the
* clock.
* <pre>{@code
* // Initialize clock with some time before the test time and let the page load
* // naturally. `Date.now` will progress as the timers fire.
* SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
* page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-12-10T08:00:00")));
* page.navigate("http://localhost:3333");
* page.clock().pauseAt(format.parse("2024-12-10T10:00:00"));
* }</pre>
*
* @param time Time to pause at.
* @since v1.45
*/
@@ -194,6 +207,19 @@ public interface Clock {
* page.clock().pauseAt("2020-02-02");
* }</pre>
*
* <p> For best results, install the clock before navigating the page and set it to a time slightly before the intended test
* time. This ensures that all timers run normally during page loading, preventing the page from getting stuck. Once the
* page has fully loaded, you can safely use {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} to pause the
* clock.
* <pre>{@code
* // Initialize clock with some time before the test time and let the page load
* // naturally. `Date.now` will progress as the timers fire.
* SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
* page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-12-10T08:00:00")));
* page.navigate("http://localhost:3333");
* page.clock().pauseAt(format.parse("2024-12-10T10:00:00"));
* }</pre>
*
* @param time Time to pause at.
* @since v1.45
*/
@@ -214,6 +240,19 @@ public interface Clock {
* page.clock().pauseAt("2020-02-02");
* }</pre>
*
* <p> For best results, install the clock before navigating the page and set it to a time slightly before the intended test
* time. This ensures that all timers run normally during page loading, preventing the page from getting stuck. Once the
* page has fully loaded, you can safely use {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} to pause the
* clock.
* <pre>{@code
* // Initialize clock with some time before the test time and let the page load
* // naturally. `Date.now` will progress as the timers fire.
* SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
* page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-12-10T08:00:00")));
* page.navigate("http://localhost:3333");
* page.clock().pauseAt(format.parse("2024-12-10T10:00:00"));
* }</pre>
*
* @param time Time to pause at.
* @since v1.45
*/
@@ -227,6 +266,10 @@ public interface Clock {
/**
* Makes {@code Date.now} and {@code new Date()} return fixed fake time at all times, keeps all the timers running.
*
* <p> Use this method for simple scenarios where you only need to test with a predefined time. For more advanced scenarios,
* use {@link com.microsoft.playwright.Clock#install Clock.install()} instead. Read docs on <a
* href="https://playwright.dev/java/docs/clock">clock emulation</a> to learn more.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.clock().setFixedTime(new Date());
@@ -241,6 +284,10 @@ public interface Clock {
/**
* Makes {@code Date.now} and {@code new Date()} return fixed fake time at all times, keeps all the timers running.
*
* <p> Use this method for simple scenarios where you only need to test with a predefined time. For more advanced scenarios,
* use {@link com.microsoft.playwright.Clock#install Clock.install()} instead. Read docs on <a
* href="https://playwright.dev/java/docs/clock">clock emulation</a> to learn more.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.clock().setFixedTime(new Date());
@@ -255,6 +302,10 @@ public interface Clock {
/**
* Makes {@code Date.now} and {@code new Date()} return fixed fake time at all times, keeps all the timers running.
*
* <p> Use this method for simple scenarios where you only need to test with a predefined time. For more advanced scenarios,
* use {@link com.microsoft.playwright.Clock#install Clock.install()} instead. Read docs on <a
* href="https://playwright.dev/java/docs/clock">clock emulation</a> to learn more.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.clock().setFixedTime(new Date());
@@ -267,7 +318,8 @@ public interface Clock {
*/
void setFixedTime(Date time);
/**
* Sets current system time but does not trigger any timers.
* Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for example
* switching from summer to winter time, or changing time zones.
*
* <p> <strong>Usage</strong>
* <pre>{@code
@@ -281,7 +333,8 @@ public interface Clock {
*/
void setSystemTime(long time);
/**
* Sets current system time but does not trigger any timers.
* Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for example
* switching from summer to winter time, or changing time zones.
*
* <p> <strong>Usage</strong>
* <pre>{@code
@@ -295,7 +348,8 @@ public interface Clock {
*/
void setSystemTime(String time);
/**
* Sets current system time but does not trigger any timers.
* Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for example
* switching from summer to winter time, or changing time zones.
*
* <p> <strong>Usage</strong>
* <pre>{@code
@@ -39,8 +39,8 @@ import java.util.*;
* });
*
* // Deconstruct console.log arguments
* msg.args().get(0).jsonValue() // hello
* msg.args().get(1).jsonValue() // 42
* msg.args().get(0).jsonValue(); // hello
* msg.args().get(1).jsonValue(); // 42
* }</pre>
*/
public interface ConsoleMessage {
@@ -599,7 +599,9 @@ public interface ElementHandle extends JSHandle {
public ScreenshotCaret caret;
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
* invisible elements, see <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
* visible elements</a> to disable that.
*/
public List<Locator> mask;
/**
@@ -673,7 +675,9 @@ public interface ElementHandle extends JSHandle {
}
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
* invisible elements, see <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
* visible elements</a> to disable that.
*/
public ScreenshotOptions setMask(List<Locator> mask) {
this.mask = mask;
@@ -3499,19 +3499,19 @@ public interface Frame {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -3541,19 +3541,19 @@ public interface Frame {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -3581,19 +3581,19 @@ public interface Frame {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -3623,19 +3623,19 @@ public interface Frame {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -734,19 +734,19 @@ public interface FrameLocator {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -776,19 +776,19 @@ public interface FrameLocator {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -816,19 +816,19 @@ public interface FrameLocator {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -858,19 +858,19 @@ public interface FrameLocator {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -48,10 +48,7 @@ import com.microsoft.playwright.options.*;
*
* <p> An example to trigger select-all with the keyboard
* <pre>{@code
* // on Windows and Linux
* page.keyboard().press("Control+A");
* // on macOS
* page.keyboard().press("Meta+A");
* page.keyboard().press("ControlOrMeta+A");
* }</pre>
*/
public interface Keyboard {
@@ -164,7 +161,7 @@ public interface Keyboard {
* Page page = browser.newPage();
* page.navigate("https://keycode.info");
* page.keyboard().press("A");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png"));
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png")));
* page.keyboard().press("ArrowLeft");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("ArrowLeft.png")));
* page.keyboard().press("Shift+O");
@@ -211,7 +208,7 @@ public interface Keyboard {
* Page page = browser.newPage();
* page.navigate("https://keycode.info");
* page.keyboard().press("A");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png"));
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("A.png")));
* page.keyboard().press("ArrowLeft");
* page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("ArrowLeft.png")));
* page.keyboard().press("Shift+O");
@@ -29,6 +29,39 @@ import java.util.regex.Pattern;
* <p> <a href="https://playwright.dev/java/docs/locators">Learn more about locators</a>.
*/
public interface Locator {
class AriaSnapshotOptions {
/**
* Generate symbolic reference for each element. One can use {@code aria-ref=<ref>} locator immediately after capturing the
* snapshot to perform actions on the element.
*/
public Boolean ref;
/**
* 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;
/**
* Generate symbolic reference for each element. One can use {@code aria-ref=<ref>} locator immediately after capturing the
* snapshot to perform actions on the element.
*/
public AriaSnapshotOptions setRef(boolean ref) {
this.ref = ref;
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 AriaSnapshotOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class BlurOptions {
/**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
@@ -580,18 +613,14 @@ public interface Locator {
}
class EvaluateOptions {
/**
* 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.
* Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation
* itself is not limited by the timeout. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout.
*/
public Double timeout;
/**
* 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.
* Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation
* itself is not limited by the timeout. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout.
*/
public EvaluateOptions setTimeout(double timeout) {
this.timeout = timeout;
@@ -600,18 +629,14 @@ public interface Locator {
}
class EvaluateHandleOptions {
/**
* 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.
* Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation
* itself is not limited by the timeout. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout.
*/
public Double timeout;
/**
* 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.
* Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation
* itself is not limited by the timeout. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout.
*/
public EvaluateHandleOptions setTimeout(double timeout) {
this.timeout = timeout;
@@ -695,6 +720,10 @@ public interface Locator {
* <article><div>Playwright</div></article>}.
*/
public Object hasText;
/**
* Only matches visible or invisible elements.
*/
public Boolean visible;
/**
* Narrows down the results of the method to those which contain elements matching this relative locator. For example,
@@ -757,6 +786,13 @@ public interface Locator {
this.hasText = hasText;
return this;
}
/**
* Only matches visible or invisible elements.
*/
public FilterOptions setVisible(boolean visible) {
this.visible = visible;
return this;
}
}
class FocusOptions {
/**
@@ -1499,7 +1535,9 @@ public interface Locator {
public ScreenshotCaret caret;
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
* invisible elements, see <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
* visible elements</a> to disable that.
*/
public List<Locator> mask;
/**
@@ -1573,7 +1611,9 @@ public interface Locator {
}
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
* invisible elements, see <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
* visible elements</a> to disable that.
*/
public ScreenshotOptions setMask(List<Locator> mask) {
this.mask = mask;
@@ -2151,7 +2191,7 @@ public interface Locator {
*
* <p> <strong>Usage</strong>
* <pre>{@code
* for (Locator li : page.getByRole('listitem').all())
* for (Locator li : page.getByRole("listitem").all())
* li.click();
* }</pre>
*
@@ -2202,6 +2242,66 @@ public interface Locator {
* @since v1.34
*/
Locator and(Locator locator);
/**
* Captures the aria snapshot of the given element. Read more about <a
* href="https://playwright.dev/java/docs/aria-snapshots">aria snapshots</a> and {@link
* com.microsoft.playwright.assertions.LocatorAssertions#matchesAriaSnapshot LocatorAssertions.matchesAriaSnapshot()} for
* the corresponding assertion.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.getByRole(AriaRole.LINK).ariaSnapshot();
* }</pre>
*
* <p> <strong>Details</strong>
*
* <p> This method captures the aria snapshot of the given element. The snapshot is a string that represents the state of the
* element and its children. The snapshot can be used to assert the state of the element in the test, or to compare it to
* state in the future.
*
* <p> The ARIA snapshot is represented using <a href="https://yaml.org/spec/1.2.2/">YAML</a> markup language:
* <ul>
* <li> The keys of the objects are the roles and optional accessible names of the elements.</li>
* <li> The values are either text content or an array of child elements.</li>
* <li> Generic static text can be represented with the {@code text} key.</li>
* </ul>
*
* <p> Below is the HTML markup and the respective ARIA snapshot:
*
* @since v1.49
*/
default String ariaSnapshot() {
return ariaSnapshot(null);
}
/**
* Captures the aria snapshot of the given element. Read more about <a
* href="https://playwright.dev/java/docs/aria-snapshots">aria snapshots</a> and {@link
* com.microsoft.playwright.assertions.LocatorAssertions#matchesAriaSnapshot LocatorAssertions.matchesAriaSnapshot()} for
* the corresponding assertion.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.getByRole(AriaRole.LINK).ariaSnapshot();
* }</pre>
*
* <p> <strong>Details</strong>
*
* <p> This method captures the aria snapshot of the given element. The snapshot is a string that represents the state of the
* element and its children. The snapshot can be used to assert the state of the element in the test, or to compare it to
* state in the future.
*
* <p> The ARIA snapshot is represented using <a href="https://yaml.org/spec/1.2.2/">YAML</a> markup language:
* <ul>
* <li> The keys of the objects are the roles and optional accessible names of the elements.</li>
* <li> The values are either text content or an array of child elements.</li>
* <li> Generic static text can be represented with the {@code text} key.</li>
* </ul>
*
* <p> Below is the HTML markup and the respective ARIA snapshot:
*
* @since v1.49
*/
String ariaSnapshot(AriaSnapshotOptions options);
/**
* Calls <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur">blur</a> on the element.
*
@@ -2550,7 +2650,6 @@ public interface Locator {
*
* <p> You can also specify {@code JSHandle} as the property value if you want live objects to be passed into the event:
* <pre>{@code
* // Note you can only create DataTransfer in Chromium and Firefox
* JSHandle dataTransfer = page.evaluateHandle("() => new DataTransfer()");
* Map<String, Object> arg = new HashMap<>();
* arg.put("dataTransfer", dataTransfer);
@@ -2599,7 +2698,6 @@ public interface Locator {
*
* <p> You can also specify {@code JSHandle} as the property value if you want live objects to be passed into the event:
* <pre>{@code
* // Note you can only create DataTransfer in Chromium and Firefox
* JSHandle dataTransfer = page.evaluateHandle("() => new DataTransfer()");
* Map<String, Object> arg = new HashMap<>();
* arg.put("dataTransfer", dataTransfer);
@@ -2647,7 +2745,6 @@ public interface Locator {
*
* <p> You can also specify {@code JSHandle} as the property value if you want live objects to be passed into the event:
* <pre>{@code
* // Note you can only create DataTransfer in Chromium and Firefox
* JSHandle dataTransfer = page.evaluateHandle("() => new DataTransfer()");
* Map<String, Object> arg = new HashMap<>();
* arg.put("dataTransfer", dataTransfer);
@@ -2763,10 +2860,6 @@ public interface Locator {
* <p> If {@code expression} throws or rejects, this method throws.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator tweets = page.locator(".tweet .retweets");
* assertEquals("10 retweets", tweets.evaluate("node => node.innerText"));
* }</pre>
*
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
@@ -2791,10 +2884,6 @@ public interface Locator {
* <p> If {@code expression} throws or rejects, this method throws.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator tweets = page.locator(".tweet .retweets");
* assertEquals("10 retweets", tweets.evaluate("node => node.innerText"));
* }</pre>
*
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
@@ -2818,10 +2907,6 @@ public interface Locator {
* <p> If {@code expression} throws or rejects, this method throws.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator tweets = page.locator(".tweet .retweets");
* assertEquals("10 retweets", tweets.evaluate("node => node.innerText"));
* }</pre>
*
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
@@ -3455,19 +3540,19 @@ public interface Locator {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -3497,19 +3582,19 @@ public interface Locator {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -3537,19 +3622,19 @@ public interface Locator {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -3579,19 +3664,19 @@ public interface Locator {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -3888,7 +3973,9 @@ public interface Locator {
*/
boolean isDisabled(IsDisabledOptions options);
/**
* Returns whether the element is <a href="https://playwright.dev/java/docs/actionability#editable">editable</a>.
* Returns whether the element is <a href="https://playwright.dev/java/docs/actionability#editable">editable</a>. If the
* target element is not an {@code <input>}, {@code <textarea>}, {@code <select>}, {@code [contenteditable]} and does not
* have a role allowing {@code [aria-readonly]}, this method throws an error.
*
* <p> <strong>NOTE:</strong> If you need to assert that an element is editable, prefer {@link
* com.microsoft.playwright.assertions.LocatorAssertions#isEditable LocatorAssertions.isEditable()} to avoid flakiness. See
@@ -3905,7 +3992,9 @@ public interface Locator {
return isEditable(null);
}
/**
* Returns whether the element is <a href="https://playwright.dev/java/docs/actionability#editable">editable</a>.
* Returns whether the element is <a href="https://playwright.dev/java/docs/actionability#editable">editable</a>. If the
* target element is not an {@code <input>}, {@code <textarea>}, {@code <select>}, {@code [contenteditable]} and does not
* have a role allowing {@code [aria-readonly]}, this method throws an error.
*
* <p> <strong>NOTE:</strong> If you need to assert that an element is editable, prefer {@link
* com.microsoft.playwright.assertions.LocatorAssertions#isEditable LocatorAssertions.isEditable()} to avoid flakiness. See
@@ -4086,17 +4175,21 @@ public interface Locator {
/**
* Creates a locator matching all elements that match one or both of the two locators.
*
* <p> Note that when both locators match something, the resulting locator will have multiple matches and violate <a
* href="https://playwright.dev/java/docs/locators#strictness">locator strictness</a> guidelines.
* <p> Note that when both locators match something, the resulting locator will have multiple matches, potentially causing a <a
* href="https://playwright.dev/java/docs/locators#strictness">locator strictness</a> violation.
*
* <p> <strong>Usage</strong>
*
* <p> Consider a scenario where you'd like to click on a "New email" button, but sometimes a security settings dialog shows up
* instead. In this case, you can wait for either a "New email" button, or a dialog and act accordingly.
*
* <p> <strong>NOTE:</strong> If both "New email" button and security dialog appear on screen, the "or" locator will match both of them, possibly
* throwing the <a href="https://playwright.dev/java/docs/locators#strictness">"strict mode violation" error</a>. In this
* case, you can use {@link com.microsoft.playwright.Locator#first Locator.first()} to only match one of them.
* <pre>{@code
* Locator newEmail = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("New"));
* Locator dialog = page.getByText("Confirm security settings");
* assertThat(newEmail.or(dialog)).isVisible();
* assertThat(newEmail.or(dialog).first()).isVisible();
* if (dialog.isVisible())
* page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Dismiss")).click();
* newEmail.click();
@@ -5127,7 +5220,9 @@ public interface Locator {
*/
void setInputFiles(FilePayload[] files, SetInputFilesOptions options);
/**
* Perform a tap gesture on the element matching the locator.
* Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually
* dispatching touch events, see the <a href="https://playwright.dev/java/docs/touch-events">emulating legacy touch
* events</a> page.
*
* <p> <strong>Details</strong>
*
@@ -5153,7 +5248,9 @@ public interface Locator {
tap(null);
}
/**
* Perform a tap gesture on the element matching the locator.
* Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually
* dispatching touch events, see the <a href="https://playwright.dev/java/docs/touch-events">emulating legacy touch
* events</a> page.
*
* <p> <strong>Details</strong>
*
@@ -959,10 +959,17 @@ public interface Page extends AutoCloseable {
}
class EmulateMediaOptions {
/**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code
* "no-preference"}. Passing {@code null} disables color scheme emulation.
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. Passing {@code null} disables color scheme emulation.
* {@code "no-preference"} is deprecated.
*/
public Optional<ColorScheme> colorScheme;
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. Passing
* {@code null} disables contrast emulation.
*/
public Optional<Contrast> contrast;
/**
* Emulates {@code "forced-colors"} media feature, supported values are {@code "active"} and {@code "none"}. Passing {@code
* null} disables forced colors emulation.
@@ -980,13 +987,23 @@ public interface Page extends AutoCloseable {
public Optional<ReducedMotion> reducedMotion;
/**
* Emulates {@code "prefers-colors-scheme"} media feature, supported values are {@code "light"}, {@code "dark"}, {@code
* "no-preference"}. Passing {@code null} disables color scheme emulation.
* Emulates <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-colors-scheme</a> media
* feature, supported values are {@code "light"} and {@code "dark"}. Passing {@code null} disables color scheme emulation.
* {@code "no-preference"} is deprecated.
*/
public EmulateMediaOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = Optional.ofNullable(colorScheme);
return this;
}
/**
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. Passing
* {@code null} disables contrast emulation.
*/
public EmulateMediaOptions setContrast(Contrast contrast) {
this.contrast = Optional.ofNullable(contrast);
return this;
}
/**
* Emulates {@code "forced-colors"} media feature, supported values are {@code "active"} and {@code "none"}. Passing {@code
* null} disables forced colors emulation.
@@ -2518,7 +2535,9 @@ public interface Page extends AutoCloseable {
public Boolean fullPage;
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
* invisible elements, see <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
* visible elements</a> to disable that.
*/
public List<Locator> mask;
/**
@@ -2613,7 +2632,9 @@ public interface Page extends AutoCloseable {
}
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
* {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
* invisible elements, see <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
* visible elements</a> to disable that.
*/
public ScreenshotOptions setMask(List<Locator> mask) {
this.mask = mask;
@@ -4151,9 +4172,9 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.dragAndDrop("#source", '#target');
* page.dragAndDrop("#source", "#target");
* // or specify exact positions relative to the top-left corners of the elements:
* page.dragAndDrop("#source", '#target', new Page.DragAndDropOptions()
* page.dragAndDrop("#source", "#target", new Page.DragAndDropOptions()
* .setSourcePosition(34, 7).setTargetPosition(10, 20));
* }</pre>
*
@@ -4172,9 +4193,9 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.dragAndDrop("#source", '#target');
* page.dragAndDrop("#source", "#target");
* // or specify exact positions relative to the top-left corners of the elements:
* page.dragAndDrop("#source", '#target', new Page.DragAndDropOptions()
* page.dragAndDrop("#source", "#target", new Page.DragAndDropOptions()
* .setSourcePosition(34, 7).setTargetPosition(10, 20));
* }</pre>
*
@@ -4214,8 +4235,6 @@ public interface Page extends AutoCloseable {
* // true
* page.evaluate("() => matchMedia('(prefers-color-scheme: light)').matches");
* // false
* page.evaluate("() => matchMedia('(prefers-color-scheme: no-preference)').matches");
* // false
* }</pre>
*
* @since v1.8
@@ -4252,8 +4271,6 @@ public interface Page extends AutoCloseable {
* // true
* page.evaluate("() => matchMedia('(prefers-color-scheme: light)').matches");
* // false
* page.evaluate("() => matchMedia('(prefers-color-scheme: no-preference)').matches");
* // false
* }</pre>
*
* @since v1.8
@@ -4560,7 +4577,7 @@ public interface Page extends AutoCloseable {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType webkit = playwright.webkit();
* Browser browser = webkit.launch({ headless: false });
* 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());
@@ -4610,7 +4627,7 @@ public interface Page extends AutoCloseable {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType webkit = playwright.webkit();
* Browser browser = webkit.launch({ headless: false });
* 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());
@@ -4662,26 +4679,27 @@ public interface Page extends AutoCloseable {
* public static void main(String[] args) {
* try (Playwright playwright = Playwright.create()) {
* BrowserType webkit = playwright.webkit();
* Browser browser = webkit.launch({ headless: false });
* Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
* Page page = browser.newPage();
* page.exposeFunction("sha256", args -> {
* String text = (String) args[0];
* MessageDigest crypto;
* try {
* crypto = MessageDigest.getInstance("SHA-256");
* String text = (String) args[0];
* MessageDigest crypto = MessageDigest.getInstance("SHA-256");
* byte[] token = crypto.digest(text.getBytes(StandardCharsets.UTF_8));
* return Base64.getEncoder().encodeToString(token);
* } catch (NoSuchAlgorithmException e) {
* return null;
* }
* byte[] token = crypto.digest(text.getBytes(StandardCharsets.UTF_8));
* return Base64.getEncoder().encodeToString(token);
* });
* page.setContent("<script>\n" +
* page.setContent(
* "<script>\n" +
* " async function onClick() {\n" +
* " document.querySelector('div').textContent = await window.sha256('PLAYWRIGHT');\n" +
* " }\n" +
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>\n");
* "<div></div>"
* );
* page.click("button");
* }
* }
@@ -4757,7 +4775,7 @@ public interface Page extends AutoCloseable {
* Frame frame = page.frame("frame-name");
* }</pre>
* <pre>{@code
* Frame frame = page.frameByUrl(Pattern.compile(".*domain.*");
* Frame frame = page.frameByUrl(Pattern.compile(".*domain.*"));
* }</pre>
*
* @param name Frame name specified in the {@code iframe}'s {@code name} attribute.
@@ -5163,19 +5181,19 @@ public interface Page extends AutoCloseable {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -5205,19 +5223,19 @@ public interface Page extends AutoCloseable {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -5245,19 +5263,19 @@ public interface Page extends AutoCloseable {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -5287,19 +5305,19 @@ public interface Page extends AutoCloseable {
* <p> You can locate by text substring, exact string, or a regular expression:
* <pre>{@code
* // Matches <span>
* page.getByText("world")
* page.getByText("world");
*
* // Matches first <div>
* page.getByText("Hello world")
* page.getByText("Hello world");
*
* // Matches second <div>
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
* page.getByText("Hello", new Page.GetByTextOptions().setExact(true));
*
* // Matches both <div>s
* page.getByText(Pattern.compile("Hello"))
* page.getByText(Pattern.compile("Hello"));
*
* // Matches second <div>
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
* page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE));
* }</pre>
*
* <p> <strong>Details</strong>
@@ -5808,8 +5826,6 @@ public interface Page extends AutoCloseable {
/**
* Returns the PDF buffer.
*
* <p> <strong>NOTE:</strong> Generating a pdf is currently only supported in Chromium headless.
*
* <p> {@code page.pdf()} generates a pdf of the page with {@code print} css media. To generate a pdf with {@code screen}
* media, call {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} before calling {@code page.pdf()}:
*
@@ -5868,8 +5884,6 @@ public interface Page extends AutoCloseable {
/**
* Returns the PDF buffer.
*
* <p> <strong>NOTE:</strong> Generating a pdf is currently only supported in Chromium headless.
*
* <p> {@code page.pdf()} generates a pdf of the page with {@code print} css media. To generate a pdf with {@code screen}
* media, call {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} before calling {@code page.pdf()}:
*
@@ -6080,24 +6094,24 @@ public interface Page extends AutoCloseable {
* <p> An example that closes a "Sign up to the newsletter" dialog when it appears:
* <pre>{@code
* // Setup the handler.
* page.addLocatorHandler(page.getByText("Sign up to the newsletter"), () => {
* page.addLocatorHandler(page.getByText("Sign up to the newsletter"), () -> {
* page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("No thanks")).click();
* });
*
* // Write the test as usual.
* page.goto("https://example.com");
* page.navigate("https://example.com");
* page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
* }</pre>
*
* <p> An example that skips the "Confirm your security details" page when it is shown:
* <pre>{@code
* // Setup the handler.
* page.addLocatorHandler(page.getByText("Confirm your security details")), () => {
* page.addLocatorHandler(page.getByText("Confirm your security details"), () -> {
* page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Remind me later")).click();
* });
*
* // Write the test as usual.
* page.goto("https://example.com");
* page.navigate("https://example.com");
* page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
* }</pre>
*
@@ -6106,19 +6120,19 @@ public interface Page extends AutoCloseable {
* handler does not hide the {@code <body>} element.
* <pre>{@code
* // Setup the handler.
* page.addLocatorHandler(page.locator("body")), () => {
* page.addLocatorHandler(page.locator("body"), () -> {
* page.evaluate("window.removeObstructionsForTestIfNeeded()");
* }, new Page.AddLocatorHandlerOptions.setNoWaitAfter(true));
* }, new Page.AddLocatorHandlerOptions().setNoWaitAfter(true));
*
* // Write the test as usual.
* page.goto("https://example.com");
* page.navigate("https://example.com");
* page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
* }</pre>
*
* <p> Handler takes the original locator as an argument. You can also automatically remove the handler after a number of
* invocations by setting {@code times}:
* <pre>{@code
* page.addLocatorHandler(page.getByLabel("Close"), locator => {
* page.addLocatorHandler(page.getByLabel("Close"), locator -> {
* locator.click();
* }, new Page.AddLocatorHandlerOptions().setTimes(1));
* }</pre>
@@ -6172,24 +6186,24 @@ public interface Page extends AutoCloseable {
* <p> An example that closes a "Sign up to the newsletter" dialog when it appears:
* <pre>{@code
* // Setup the handler.
* page.addLocatorHandler(page.getByText("Sign up to the newsletter"), () => {
* page.addLocatorHandler(page.getByText("Sign up to the newsletter"), () -> {
* page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("No thanks")).click();
* });
*
* // Write the test as usual.
* page.goto("https://example.com");
* page.navigate("https://example.com");
* page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
* }</pre>
*
* <p> An example that skips the "Confirm your security details" page when it is shown:
* <pre>{@code
* // Setup the handler.
* page.addLocatorHandler(page.getByText("Confirm your security details")), () => {
* page.addLocatorHandler(page.getByText("Confirm your security details"), () -> {
* page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Remind me later")).click();
* });
*
* // Write the test as usual.
* page.goto("https://example.com");
* page.navigate("https://example.com");
* page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
* }</pre>
*
@@ -6198,19 +6212,19 @@ public interface Page extends AutoCloseable {
* handler does not hide the {@code <body>} element.
* <pre>{@code
* // Setup the handler.
* page.addLocatorHandler(page.locator("body")), () => {
* page.addLocatorHandler(page.locator("body"), () -> {
* page.evaluate("window.removeObstructionsForTestIfNeeded()");
* }, new Page.AddLocatorHandlerOptions.setNoWaitAfter(true));
* }, new Page.AddLocatorHandlerOptions().setNoWaitAfter(true));
*
* // Write the test as usual.
* page.goto("https://example.com");
* page.navigate("https://example.com");
* page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
* }</pre>
*
* <p> Handler takes the original locator as an argument. You can also automatically remove the handler after a number of
* invocations by setting {@code times}:
* <pre>{@code
* page.addLocatorHandler(page.getByLabel("Close"), locator => {
* page.addLocatorHandler(page.getByLabel("Close"), locator -> {
* locator.click();
* }, new Page.AddLocatorHandlerOptions().setTimes(1));
* }</pre>
@@ -6303,8 +6317,8 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
@@ -6362,8 +6376,8 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
@@ -6419,8 +6433,8 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
@@ -6478,8 +6492,8 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
@@ -6535,8 +6549,8 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
@@ -6594,8 +6608,8 @@ public interface Page extends AutoCloseable {
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the
* context options was provided and the passed URL is a path, it gets merged via the <a
* @param url A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If {@code baseURL} is set in
* the context options and the provided URL is a string that does not start with {@code *}, it is resolved using the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL">{@code new URL()}</a> constructor.
* @param handler handler function to route the request.
* @since v1.8
@@ -6641,8 +6655,8 @@ public interface Page extends AutoCloseable {
* examples.
* <pre>{@code
* page.routeWebSocket("/ws", ws -> {
* ws.onMessage(message -> {
* if ("request".equals(message))
* ws.onMessage(frame -> {
* if ("request".equals(frame.text()))
* ws.send("response");
* });
* });
@@ -6666,8 +6680,8 @@ public interface Page extends AutoCloseable {
* examples.
* <pre>{@code
* page.routeWebSocket("/ws", ws -> {
* ws.onMessage(message -> {
* if ("request".equals(message))
* ws.onMessage(frame -> {
* if ("request".equals(frame.text()))
* ws.send("response");
* });
* });
@@ -6691,8 +6705,8 @@ public interface Page extends AutoCloseable {
* examples.
* <pre>{@code
* page.routeWebSocket("/ws", ws -> {
* ws.onMessage(message -> {
* if ("request".equals(message))
* ws.onMessage(frame -> {
* if ("request".equals(frame.text()))
* ws.send("response");
* });
* });
@@ -7193,7 +7207,7 @@ public interface Page extends AutoCloseable {
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Page#setDefaultNavigationTimeout Page.setDefaultNavigationTimeout()} takes priority over
* {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()}.
*
* @param timeout Maximum time in milliseconds
* @param timeout Maximum time in milliseconds. Pass {@code 0} to disable timeout.
* @since v1.8
*/
void setDefaultTimeout(double timeout);
@@ -363,15 +363,17 @@ public interface Route {
*
* <p> <strong>Details</strong>
*
* <p> Note that any overrides such as {@code url} or {@code headers} only apply to the request being routed. If this request
* results in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header
* through redirects, use the combination of {@link com.microsoft.playwright.Route#fetch Route.fetch()} and {@link
* com.microsoft.playwright.Route#fulfill Route.fulfill()} instead.
* <p> The {@code headers} option applies to both the routed request and any redirects it initiates. However, {@code url},
* {@code method}, and {@code postData} only apply to the original request and are not carried over to redirected requests.
*
* <p> {@link com.microsoft.playwright.Route#resume Route.resume()} will immediately send the request to the network, other
* matching handlers won't be invoked. Use {@link com.microsoft.playwright.Route#fallback Route.fallback()} If you want
* next matching handler in the chain to be invoked.
*
* <p> <strong>NOTE:</strong> The {@code Cookie} header cannot be overridden using this method. If a value is provided, it will be ignored, and the
* cookie will be loaded from the browser's cookie store. To set custom cookies, use {@link
* com.microsoft.playwright.BrowserContext#addCookies BrowserContext.addCookies()}.
*
* @since v1.8
*/
default void resume() {
@@ -393,15 +395,17 @@ public interface Route {
*
* <p> <strong>Details</strong>
*
* <p> Note that any overrides such as {@code url} or {@code headers} only apply to the request being routed. If this request
* results in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header
* through redirects, use the combination of {@link com.microsoft.playwright.Route#fetch Route.fetch()} and {@link
* com.microsoft.playwright.Route#fulfill Route.fulfill()} instead.
* <p> The {@code headers} option applies to both the routed request and any redirects it initiates. However, {@code url},
* {@code method}, and {@code postData} only apply to the original request and are not carried over to redirected requests.
*
* <p> {@link com.microsoft.playwright.Route#resume Route.resume()} will immediately send the request to the network, other
* matching handlers won't be invoked. Use {@link com.microsoft.playwright.Route#fallback Route.fallback()} If you want
* next matching handler in the chain to be invoked.
*
* <p> <strong>NOTE:</strong> The {@code Cookie} header cannot be overridden using this method. If a value is provided, it will be ignored, and the
* cookie will be loaded from the browser's cookie store. To set custom cookies, use {@link
* com.microsoft.playwright.BrowserContext#addCookies BrowserContext.addCookies()}.
*
* @since v1.8
*/
void resume(ResumeOptions options);
@@ -20,6 +20,9 @@ package com.microsoft.playwright;
/**
* The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the
* touchscreen can only be used in browser contexts that have been initialized with {@code hasTouch} set to true.
*
* <p> This class is limited to emulating tap gestures. For examples of other gestures simulated by manually dispatching touch
* events, see the <a href="https://playwright.dev/java/docs/touch-events">emulating legacy touch events</a> page.
*/
public interface Touchscreen {
/**
@@ -16,6 +16,7 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.nio.file.Path;
/**
@@ -143,6 +144,29 @@ public interface Tracing {
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
* com.microsoft.playwright.Tracing#group Tracing.group()} call.
*/
public Location location;
/**
* Specifies a custom location for the group to be shown in the trace viewer. Defaults to the location of the {@link
* com.microsoft.playwright.Tracing#group Tracing.group()} call.
*/
public GroupOptions setLocation(String file) {
return setLocation(new Location(file));
}
/**
* Specifies a custom location for the group to be shown in the trace viewer. Defaults to the location of the {@link
* com.microsoft.playwright.Tracing#group Tracing.group()} call.
*/
public GroupOptions setLocation(Location location) {
this.location = location;
return this;
}
}
class StopOptions {
/**
* Export trace into the file with the given path.
@@ -271,6 +295,56 @@ public interface Tracing {
* @since v1.15
*/
void startChunk(StartChunkOptions options);
/**
* <strong>NOTE:</strong> Use {@code test.step} instead when available.
*
* <p> Creates a new group within the trace, assigning any subsequent API calls to this group, until {@link
* com.microsoft.playwright.Tracing#groupEnd Tracing.groupEnd()} is called. Groups can be nested and will be visible in the
* trace viewer.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* // All actions between group and groupEnd
* // will be shown in the trace viewer as a group.
* page.context().tracing().group("Open Playwright.dev > API");
* page.navigate("https://playwright.dev/");
* page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("API")).click();
* page.context().tracing().groupEnd();
* }</pre>
*
* @param name Group name shown in the trace viewer.
* @since v1.49
*/
default void group(String name) {
group(name, null);
}
/**
* <strong>NOTE:</strong> Use {@code test.step} instead when available.
*
* <p> Creates a new group within the trace, assigning any subsequent API calls to this group, until {@link
* com.microsoft.playwright.Tracing#groupEnd Tracing.groupEnd()} is called. Groups can be nested and will be visible in the
* trace viewer.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* // All actions between group and groupEnd
* // will be shown in the trace viewer as a group.
* page.context().tracing().group("Open Playwright.dev > API");
* page.navigate("https://playwright.dev/");
* page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("API")).click();
* page.context().tracing().groupEnd();
* }</pre>
*
* @param name Group name shown in the trace viewer.
* @since v1.49
*/
void group(String name, GroupOptions options);
/**
* Closes the last group created by {@link com.microsoft.playwright.Tracing#group Tracing.group()}.
*
* @since v1.49
*/
void groupEnd();
/**
* Stop tracing.
*
@@ -20,7 +20,10 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* The {@code WebSocket} class represents websocket connections in the page.
* The {@code WebSocket} class represents WebSocket connections within a page. It provides the ability to inspect and
* manipulate the data being transmitted and received.
*
* <p> If you want to intercept or modify WebSocket frames, consider using {@code WebSocketRoute}.
*/
public interface WebSocket {
@@ -31,8 +31,8 @@ import java.util.function.Consumer;
* WebSocket. Here is an example that responds to a {@code "request"} with a {@code "response"}.
* <pre>{@code
* page.routeWebSocket("wss://example.com/ws", ws -> {
* ws.onMessage(message -> {
* if ("request".equals(message))
* ws.onMessage(frame -> {
* if ("request".equals(frame.text()))
* ws.send("response");
* });
* });
@@ -45,8 +45,8 @@ import java.util.function.Consumer;
* <p> Here is another example that handles JSON messages:
* <pre>{@code
* page.routeWebSocket("wss://example.com/ws", ws -> {
* ws.onMessage(message -> {
* JsonObject json = new JsonParser().parse(message).getAsJsonObject();
* ws.onMessage(frame -> {
* JsonObject json = new JsonParser().parse(frame.text()).getAsJsonObject();
* if ("question".equals(json.get("request").getAsString())) {
* Map<String, String> result = new HashMap();
* result.put("response", "answer");
@@ -67,11 +67,11 @@ import java.util.function.Consumer;
* <pre>{@code
* page.routeWebSocket("/ws", ws -> {
* WebSocketRoute server = ws.connectToServer();
* ws.onMessage(message -> {
* if ("request".equals(message))
* ws.onMessage(frame -> {
* if ("request".equals(frame.text()))
* server.send("request2");
* else
* server.send(message);
* server.send(frame.text());
* });
* });
* }</pre>
@@ -92,13 +92,13 @@ import java.util.function.Consumer;
* <pre>{@code
* page.routeWebSocket("/ws", ws -> {
* WebSocketRoute server = ws.connectToServer();
* ws.onMessage(message -> {
* if (!"blocked-from-the-page".equals(message))
* server.send(message);
* ws.onMessage(frame -> {
* if (!"blocked-from-the-page".equals(frame.text()))
* server.send(frame.text());
* });
* server.onMessage(message -> {
* if (!"blocked-from-the-server".equals(message))
* ws.send(message);
* server.onMessage(frame -> {
* if (!"blocked-from-the-server".equals(frame.text()))
* ws.send(frame.text());
* });
* });
* }</pre>
@@ -21,15 +21,15 @@ package com.microsoft.playwright.assertions;
* The {@code APIResponseAssertions} class provides assertion methods that can be used to make assertions about the {@code
* APIResponse} in the tests.
* <pre>{@code
* ...
* // ...
* import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
*
* public class TestPage {
* ...
* // ...
* @Test
* void navigatesToLoginPage() {
* ...
* APIResponse response = page.request().get('https://playwright.dev');
* // ...
* APIResponse response = page.request().get("https://playwright.dev");
* assertThat(response).isOK();
* }
* }
@@ -16,6 +16,7 @@
package com.microsoft.playwright.assertions;
import java.util.*;
import java.util.regex.Pattern;
import com.microsoft.playwright.options.AriaRole;
@@ -23,14 +24,14 @@ import com.microsoft.playwright.options.AriaRole;
* The {@code LocatorAssertions} class provides assertion methods that can be used to make assertions about the {@code
* Locator} state in the tests.
* <pre>{@code
* ...
* // ...
* import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
*
* public class TestLocator {
* ...
* // ...
* @Test
* void statusBecomesSubmitted() {
* ...
* // ...
* page.getByRole(AriaRole.BUTTON).click();
* assertThat(page.locator(".status")).hasText("Submitted");
* }
@@ -58,16 +59,37 @@ public interface LocatorAssertions {
}
}
class IsCheckedOptions {
/**
* Provides state to assert for. Asserts for input to be checked by default. This option can't be used when {@code
* indeterminate} is set to true.
*/
public Boolean checked;
/**
* Asserts that the element is in the indeterminate (mixed) state. Only supported for checkboxes and radio buttons. This
* option can't be true when {@code checked} is provided.
*/
public Boolean indeterminate;
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public Double timeout;
/**
* Provides state to assert for. Asserts for input to be checked by default. This option can't be used when {@code
* indeterminate} is set to true.
*/
public IsCheckedOptions setChecked(boolean checked) {
this.checked = checked;
return this;
}
/**
* Asserts that the element is in the indeterminate (mixed) state. Only supported for checkboxes and radio buttons. This
* option can't be true when {@code checked} is provided.
*/
public IsCheckedOptions setIndeterminate(boolean indeterminate) {
this.indeterminate = indeterminate;
return this;
}
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
@@ -216,6 +238,20 @@ public interface LocatorAssertions {
return this;
}
}
class ContainsClassOptions {
/**
* 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 ContainsClassOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class ContainsTextOptions {
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
@@ -281,6 +317,33 @@ public interface LocatorAssertions {
return this;
}
}
class HasAccessibleErrorMessageOptions {
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
*/
public Boolean ignoreCase;
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public Double timeout;
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
*/
public HasAccessibleErrorMessageOptions setIgnoreCase(boolean ignoreCase) {
this.ignoreCase = ignoreCase;
return this;
}
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/
public HasAccessibleErrorMessageOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class HasAccessibleNameOptions {
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
@@ -485,6 +548,20 @@ public interface LocatorAssertions {
return this;
}
}
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;
}
}
/**
* Makes the assertion check for the opposite condition. For example, this code tests that the Locator doesn't contain text
* {@code "error"}:
@@ -793,6 +870,98 @@ public interface LocatorAssertions {
* @since v1.20
*/
void isVisible(IsVisibleOptions options);
/**
* Ensures the {@code Locator} points to an element with given CSS classes. All classes from the asserted value, separated
* by spaces, must be present in the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/classList">Element.classList</a> in any order.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).containsClass("middle selected row");
* assertThat(page.locator("#component")).containsClass("selected");
* assertThat(page.locator("#component")).containsClass("row middle");
* }</pre>
*
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class lists. Each element's class attribute is matched against the corresponding class in the array:
* <pre>{@code
* assertThat(page.locator("list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
* }</pre>
*
* @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
* @since v1.52
*/
default void containsClass(String expected) {
containsClass(expected, null);
}
/**
* Ensures the {@code Locator} points to an element with given CSS classes. All classes from the asserted value, separated
* by spaces, must be present in the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/classList">Element.classList</a> in any order.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).containsClass("middle selected row");
* assertThat(page.locator("#component")).containsClass("selected");
* assertThat(page.locator("#component")).containsClass("row middle");
* }</pre>
*
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class lists. Each element's class attribute is matched against the corresponding class in the array:
* <pre>{@code
* assertThat(page.locator("list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
* }</pre>
*
* @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
* @since v1.52
*/
void containsClass(String expected, ContainsClassOptions options);
/**
* Ensures the {@code Locator} points to an element with given CSS classes. All classes from the asserted value, separated
* by spaces, must be present in the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/classList">Element.classList</a> in any order.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).containsClass("middle selected row");
* assertThat(page.locator("#component")).containsClass("selected");
* assertThat(page.locator("#component")).containsClass("row middle");
* }</pre>
*
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class lists. Each element's class attribute is matched against the corresponding class in the array:
* <pre>{@code
* assertThat(page.locator("list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
* }</pre>
*
* @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
* @since v1.52
*/
default void containsClass(List<String> expected) {
containsClass(expected, null);
}
/**
* Ensures the {@code Locator} points to an element with given CSS classes. All classes from the asserted value, separated
* by spaces, must be present in the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/classList">Element.classList</a> in any order.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).containsClass("middle selected row");
* assertThat(page.locator("#component")).containsClass("selected");
* assertThat(page.locator("#component")).containsClass("row middle");
* }</pre>
*
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class lists. Each element's class attribute is matched against the corresponding class in the array:
* <pre>{@code
* assertThat(page.locator("list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
* }</pre>
*
* @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
* @since v1.52
*/
void containsClass(List<String> expected, ContainsClassOptions options);
/**
* Ensures the {@code Locator} points to an element that contains the given text. All nested elements will be considered
* when computing the text content of the element. You can use regular expressions for the value as well.
@@ -1205,6 +1374,66 @@ public interface LocatorAssertions {
* @since v1.44
*/
void hasAccessibleDescription(Pattern description, HasAccessibleDescriptionOptions options);
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/aria/#aria-errormessage">aria errormessage</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("username-input");
* assertThat(locator).hasAccessibleErrorMessage("Username is required.");
* }</pre>
*
* @param errorMessage Expected accessible error message.
* @since v1.50
*/
default void hasAccessibleErrorMessage(String errorMessage) {
hasAccessibleErrorMessage(errorMessage, null);
}
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/aria/#aria-errormessage">aria errormessage</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("username-input");
* assertThat(locator).hasAccessibleErrorMessage("Username is required.");
* }</pre>
*
* @param errorMessage Expected accessible error message.
* @since v1.50
*/
void hasAccessibleErrorMessage(String errorMessage, HasAccessibleErrorMessageOptions options);
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/aria/#aria-errormessage">aria errormessage</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("username-input");
* assertThat(locator).hasAccessibleErrorMessage("Username is required.");
* }</pre>
*
* @param errorMessage Expected accessible error message.
* @since v1.50
*/
default void hasAccessibleErrorMessage(Pattern errorMessage) {
hasAccessibleErrorMessage(errorMessage, null);
}
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/aria/#aria-errormessage">aria errormessage</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* Locator locator = page.getByTestId("username-input");
* assertThat(locator).hasAccessibleErrorMessage("Username is required.");
* }</pre>
*
* @param errorMessage Expected accessible error message.
* @since v1.50
*/
void hasAccessibleErrorMessage(Pattern errorMessage, HasAccessibleErrorMessageOptions options);
/**
* Ensures the {@code Locator} points to an element with a given <a
* href="https://w3c.github.io/accname/#dfn-accessible-name">accessible name</a>.
@@ -1322,16 +1551,19 @@ public interface LocatorAssertions {
*/
void hasAttribute(String name, Pattern value, HasAttributeOptions options);
/**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a
* relaxed regular expression.
* Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre>
@@ -1343,16 +1575,19 @@ public interface LocatorAssertions {
hasClass(expected, null);
}
/**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a
* relaxed regular expression.
* Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre>
@@ -1362,16 +1597,19 @@ public interface LocatorAssertions {
*/
void hasClass(String expected, HasClassOptions options);
/**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a
* relaxed regular expression.
* Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre>
@@ -1383,16 +1621,19 @@ public interface LocatorAssertions {
hasClass(expected, null);
}
/**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a
* relaxed regular expression.
* Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre>
@@ -1402,16 +1643,19 @@ public interface LocatorAssertions {
*/
void hasClass(Pattern expected, HasClassOptions options);
/**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a
* relaxed regular expression.
* Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre>
@@ -1423,16 +1667,19 @@ public interface LocatorAssertions {
hasClass(expected, null);
}
/**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a
* relaxed regular expression.
* Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre>
@@ -1442,16 +1689,19 @@ public interface LocatorAssertions {
*/
void hasClass(String[] expected, HasClassOptions options);
/**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a
* relaxed regular expression.
* Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre>
@@ -1463,16 +1713,19 @@ public interface LocatorAssertions {
hasClass(expected, null);
}
/**
* Ensures the {@code Locator} points to an element with given CSS classes. This needs to be a full match or using a
* relaxed regular expression.
* Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match
* the element's {@code class} attribute. To match individual classes use {@link
* com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
* assertThat(page.locator("#component")).hasClass("selected row");
* assertThat(page.locator("#component")).hasClass("middle selected row");
* assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
* }</pre>
*
* <p> Note that if array is passed as an expected value, entire lists of elements can be asserted:
* <p> When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected
* class values. Each element's class attribute is matched against the corresponding string or regular expression in the
* array:
* <pre>{@code
* assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"});
* }</pre>
@@ -2097,7 +2350,7 @@ public interface LocatorAssertions {
*
* <p> For example, given the following element:
* <pre>{@code
* page.locator("id=favorite-colors").selectOption(["R", "G"]);
* page.locator("id=favorite-colors").selectOption(new String[]{"R", "G"});
* assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
* }</pre>
*
@@ -2115,7 +2368,7 @@ public interface LocatorAssertions {
*
* <p> For example, given the following element:
* <pre>{@code
* page.locator("id=favorite-colors").selectOption(["R", "G"]);
* page.locator("id=favorite-colors").selectOption(new String[]{"R", "G"});
* assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
* }</pre>
*
@@ -2131,7 +2384,7 @@ public interface LocatorAssertions {
*
* <p> For example, given the following element:
* <pre>{@code
* page.locator("id=favorite-colors").selectOption(["R", "G"]);
* page.locator("id=favorite-colors").selectOption(new String[]{"R", "G"});
* assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
* }</pre>
*
@@ -2149,7 +2402,7 @@ public interface LocatorAssertions {
*
* <p> For example, given the following element:
* <pre>{@code
* page.locator("id=favorite-colors").selectOption(["R", "G"]);
* page.locator("id=favorite-colors").selectOption(new String[]{"R", "G"});
* assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
* }</pre>
*
@@ -2157,5 +2410,39 @@ public interface LocatorAssertions {
* @since v1.23
*/
void hasValues(Pattern[] values, HasValuesOptions options);
/**
* Asserts that the target element 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.locator("body")).matchesAriaSnapshot("""
* - heading "todos"
* - textbox "What needs to be done?"
* """);
* }</pre>
*
* @since v1.49
*/
default void matchesAriaSnapshot(String expected) {
matchesAriaSnapshot(expected, null);
}
/**
* Asserts that the target element 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.locator("body")).matchesAriaSnapshot("""
* - heading "todos"
* - textbox "What needs to be done?"
* """);
* }</pre>
*
* @since v1.49
*/
void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions options);
}
@@ -22,14 +22,14 @@ import java.util.regex.Pattern;
* The {@code PageAssertions} class provides assertion methods that can be used to make assertions about the {@code Page}
* state in the tests.
* <pre>{@code
* ...
* // ...
* import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
*
* public class TestPage {
* ...
* // ...
* @Test
* void navigatesToLoginPage() {
* ...
* // ...
* page.getByText("Sign in").click();
* assertThat(page).hasURL(Pattern.compile(".*\/login"));
* }
@@ -54,7 +54,7 @@ public interface PageAssertions {
class HasURLOptions {
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
* expression parameter if specified. A provided predicate ignores this flag.
*/
public Boolean ignoreCase;
/**
@@ -64,7 +64,7 @@ public interface PageAssertions {
/**
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
* expression flag if specified.
* expression parameter if specified. A provided predicate ignores this flag.
*/
public HasURLOptions setIgnoreCase(boolean ignoreCase) {
this.ignoreCase = ignoreCase;
@@ -30,14 +30,13 @@ import com.microsoft.playwright.impl.PageAssertionsImpl;
*
* <p> Consider the following example:
* <pre>{@code
* ...
* import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
*
* public class TestExample {
* ...
* // ...
* @Test
* void statusBecomesSubmitted() {
* ...
* // ...
* page.locator("#submit-button").click();
* assertThat(page.locator(".status")).hasText("Submitted");
* }
@@ -46,22 +46,20 @@ class APIResponseImpl implements APIResponse {
@Override
public byte[] body() {
return context.withLogging("APIResponse.body", () -> {
try {
JsonObject params = new JsonObject();
params.addProperty("fetchUid", fetchUid());
JsonObject json = context.sendMessage("fetchResponseBody", params).getAsJsonObject();
if (!json.has("binary")) {
throw new PlaywrightException("Response has been disposed");
}
return Base64.getDecoder().decode(json.get("binary").getAsString());
} catch (PlaywrightException e) {
if (isSafeCloseError(e)) {
throw new PlaywrightException("Response has been disposed");
}
throw e;
try {
JsonObject params = new JsonObject();
params.addProperty("fetchUid", fetchUid());
JsonObject json = context.sendMessage("fetchResponseBody", params).getAsJsonObject();
if (!json.has("binary")) {
throw new PlaywrightException("Response has been disposed");
}
});
return Base64.getDecoder().decode(json.get("binary").getAsString());
} catch (PlaywrightException e) {
if (isSafeCloseError(e)) {
throw new PlaywrightException("Response has been disposed");
}
throw e;
}
}
@Override
@@ -47,7 +47,6 @@ class AssertionsBase {
options = new FrameExpectOptions();
}
options.expectedText = expectedText;
options.isNot = isNot;
expectImpl(expression, options, expected, message);
}
@@ -55,13 +54,14 @@ class AssertionsBase {
if (expectOptions.timeout == null) {
expectOptions.timeout = AssertionsTimeout.defaultTimeout;
}
if (expectOptions.isNot) {
expectOptions.isNot = isNot;
if (isNot) {
message = message.replace("expected to", "expected not to");
}
FrameExpectResult result = actualLocator.expect(expression, expectOptions);
if (result.matches == isNot) {
Object actual = result.received == null ? null : Serialization.deserialize(result.received);
String log = String.join("\n", result.log);
String log = (result.log == null) ? "" : String.join("\n", result.log);
if (!log.isEmpty()) {
log = "\nCall log:\n" + log;
}
@@ -480,7 +480,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public void route(String url, Consumer<Route> handler, RouteOptions options) {
route(new UrlMatcher(baseUrl, url), handler, options);
route(UrlMatcher.forGlob(baseUrl, url, this.connection.localUtils, false), handler, options);
}
@Override
@@ -502,7 +502,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
recordIntoHar(null, har, options);
return;
}
UrlMatcher matcher = UrlMatcher.forOneOf(baseUrl, options.url);
UrlMatcher matcher = UrlMatcher.forOneOf(baseUrl, options.url, this.connection.localUtils, false);
HARRouter harRouter = new HARRouter(connection.localUtils, har, options.notFound);
onClose(context -> harRouter.dispose());
route(matcher, route -> harRouter.handle(route), null);
@@ -517,7 +517,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public void routeWebSocket(String url, Consumer<WebSocketRoute> handler) {
routeWebSocketImpl(new UrlMatcher(baseUrl, url), handler);
routeWebSocketImpl(UrlMatcher.forGlob(baseUrl, url, this.connection.localUtils, true), handler);
}
@Override
@@ -623,14 +623,22 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public String storageState(StorageStateOptions options) {
return withLogging("BrowserContext.storageState", () -> {
JsonElement json = sendMessage("storageState");
String storageState = json.toString();
if (options != null && options.path != null) {
Utils.writeToFile(storageState.getBytes(StandardCharsets.UTF_8), options.path);
}
return storageState;
});
return withLogging("BrowserContext.storageState", () -> storageStateImpl(options));
}
private String storageStateImpl(StorageStateOptions options) {
if (options == null) {
options = new StorageStateOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("path");
JsonElement json = sendMessage("storageState", params);
String storageState = json.toString();
if (options.path != null) {
Utils.writeToFile(storageState.getBytes(StandardCharsets.UTF_8), options.path);
}
return storageState;
}
@Override
@@ -648,7 +656,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public void unroute(String url, Consumer<Route> handler) {
unroute(new UrlMatcher(this.baseUrl, url), handler);
unroute(UrlMatcher.forGlob(this.baseUrl, url, this.connection.localUtils, false), handler);
}
@Override
@@ -26,6 +26,7 @@ import com.microsoft.playwright.options.HarContentPolicy;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.function.Consumer;
import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter;
@@ -196,6 +197,10 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
if (!userDataDir.isAbsolute() && !userDataDir.toString().isEmpty()) {
Path cwd = Paths.get("").toAbsolutePath();
userDataDir = cwd.resolve(userDataDir);
}
params.addProperty("userDataDir", userDataDir.toString());
if (recordHar != null) {
params.add("recordHar", recordHar);
@@ -12,18 +12,24 @@ class ClockImpl implements Clock {
this.browserContext = browserContext;
}
private void sendMessageWithLogging(String method, JsonObject params) {
String capitalizedMethod = method.substring(0, 1).toUpperCase() + method.substring(1);
browserContext.withLogging("Clock." + method,
() -> browserContext.sendMessage("clock" + capitalizedMethod, params));
}
@Override
public void fastForward(long ticks) {
JsonObject params = new JsonObject();
params.addProperty("ticksNumber", ticks);
browserContext.sendMessage("clockFastForward", params);
sendMessageWithLogging("fastForward", params);
}
@Override
public void fastForward(String ticks) {
JsonObject params = new JsonObject();
params.addProperty("ticksString", ticks);
browserContext.sendMessage("clockFastForward", params);
sendMessageWithLogging("fastForward", params);
}
@Override
@@ -32,89 +38,89 @@ class ClockImpl implements Clock {
if (options != null) {
parseTime(options.time, params);
}
browserContext.sendMessage("clockInstall", params);
sendMessageWithLogging("install", params);
}
@Override
public void runFor(long ticks) {
JsonObject params = new JsonObject();
params.addProperty("ticksNumber", ticks);
browserContext.sendMessage("clockRunFor", params);
sendMessageWithLogging("runFor", params);
}
@Override
public void runFor(String ticks) {
JsonObject params = new JsonObject();
params.addProperty("ticksString", ticks);
browserContext.sendMessage("clockRunFor", params);
sendMessageWithLogging("runFor", params);
}
@Override
public void pauseAt(long time) {
JsonObject params = new JsonObject();
params.addProperty("timeNumber", time);
browserContext.sendMessage("clockPauseAt", params);
sendMessageWithLogging("pauseAt", params);
}
@Override
public void pauseAt(String time) {
JsonObject params = new JsonObject();
params.addProperty("timeString", time);
browserContext.sendMessage("clockPauseAt", params);
sendMessageWithLogging("pauseAt", params);
}
@Override
public void pauseAt(Date time) {
JsonObject params = new JsonObject();
params.addProperty("timeNumber", time.getTime());
browserContext.sendMessage("clockPauseAt", params);
sendMessageWithLogging("pauseAt", params);
}
@Override
public void resume() {
browserContext.sendMessage("clockResume");
sendMessageWithLogging("resume", new JsonObject());
}
@Override
public void setFixedTime(long time) {
JsonObject params = new JsonObject();
params.addProperty("timeNumber", time);
browserContext.sendMessage("clockSetFixedTime", params);
sendMessageWithLogging("setFixedTime", params);
}
@Override
public void setFixedTime(String time) {
JsonObject params = new JsonObject();
params.addProperty("timeString", time);
browserContext.sendMessage("clockSetFixedTime", params);
sendMessageWithLogging("setFixedTime", params);
}
@Override
public void setFixedTime(Date time) {
JsonObject params = new JsonObject();
params.addProperty("timeNumber", time.getTime());
browserContext.sendMessage("clockSetFixedTime", params);
sendMessageWithLogging("setFixedTime", params);
}
@Override
public void setSystemTime(long time) {
JsonObject params = new JsonObject();
params.addProperty("timeNumber", time);
browserContext.sendMessage("clockSetSystemTime", params);
sendMessageWithLogging("setSystemTime", params);
}
@Override
public void setSystemTime(String time) {
JsonObject params = new JsonObject();
params.addProperty("timeString", time);
browserContext.sendMessage("clockSetSystemTime", params);
sendMessageWithLogging("setSystemTime", params);
}
@Override
public void setSystemTime(Date time) {
JsonObject params = new JsonObject();
params.addProperty("timeNumber", time.getTime());
browserContext.sendMessage("clockSetSystemTime", params);
sendMessageWithLogging("setSystemTime", params);
}
private static void parseTime(Object time, JsonObject params) {
@@ -1031,7 +1031,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
List<Waitable<Response>> waitables = new ArrayList<>();
if (matcher == null) {
matcher = UrlMatcher.forOneOf(page.context().baseUrl, options.url);
matcher = UrlMatcher.forOneOf(page.context().baseUrl, options.url, this.connection.localUtils, false);
}
logger.log("waiting for navigation " + matcher);
waitables.add(new WaitForNavigationHelper(matcher, options.waitUntil, logger));
@@ -1078,7 +1078,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public void waitForURL(String url, WaitForURLOptions options) {
waitForURL(new UrlMatcher(page.context().baseUrl, url), options);
waitForURL(UrlMatcher.forGlob(page.context().baseUrl, url, this.connection.localUtils, false), options);
}
@Override
@@ -136,6 +136,6 @@ class FrameLocatorImpl implements FrameLocator {
@Override
public Locator owner() {
return new LocatorImpl(frame, frameSelector);
return new LocatorImpl(frame, frameSelector, null);
}
}
@@ -21,10 +21,11 @@ import com.google.gson.JsonObject;
import java.nio.file.Path;
import java.util.List;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.gson;
class LocalUtils extends ChannelOwner {
public class LocalUtils extends ChannelOwner {
LocalUtils(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
markAsInternalType();
@@ -59,4 +60,16 @@ class LocalUtils extends ChannelOwner {
JsonObject json = connection.localUtils().sendMessage("tracingStarted", params).getAsJsonObject();
return json.get("stacksId").getAsString();
}
public Pattern globToRegex(String glob, String baseURL, boolean webSocketUrl) {
JsonObject params = new JsonObject();
params.addProperty("glob", glob);
if (baseURL != null) {
params.addProperty("baseURL", baseURL);
}
params.addProperty("webSocketUrl", webSocketUrl);
JsonObject json = connection.localUtils().sendMessage("globToRegex", params).getAsJsonObject();
String regex = json.get("regex").getAsString();
return Pattern.compile(regex);
}
}
@@ -22,7 +22,9 @@ import com.microsoft.playwright.options.AriaRole;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.serializeArgument;
@@ -37,6 +39,25 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
super((LocatorImpl) locator, isNot);
}
@Override
public void containsClass(String classname, ContainsClassOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = classname;
expectImpl("to.contain.class", expected, classname, "Locator expected to contain class", convertType(options, FrameExpectOptions.class));
}
@Override
public void containsClass(List<String> classnames, ContainsClassOptions options) {
List<ExpectedTextValue> list = new ArrayList<>();
for (String text : classnames) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
list.add(expected);
}
expectImpl("to.contain.class.array", list, classnames, "Locator expected to contain classes", convertType(options, FrameExpectOptions.class));
}
@Override
public void containsText(String text, ContainsTextOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
@@ -88,6 +109,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = description;
expected.ignoreCase = shouldIgnoreCase(options);
expected.normalizeWhiteSpace = true;
expectImpl("to.have.accessible.description", expected, description, "Locator expected to have accessible description", convertType(options, FrameExpectOptions.class));
}
@@ -95,14 +117,33 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
public void hasAccessibleDescription(Pattern pattern, HasAccessibleDescriptionOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
expected.normalizeWhiteSpace = true;
expectImpl("to.have.accessible.description", expected, pattern, "Locator expected to have accessible description", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasAccessibleErrorMessage(String errorMessage, HasAccessibleErrorMessageOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = errorMessage;
expected.ignoreCase = shouldIgnoreCase(options);
expected.normalizeWhiteSpace = true;
expectImpl("to.have.accessible.error.message", expected, errorMessage, "Locator expected to have accessible error message", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasAccessibleErrorMessage(Pattern pattern, HasAccessibleErrorMessageOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
expected.normalizeWhiteSpace = true;
expectImpl("to.have.accessible.error.message", expected, pattern, "Locator expected to have accessible error message", convertType(options, FrameExpectOptions.class));
}
@Override
public void hasAccessibleName(String name, HasAccessibleNameOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = name;
expected.ignoreCase = shouldIgnoreCase(options);
expected.normalizeWhiteSpace = true;
expectImpl("to.have.accessible.name", expected, name, "Locator expected to have accessible name", convertType(options, FrameExpectOptions.class));
}
@@ -110,6 +151,7 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
public void hasAccessibleName(Pattern pattern, HasAccessibleNameOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.ignoreCase = shouldIgnoreCase(options);
expected.normalizeWhiteSpace = true;
expectImpl("to.have.accessible.name", expected, pattern, "Locator expected to have accessible name", convertType(options, FrameExpectOptions.class));
}
@@ -326,12 +368,42 @@ public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAsse
expectImpl("to.have.values", list, patterns, "Locator expected to have values matching regex", convertType(options, FrameExpectOptions.class));
}
@Override
public void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions snapshotOptions) {
if (snapshotOptions == null) {
snapshotOptions = new MatchesAriaSnapshotOptions();
}
FrameExpectOptions options = convertType(snapshotOptions, FrameExpectOptions.class);
options.expectedValue = serializeArgument(expected);
expectImpl("to.match.aria", options, expected,"Locator expected to match Aria snapshot");
}
@Override
public void isChecked(IsCheckedOptions options) {
boolean unchecked = options != null && options.checked != null && !options.checked;
String expression = unchecked ? "to.be.unchecked" : "to.be.checked";
String message = "Locator expected to be " + (unchecked ? "un" : "") + "checked";
expectTrue(expression, message, convertType(options, FrameExpectOptions.class));
if (options == null) {
options = new IsCheckedOptions();
}
Map<String, Boolean> expectedValue = new HashMap<>();
if (options.indeterminate != null) {
expectedValue.put("indeterminate", options.indeterminate);
}
if (options.checked != null) {
expectedValue.put("checked", options.checked);
}
String expected;
if (options.indeterminate != null && options.indeterminate) {
expected = "indeterminate";
} else {
boolean unchecked = options.checked != null && !options.checked;
expected = unchecked ? "unchecked" : "checked";
}
String message = "Locator expected to be";
FrameExpectOptions expectOptions = convertType(options, FrameExpectOptions.class);
expectOptions.expectedValue = serializeArgument(expectedValue);
expectImpl("to.be.checked", expectOptions, expected, message);
}
@Override
@@ -21,29 +21,25 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.LocatorUtils.*;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.convertType;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
class LocatorImpl implements Locator {
final FrameImpl frame;
final String selector;
LocatorImpl(FrameImpl frame, String frameSelector) {
this(frame, frameSelector, null);
LocatorImpl(FrameImpl frame, String selector, LocatorOptions options) {
this(frame, selector, options, null);
}
public LocatorImpl(FrameImpl frame, String selector, LocatorOptions options) {
private LocatorImpl(FrameImpl frame, String selector, LocatorOptions options, Boolean visible) {
this.frame = frame;
if (options != null) {
if (options.hasText != null) {
@@ -65,6 +61,9 @@ class LocatorImpl implements Locator {
selector += " >> internal:has-not=" + gson().toJson(locator.selector);
}
}
if (visible != null) {
selector += " >> visible=" + visible;
}
this.selector = selector;
}
@@ -119,6 +118,21 @@ class LocatorImpl implements Locator {
return new LocatorImpl(frame, selector + " >> internal:and=" + gson().toJson(other.selector), null);
}
@Override
public String ariaSnapshot(AriaSnapshotOptions options) {
return frame.withLogging("Locator.ariaSnapshot", () -> ariaSnapshotImpl(options));
}
private String ariaSnapshotImpl(AriaSnapshotOptions options) {
if (options == null) {
options = new AriaSnapshotOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
JsonObject result = frame.sendMessage("ariaSnapshot", params).getAsJsonObject();
return result.get("snapshot").getAsString();
}
@Override
public void blur(BlurOptions options) {
frame.withLogging("Locator.blur", () -> blurImpl(options));
@@ -237,7 +251,8 @@ class LocatorImpl implements Locator {
@Override
public Locator filter(FilterOptions options) {
return new LocatorImpl(frame, selector, convertType(options,LocatorOptions.class));
Boolean visible = (options == null) ? null : options.visible;
return new LocatorImpl(frame, selector, convertType(options, LocatorOptions.class), visible);
}
@Override
@@ -650,9 +665,6 @@ class LocatorImpl implements Locator {
}
private FrameExpectResult expectImpl(String expression, FrameExpectOptions options) {
if (options == null) {
options = new FrameExpectOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
params.addProperty("expression", expression);
@@ -785,7 +785,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Frame frameByUrl(String glob) {
return frameFor(new UrlMatcher(browserContext.baseUrl, glob));
return frameFor(UrlMatcher.forGlob(browserContext.baseUrl, glob, this.connection.localUtils, false));
}
@Override
@@ -1105,7 +1105,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void route(String url, Consumer<Route> handler, RouteOptions options) {
route(new UrlMatcher(browserContext.baseUrl, url), handler, options);
route(UrlMatcher.forGlob(browserContext.baseUrl, url, this.connection.localUtils, false), handler, options);
}
@Override
@@ -1127,7 +1127,7 @@ public class PageImpl extends ChannelOwner implements Page {
browserContext.recordIntoHar(this, har, convertType(options, BrowserContext.RouteFromHAROptions.class));
return;
}
UrlMatcher matcher = UrlMatcher.forOneOf(browserContext.baseUrl, options.url);
UrlMatcher matcher = UrlMatcher.forOneOf(browserContext.baseUrl, options.url, this.connection.localUtils, false);
HARRouter harRouter = new HARRouter(connection.localUtils, har, options.notFound);
onClose(context -> harRouter.dispose());
route(matcher, route -> harRouter.handle(route), null);
@@ -1142,7 +1142,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void routeWebSocket(String url, Consumer<WebSocketRoute> handler) {
routeWebSocketImpl(new UrlMatcher(browserContext.baseUrl, url), handler);
routeWebSocketImpl(UrlMatcher.forGlob(browserContext.baseUrl, url, this.connection.localUtils, true), handler);
}
@Override
@@ -1365,7 +1365,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void unroute(String url, Consumer<Route> handler) {
unroute(new UrlMatcher(browserContext.baseUrl, url), handler);
unroute(UrlMatcher.forGlob(browserContext.baseUrl, url, this.connection.localUtils, false), handler);
}
@Override
@@ -1508,7 +1508,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Request waitForRequest(String urlGlob, WaitForRequestOptions options, Runnable code) {
return waitForRequest(new UrlMatcher(browserContext.baseUrl, urlGlob), null, options, code);
return waitForRequest(UrlMatcher.forGlob(browserContext.baseUrl, urlGlob, this.connection.localUtils, false), null, options, code);
}
@Override
@@ -1553,7 +1553,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Response waitForResponse(String urlGlob, WaitForResponseOptions options, Runnable code) {
return waitForResponse(new UrlMatcher(browserContext.baseUrl, urlGlob), null, options, code);
return waitForResponse(UrlMatcher.forGlob(browserContext.baseUrl, urlGlob, this.connection.localUtils, false), null, options, code);
}
@Override
@@ -1606,7 +1606,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void waitForURL(String url, WaitForURLOptions options) {
waitForURL(new UrlMatcher(browserContext.baseUrl, url), options);
waitForURL(UrlMatcher.forGlob(browserContext.baseUrl, url, this.connection.localUtils, false), options);
}
@Override
@@ -90,8 +90,12 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
sharedSelectors.removeChannel(selectors);
}
public LocalUtils localUtils() {
return connection.localUtils;
}
public JsonArray deviceDescriptors() {
return connection.localUtils.deviceDescriptors();
return localUtils().deviceDescriptors();
}
@Override
@@ -53,6 +53,7 @@ public class RequestImpl extends ChannelOwner implements Request {
RequestImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
markAsInternalType();
if (initializer.has("redirectedFrom")) {
redirectedFrom = connection.getExistingObject(initializer.getAsJsonObject("redirectedFrom").get("guid").getAsString());
@@ -40,6 +40,7 @@ public class ResponseImpl extends ChannelOwner implements Response {
ResponseImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
markAsInternalType();
headers = new RawHeaders(asList(gson().fromJson(initializer.getAsJsonArray("headers"), HttpHeader[].class)));
request = connection.getExistingObject(initializer.getAsJsonObject("request").get("guid").getAsString());
request.timing = gson().fromJson(initializer.get("timing"), Timing.class);
@@ -47,6 +47,7 @@ class Serialization {
.registerTypeAdapter(SameSiteAttribute.class, new SameSiteAdapter().nullSafe())
.registerTypeAdapter(BrowserChannel.class, new ToLowerCaseAndDashSerializer<BrowserChannel>())
.registerTypeAdapter(ColorScheme.class, new ToLowerCaseAndDashSerializer<ColorScheme>())
.registerTypeAdapter(Contrast.class, new ToLowerCaseAndDashSerializer<Contrast>())
.registerTypeAdapter(Media.class, new ToLowerCaseSerializer<Media>())
.registerTypeAdapter(ForcedColors.class, new ToLowerCaseSerializer<ForcedColors>())
.registerTypeAdapter(HttpCredentialsSend.class, new ToLowerCaseSerializer<HttpCredentialsSend>())
@@ -425,6 +426,7 @@ class Serialization {
private static boolean isSupported(Type type) {
return new TypeToken<Optional<Media>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
new TypeToken<Optional<ColorScheme>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
new TypeToken<Optional<Contrast>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
new TypeToken<Optional<ForcedColors>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
new TypeToken<Optional<ReducedMotion>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
new TypeToken<Optional<ViewportSize>>() {}.getType().getTypeName().equals(type.getTypeName());
@@ -86,6 +86,25 @@ class TracingImpl extends ChannelOwner implements Tracing {
tracingStartChunk(options.name, options.title);
}
@Override
public void group(String name, GroupOptions options) {
withLogging("Tracing.group", () -> groupImpl(name, options));
}
private void groupImpl(String name, GroupOptions options) {
if (options == null) {
options = new GroupOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("name", name);
sendMessage("tracingGroup", params);
}
@Override
public void groupEnd() {
withLogging("Tracing.groupEnd", () -> sendMessage("tracingGroupEnd"));
}
private void tracingStartChunk(String name, String title) {
JsonObject params = new JsonObject();
if (name != null) {
@@ -16,37 +16,27 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Utils.globToRegex;
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
class UrlMatcher {
final Object rawSource;
private final Predicate<String> predicate;
public final String glob;
public final Pattern pattern;
public final Predicate<String> predicate;
private static Predicate<String> toPredicate(Pattern pattern) {
return s -> pattern.matcher(s).find();
}
static UrlMatcher any() {
return new UrlMatcher((Object) null, null);
}
static UrlMatcher forOneOf(URL baseUrl, Object object) {
static UrlMatcher forOneOf(URL baseUrl, Object object, LocalUtils localUtils, boolean isWebSocketUrl) {
if (object == null) {
return UrlMatcher.any();
return new UrlMatcher(null, null, null);
}
if (object instanceof String) {
return new UrlMatcher(baseUrl, (String) object);
return UrlMatcher.forGlob(baseUrl, (String) object, localUtils, isWebSocketUrl);
}
if (object instanceof Pattern) {
return new UrlMatcher((Pattern) object);
@@ -58,34 +48,48 @@ class UrlMatcher {
}
static String resolveUrl(URL baseUrl, String spec) {
return resolveUrl(baseUrl.toString(), spec);
}
private static String resolveUrl(String baseUrl, String spec) {
if (baseUrl == null) {
return spec;
}
try {
return new URL(baseUrl, spec).toString();
} catch (MalformedURLException e) {
// Join using URI instead of URL since URL doesn't handle ws(s) protocols.
return new URI(baseUrl).resolve(spec).toString();
} catch (URISyntaxException e) {
return spec;
}
}
UrlMatcher(URL base, String url) {
this(url, toPredicate(Pattern.compile(globToRegex(resolveUrl(base, url)))).or(s -> url == null || url.equals(s)));
static UrlMatcher forGlob(URL baseURL, String glob, LocalUtils localUtils, boolean isWebSocketUrl) {
Pattern pattern = localUtils.globToRegex(glob, baseURL != null ? baseURL.toString() : null, isWebSocketUrl);
return new UrlMatcher(glob, pattern, null);
}
UrlMatcher(Pattern pattern) {
this(pattern, toPredicate(pattern));
}
UrlMatcher(Predicate<String> predicate) {
this(predicate, predicate);
this(null, pattern, null);
}
private UrlMatcher(Object rawSource, Predicate<String> predicate) {
this.rawSource = rawSource;
UrlMatcher(Predicate<String> predicate) {
this(null, null, predicate);
}
private UrlMatcher(String glob, Pattern pattern, Predicate<String> predicate) {
this.glob = glob;
this.pattern = pattern;
this.predicate = predicate;
}
boolean test(String value) {
return predicate == null || predicate.test(value);
if (pattern != null) {
return pattern.matcher(value).find();
}
if (predicate != null) {
return predicate.test(value);
}
return true;
}
@Override
@@ -93,25 +97,40 @@ class UrlMatcher {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UrlMatcher that = (UrlMatcher) o;
if (rawSource instanceof Pattern && that.rawSource instanceof Pattern) {
Pattern a = (Pattern) rawSource;
Pattern b = (Pattern) that.rawSource;
return a.pattern().equals(b.pattern()) && a.flags() == b.flags();
if (pattern != null) {
return that.pattern != null && pattern.pattern().equals(that.pattern.pattern()) && pattern.flags() == that.pattern.flags();
}
return Objects.equals(rawSource, that.rawSource);
if (predicate != null) {
return predicate.equals(that.predicate);
}
if (glob != null) {
return glob.equals(that.glob);
}
return that.pattern == null && that.predicate == null && that.glob == null;
}
@Override
public int hashCode() {
return Objects.hash(rawSource);
if (pattern != null) {
return pattern.hashCode();
}
if (predicate != null) {
return predicate.hashCode();
}
if (glob != null) {
return glob.hashCode();
}
return 0;
}
@Override
public String toString() {
if (rawSource == null)
return "<any>";
if (rawSource instanceof Predicate)
return "matching predicate";
return rawSource.toString();
if (glob != null)
return String.format("<glob pattern=\"%s\">", glob);
if (pattern != null)
return String.format("<regex pattern=\"%s\" flags=\"%s\">", pattern.pattern(), toJsRegexFlags(pattern));
if (this.predicate != null)
return "<predicate>";
return "<true>";
}
}
@@ -17,7 +17,6 @@
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 com.microsoft.playwright.options.ClientCertificate;
@@ -32,7 +31,6 @@ import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -91,79 +89,6 @@ public class Utils {
return convertType(f, (Class<T>) f.getClass());
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping
static Set<Character> escapeGlobChars = new HashSet<>(Arrays.asList('$', '^', '+', '.', '*', '(', ')', '|', '\\', '?', '{', '}', '[', ']'));
static String globToRegex(String glob) {
StringBuilder tokens = new StringBuilder();
tokens.append('^');
boolean inGroup = false;
for (int i = 0; i < glob.length(); ++i) {
char c = glob.charAt(i);
if (c == '\\' && i + 1 < glob.length()) {
char nextChar = glob.charAt(++i);
if (escapeGlobChars.contains(nextChar)) {
tokens.append('\\');
}
tokens.append(nextChar);
continue;
}
if (c == '*') {
boolean beforeDeep = i < 1 || glob.charAt(i - 1) == '/';
int starCount = 1;
while (i + 1 < glob.length() && glob.charAt(i + 1) == '*') {
starCount++;
i++;
}
boolean afterDeep = i + 1 >= glob.length() || glob.charAt(i + 1) == '/';
boolean isDeep = starCount > 1 && beforeDeep && afterDeep;
if (isDeep) {
tokens.append("((?:[^/]*(?:\\/|$))*)");
i++;
} else {
tokens.append("([^/]*)");
}
continue;
}
switch (c) {
case '?':
tokens.append('.');
break;
case '[':
tokens.append('[');
break;
case ']':
tokens.append(']');
break;
case '{':
inGroup = true;
tokens.append('(');
break;
case '}':
inGroup = false;
tokens.append(')');
break;
case ',':
if (inGroup) {
tokens.append('|');
break;
}
tokens.append("\\").append(c);
break;
default:
if (escapeGlobChars.contains(c)) {
tokens.append('\\');
}
tokens.append(c);
break;
}
}
tokens.append('$');
return tokens.toString();
}
static String mimeType(Path path) {
String mimeType;
try {
@@ -434,7 +359,7 @@ public class Utils {
}
String pfxBase64 = base64Buffer(cert.pfx, cert.pfxPath);
if (pfxBase64 != null) {
params.addProperty("pfx", pfxBase64);
jsonCert.addProperty("pfx", pfxBase64);
}
} catch (IOException e) {
throw new PlaywrightException("Failed to read from file", e);
@@ -462,13 +387,11 @@ public class Utils {
JsonArray jsonPatterns = new JsonArray();
for (UrlMatcher matcher: matchers) {
JsonObject jsonPattern = new JsonObject();
Object urlFilter = matcher.rawSource;
if (urlFilter instanceof String) {
jsonPattern.addProperty("glob", (String) urlFilter);
} else if (urlFilter instanceof Pattern) {
Pattern pattern = (Pattern) urlFilter;
jsonPattern.addProperty("regexSource", pattern.pattern());
jsonPattern.addProperty("regexFlags", toJsRegexFlags(pattern));
if (matcher.glob != null) {
jsonPattern.addProperty("glob", matcher.glob);
} else if (matcher.pattern != null) {
jsonPattern.addProperty("regexSource", matcher.pattern.pattern());
jsonPattern.addProperty("regexFlags", toJsRegexFlags(matcher.pattern));
} else {
// Match all requests.
jsonPattern.addProperty("glob", "**/*");
@@ -20,6 +20,7 @@ import java.util.Collection;
class WaitableRace<T> implements Waitable<T> {
private final Collection<Waitable<T>> waitables;
private Waitable<T> firstReady;
WaitableRace(Collection<Waitable<T>> waitables) {
this.waitables = waitables;
@@ -27,8 +28,12 @@ class WaitableRace<T> implements Waitable<T> {
@Override
public boolean isDone() {
for (Waitable w : waitables) {
if (firstReady != null) {
return true;
}
for (Waitable<T> w : waitables) {
if (w.isDone()) {
firstReady = w;
return true;
}
}
@@ -37,14 +42,11 @@ class WaitableRace<T> implements Waitable<T> {
@Override
public T get() {
assert isDone();
dispose();
for (Waitable<T> w : waitables) {
if (w.isDone()) {
return w.get();
}
try {
return firstReady.get();
} finally {
dispose();
}
throw new IllegalStateException("At least one element must be ready");
}
@Override
@@ -69,6 +69,7 @@ class WebSocketRouteImpl extends ChannelOwner implements WebSocketRoute {
WebSocketRouteImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
markAsInternalType();
}
@Override
@@ -0,0 +1,22 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public enum Contrast {
NO_PREFERENCE,
MORE
}
@@ -23,7 +23,7 @@ import java.nio.file.Path;
* The {@code FormData} is used create form data that is sent via {@code APIRequestContext}.
* <pre>{@code
* import com.microsoft.playwright.options.FormData;
* ...
* // ...
* FormData form = FormData.create()
* .set("firstName", "John")
* .set("lastName", "Doe")
@@ -43,7 +43,7 @@ public interface FormData {
* existing set of values.
* <pre>{@code
* import com.microsoft.playwright.options.FormData;
* ...
* // ...
* FormData form = FormData.create()
* // Only name and value are set.
* .append("firstName", "John")
@@ -70,7 +70,7 @@ public interface FormData {
* existing set of values.
* <pre>{@code
* import com.microsoft.playwright.options.FormData;
* ...
* // ...
* FormData form = FormData.create()
* // Only name and value are set.
* .append("firstName", "John")
@@ -97,7 +97,7 @@ public interface FormData {
* existing set of values.
* <pre>{@code
* import com.microsoft.playwright.options.FormData;
* ...
* // ...
* FormData form = FormData.create()
* // Only name and value are set.
* .append("firstName", "John")
@@ -124,7 +124,7 @@ public interface FormData {
* existing set of values.
* <pre>{@code
* import com.microsoft.playwright.options.FormData;
* ...
* // ...
* FormData form = FormData.create()
* // Only name and value are set.
* .append("firstName", "John")
@@ -151,7 +151,7 @@ public interface FormData {
* existing set of values.
* <pre>{@code
* import com.microsoft.playwright.options.FormData;
* ...
* // ...
* FormData form = FormData.create()
* // Only name and value are set.
* .append("firstName", "John")
@@ -179,7 +179,7 @@ public interface FormData {
* Sets a field on the form. File values can be passed either as {@code Path} or as {@code FilePayload}.
* <pre>{@code
* import com.microsoft.playwright.options.FormData;
* ...
* // ...
* FormData form = FormData.create()
* // Only name and value are set.
* .set("firstName", "John")
@@ -200,7 +200,7 @@ public interface FormData {
* Sets a field on the form. File values can be passed either as {@code Path} or as {@code FilePayload}.
* <pre>{@code
* import com.microsoft.playwright.options.FormData;
* ...
* // ...
* FormData form = FormData.create()
* // Only name and value are set.
* .set("firstName", "John")
@@ -221,7 +221,7 @@ public interface FormData {
* Sets a field on the form. File values can be passed either as {@code Path} or as {@code FilePayload}.
* <pre>{@code
* import com.microsoft.playwright.options.FormData;
* ...
* // ...
* FormData form = FormData.create()
* // Only name and value are set.
* .set("firstName", "John")
@@ -242,7 +242,7 @@ public interface FormData {
* Sets a field on the form. File values can be passed either as {@code Path} or as {@code FilePayload}.
* <pre>{@code
* import com.microsoft.playwright.options.FormData;
* ...
* // ...
* FormData form = FormData.create()
* // Only name and value are set.
* .set("firstName", "John")
@@ -263,7 +263,7 @@ public interface FormData {
* Sets a field on the form. File values can be passed either as {@code Path} or as {@code FilePayload}.
* <pre>{@code
* import com.microsoft.playwright.options.FormData;
* ...
* // ...
* FormData form = FormData.create()
* // Only name and value are set.
* .set("firstName", "John")
@@ -0,0 +1,35 @@
/*
* 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 Location {
public String file;
public Integer line;
public Integer column;
public Location(String file) {
this.file = file;
}
public Location setLine(int line) {
this.line = line;
return this;
}
public Location setColumn(int column) {
this.column = column;
return this;
}
}
@@ -218,7 +218,7 @@ public class Server implements HttpHandler {
}
long contentLength = body.size();
// -1 means no body, 0 means chunked encoding.
exchange.sendResponseHeaders(200, contentLength == 0 ? -1 : contentLength);
exchange.sendResponseHeaders(200, (contentLength == 0 || exchange.getRequestMethod().equals("HEAD")) ? -1 : contentLength);
if (contentLength > 0) {
exchange.getResponseBody().write(body.toByteArray());
}
@@ -299,6 +299,13 @@ public class TestBrowserContextBasic {
assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
}
@Test
void waitForConditionThatMayChangeToFalse(BrowserContext context) {
int[] var = {0};
context.waitForCondition(() -> ++var[0] == 1);
assertEquals(1, var[0], "The predicate should be called only once.");
}
@Test
void shouldPropagateCloseReasonToPendingActions(Browser browser) {
BrowserContext context = browser.newContext();
@@ -16,6 +16,7 @@
package com.microsoft.playwright;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.junit.jupiter.api.Test;
@@ -49,7 +50,12 @@ public class TestBrowserContextCDPSession extends TestBase {
cdpSession.send("Network.enable");
List<JsonElement> events = new ArrayList<>();
cdpSession.on("Network.requestWillBeSent", events::add);
cdpSession.on("Network.requestWillBeSent", (JsonObject jsonObject) -> {
// Only register main request, ignore favicon requests.
if ("Document".equals(jsonObject.get("type").getAsString())) {
events.add(jsonObject);
}
});
page.navigate(server.EMPTY_PAGE);
assertEquals(1, events.size());
@@ -126,7 +132,7 @@ public class TestBrowserContextCDPSession extends TestBase {
page.close();
PlaywrightException exception = assertThrows(PlaywrightException.class, session::detach);
assertTrue(exception.getMessage().contains("Target page, context or browser has been closed"));
assertTrue(exception.getMessage().contains("Target page, context or browser has been closed"), exception.getMessage());
context.close();
}
@@ -136,8 +142,14 @@ public class TestBrowserContextCDPSession extends TestBase {
cdpSession.send("Network.enable");
List<JsonObject> events = new ArrayList<>();
cdpSession.on("Network.requestWillBeSent", events::add);
cdpSession.on("Network.requestWillBeSent", events::add);
Consumer<JsonObject> listener1 = (JsonObject jsonObject) -> {
// Only register main request, ignore favicon requests.
if ("Document".equals(jsonObject.get("type").getAsString())) {
events.add(jsonObject);
}
};
cdpSession.on("Network.requestWillBeSent", listener1);
cdpSession.on("Network.requestWillBeSent", listener1);
page.navigate(server.EMPTY_PAGE);
assertEquals(2, events.size());
@@ -149,9 +161,15 @@ public class TestBrowserContextCDPSession extends TestBase {
cdpSession.send("Network.enable");
List<JsonObject> events = new ArrayList<>();
Consumer<JsonObject> listener1 = events::add;
Consumer<JsonObject> listener1 = (JsonObject jsonObject) -> {
// Only register main request, ignore favicon requests.
if ("Document".equals(jsonObject.get("type").getAsString())) {
events.add(jsonObject);
}
};
Consumer<JsonObject> listener2 = listener1::accept;
cdpSession.on("Network.requestWillBeSent", listener1);
cdpSession.on("Network.requestWillBeSent", events::add);
cdpSession.on("Network.requestWillBeSent", listener2);
page.navigate(server.EMPTY_PAGE);
assertEquals(2, events.size());
@@ -160,6 +178,6 @@ public class TestBrowserContextCDPSession extends TestBase {
events.clear();
page.navigate(server.EMPTY_PAGE);
assertEquals(1, events.size());
assertEquals(1, events.size(), new Gson().toJson(events));
}
}
@@ -25,12 +25,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestBrowserContextCredentials extends TestBase {
static boolean isChromiumHeadful() {
return isChromium() && isHeadful();
static boolean isChromiumHeadedLike() {
// --headless=new, the default in all Chromium channels, is like headless.
return isChromium() && (isHeadful() || getBrowserChannelFromEnv() != null);
}
@Test
@DisabledIf(value="isChromiumHeadful", disabledReason="fail")
@DisabledIf(value="isChromiumHeadedLike", disabledReason="fail")
void shouldFailWithoutCredentials() {
server.setAuth("/empty.html", "user", "pass");
Response response = page.navigate(server.EMPTY_PAGE);
@@ -103,6 +104,7 @@ public class TestBrowserContextCredentials extends TestBase {
}
@Test
@DisabledIf(value="isChromiumHeadedLike", disabledReason="fail")
void shouldFailWithCorrectCredentialsAndWrongOriginScheme() {
server.setAuth("/empty.html", "user", "pass");
final HttpCredentials httpCredentials = new HttpCredentials("user", "pass");
@@ -115,6 +117,7 @@ public class TestBrowserContextCredentials extends TestBase {
}
@Test
@DisabledIf(value="isChromiumHeadedLike", disabledReason="fail")
void shouldFailWithCorrectCredentialsAndWrongOriginHostname() {
server.setAuth("/empty.html", "user", "pass");
final HttpCredentials httpCredentials = new HttpCredentials("user", "pass");
@@ -127,6 +130,7 @@ public class TestBrowserContextCredentials extends TestBase {
}
@Test
@DisabledIf(value="isChromiumHeadedLike", disabledReason="fail")
void shouldFailWithCorrectCredentialsAndWrongOriginPort() {
server.setAuth("/empty.html", "user", "pass");
final HttpCredentials httpCredentials = new HttpCredentials("user", "pass");
@@ -22,8 +22,9 @@ import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.*;
import java.nio.charset.Charset;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.text.ParseException;
@@ -34,7 +35,7 @@ import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import static com.microsoft.playwright.Utils.*;
import static com.microsoft.playwright.Utils.mapOf;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
@@ -29,6 +29,7 @@ import java.nio.file.Path;
import static com.microsoft.playwright.Utils.assertJsonEquals;
import static com.microsoft.playwright.Utils.mapOf;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestBrowserContextStorageState extends TestBase {
@@ -169,4 +170,101 @@ public class TestBrowserContextStorageState extends TestBase {
" }]\n" +
"}]}", new Gson().fromJson(storageState, JsonObject.class));
}
@Test
void shouldSupportIndexedDB() {
page.navigate(server.PREFIX + "/to-do-notifications/index.html");
page.locator("label:has-text('Task title')").fill("Pet the cat");
page.locator("label:has-text('Hours')").fill("1");
page.locator("label:has-text('Mins')").fill("1");
page.locator("text=Add Task").click();
String storageState = page.context().storageState(new BrowserContext.StorageStateOptions().setIndexedDB(true));
assertJsonEquals("{\"cookies\":[],\"origins\":[\n" +
" {\n" +
" \"origin\": \"" + server.PREFIX + "\",\n" +
" \"localStorage\": [],\n" +
" \"indexedDB\": [\n" +
" {\n" +
" \"name\": \"toDoList\",\n" +
" \"version\": 4,\n" +
" \"stores\": [\n" +
" {\n" +
" \"name\": \"toDoList\",\n" +
" \"autoIncrement\": false,\n" +
" \"keyPath\": \"taskTitle\",\n" +
" \"records\": [\n" +
" {\n" +
" \"valueEncoded\": {\n" +
" \"id\": 1,\n" +
" \"o\": [\n" +
" {\"k\": \"taskTitle\", \"v\": \"Pet the cat\"},\n" +
" {\"k\": \"hours\", \"v\": \"1\"},\n" +
" {\"k\": \"minutes\", \"v\": \"1\"},\n" +
" {\"k\": \"day\", \"v\": \"01\"},\n" +
" {\"k\": \"month\", \"v\": \"January\"},\n" +
" {\"k\": \"year\", \"v\": \"2025\"},\n" +
" {\"k\": \"notified\", \"v\": \"no\"},\n" +
" {\"k\": \"signature\", \"v\": { \"ta\": {\"b\":\"c2lnbmVkIGJ5IHNpbW9u\",\"k\":\"ui8\"}}}\n" +
" ]\n" +
" }\n" +
" }\n" +
" ],\n" +
" \"indexes\": [\n" +
" {\n" +
" \"name\": \"day\",\n" +
" \"keyPath\": \"day\",\n" +
" \"multiEntry\": false,\n" +
" \"unique\": false\n" +
" },\n" +
" {\n" +
" \"name\": \"hours\",\n" +
" \"keyPath\": \"hours\",\n" +
" \"multiEntry\": false,\n" +
" \"unique\": false\n" +
" },\n" +
" {\n" +
" \"name\": \"minutes\",\n" +
" \"keyPath\": \"minutes\",\n" +
" \"multiEntry\": false,\n" +
" \"unique\": false\n" +
" },\n" +
" {\n" +
" \"name\": \"month\",\n" +
" \"keyPath\": \"month\",\n" +
" \"multiEntry\": false,\n" +
" \"unique\": false\n" +
" },\n" +
" {\n" +
" \"name\": \"notified\",\n" +
" \"keyPath\": \"notified\",\n" +
" \"multiEntry\": false,\n" +
" \"unique\": false\n" +
" },\n" +
" {\n" +
" \"name\": \"year\",\n" +
" \"keyPath\": \"year\",\n" +
" \"multiEntry\": false,\n" +
" \"unique\": false\n" +
" }\n" +
" ]\n" +
" }\n" +
" ]\n" +
" }\n" +
" ]\n" +
" }\n" +
"]}", new Gson().fromJson(storageState, JsonObject.class));
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setStorageState(storageState));
assertEquals(storageState, context.storageState(new BrowserContext.StorageStateOptions().setIndexedDB(true)));
Page recreatedPage = context.newPage();
recreatedPage.navigate(server.PREFIX + "/to-do-notifications/index.html");
assertThat(recreatedPage.locator("#task-list")).matchesAriaSnapshot("\n" +
" - list:\n" +
" - listitem:\n" +
" - text: /Pet the cat/\n");
assertEquals("{\"cookies\":[],\"origins\":[]}", context.storageState(
new BrowserContext.StorageStateOptions().setIndexedDB(false)));
}
}
@@ -1,7 +1,6 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.ClientCertificate;
import com.microsoft.playwright.options.Proxy;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -120,6 +119,25 @@ public class TestClientCertificates extends TestBase {
request.dispose();
}
@Test
public void passWithTrustedClientCertificatesPfx() {
APIRequest.NewContextOptions requestOptions = new APIRequest.NewContextOptions()
.setIgnoreHTTPSErrors(true) // TODO: remove once we can pass a custom CA.
.setClientCertificates(asList(
new ClientCertificate(customServer.origin)
.setPfxPath(asset("client-certificates/client/trusted/client_keystore.p12"))
.setPassphrase("passphrase")));
APIRequestContext request = playwright.request().newContext(requestOptions);
APIResponse response = request.get(customServer.url);
assertEquals(customServer.url, response.url());
assertEquals(200, response.status());
assertTrue(response.text().contains("Hello CN=Alice, your certificate was issued by O=Client Certificate Demo,CN=localhost!"), response.text());
request.dispose();
}
static boolean isWebKitMacOS() {
return isWebKit() && isMac;
}
@@ -16,6 +16,7 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.Contrast;
import com.microsoft.playwright.options.Geolocation;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
@@ -28,6 +29,7 @@ import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -282,4 +284,27 @@ public class TestDefaultBrowserContext2 extends TestBase {
assertEquals(1, fields.size());
assertEquals("200MB.zip", fields.get(0).filename);
assertEquals(200 * 1024 * 1024, fields.get(0).content.length());
}}
}
@Test
void shouldSupportContrastOption() {
Page page = launchPersistent(new BrowserType.LaunchPersistentContextOptions().setContrast(Contrast.MORE));
assertEquals(true, page.evaluate("() => matchMedia('(prefers-contrast: more)').matches"));
assertEquals(false, page.evaluate("() => matchMedia('(prefers-contrast: no-preference)').matches"));
}
static boolean tempDirCanBeOnDifferentRoot() {
return isWindows;
}
@Test
@DisabledIf(value="tempDirCanBeOnDifferentRoot", disabledReason="IllegalArgument 'other' has different root on GitHub Actions.")
void shouldAcceptRelativeUserDataDir(@TempDir Path tmpDir) throws Exception {
Path userDataDir = tempDir.resolve("user-data-dir");
Path cwd = Paths.get("").toAbsolutePath();
Path relativePath = cwd.relativize(userDataDir);
BrowserContext context = browserType.launchPersistentContext(relativePath);
assertTrue(Files.list(userDataDir).count() > 0);
context.close();
}
}
@@ -28,7 +28,7 @@ public class TestElementHandleSelectText extends TestBase {
ElementHandle textarea = page.querySelector("textarea");
textarea.evaluate("textarea => textarea.value = 'some value'");
textarea.selectText();
if (isFirefox() || isWebKit()) {
if (isFirefox()) {
assertEquals(0, textarea.evaluate("el => el.selectionStart"));
assertEquals(10, textarea.evaluate("el => el.selectionEnd"));
} else {
@@ -42,7 +42,7 @@ public class TestElementHandleSelectText extends TestBase {
ElementHandle input = page.querySelector("input");
input.evaluate("input => input.value = 'some value'");
input.selectText();
if (isFirefox() || isWebKit()) {
if (isFirefox()) {
assertEquals(0, input.evaluate("el => el.selectionStart"));
assertEquals(10, input.evaluate("el => el.selectionEnd"));
} else {
@@ -17,6 +17,7 @@
package com.microsoft.playwright;
import com.google.gson.Gson;
import com.microsoft.playwright.APIRequest.NewContextOptions;
import com.microsoft.playwright.options.HttpCredentials;
import com.microsoft.playwright.options.HttpCredentialsSend;
import com.microsoft.playwright.options.HttpHeader;
@@ -37,6 +38,8 @@ import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
public class TestGlobalFetch extends TestBase {
private static final List<String> HTTP_METHODS = asList("GET", "PUT", "POST", "OPTIONS", "HEAD", "PATCH");
@Test
void shouldHaveJavaInDefaultUesrAgent() throws ExecutionException, InterruptedException {
APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions());
@@ -337,7 +340,7 @@ public class TestGlobalFetch extends TestBase {
for (String method : new String[] {"head", "put", "trace"}) {
server.setRoute("/empty.html", exchange -> {
exchange.getResponseHeaders().set("Content-type", "text/plain");
exchange.sendResponseHeaders(404, 10);
exchange.sendResponseHeaders(404, exchange.getRequestMethod().equals("HEAD") ? -1 : 10);
try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("Not found.");
}
@@ -358,7 +361,7 @@ public class TestGlobalFetch extends TestBase {
server.setRedirect("/b/c/redirect4", "/simple.json");
APIRequestContext request = playwright.request().newContext();
for (String method : new String[] {"GET", "PUT", "POST", "OPTIONS", "HEAD", "PATCH"}) {
for (String method : HTTP_METHODS) {
for (int maxRedirects = 1; maxRedirects < 4; maxRedirects++) {
int currMaxRedirects = maxRedirects;
PlaywrightException exception = assertThrows(PlaywrightException.class,
@@ -370,13 +373,69 @@ public class TestGlobalFetch extends TestBase {
request.dispose();
}
@Test
void shouldUseMaxRedirectsFromFetchWhenProvidedOverridingNewContext() {
server.setRedirect("/a/redirect1", "/b/c/redirect2");
server.setRedirect("/b/c/redirect2", "/b/c/redirect3");
server.setRedirect("/b/c/redirect3", "/b/c/redirect4");
server.setRedirect("/b/c/redirect4", "/simple.json");
APIRequestContext request = playwright.request().newContext(new NewContextOptions().setMaxRedirects(1));
for (String method : HTTP_METHODS) {
APIResponse response = request.fetch(server.PREFIX + "/a/redirect1",
RequestOptions.create().setMethod(method).setMaxRedirects(4));
assertEquals(200, response.status());
}
request.dispose();
}
@Test
void shouldFollowRedirectsUpToMaxRedirectsLimitSetInNewContext() {
server.setRedirect("/a/redirect1", "/b/c/redirect2");
server.setRedirect("/b/c/redirect2", "/b/c/redirect3");
server.setRedirect("/b/c/redirect3", "/b/c/redirect4");
server.setRedirect("/b/c/redirect4", "/simple.json");
for (String method : HTTP_METHODS) {
for (int maxRedirects = 1; maxRedirects <= 4; maxRedirects++) {
int currMaxRedirects = maxRedirects;
APIRequestContext request = playwright.request().newContext(new NewContextOptions().setMaxRedirects(currMaxRedirects));
if (maxRedirects < 4) {
PlaywrightException exception = assertThrows(PlaywrightException.class,
() -> request.fetch(server.PREFIX + "/a/redirect1",
RequestOptions.create().setMethod(method)));
assertTrue(exception.getMessage().contains("Max redirect count exceeded"), exception.getMessage());
} else {
APIResponse response = request.fetch(server.PREFIX + "/a/redirect1", RequestOptions.create().setMethod(method));
assertEquals(200, response.status());
}
request.dispose();
}
}
}
@Test
void shouldNotFollowRedirectsWhenMaxRedirectsIsSetTo0InNewContext() {
server.setRedirect("/a/redirect1", "/b/c/redirect2");
server.setRedirect("/b/c/redirect2", "/simple.json");
APIRequestContext request = playwright.request().newContext(new NewContextOptions().setMaxRedirects(0));
for (String method : HTTP_METHODS) {
APIResponse response = request.fetch(server.PREFIX + "/a/redirect1",
RequestOptions.create().setMethod(method));
assertEquals("/b/c/redirect2", response.headers().get("location"));
assertEquals(302, response.status());
}
request.dispose();
}
@Test
void shouldNotFollowRedirectsWhenMaxRedirectsIsSetTo0() {
server.setRedirect("/a/redirect1", "/b/c/redirect2");
server.setRedirect("/b/c/redirect2", "/simple.json");
APIRequestContext request = playwright.request().newContext();
for (String method : new String[] {"GET", "PUT", "POST", "OPTIONS", "HEAD", "PATCH"}) {
for (String method : HTTP_METHODS) {
APIResponse response = request.fetch(server.PREFIX + "/a/redirect1",
RequestOptions.create().setMethod(method).setMaxRedirects(0));
assertEquals("/b/c/redirect2", response.headers().get("location"));
@@ -391,7 +450,7 @@ public class TestGlobalFetch extends TestBase {
server.setRedirect("/b/c/redirect2", "/simple.json");
APIRequestContext request = playwright.request().newContext();
for (String method : new String[] {"GET", "PUT", "POST", "OPTIONS", "HEAD", "PATCH"}) {
for (String method : HTTP_METHODS) {
PlaywrightException exception = assertThrows(PlaywrightException.class,
() -> request.fetch(server.PREFIX + "/a/redirect1",
RequestOptions.create().setMethod(method).setMaxRedirects(-1)));
@@ -548,4 +607,36 @@ public class TestGlobalFetch extends TestBase {
assertEquals(4, requestCount[0]);
requestContext.dispose();
}
@Test
public void shouldThrowWhenFailOnStatusCodeIsSetToTrue() {
APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions().setFailOnStatusCode(true));
server.setRoute("/empty.html", exchange -> {
exchange.getResponseHeaders().set("Content-Length", "10");
exchange.getResponseHeaders().set("Content-type", "text/plain");
exchange.sendResponseHeaders(404, 10);
try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("Not found.");
}
});
PlaywrightException error = assertThrows(PlaywrightException.class, () -> {
request.get(server.EMPTY_PAGE);
});
assertTrue(error.getMessage().contains("404 Not Found"));
}
@Test
public void shouldNotThrowWhenFailOnStatusCodeIsSetToFalse() {
APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions().setFailOnStatusCode(false));
server.setRoute("/empty.html", exchange -> {
exchange.getResponseHeaders().set("Content-Length", "10");
exchange.getResponseHeaders().set("Content-type", "text/plain");
exchange.sendResponseHeaders(404, 10);
try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("Not found.");
}
});
APIResponse response = request.get(server.EMPTY_PAGE);
assertEquals(404, response.status());
}
}
@@ -18,10 +18,12 @@ package com.microsoft.playwright;
import com.microsoft.playwright.assertions.LocatorAssertions;
import com.microsoft.playwright.assertions.PlaywrightAssertions;
import org.junit.jupiter.api.Disabled;
import com.microsoft.playwright.assertions.LocatorAssertions.ContainsClassOptions;
import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;
import static java.util.Arrays.asList;
import java.util.regex.Pattern;
import static com.microsoft.playwright.Utils.mapOf;
@@ -657,9 +659,9 @@ public class TestLocatorAssertions extends TestBase {
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> {
assertThat(locator).isChecked(new LocatorAssertions.IsCheckedOptions().setTimeout(1000));
});
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be checked"), e.getMessage());
assertEquals("checked", e.getExpected().getStringRepresentation());
assertEquals("unchecked", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to be: checked"), e.getMessage());
}
@Test
@@ -669,9 +671,10 @@ public class TestLocatorAssertions extends TestBase {
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> {
assertThat(locator).not().isChecked(new LocatorAssertions.IsCheckedOptions().setTimeout(1000));
});
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be checked"), e.getMessage());
assertEquals("checked", e.getExpected().getStringRepresentation());
assertEquals("checked", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected not to be: checked"), e.getMessage());
}
@Test
@@ -687,7 +690,7 @@ public class TestLocatorAssertions extends TestBase {
Locator locator = page.locator("input");
AssertionFailedError error = assertThrows(AssertionFailedError.class,
() -> assertThat(locator).isChecked(new LocatorAssertions.IsCheckedOptions().setChecked(false).setTimeout(1000)));
assertTrue(error.getMessage().contains("Locator expected to be unchecked"), error.getMessage());
assertTrue(error.getMessage().contains("Locator expected to be: unchecked"), error.getMessage());
}
@Test
@@ -789,6 +792,14 @@ public class TestLocatorAssertions extends TestBase {
assertThat(locator).not().isEditable(new LocatorAssertions.IsEditableOptions().setEditable(false));
}
@Test
void isEditableThrowsOnNonInputElement() {
page.setContent("<button>");
Locator locator = page.locator("button");
PlaywrightException e = assertThrows(PlaywrightException.class, () -> assertThat(locator).isEditable());
assertTrue(e.getMessage().contains("Element is not an <input>, <textarea>, <select> or [contenteditable] and does not have a role allowing [aria-readonly]"), e.getMessage());
}
@Test
void isEmptyPass() {
page.setContent("<input></input>");
@@ -1083,4 +1094,49 @@ public class TestLocatorAssertions extends TestBase {
// Restore default.
PlaywrightAssertions.setDefaultAssertionTimeout(5_000);
}
@Test
void containsClassPass() {
page.setContent("<div class='foo bar baz'></div>");
Locator locator = page.locator("div");
assertThat(locator).containsClass("");
assertThat(locator).containsClass("bar");
assertThat(locator).containsClass("baz bar");
assertThat(locator).containsClass(" bar foo ");
assertThat(locator).not().containsClass(" baz not-matching");
}
@Test
void containsClassPassWithSvgs() {
page.setContent("<svg class='c1 c2' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'></svg>");
assertThat(page.locator("svg")).containsClass("c1");
assertThat(page.locator("svg")).containsClass("c2 c1");
}
@Test
void containsClassFail() {
page.setContent("<div class='bar baz'></div>");
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> {
assertThat(page.locator("div")).containsClass("does-not-exist", new ContainsClassOptions().setTimeout(1000));
});
assertTrue(e.getMessage().contains("Locator.expect with timeout 1000ms"), e.getMessage());
}
@Test
void containsClassPassWithArray() {
page.setContent("<div class='foo'></div><div class='hello bar'></div><div class='baz'></div>");
Locator locator = page.locator("div");
assertThat(locator).containsClass(asList("foo", "hello", "baz"));
assertThat(locator).not().hasClass(new String[]{"not-there", "hello", "baz"}); // Class not there
assertThat(locator).not().hasClass(new String[]{"foo", "hello"}); // length mismatch
}
@Test
void containsClassFailWithArray() {
page.setContent("<div class='foo'></div><div class='bar'></div><div class='bar'></div>");
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> {
assertThat(page.locator("div")).containsClass(asList("foo", "bar", "baz"), new ContainsClassOptions().setTimeout(1000));
});
assertTrue(e.getMessage().contains("Locator.expect with timeout 1000ms"), e.getMessage());
}
}
@@ -73,9 +73,9 @@ public class TestLocatorAssertions2 extends TestBase {
void isAttachedEventually() {
page.setContent("<div></div>");
Locator locator = page.locator("span");
page.evalOnSelector("div", "div => setTimeout(() => {\n" +
" div.innerHTML = '<span>Hello</span>'\n" +
" }, 100)");
page.evalOnSelector("div", "div => setTimeout(() => {\n" +
" div.innerHTML = '<span>Hello</span>'\n" +
" }, 100)");
assertThat(locator).isAttached();
}
@@ -83,9 +83,9 @@ public class TestLocatorAssertions2 extends TestBase {
void isAttachedEventuallyWithNot() {
page.setContent("<div><span>Hello</span></div>");
Locator locator = page.locator("span");
page.evalOnSelector("div", "div => setTimeout(() => {\n" +
" div.textContent = '';\n" +
" }, 0)");
page.evalOnSelector("div", "div => setTimeout(() => {\n" +
" div.textContent = '';\n" +
" }, 0)");
assertThat(locator).not().isAttached();
}
@@ -129,6 +129,9 @@ public class TestLocatorAssertions2 extends TestBase {
assertThat(page.locator("div")).hasAccessibleName(Pattern.compile("ell\\w"));
assertThat(page.locator("div")).not().hasAccessibleName(Pattern.compile("hello"));
assertThat(page.locator("div")).hasAccessibleName(Pattern.compile("hello"), new LocatorAssertions.HasAccessibleNameOptions().setIgnoreCase(true));
page.setContent("<button>foo&nbsp;bar\nbaz</button>");
assertThat(page.locator("button")).hasAccessibleName("foo bar baz");
}
@Test
@@ -141,6 +144,10 @@ public class TestLocatorAssertions2 extends TestBase {
assertThat(page.locator("div")).hasAccessibleDescription(Pattern.compile("ell\\w"));
assertThat(page.locator("div")).not().hasAccessibleDescription(Pattern.compile("hello"));
assertThat(page.locator("div")).hasAccessibleDescription(Pattern.compile("hello"), new LocatorAssertions.HasAccessibleDescriptionOptions().setIgnoreCase(true));
page.setContent("<div role=\"button\" aria-describedby=\"desc\"></div>\n" +
" <span id=\"desc\">foo&nbsp;bar\nbaz</span>");
assertThat(page.locator("div")).hasAccessibleDescription("foo bar baz");
}
@Test
@@ -150,4 +157,67 @@ public class TestLocatorAssertions2 extends TestBase {
assertThat(page.locator("div")).hasRole(AriaRole.BUTTON);
assertThat(page.locator("div")).not().hasRole(AriaRole.CHECKBOX);
}
@Test
void toHaveAccessibleErrorMessage() {
page.setContent("<form>" +
"<input role=\"textbox\" aria-invalid=\"true\" aria-errormessage=\"error-message\" />" +
"<div id=\"error-message\">Hello</div>" +
"<div id=\"irrelevant-error\">This should not be considered.</div>" +
"</form>");
Locator locator = page.locator("input[role=\"textbox\"]");
assertThat(locator).hasAccessibleErrorMessage("Hello");
assertThat(locator).not().hasAccessibleErrorMessage("hello");
assertThat(locator).hasAccessibleErrorMessage("hello", new LocatorAssertions.HasAccessibleErrorMessageOptions().setIgnoreCase(true));
assertThat(locator).hasAccessibleErrorMessage(Pattern.compile("ell\\w"));
assertThat(locator).not().hasAccessibleErrorMessage(Pattern.compile("hello"));
assertThat(locator).hasAccessibleErrorMessage(Pattern.compile("hello"), new LocatorAssertions.HasAccessibleErrorMessageOptions().setIgnoreCase(true));
assertThat(locator).not().hasAccessibleErrorMessage("This should not be considered.");
}
@Test
void toHaveAccessibleErrorMessageShouldHandleMultipleAriaErrorMessageReferences() {
page.setContent("<form>\n" +
" <input role=\"textbox\" aria-invalid=\"true\" aria-errormessage=\"error1 error2\" />\n" +
" <div id=\"error1\">First error message.</div>\n" +
" <div id=\"error2\">Second error message.</div>\n" +
" <div id=\"irrelevant-error\">This should not be considered.</div>\n" +
"</form>");
Locator locator = page.locator("input[role=\"textbox\"]");
assertThat(locator).hasAccessibleErrorMessage("First error message. Second error message.");
assertThat(locator).hasAccessibleErrorMessage(Pattern.compile("first error message.", Pattern.CASE_INSENSITIVE));
assertThat(locator).hasAccessibleErrorMessage(Pattern.compile("second error message.", Pattern.CASE_INSENSITIVE));
assertThat(locator).not().hasAccessibleErrorMessage(Pattern.compile("This should not be considered.", Pattern.CASE_INSENSITIVE));
}
@Test
void toBeEditableWithIndeterminateTrue() {
page.setContent("<input type=checkbox></input>");
page.locator("input").evaluate("e => e.indeterminate = true");
Locator locator = page.locator("input");
assertThat(locator).isChecked(new LocatorAssertions.IsCheckedOptions().setIndeterminate(true));
}
@Test
void toBeEditableWithIndeterminateTrueAndChecked() {
page.setContent("<input type=checkbox></input>");
page.locator("input").evaluate("e => e.indeterminate = true");
Locator locator = page.locator("input");
PlaywrightException e = assertThrows(PlaywrightException.class, () ->
assertThat(locator).isChecked(new LocatorAssertions.IsCheckedOptions().setIndeterminate(true).setChecked(false)));
assertTrue(e.getMessage().contains("Can't assert indeterminate and checked at the same time"), e.getMessage());
}
@Test
void toBeEditableFailWithIndeterminateTrue() {
page.setContent("<input type=checkbox></input>");
Locator locator = page.locator("input");
AssertionFailedError e = assertThrows(AssertionFailedError.class, () ->
assertThat(locator).isChecked(new LocatorAssertions.IsCheckedOptions().setIndeterminate(true).setTimeout(1000)));
// TODO: should be "assertThat().isChecked() with timeout 1000ms"
assertTrue(e.getMessage().contains("Locator.expect with timeout 1000ms"), e.getMessage());
}
}
@@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test;
import java.util.regex.Pattern;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
public class TestLocatorMisc extends TestBase{
@@ -123,4 +124,20 @@ public class TestLocatorMisc extends TestBase{
page.locator("input").pressSequentially("hello");
assertEquals("hello", page.evalOnSelector("input", "input => input.value"));
}
@Test
public void shouldSupportFilterVisible() {
page.setContent("<div>\n" +
" <div class=\"item\" style=\"display: none\">Hidden data0</div>\n" +
" <div class=\"item\">visible data1</div>\n" +
" <div class=\"item\" style=\"display: none\">Hidden data1</div>\n" +
" <div class=\"item\">visible data2</div>\n" +
" <div class=\"item\" style=\"display: none\">Hidden data2</div>\n" +
" <div class=\"item\">visible data3</div>\n" +
"</div>");
Locator locator = page.locator(".item").filter(new Locator.FilterOptions().setVisible(true)).nth(1);
assertThat(locator).hasText("visible data2");
assertThat(page.locator(".item").filter(new Locator.FilterOptions().setVisible(true)).getByText("data3")).hasText("visible data3");
assertThat(page.locator(".item").filter(new Locator.FilterOptions().setVisible(false)).getByText("data1")).hasText("Hidden data1");
}
}
@@ -0,0 +1,114 @@
package com.microsoft.playwright;
import com.microsoft.playwright.Locator.AriaSnapshotOptions;
import com.microsoft.playwright.junit.FixtureTest;
import com.microsoft.playwright.junit.UsePlaywright;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@FixtureTest
@UsePlaywright
public class TestPageAriaSnapshot {
public static String unshift(String snapshot) {
List<String> lines = Arrays.asList(snapshot.split("\n"));
int whitespacePrefixLength = 100;
Pattern pattern = Pattern.compile("^(\\s*).*");
for (String line : lines) {
if (line.trim().isEmpty())
continue;
Matcher matcher = pattern.matcher(line);
if (!matcher.matches()) {
continue;
}
String match = matcher.group(1);
if (match.length() < whitespacePrefixLength) {
whitespacePrefixLength = match.length();
}
}
final int prefixLength = whitespacePrefixLength;
return lines.stream()
.filter(line -> !line.trim().isEmpty())
.map(line -> line.substring(prefixLength))
.collect(Collectors.joining("\n"));
}
private static void checkAndMatchSnapshot(Locator locator, String snapshot) {
assertEquals(unshift(snapshot), locator.ariaSnapshot());
assertThat(locator).matchesAriaSnapshot(snapshot);
}
@Test
void shouldSnapshot(Page page) {
page.setContent("<h1>title</h1>");
checkAndMatchSnapshot(page.locator("body"), "- heading \"title\" [level=1]");
}
@Test
void shouldSnapshotList(Page page) {
page.setContent("<h1>title</h1><h1>title 2</h1>");
checkAndMatchSnapshot(page.locator("body"), "" +
" - heading \"title\" [level=1]\n" +
" - heading \"title 2\" [level=1]");
}
@Test
void shouldSnapshotListWithAccessibleName(Page page) {
page.setContent("<ul aria-label=\"my list\"><li>one</li><li>two</li></ul>");
checkAndMatchSnapshot(page.locator("body"), "- list \"my list\":\n - listitem: one\n - listitem: two");
}
@Test
void shouldSnapshotComplex(Page page) {
page.setContent("<ul><li><a href='about:blank'>link</a></li></ul>");
checkAndMatchSnapshot(page.locator("body"), "- list:\n - listitem:\n - link \"link\":\n - /url: about:blank");
}
@Test
void shouldSnapshotRef(Page page) {
page.setContent("<ul><li>foo</li></ul>");
assertEquals(unshift("- list [ref=s1e3]:\n - listitem [ref=s1e4]: foo"), page.locator("body").ariaSnapshot(new AriaSnapshotOptions().setRef(true)));
}
@Test
void shouldAllowTextNodes(Page page) {
page.setContent("<h1>Microsoft</h1><div>Open source projects and samples from Microsoft</div>");
checkAndMatchSnapshot(page.locator("body"), "" +
" - heading \"Microsoft\" [level=1]\n" +
" - text: Open source projects and samples from Microsoft");
}
@Test
void shouldSnapshotDetailsVisibility(Page page) {
page.setContent("<details><summary>Summary</summary><div>Details</div></details>");
checkAndMatchSnapshot(page.locator("body"), "- group: Summary");
}
@Test
void shouldSnapshotChildren(Page page) {
page.setContent("<ul><li><img />One</li><li>Two</li><li>Three</li></ul>");
assertThat(page.locator("body")).matchesAriaSnapshot("- list:\n - /children: equal\n - listitem\n - listitem: Two\n - listitem: Three");
assertThat(page.locator("body")).not().matchesAriaSnapshot("- list:\n - /children: equal\n - listitem\n - listitem: Two");
assertThat(page.locator("body")).matchesAriaSnapshot("- list:\n - /children: deep-equal\n - listitem:\n - img\n - text: One\n - listitem: Two\n - listitem: Three");
assertThat(page.locator("body")).not().matchesAriaSnapshot("- list:\n - /children: deep-equal\n - listitem:\n - text: One\n - listitem: Two\n - listitem: Three");
assertThat(page.locator("body")).matchesAriaSnapshot("- list:\n - /children: deep-equal\n - listitem:\n - /children: contain\n - text: One\n - listitem: Two\n - listitem: Three");
}
@Test
void shouldMatchUrl(Page page) {
page.setContent("<a href='https://example.com'>Link</a>");
assertThat(page.locator("body")).matchesAriaSnapshot("" +
"- link:\n" +
" - /url: /.*example.com/");
}
}
@@ -290,8 +290,9 @@ public class TestPageClock {
Page popup = page.waitForPopup(() -> {
page.evaluate("url => window.open(url)", server.PREFIX + "/popup.html");
});
popup.waitForURL(server.PREFIX + "/popup.html");
Double popupTime = (Double) popup.evaluate("time");
assertTrue(popupTime >= 2000);
assertTrue(popupTime >= 2000, "popupTime = " + popupTime);
}
@Test
@@ -310,6 +311,7 @@ public class TestPageClock {
Page popup = page.waitForPopup(() -> {
page.evaluate("url => window.open(url)", server.PREFIX + "/popup.html");
});
popup.waitForURL(server.PREFIX + "/popup.html");
Object popupTime = popup.evaluate("time");
assertEquals(1000, popupTime);
}
@@ -389,8 +391,8 @@ public class TestPageClock {
page.clock().install(new Clock.InstallOptions().setTime(0));
page.navigate("data:text/html,");
page.clock().pauseAt(1000);
page.waitForTimeout(1000);
page.clock().resume();
// Internally wait to make sure the clock is paused and not running.
page.waitForTimeout(1111);
int now = (int) page.evaluate("() => Date.now()");
assertTrue(now >= 0 && now <= 1000);
}
@@ -16,6 +16,7 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.Contrast;
import com.microsoft.playwright.options.ForcedColors;
import com.microsoft.playwright.options.ReducedMotion;
import org.junit.jupiter.api.Test;
@@ -171,4 +172,17 @@ public class TestPageEmulateMedia extends TestBase {
page.emulateMedia(new Page.EmulateMediaOptions().setForcedColors(null));
assertEquals(true, page.evaluate("() => matchMedia('(forced-colors: none)').matches"));
}
@Test
void shouldEmulateContrast() {
assertEquals(true, page.evaluate("matchMedia('(prefers-contrast: no-preference)').matches"));
page.emulateMedia(new Page.EmulateMediaOptions().setContrast(Contrast.NO_PREFERENCE));
assertEquals(true, page.evaluate("matchMedia('(prefers-contrast: no-preference)').matches"));
assertEquals(false, page.evaluate("matchMedia('(prefers-contrast: more)').matches"));
page.emulateMedia(new Page.EmulateMediaOptions().setContrast(Contrast.MORE));
assertEquals(false, page.evaluate("matchMedia('(prefers-contrast: no-preference)').matches"));
assertEquals(true, page.evaluate("matchMedia('(prefers-contrast: more)').matches"));
page.emulateMedia(new Page.EmulateMediaOptions().setContrast(null));
assertEquals(true, page.evaluate("matchMedia('(prefers-contrast: no-preference)').matches"));
}
}
@@ -18,11 +18,12 @@ package com.microsoft.playwright;
import org.junit.jupiter.api.Test;
import java.io.OutputStreamWriter;
import java.io.Writer;
import com.microsoft.playwright.impl.PlaywrightImpl;
import java.util.HashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.*;
@@ -141,13 +142,82 @@ public class TestPageInterception extends TestBase {
}
@Test
void shouldProperlyHandleCharacterSetsInGlobs() {
page.route("**/[a-z]*.html", route -> {
APIResponse response = route.fetch(new Route.FetchOptions().setUrl(server.PREFIX + "/one-style.html"));
route.fulfill(new Route.FulfillOptions().setResponse(response));
});
Response response = page.navigate(server.PREFIX + "/empty.html");
assertEquals(200, response.status());
assertTrue(response.text().contains("one-style.css"), response.text());
void shouldWorkWithGlob() {
assertTrue(globToRegex("**/*.js").matcher("https://localhost:8080/foo.js").find());
assertFalse(globToRegex("**/*.css").matcher("https://localhost:8080/foo.js").find());
assertFalse(globToRegex("*.js").matcher("https://localhost:8080/foo.js").find());
assertTrue(globToRegex("https://**/*.js").matcher("https://localhost:8080/foo.js").find());
assertTrue(globToRegex("http://localhost:8080/simple/path.js").matcher("http://localhost:8080/simple/path.js").find());
assertTrue(globToRegex("**/{a,b}.js").matcher("https://localhost:8080/a.js").find());
assertTrue(globToRegex("**/{a,b}.js").matcher("https://localhost:8080/b.js").find());
assertFalse(globToRegex("**/{a,b}.js").matcher("https://localhost:8080/c.js").find());
assertTrue(globToRegex("**/*.{png,jpg,jpeg}").matcher("https://localhost:8080/c.jpg").find());
assertTrue(globToRegex("**/*.{png,jpg,jpeg}").matcher("https://localhost:8080/c.jpeg").find());
assertTrue(globToRegex("**/*.{png,jpg,jpeg}").matcher("https://localhost:8080/c.png").find());
assertFalse(globToRegex("**/*.{png,jpg,jpeg}").matcher("https://localhost:8080/c.css").find());
assertTrue(globToRegex("foo*").matcher("foo.js").find());
assertFalse(globToRegex("foo*").matcher("foo/bar.js").find());
assertFalse(globToRegex("http://localhost:3000/signin-oidc*").matcher("http://localhost:3000/signin-oidc/foo").find());
assertTrue(globToRegex("http://localhost:3000/signin-oidc*").matcher("http://localhost:3000/signin-oidcnice").find());
// range [] is NOT supported
assertTrue(globToRegex("**/api/v[0-9]").matcher("http://example.com/api/v[0-9]").find());
assertFalse(globToRegex("**/api/v[0-9]").matcher("http://example.com/api/version").find());
// query params
assertTrue(globToRegex("**/api\\?param").matcher("http://example.com/api?param").find());
assertFalse(globToRegex("**/api\\?param").matcher("http://example.com/api-param").find());
assertTrue(globToRegex("**/three-columns/settings.html\\?**id=settings-**").matcher("http://mydomain:8080/blah/blah/three-columns/settings.html?id=settings-e3c58efe-02e9-44b0-97ac-dd138100cf7c&blah").find());
assertEquals("^\\?$", globToRegex("\\?").pattern());
assertEquals("^\\\\$", globToRegex("\\").pattern());
assertEquals("^\\\\$", globToRegex("\\\\").pattern());
assertEquals("^\\[$", globToRegex("\\[").pattern());
assertEquals("^\\[a-z\\]$", globToRegex("[a-z]").pattern());
assertEquals("^\\$\\^\\+\\.\\*\\(\\)\\|\\?\\{\\}\\[\\]$", globToRegex("$^+.\\*()|\\?\\{\\}\\[\\]").pattern());
assertTrue(urlMatches(null, "http://playwright.dev/", "http://playwright.dev"));
assertTrue(urlMatches(null, "http://playwright.dev/?a=b", "http://playwright.dev?a=b"));
assertTrue(urlMatches(null, "http://playwright.dev/", "h*://playwright.dev"));
assertTrue(urlMatches(null, "http://api.playwright.dev/?x=y", "http://*.playwright.dev?x=y"));
assertTrue(urlMatches(null, "http://playwright.dev/foo/bar", "**/foo/**"));
assertTrue(urlMatches("http://playwright.dev", "http://playwright.dev/?x=y", "?x=y"));
assertTrue(urlMatches("http://playwright.dev/foo/", "http://playwright.dev/foo/bar?x=y", "./bar?x=y"));
// This is not supported, we treat ? as a query separator.
assertFalse(urlMatches(null, "http://localhost:8080/Simple/path.js", "http://localhost:8080/?imple/path.js"));
assertFalse(urlMatches(null, "http://playwright.dev/", "http://playwright.?ev"));
assertTrue(urlMatches(null, "http://playwright./?ev", "http://playwright.?ev"));
assertFalse(urlMatches(null, "http://playwright.dev/foo", "http://playwright.dev/f??"));
assertTrue(urlMatches(null, "http://playwright.dev/f??", "http://playwright.dev/f??"));
assertTrue(urlMatches(null, "http://playwright.dev/?x=y", "http://playwright.dev\\\\?x=y"));
assertTrue(urlMatches(null, "http://playwright.dev/?x=y", "http://playwright.dev/\\\\?x=y"));
assertTrue(urlMatches("http://playwright.dev/foo", "http://playwright.dev/foo?bar", "?bar"));
assertTrue(urlMatches("http://playwright.dev/foo", "http://playwright.dev/foo?bar", "\\\\?bar"));
assertTrue(urlMatches("http://first.host/", "http://second.host/foo", "**/foo"));
assertTrue(urlMatches("http://playwright.dev/", "http://localhost/", "*//localhost/"));
}
Pattern globToRegex(String glob) {
return globToRegex(glob, null, false);
}
Pattern globToRegex(String glob, String baseURL, boolean webSocketUrl) {
return ((PlaywrightImpl) playwright).localUtils().globToRegex(glob, baseURL, webSocketUrl);
}
boolean urlMatches(String baseURL, String urlString, String match) {
if (match == null) {
return true;
}
String glob = (String) match;
if (glob.isEmpty()) {
return true;
}
return globToRegex(glob, baseURL, false).matcher(urlString).find();
}
}
@@ -16,9 +16,12 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.HttpHeader;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@@ -77,4 +80,83 @@ public class TestPageRequestContinue extends TestBase {
e.getMessage().contains("frame was detached"), e.getMessage());
assertTrue(done[0]);
}
@Test
@DisabledIf(value = "com.microsoft.playwright.TestBase#isFirefox", disabledReason = "We currently clear all headers during interception in firefox")
void continueShouldNotPropagateCookieOverrideToRedirects() throws ExecutionException, InterruptedException {
// https://github.com/microsoft/playwright/issues/35168
server.setRoute("/set-cookie", exchange -> {
exchange.getResponseHeaders().add("Set-Cookie", "foo=bar;");
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().close();
});
page.navigate(server.PREFIX + "/set-cookie");
assertEquals("foo=bar", page.evaluate("() => document.cookie"));
server.setRedirect("/redirect", server.PREFIX + "/empty.html");
page.route("**/redirect", route -> {
Map<String, String> headers = new HashMap<>(route.request().allHeaders());
headers.put("cookie", "override");
route.resume(new Route.ResumeOptions().setHeaders(headers));
});
Future<Server.Request> serverRequest = server.futureRequest("/empty.html");
page.navigate(server.PREFIX + "/redirect");
assertEquals(asList("foo=bar"), serverRequest.get().headers.get("cookie"));
}
@Test
@DisabledIf(value = "com.microsoft.playwright.TestBase#isFirefox", disabledReason = "We currently clear all headers during interception in firefox")
void continueShouldNotOverrideCookie() throws ExecutionException, InterruptedException {
// https://github.com/microsoft/playwright/issues/35168
server.setRoute("/set-cookie", exchange -> {
exchange.getResponseHeaders().add("Set-Cookie", "foo=bar;");
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().close();
});
page.navigate(server.PREFIX + "/set-cookie");
assertEquals("foo=bar", page.evaluate("() => document.cookie"));
page.route("**", route -> {
Map<String, String> headers = new HashMap<>(route.request().allHeaders());
headers.put("cookie", "override");
headers.put("custom", "value");
route.resume(new Route.ResumeOptions().setHeaders(headers));
});
Future<Server.Request> serverRequest = server.futureRequest("/empty.html");
page.navigate(server.EMPTY_PAGE);
// Original cookie from the browser's cookie jar should be sent.
assertEquals(asList("foo=bar"), serverRequest.get().headers.get("cookie"));
assertEquals(asList("value"), serverRequest.get().headers.get("custom"));
}
@Test
void redirectAfterContinueShouldBeAbleToDeleteCookie() throws ExecutionException, InterruptedException {
// https://github.com/microsoft/playwright/issues/35168
server.setRoute("/set-cookie", exchange -> {
exchange.getResponseHeaders().add("Set-Cookie", "foo=bar;");
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().close();
});
page.navigate(server.PREFIX + "/set-cookie");
assertEquals("foo=bar", page.evaluate("() => document.cookie"));
server.setRoute("/delete-cookie", exchange -> {
exchange.getResponseHeaders().add("Set-Cookie", "foo=bar; expires=Thu, 01 Jan 1970 00:00:00 GMT");
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().close();
});
server.setRedirect("/redirect", "/delete-cookie");
page.route("**/redirect", route -> {
// Pass original headers explicitly when continuing.
route.resume(new Route.ResumeOptions().setHeaders(route.request().allHeaders()));
});
page.navigate(server.PREFIX + "/redirect");
Future<Server.Request> serverRequest = server.futureRequest("/empty.html");
page.navigate(server.EMPTY_PAGE);
assertNull(serverRequest.get().headers.get("cookie"));
}
}
@@ -16,7 +16,7 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.Cookie;
import com.microsoft.playwright.options.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
@@ -98,7 +98,19 @@ public class TestPageRoute extends TestBase {
}
@Test
void shouldSupportQuestionMarkInGlobPattern() {
void shouldUnrouteNonExistentPatternHandler() {
List<Integer> intercepted = new ArrayList<>();
page.route(Pattern.compile("empty.html"), route -> {
intercepted.add(1);
route.fallback();
});
page.unroute("**/*");
page.navigate(server.EMPTY_PAGE);
assertEquals(asList( 1), intercepted);
}
@Test
void shouldNotSupportQuestionMarkInGlobPattern() {
server.setRoute("/index", exchange -> {
exchange.sendResponseHeaders(200, 0);
try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) {
@@ -111,6 +123,18 @@ public class TestPageRoute extends TestBase {
writer.write("index123hello");
}
});
server.setRoute("/index?hello", exchange -> {
exchange.sendResponseHeaders(200, 0);
try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("index?hello");
}
});
server.setRoute("/index1hello", exchange -> {
exchange.sendResponseHeaders(200, 0);
try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("index1hello");
}
});
page.route("**/index?hello", route -> {
route.fulfill(new Route.FulfillOptions().setBody("intercepted any character"));
@@ -127,7 +151,8 @@ public class TestPageRoute extends TestBase {
assertTrue(page.content().contains("index-no-hello"), page.content());
page.navigate(server.PREFIX + "/index1hello");
assertTrue(page.content().contains("intercepted any character"), page.content());
assertFalse(page.content().contains("intercepted any character"), page.content());
assertTrue(page.content().contains("index1hello"), page.content());
page.navigate(server.PREFIX + "/index123hello");
assertTrue(page.content().contains("index123hello"), page.content());
@@ -253,7 +253,7 @@ public class TestPageScreenshot extends TestBase {
}
static boolean isScreenshotTestDisabled() {
if (isWebKit()) {
if (isWebKit() || isChromium()) {
// Array lengths differ.
return true;
}
@@ -25,6 +25,7 @@ import java.util.List;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.junit.jupiter.api.Assertions.*;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
public class TestPageSelectOption extends TestBase {
@Test
@@ -238,4 +239,64 @@ public class TestPageSelectOption extends TestBase {
assertEquals(asList("blue"), page.evaluate("() => window['result'].onChange"));
}
@Test
void shouldWaitForOptionToBeEnabled() {
page.setContent(
"<select disabled>\n" +
" <option>one</option>\n" +
" <option>two</option>\n" +
"</select>\n" +
"\n" +
"<script>\n" +
"function hydrate() {\n" +
" const select = document.querySelector('select');\n" +
" select.removeAttribute('disabled');\n" +
" select.addEventListener('change', () => {\n" +
" window['result'] = select.value;\n" +
" });\n" +
"}\n" +
"</script>");
page.evaluate("() => setTimeout(hydrate, 1000)");
page.locator("select").selectOption("two");
assertEquals("two", page.evaluate("window['result']"));
assertThat(page.locator("select")).hasValue("two");
}
@Test
void shouldWaitForSelectToBeSwapped() {
page.setContent(
"<select disabled>\n" +
" <option>one</option>\n" +
" <option>two</option>\n" +
"</select>\n" +
"\n" +
"<script>\n" +
"function hydrate() {\n" +
" const select = document.querySelector('select');\n" +
" select.remove();\n" +
"\n" +
" const newSelect = document.createElement('select');\n" +
" const option1 = document.createElement('option');\n" +
" option1.textContent = 'one';\n" +
" newSelect.appendChild(option1);\n" +
" const option2 = document.createElement('option');\n" +
" option2.textContent = 'two';\n" +
" newSelect.appendChild(option2);\n" +
"\n" +
" document.body.appendChild(newSelect);\n" +
"\n" +
" newSelect.addEventListener('change', () => {\n" +
" window['result'] = newSelect.value;\n" +
" });\n" +
"}\n" +
"</script>");
page.evaluate("() => setTimeout(window.hydrate, 1000)");
page.locator("select").selectOption("two");
assertThat(page.locator("select")).hasValue("two");
assertEquals("two", page.evaluate("window['result']"));
}
}
@@ -123,7 +123,7 @@ public class TestPageSetInputFiles extends TestBase {
}
FileChooser fileChooser = page.waitForFileChooser(() -> input.click());
fileChooser.setFiles(uploadFiles.toArray(new Path[0]));
Object filesLen = page.getByRole(AriaRole.TEXTBOX).evaluate("e => e.files.length");
Object filesLen = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Choose File")).evaluate("e => e.files.length");
assertTrue(fileChooser.isMultiple());
assertEquals(filesCount, filesLen);
}
@@ -471,10 +471,10 @@ public class TestPageSetInputFiles extends TestBase {
}
@Test
void shouldUploadAFolderAndThrowForMultipleDirectories() throws IOException {
void shouldUploadAFolderAndThrowForMultipleDirectories(@TempDir Path tmpDir) throws IOException {
page.navigate(server.PREFIX + "/input/folderupload.html");
Locator input = page.locator("input[name=\"file1\"]");
Path dir = Paths.get("file-upload-test"); // Adjust path as necessary
Path dir = tmpDir.resolve("file-upload-test");
Files.createDirectories(dir.resolve("folder1"));
writeFile(dir.resolve("folder1").resolve("file1.txt"), "file1 content");
Files.createDirectories(dir.resolve("folder2"));
@@ -485,11 +485,11 @@ public class TestPageSetInputFiles extends TestBase {
}
@Test
void shouldThrowIfADirectoryAndFilesArePassed() throws IOException {
void shouldThrowIfADirectoryAndFilesArePassed(@TempDir Path tmpDir) throws IOException {
// Skipping conditions based on environment not directly translatable to Java; needs custom implementation
page.navigate(server.PREFIX + "/input/folderupload.html");
Locator input = page.locator("input[name=\"file1\"]");
Path dir = Paths.get("file-upload-test"); // Adjust path as necessary
Path dir = tmpDir.resolve("file-upload-test");
Files.createDirectories(dir.resolve("folder1"));
writeFile(dir.resolve("folder1").resolve("file1.txt"), "file1 content");
PlaywrightException e = assertThrows(PlaywrightException.class,
@@ -498,10 +498,10 @@ public class TestPageSetInputFiles extends TestBase {
}
@Test
void shouldThrowWhenUploadingAFolderInANormalFileUploadInput() throws IOException {
void shouldThrowWhenUploadingAFolderInANormalFileUploadInput(@TempDir Path tmpDir) throws IOException {
page.navigate(server.PREFIX + "/input/fileupload.html");
Locator input = page.locator("input[name=\"file1\"]");
Path dir = Paths.get("file-upload-test"); // Adjust path as necessary
Path dir = tmpDir.resolve("file-upload-test");
Files.createDirectories(dir);
writeFile(dir.resolve("file1.txt"), "file1 content");
PlaywrightException e = assertThrows(PlaywrightException.class,
@@ -30,7 +30,6 @@ import static org.junit.jupiter.api.Assertions.*;
public class TestPdf extends TestBase {
@Test
@EnabledIf(value="com.microsoft.playwright.TestBase#isChromium", disabledReason="skip")
@DisabledIf(value="com.microsoft.playwright.TestBase#isHeadful", disabledReason="skip")
void shouldBeAbleToSaveFile(@TempDir Path tempDir) throws IOException {
Path path = tempDir.resolve("output.pdf");
page.pdf(new Page.PdfOptions().setPath(path));
@@ -40,7 +39,6 @@ public class TestPdf extends TestBase {
@Test
@EnabledIf(value="com.microsoft.playwright.TestBase#isChromium", disabledReason="skip")
@DisabledIf(value="com.microsoft.playwright.TestBase#isHeadful", disabledReason="skip")
void shouldSupportFractionalScaleValue(@TempDir Path tempDir) throws IOException {
Path path = tempDir.resolve("output.pdf");
page.pdf(new Page.PdfOptions().setPath(path).setScale(0.5));
@@ -51,7 +49,6 @@ public class TestPdf extends TestBase {
@Test
@EnabledIf(value="com.microsoft.playwright.TestBase#isChromium", disabledReason="Printing to pdf is currently only supported in headless chromium.")
@DisabledIf(value="com.microsoft.playwright.TestBase#isHeadful", disabledReason="Printing to pdf is currently only supported in headless chromium.")
void shouldBeAbleToGenerateOutline(@TempDir Path tempDir) throws IOException {
page.navigate(server.PREFIX + "/headings.html");
Path outputFileNoOutline = tempDir.resolve("outputNoOutline.pdf");
@@ -9,6 +9,8 @@ import org.junit.jupiter.params.provider.ValueSource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.regex.Pattern;
@@ -317,4 +319,76 @@ public class TestRouteWebSocket {
"close code=3008 reason=oops"),
page.evaluate("window.log"));
}
@Test
public void shouldWorkWithBaseURL(Browser browser) throws Exception {
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setBaseURL("http://localhost:" + webSocketServer.getPort()));
Page newPage = context.newPage();
newPage.routeWebSocket("/ws", ws -> {
ws.onMessage(message -> {
if (message.text() != null) {
ws.send(message.text());
} else {
ws.send(message.binary());
}
});
});
setupWS(newPage, webSocketServer.getPort(), "blob");
newPage.evaluate("async () => {\n" +
" await window.wsOpened;\n" +
" window.ws.send('echo');\n" +
" }");
newPage.waitForCondition(() -> {
Boolean result = (Boolean) newPage.evaluate("() => window.log.length >= 2");
return result;
});
assertEquals(
asList("open", "message: data=echo origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId="),
newPage.evaluate("window.log"));
}
@Test
public void shouldWorkWithNoTrailingSlash(Page page) throws Exception {
List<String> log = new ArrayList<>();
// No trailing slash in the route pattern
page.routeWebSocket("ws://localhost:" + webSocketServer.getPort(), ws -> {
ws.onMessage(message -> {
log.add(message.text());
ws.send("response");
});
});
page.navigate("about:blank");
page.evaluate("({ port }) => {\n" +
" window.log = [];\n" +
" // No trailing slash in WebSocket URL\n" +
" window.ws = new WebSocket('ws://localhost:' + port);\n" +
" window.ws.addEventListener('message', event => window.log.push(event.data));\n" +
"}", mapOf("port", webSocketServer.getPort()));
// Wait for WebSocket to be ready (readyState === 1)
page.waitForCondition(() -> {
Integer result = (Integer) page.evaluate("() => window.ws.readyState");
return result == 1;
});
page.evaluate("() => window.ws.send('query')");
// Wait and verify server received message
page.waitForCondition(() -> log.size() >= 1);
assertEquals(asList("query"), log);
// Wait and verify client received response
page.waitForCondition(() -> {
Boolean result = (Boolean) page.evaluate("() => window.log.length >= 1");
return result;
});
assertEquals(asList("response"), page.evaluate("window.log"));
}
}
@@ -18,6 +18,7 @@ package com.microsoft.playwright;
import com.microsoft.playwright.options.AriaRole;
import org.junit.jupiter.api.Test;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import java.util.regex.Pattern;
@@ -241,6 +242,66 @@ public class TestSelectorsRole extends TestBase {
), page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setDisabled(false)).evaluateAll("els => els.map(e => e.outerHTML)"));
}
@Test
void shouldInheritDisabledFromTheAncestor() {
page.setContent(
"<span aria-disabled=\"true\">\n" +
" <button>Click me!</button>\n" +
"</span>");
assertThat(page.locator("button")).isDisabled();
page.setContent(
"<span aria-disabled=\"true\">\n" +
" <h1>Heading</h1>\n" +
"</span>");
// Non-control roles do not inherit disabled state
assertThat(page.locator("h1")).isEnabled();
}
@Test
void shouldSupportDisabledFieldset() {
page.setContent(
"<fieldset disabled>\n" +
" <input></input>\n" +
" <button data-testid=\"inside-fieldset-element\">x</button>\n" +
" <legend>\n" +
" <button data-testid=\"inside-legend-element\">legend</button>\n" +
" </legend>\n" +
"</fieldset>\n" +
"\n" +
"<fieldset disabled>\n" +
" <legend>\n" +
" <div>\n" +
" <button data-testid=\"nested-inside-legend-element\">x</button>\n" +
" </div>\n" +
" </legend>\n" +
"</fieldset>\n" +
"\n" +
"<fieldset disabled>\n" +
" <div></div>\n" +
" <legend>\n" +
" <button data-testid=\"first-legend-element\">x</button>\n" +
" </legend>\n" +
" <legend>\n" +
" <button data-testid=\"second-legend-element\">x</button>\n" +
" </legend>\n" +
"</fieldset>\n" +
"\n" +
"<fieldset disabled>\n" +
" <fieldset>\n" +
" <button data-testid=\"deep-button\">x</button>\n" +
" </fieldset>\n" +
"</fieldset>");
assertThat(page.getByTestId("inside-legend-element")).isEnabled();
assertThat(page.getByTestId("nested-inside-legend-element")).isEnabled();
assertThat(page.getByTestId("first-legend-element")).isEnabled();
// Only the first legend is exempt from disabled fieldset
assertThat(page.getByTestId("second-legend-element")).isDisabled();
// Nested fieldsets inherit disabled state
assertThat(page.getByTestId("deep-button")).isDisabled();
}
@Test
void shouldSupportLevel() {
page.setContent("<h1>Hello</h1>\n" +
@@ -357,15 +418,6 @@ public class TestSelectorsRole extends TestBase {
"<div role=\"button\" aria-label=\"Hallo\"></div>"
), page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("^H[ae]llo$"))).evaluateAll("els => els.map(e => e.outerHTML)"));
assertEquals(asList(
"<div role=\"button\" aria-label=\" Hello \"></div>",
"<div role=\"button\" aria-label=\"Hallo\"></div>"
), page.locator("role=button[name=/h.*o/i]").evaluateAll("els => els.map(e => e.outerHTML)"));
assertEquals(asList(
"<div role=\"button\" aria-label=\" Hello \"></div>",
"<div role=\"button\" aria-label=\"Hallo\"></div>"
), page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("h.*o", Pattern.CASE_INSENSITIVE))).evaluateAll("els => els.map(e => e.outerHTML)"));
assertEquals(asList(
"<div role=\"button\" aria-label=\" Hello \"></div>",
"<div role=\"button\" aria-label=\"Hello\" aria-hidden=\"true\"></div>"
@@ -16,6 +16,12 @@
package com.microsoft.playwright;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.microsoft.playwright.options.AriaRole;
import com.microsoft.playwright.options.Location;
import com.microsoft.playwright.options.MouseButton;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -26,12 +32,14 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
public class TestTracing extends TestBase {
@Override
@@ -154,4 +162,151 @@ public class TestTracing extends TestBase {
}
}
@Test
void canCallTracingGroupGroupEndAtAnyTimeAndAutoClose(@TempDir Path tempDir) throws Exception {
context.tracing().group("ignored");
context.tracing().groupEnd();
context.tracing().group("ignored2");
context.tracing().start(new Tracing.StartOptions());
context.tracing().group("actual");
page.navigate(server.EMPTY_PAGE);
Path traceFile1 = tempDir.resolve("trace1.zip");
context.tracing().stopChunk(new Tracing.StopChunkOptions().setPath(traceFile1));
context.tracing().group("ignored3");
context.tracing().groupEnd();
context.tracing().groupEnd();
context.tracing().groupEnd();
List<TraceEvent> events = parseTraceEvents(traceFile1);
List<TraceEvent> groups = events.stream().filter(e -> "tracingGroup".equals(e.method)).collect(Collectors.toList());
assertEquals(1, groups.size());
assertEquals("actual", groups.get(0).apiName);
}
@Test
void traceGroupGroupEnd(@TempDir Path tempDir) throws Exception {
context.tracing().start(new Tracing.StartOptions());
context.tracing().group("outer group");
page.navigate("data:text/html,<!DOCTYPE html><body><div>Hello world</div></body>");
context.tracing().group("inner group 1", new Tracing.GroupOptions().setLocation(new Location("foo.java").setLine(17).setColumn(1)));
page.locator("body").click();
context.tracing().groupEnd();
context.tracing().group("inner group 2");
assertTrue(page.locator("text=Hello").isVisible());
context.tracing().groupEnd();
context.tracing().groupEnd();
Path traceFile1 = tempDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1));
List<TraceEvent> events = parseTraceEvents(traceFile1);
List<String> calls = events.stream().filter(e -> e.apiName != null).map(e -> e.apiName).collect(Collectors.toList());
assertEquals(asList("outer group", "Page.navigate", "inner group 1", "Frame.click", "inner group 2", "Page.isVisible"), calls);
}
@Test
void shouldTraceVariousAPIs(@TempDir Path tempDir) throws Exception {
context.tracing().start(new Tracing.StartOptions());
page.clock().install();
page.setContent("<input type='text' />");
page.locator("input").click(new Locator.ClickOptions().setButton(MouseButton.RIGHT));
page.getByRole(AriaRole.TEXTBOX).click();
page.keyboard().type("Hello world this is a very long string what happens when it overflows?");
page.keyboard().press("Control+c");
page.keyboard().down("Shift");
page.keyboard().insertText("Hello world");
page.keyboard().up("Shift");
page.mouse().move(0, 0);
page.mouse().down();
page.mouse().move(100, 200);
page.mouse().wheel(5, 7);
page.mouse().up();
page.clock().fastForward(1000);
page.clock().fastForward("30:00");
page.clock().pauseAt("2050-02-02");
page.clock().runFor(10);
page.clock().setFixedTime("2050-02-02");
page.clock().setSystemTime("2050-02-02");
page.clock().resume();
page.locator("input").click(new Locator.ClickOptions().setButton(MouseButton.RIGHT));
Path traceFile1 = tempDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1));
List<TraceEvent> events = parseTraceEvents(traceFile1);
List<String> calls = events.stream().filter(e -> e.apiName != null).map(e -> e.apiName)
.collect(Collectors.toList());
assertEquals(asList(
"Clock.install",
"Page.setContent",
"Frame.click",
"Frame.click",
"Keyboard.type",
"Keyboard.press",
"Keyboard.down",
"Keyboard.insertText",
"Keyboard.up",
"Mouse.move",
"Mouse.down",
"Mouse.move",
"Mouse.wheel",
"Mouse.up",
"Clock.fastForward",
"Clock.fastForward",
"Clock.pauseAt",
"Clock.runFor",
"Clock.setFixedTime",
"Clock.setSystemTime",
"Clock.resume",
"Frame.click"),
calls);
}
@Test
public void shouldNotRecordNetworkActions(@TempDir Path tempDir) throws IOException {
context.tracing().start(new Tracing.StartOptions());
page.onRequest(request -> {
request.allHeaders();
});
page.onResponse(response -> {
response.text();
});
page.navigate(server.EMPTY_PAGE);
Path traceFile1 = tempDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(traceFile1));
List<TraceEvent> events = parseTraceEvents(traceFile1);
List<String> calls = events.stream().filter(e -> e.apiName != null).map(e -> e.apiName)
.collect(Collectors.toList());
assertEquals(asList("Page.navigate"), calls);
}
private static class TraceEvent {
String type;
String name;
String apiName;
String method;
Double startTime;
Double endTime;
String callId;
}
private static List<TraceEvent> parseTraceEvents(Path traceFile) throws IOException {
Map<String, byte[]> files = Utils.parseZip(traceFile);
Map<String, byte[]> traces = files.entrySet().stream().filter(e -> e.getKey().endsWith(".trace")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
assertNotNull(traces.get("trace.trace"));
return Arrays.stream(new String(traces.get("trace.trace"), UTF_8)
.split("\n"))
.map(s -> new Gson().fromJson(s, TraceEvent.class))
.collect(Collectors.toList());
}
}
@@ -0,0 +1,116 @@
CC0 1.0 Universal
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator and
subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for the
purpose of contributing to a commons of creative, cultural and scientific
works ("Commons") that the public can reliably and without fear of later
claims of infringement build upon, modify, incorporate in other works, reuse
and redistribute as freely as possible in any form whatsoever and for any
purposes, including without limitation commercial purposes. These owners may
contribute to the Commons to promote the ideal of a free culture and the
further production of creative, cultural and scientific works, or to gain
reputation or greater distribution for their Work in part through the use and
efforts of others.
For these and/or other purposes and motivations, and without any expectation
of additional consideration or compensation, the person associating CC0 with a
Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
and publicly distribute the Work under its terms, with knowledge of his or her
Copyright and Related Rights in the Work and the meaning and intended legal
effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not limited
to, the following:
i. the right to reproduce, adapt, distribute, perform, display, communicate,
and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or likeness
depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data in
a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation thereof,
including any amended or successor version of such directive); and
vii. other similar, equivalent or corresponding rights throughout the world
based on applicable law or treaty, and any national implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention of,
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
and Related Rights and associated claims and causes of action, whether now
known or unknown (including existing as well as future claims and causes of
action), in the Work (i) in all territories worldwide, (ii) for the maximum
duration provided by applicable law or treaty (including future time
extensions), (iii) in any current or future medium and for any number of
copies, and (iv) for any purpose whatsoever, including without limitation
commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
the Waiver for the benefit of each member of the public at large and to the
detriment of Affirmer's heirs and successors, fully intending that such Waiver
shall not be subject to revocation, rescission, cancellation, termination, or
any other legal or equitable action to disrupt the quiet enjoyment of the Work
by the public as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason be
judged legally invalid or ineffective under applicable law, then the Waiver
shall be preserved to the maximum extent permitted taking into account
Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
is so judged Affirmer hereby grants to each affected person a royalty-free,
non transferable, non sublicensable, non exclusive, irrevocable and
unconditional license to exercise Affirmer's Copyright and Related Rights in
the Work (i) in all territories worldwide, (ii) for the maximum duration
provided by applicable law or treaty (including future time extensions), (iii)
in any current or future medium and for any number of copies, and (iv) for any
purpose whatsoever, including without limitation commercial, advertising or
promotional purposes (the "License"). The License shall be deemed effective as
of the date CC0 was applied by Affirmer to the Work. Should any part of the
License for any reason be judged legally invalid or ineffective under
applicable law, such partial invalidity or ineffectiveness shall not
invalidate the remainder of the License, and in such case Affirmer hereby
affirms that he or she will not (i) exercise any of his or her remaining
Copyright and Related Rights in the Work or (ii) assert any associated claims
and causes of action with respect to the Work, in either case contrary to
Affirmer's express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or warranties
of any kind concerning the Work, express, implied, statutory or otherwise,
including without limitation warranties of title, merchantability, fitness
for a particular purpose, non infringement, or the absence of latent or
other defects, accuracy, or the present or absence of errors, whether or not
discoverable, all to the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without limitation
any person's Copyright and Related Rights in the Work. Further, Affirmer
disclaims responsibility for obtaining any necessary consents, permissions
or other rights required for any use of the Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to this
CC0 or use of the Work.
For more information, please see
<http://creativecommons.org/publicdomain/zero/1.0/>
@@ -0,0 +1 @@
Source: https://github.com/mdn/dom-examples/tree/main/to-do-notifications
@@ -0,0 +1,108 @@
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=380">
<script src="scripts/todo.js"></script>
<title>To-do list with Notifications</title>
<link href="style/style.css" type="text/css" rel="stylesheet">
</head>
<body>
<h1>To-do list</h1>
<div class="task-box">
<ul id="task-list">
</ul>
</div>
<div class="form-box">
<h2>Add new to-do item.</h2>
<form id="task-form" action="index.html">
<div class="full-width"><label for="title">Task title:</label><input type="text" id="title" required></div>
<div class="half-width"><label for="deadline-hours">Hours (hh):</label><input type="number" id="deadline-hours" required></div>
<div class="half-width"><label for="deadline-minutes">Mins (mm):</label><input type="number" id="deadline-minutes" required></div>
<div class="third-width"><label for="deadline-day">Day:</label>
<select id="deadline-day" required>
<option value="01">01</option>
<option value="02">02</option>
<option value="03">03</option>
<option value="04">04</option>
<option value="05">05</option>
<option value="06">06</option>
<option value="07">07</option>
<option value="08">08</option>
<option value="09">09</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
<option value="13">13</option>
<option value="14">14</option>
<option value="15">15</option>
<option value="16">16</option>
<option value="17">17</option>
<option value="18">18</option>
<option value="19">19</option>
<option value="20">20</option>
<option value="21">21</option>
<option value="22">22</option>
<option value="23">23</option>
<option value="24">24</option>
<option value="25">25</option>
<option value="26">26</option>
<option value="27">27</option>
<option value="28">28</option>
<option value="29">29</option>
<option value="30">30</option>
<option value="31">31</option>
</select></div>
<div class="third-width"><label for="deadline-month">Month:</label>
<select id="deadline-month" required>
<option value="January">January</option>
<option value="February">February</option>
<option value="March">March</option>
<option value="April">April</option>
<option value="May">May</option>
<option value="June">June</option>
<option value="July">July</option>
<option value="August">August</option>
<option value="September">September</option>
<option value="October">October</option>
<option value="November">November</option>
<option value="December">December</option>
</select></div>
<div class="third-width"><label for="deadline-year">Year:</label>
<select id="deadline-year" required>
<option value="2025">2025</option>
<option value="2024">2024</option>
<option value="2023">2023</option>
<option value="2022">2022</option>
<option value="2021">2021</option>
<option value="2020">2020</option>
<option value="2019">2019</option>
<option value="2018">2018</option>
</select></div>
<div><input type="submit" id="submit" value="Add Task"></div>
<div></div>
</form>
</div>
<div id="toolbar">
<ul id="notifications">
</ul>
<button id="enable">
Enable notifications
</button>
</div>
</body>
</html>
@@ -0,0 +1,18 @@
{
"version": "0.1",
"name": "To-do list",
"description": "Store to-do items on your device, and be notified when the deadlines are up.",
"launch_path": "/to-do-notifications/index.html",
"icons": {
"128": "/to-do-notifications/img/icon-128.png"
},
"developer": {
"name": "Chris Mills",
"url": "http://chrisdavidmills.github.io/to-do-notifications/"
},
"permissions": {
"desktop-notification": {
"description": "Needed for creating system notifications."
}
}
}
@@ -0,0 +1,354 @@
window.onload = () => {
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
// Hold an instance of a db object for us to store the IndexedDB data in
let db;
// Create a reference to the notifications list in the bottom of the app; we will write database messages into this list by
// appending list items as children of this element
const note = document.getElementById('notifications');
// All other UI elements we need for the app
const taskList = document.getElementById('task-list');
const taskForm = document.getElementById('task-form');
const title = document.getElementById('title');
const hours = document.getElementById('deadline-hours');
const minutes = document.getElementById('deadline-minutes');
const day = document.getElementById('deadline-day');
const month = document.getElementById('deadline-month');
const year = document.getElementById('deadline-year');
const notificationBtn = document.getElementById('enable');
// Do an initial check to see what the notification permission state is
if (Notification.permission === 'denied' || Notification.permission === 'default') {
notificationBtn.style.display = 'block';
} else {
notificationBtn.style.display = 'none';
}
note.appendChild(createListItem('App initialised.'));
// Let us open our database
const DBOpenRequest = window.indexedDB.open('toDoList', 4);
// Register two event handlers to act on the database being opened successfully, or not
DBOpenRequest.onerror = (event) => {
note.appendChild(createListItem('Error loading database.'));
};
DBOpenRequest.onsuccess = (event) => {
note.appendChild(createListItem('Database initialised.'));
// Store the result of opening the database in the db variable. This is used a lot below
db = DBOpenRequest.result;
// Run the displayData() function to populate the task list with all the to-do list data already in the IndexedDB
displayData();
};
// This event handles the event whereby a new version of the database needs to be created
// Either one has not been created before, or a new version number has been submitted via the
// window.indexedDB.open line above
//it is only implemented in recent browsers
DBOpenRequest.onupgradeneeded = (event) => {
db = event.target.result;
db.onerror = (event) => {
note.appendChild(createListItem('Error loading database.'));
};
// Create an objectStore for this database
const objectStore = db.createObjectStore('toDoList', { keyPath: 'taskTitle' });
// Define what data items the objectStore will contain
objectStore.createIndex('hours', 'hours', { unique: false });
objectStore.createIndex('minutes', 'minutes', { unique: false });
objectStore.createIndex('day', 'day', { unique: false });
objectStore.createIndex('month', 'month', { unique: false });
objectStore.createIndex('year', 'year', { unique: false });
objectStore.createIndex('notified', 'notified', { unique: false });
note.appendChild(createListItem('Object store created.'));
};
function displayData() {
// First clear the content of the task list so that you don't get a huge long list of duplicate stuff each time
// the display is updated.
while (taskList.firstChild) {
taskList.removeChild(taskList.lastChild);
}
// Open our object store and then get a cursor list of all the different data items in the IDB to iterate through
const objectStore = db.transaction('toDoList').objectStore('toDoList');
objectStore.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
// Check if there are no (more) cursor items to iterate through
if (!cursor) {
// No more items to iterate through, we quit.
note.appendChild(createListItem('Entries all displayed.'));
return;
}
// Check which suffix the deadline day of the month needs
const { hours, minutes, day, month, year, notified, taskTitle } = cursor.value;
const ordDay = ordinal(day);
// Build the to-do list entry and put it into the list item.
const toDoText = `${taskTitle}${hours}:${minutes}, ${month} ${ordDay} ${year}.`;
const listItem = createListItem(toDoText);
if (notified === 'yes') {
listItem.style.textDecoration = 'line-through';
listItem.style.color = 'rgba(255, 0, 0, 0.5)';
}
// Put the item item inside the task list
taskList.appendChild(listItem);
// Create a delete button inside each list item,
const deleteButton = document.createElement('button');
listItem.appendChild(deleteButton);
deleteButton.textContent = 'X';
// Set a data attribute on our delete button to associate the task it relates to.
deleteButton.setAttribute('data-task', taskTitle);
// Associate action (deletion) when clicked
deleteButton.onclick = (event) => {
deleteItem(event);
};
// continue on to the next item in the cursor
cursor.continue();
};
};
// Add listener for clicking the submit button
taskForm.addEventListener('submit', addData, false);
function addData(e) {
// Prevent default, as we don't want the form to submit in the conventional way
e.preventDefault();
// Stop the form submitting if any values are left empty.
// This should never happen as there is the required attribute
if (title.value === '' || hours.value === null || minutes.value === null || day.value === '' || month.value === '' || year.value === null) {
note.appendChild(createListItem('Data not submitted — form incomplete.'));
return;
}
// Grab the values entered into the form fields and store them in an object ready for being inserted into the IndexedDB
const newItem = [
{ taskTitle: title.value, hours: hours.value, minutes: minutes.value, day: day.value, month: month.value, year: year.value, notified: 'no', signature: new TextEncoder().encode("signed by simon") },
];
// Open a read/write DB transaction, ready for adding the data
const transaction = db.transaction(['toDoList'], 'readwrite');
// Report on the success of the transaction completing, when everything is done
transaction.oncomplete = () => {
note.appendChild(createListItem('Transaction completed: database modification finished.'));
// Update the display of data to show the newly added item, by running displayData() again.
displayData();
};
// Handler for any unexpected error
transaction.onerror = () => {
note.appendChild(createListItem(`Transaction not opened due to error: ${transaction.error}`));
};
// Call an object store that's already been added to the database
const objectStore = transaction.objectStore('toDoList');
console.log(objectStore.indexNames);
console.log(objectStore.keyPath);
console.log(objectStore.name);
console.log(objectStore.transaction);
console.log(objectStore.autoIncrement);
// Make a request to add our newItem object to the object store
const objectStoreRequest = objectStore.add(newItem[0]);
objectStoreRequest.onsuccess = (event) => {
// Report the success of our request
// (to detect whether it has been succesfully
// added to the database, you'd look at transaction.oncomplete)
note.appendChild(createListItem('Request successful.'));
// Clear the form, ready for adding the next entry
title.value = '';
hours.value = null;
minutes.value = null;
day.value = 01;
month.value = 'January';
year.value = 2020;
};
};
function deleteItem(event) {
// Retrieve the name of the task we want to delete
const dataTask = event.target.getAttribute('data-task');
// Open a database transaction and delete the task, finding it by the name we retrieved above
const transaction = db.transaction(['toDoList'], 'readwrite');
transaction.objectStore('toDoList').delete(dataTask);
// Report that the data item has been deleted
transaction.oncomplete = () => {
// Delete the parent of the button, which is the list item, so it is no longer displayed
event.target.parentNode.parentNode.removeChild(event.target.parentNode);
note.appendChild(createListItem(`Task "${dataTask}" deleted.`));
};
};
// Check whether the deadline for each task is up or not, and responds appropriately
function checkDeadlines() {
// First of all check whether notifications are enabled or denied
if (Notification.permission === 'denied' || Notification.permission === 'default') {
notificationBtn.style.display = 'block';
} else {
notificationBtn.style.display = 'none';
}
// Grab the current time and date
const now = new Date();
// From the now variable, store the current minutes, hours, day of the month, month, year and seconds
const minuteCheck = now.getMinutes();
const hourCheck = now.getHours();
const dayCheck = now.getDate(); // Do not use getDay() that returns the day of the week, 1 to 7
const monthCheck = now.getMonth();
const yearCheck = now.getFullYear(); // Do not use getYear() that is deprecated.
// Open a new transaction
const objectStore = db.transaction(['toDoList'], 'readwrite').objectStore('toDoList');
// Open a cursor to iterate through all the data items in the IndexedDB
objectStore.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) return;
const { hours, minutes, day, month, year, notified, taskTitle } = cursor.value;
// convert the month names we have installed in the IDB into a month number that JavaScript will understand.
// The JavaScript date object creates month values as a number between 0 and 11.
const monthNumber = MONTHS.indexOf(month);
if (monthNumber === -1) throw new Error('Incorrect month entered in database.');
// Check if the current hours, minutes, day, month and year values match the stored values for each task.
// The parseInt() function transforms the value from a string to a number for comparison
// (taking care of leading zeros, and removing spaces and underscores from the string).
let matched = parseInt(hours) === hourCheck;
matched &&= parseInt(minutes) === minuteCheck;
matched &&= parseInt(day) === dayCheck;
matched &&= parseInt(monthNumber) === monthCheck;
matched &&= parseInt(year) === yearCheck;
if (matched && notified === 'no') {
// If the numbers all do match, run the createNotification() function to create a system notification
// but only if the permission is set
if (Notification.permission === 'granted') {
createNotification(taskTitle);
}
}
// Move on to the next cursor item
cursor.continue();
};
};
// Ask for permission when the 'Enable notifications' button is clicked
function askNotificationPermission() {
// Function to actually ask the permissions
function handlePermission(permission) {
// Whatever the user answers, we make sure Chrome stores the information
if (!Reflect.has(Notification, 'permission')) {
Notification.permission = permission;
}
// Set the button to shown or hidden, depending on what the user answers
if (Notification.permission === 'denied' || Notification.permission === 'default') {
notificationBtn.style.display = 'block';
} else {
notificationBtn.style.display = 'none';
}
};
// Check if the browser supports notifications
if (!Reflect.has(window, 'Notification')) {
console.log('This browser does not support notifications.');
} else {
if (checkNotificationPromise()) {
Notification.requestPermission().then(handlePermission);
} else {
Notification.requestPermission(handlePermission);
}
}
};
// Check whether browser supports the promise version of requestPermission()
// Safari only supports the old callback-based version
function checkNotificationPromise() {
try {
Notification.requestPermission().then();
} catch(e) {
return false;
}
return true;
};
// Wire up notification permission functionality to 'Enable notifications' button
notificationBtn.addEventListener('click', askNotificationPermission);
function createListItem(contents) {
const listItem = document.createElement('li');
listItem.textContent = contents;
return listItem;
};
// Create a notification with the given title
function createNotification(title) {
// Create and show the notification
const img = '/to-do-notifications/img/icon-128.png';
const text = `HEY! Your task "${title}" is now overdue.`;
const notification = new Notification('To do list', { body: text, icon: img });
// We need to update the value of notified to 'yes' in this particular data object, so the
// notification won't be set off on it again
// First open up a transaction
const objectStore = db.transaction(['toDoList'], 'readwrite').objectStore('toDoList');
// Get the to-do list object that has this title as its title
const objectStoreTitleRequest = objectStore.get(title);
objectStoreTitleRequest.onsuccess = () => {
// Grab the data object returned as the result
const data = objectStoreTitleRequest.result;
// Update the notified value in the object to 'yes'
data.notified = 'yes';
// Create another request that inserts the item back into the database
const updateTitleRequest = objectStore.put(data);
// When this new request succeeds, run the displayData() function again to update the display
updateTitleRequest.onsuccess = () => {
displayData();
};
};
};
// Using a setInterval to run the checkDeadlines() function every second
setInterval(checkDeadlines, 1000);
}
// Helper function returning the day of the month followed by an ordinal (st, nd, or rd)
function ordinal(day) {
const n = day.toString();
const last = n.slice(-1);
if (last === '1' && n !== '11') return `${n}st`;
if (last === '2' && n !== '12') return `${n}nd`;
if (last === '3' && n !== '13') return `${n}rd`;
return `${n}th`;
};
@@ -0,0 +1,248 @@
/* Basic set up + sizing for containers */
html,
body {
margin: 0;
}
html {
width: 100%;
height: 100%;
font-size: 10px;
font-family: Georgia, "Times New Roman", Times, serif;
background: #111;
}
body {
width: 50rem;
position: relative;
background: #d88;
margin: 0 auto;
border-left: 2px solid #d33;
border-right: 2px solid #d33;
}
h1,
h2 {
text-align: center;
background: #d88;
font-family: Arial, Helvetica, sans-serif;
}
h1 {
font-size: 6rem;
margin: 0;
background: #d66;
}
h2 {
font-size: 2.4rem;
}
/* Bottom toolbar styling */
#toolbar {
position: relative;
height: 6rem;
width: 100%;
background: #d66;
border-top: 2px solid #d33;
border-bottom: 2px solid #d33;
}
#enable,
input[type="submit"] {
line-height: 1.8;
font-size: 1.3rem;
border-radius: 5px;
border: 1px solid black;
color: black;
text-shadow: 1px 1px 1px black;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
inset 0px 5px 3px rgba(255, 255, 255, 0.2),
inset 0px -5px 3px rgba(0, 0, 0, 0.2);
}
#enable {
position: absolute;
bottom: 0.3rem;
right: 0.3rem;
}
#notifications {
margin: 0;
position: relative;
padding: 0.3rem;
background: #ddd;
position: absolute;
top: 0rem;
left: 0rem;
height: 5.4rem;
width: 50%;
overflow: auto;
line-height: 1.2;
}
#notifications li {
margin-left: 1.5rem;
}
/* New item form styling */
.form-box {
background: #d66;
width: 85%;
padding: 1rem;
margin: 2rem auto;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.7);
}
form div {
margin-bottom: 1rem;
}
form .full-width {
margin: 1rem auto 2rem;
width: 100%;
}
form .half-width {
width: 50%;
float: left;
}
form .third-width {
width: 33%;
float: left;
}
form div label {
width: 10rem;
float: left;
padding-right: 1rem;
font-size: 1.6rem;
line-height: 1.6;
}
form .full-width input {
width: 30rem;
}
form .half-width input {
width: 8.75rem;
}
form .third-width select {
width: 13.5rem;
}
form div input[type="submit"] {
clear: both;
width: 20rem;
display: block;
height: 3rem;
margin: 0 auto;
position: relative;
top: 0.5rem;
}
/* || tasks box */
.task-box {
width: 85%;
padding: 1rem;
margin: 2rem auto;
font-size: 1.8rem;
}
.task-box ul {
margin: 0;
padding: 0;
}
.task-box li {
list-style-type: none;
padding: 1rem;
border-bottom: 2px solid #d33;
}
.task-box li:last-child {
border-bottom: none;
}
.task-box li:last-child {
margin-bottom: 0rem;
}
.task-box button {
margin-left: 2rem;
font-size: 1.6rem;
border: 1px solid #eee;
border-radius: 5px;
box-shadow: inset 0 -2px 5px rgba(0, 0, 0, 0.5) 1px 1px 1px black;
}
/* setting cursor for interactive controls */
button,
input[type="submit"],
select {
cursor: pointer;
}
/* media query for small screens */
@media (max-width: 32rem) {
body {
width: 100%;
border-left: none;
border-right: none;
}
form div {
clear: both;
}
form .full-width {
margin: 1rem auto;
}
form .half-width {
width: 100%;
float: none;
}
form .third-width {
width: 100%;
float: none;
}
form div label {
width: 36%;
padding-left: 1rem;
}
form input,
form select,
form label {
line-height: 2.5rem;
font-size: 2rem;
}
form .full-width input {
width: 50%;
}
form .half-width input {
width: 50%;
}
form .third-width select {
width: 50%;
}
#enable {
right: 1rem;
}
}
+23 -10
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.46.0-SNAPSHOT</version>
<version>1.52.0</version>
<packaging>pom</packaging>
<name>Playwright Parent Project</name>
<description>Java library to automate Chromium, Firefox and WebKit with a single API.
@@ -44,10 +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.11.0</gson.version>
<junit.version>5.11.1</junit.version>
<gson.version>2.12.1</gson.version>
<junit.version>5.12.1</junit.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<websocket.version>1.5.7</websocket.version>
<websocket.version>1.6.0</websocket.version>
<slf4j.version>2.0.17</slf4j.version>
<opentest4j.version>1.3.0</opentest4j.version>
</properties>
@@ -91,6 +92,17 @@
<version>${websocket.version}</version>
<scope>test</scope>
</dependency>
<!--
The following slf4j-simple dependency resolves the warning:
'SLF4J(W): No SLF4J providers were found.'
This warning is produced by the org.java-websocket library.
-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
@@ -106,7 +118,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>3.4.0</version>
<version>3.4.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -116,17 +128,17 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<version>3.14.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>3.1.3</version>
<version>3.1.4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.1.3</version>
<version>3.1.4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -136,7 +148,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.10.1</version>
<version>3.11.2</version>
<configuration>
<additionalOptions>--allow-script-in-comments</additionalOptions>
<failOnError>false</failOnError>
@@ -147,7 +159,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.0</version>
<version>3.5.3</version>
<configuration>
<properties>
<configurationParameters>
@@ -159,6 +171,7 @@
</configurationParameters>
</properties>
<failIfNoTests>false</failIfNoTests>
<rerunFailingTestsCount>${env.PW_MAX_RETRIES}</rerunFailingTestsCount>
<!-- Activate the use of TCP to transmit events to the plugin and avoid
[WARNING] Corrupted STDOUT by directly writing to native stream in forked JVM -->
<forkNode implementation="org.apache.maven.plugin.surefire.extensions.SurefireForkNodeFactory"/>
+1 -1
View File
@@ -1 +1 @@
1.48.1
1.52.0
+11 -1
View File
@@ -24,4 +24,14 @@ else
fi;
./generate_api.sh
./update_readme.sh
./update_readme.sh
node -e "$(cat <<EOF
let [majorVersion, minorVersion] = process.argv[1].split('-')[0].split('.').map(part => parseInt(part, 10));
minorVersion[1]--;
const previousMajorVersion = majorVersion + '.' + minorVersion + '.0';
fs.writeFileSync('../examples/pom.xml', fs.readFileSync('../examples/pom.xml', 'utf8')
.replace(/<playwright\.version>.*<\/playwright\.version>/, '<playwright\.version>' + previousMajorVersion + '</playwright\.version>')
);
EOF
)" $NEW_VERSION
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>api-generator</artifactId>
<version>1.46.0-SNAPSHOT</version>
<version>1.52.0</version>
<name>Playwright - API Generator</name>
<description>
This is an internal module used to generate Java API from the upstream Playwright
@@ -986,7 +986,7 @@ class Interface extends TypeDefinition {
if (methods.stream().anyMatch(m -> "create".equals(m.jsonName))) {
output.add("import com.microsoft.playwright.impl." + jsonName + "Impl;");
}
if (asList("Page", "Request", "Response", "APIRequestContext", "APIRequest", "APIResponse", "FileChooser", "Frame", "FrameLocator", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard").contains(jsonName)) {
if (asList("Page", "Request", "Response", "APIRequestContext", "APIRequest", "APIResponse", "FileChooser", "Frame", "FrameLocator", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard", "Tracing").contains(jsonName)) {
output.add("import com.microsoft.playwright.options.*;");
}
if ("Download".equals(jsonName)) {
@@ -998,7 +998,7 @@ class Interface extends TypeDefinition {
if ("Clock".equals(jsonName)) {
output.add("import java.util.Date;");
}
if (asList("Page", "Frame", "ElementHandle", "Locator", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright").contains(jsonName)) {
if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright").contains(jsonName)) {
output.add("import java.util.*;");
}
if (asList("WebSocketRoute").contains(jsonName)) {
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-cli-fatjar</artifactId>
<version>1.46.0-SNAPSHOT</version>
<version>1.52.0</version>
<name>Test Playwright Command Line FatJar</name>
<properties>
<compiler.version>1.8</compiler.version>
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-cli-version</artifactId>
<version>1.46.0-SNAPSHOT</version>
<version>1.52.0</version>
<name>Test Playwright Command Line Version</name>
<properties>
<compiler.version>1.8</compiler.version>
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-local-installation</artifactId>
<version>1.46.0-SNAPSHOT</version>
<version>1.52.0</version>
<name>Test local installation</name>
<description>Runs Playwright test suite (copied from playwright module) against locally cached Playwright</description>
<properties>
+1 -1
View File
@@ -9,7 +9,7 @@
</parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-spring-boot-starter</artifactId>
<version>1.46.0-SNAPSHOT</version>
<version>1.52.0</version>
<name>Test Playwright With Spring Boot</name>
<properties>
<spring.version>2.4.3</spring.version>

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