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

Compare commits

...

43 Commits

Author SHA1 Message Date
Dmitry Gozman 50508ed2fa chore: roll to driver 1.17.1, mark v1.17.2 (#729) 2021-12-02 11:15:07 -08:00
Max Schmitt e863a78755 feat(roll): roll Playwright 1.17.0-beta-1638222602000 (#723) 2021-11-30 16:42:27 +01:00
Andrey Lushnikov b1c5ec2083 chore: roll driver to 1.17.0-rc1 (#710) 2021-11-18 08:53:44 -08:00
Andrey Lushnikov eca3491203 cherry-pick(#709): chore: proper driver URL detection
Since Nov 16, 2021, we have the following conventions:

- Drivers published from tip-of-tree have an `-alpha` version and are
  stored at `/next` subfolder on Azure Storage.
- Drivers auto-published for each commit of the release branch have a `-beta` version and are
  stored at `/next` subfolder on Azure Storage.
- Drivers published due to a release might have `-rc` as part of the
  version, and are stored in root subfolder on Azure Storage.

We no longer have driver versions that include "next" as part of the
version. I kept it for backwards compatibility.
2021-11-17 18:33:32 -08:00
Andrey Lushnikov 558df1fc60 chore: mark v1.17.0 (#701) 2021-11-15 14:52:44 -08:00
Andrey Lushnikov ba5bd2c9ac chore: roll to beta driver version (#700) 2021-11-15 14:43:57 -08:00
Max Schmitt 4be749f045 devops: use main branch instead of master (#699) 2021-11-13 14:39:27 +01:00
Yury Semikhatsky f515d9f318 feat: frame locators, roll driver (#695) 2021-11-10 09:09:04 -08:00
Yury Semikhatsky 2f706012a7 feat: roll driver (#694) 2021-11-05 18:29:38 -07:00
Yury Semikhatsky 853b5062e7 tests: unflake wheel test, add more logs (#688) 2021-11-02 13:33:43 -07:00
Yury Semikhatsky 2d0d941e18 feat: support wait until commit, roll driver (#687) 2021-11-02 13:07:56 -07:00
Yury Semikhatsky 49a54d7ee4 fix(tests): do not use redirectTestOutputToFile (#685) 2021-11-01 13:08:19 -07:00
Yury Semikhatsky 44a85c1dc3 chore: roll driver (#681) 2021-11-01 11:31:09 -07:00
Yury Semikhatsky 5d7ee12f4a fix(route): do not wait for driver ack (#680) 2021-10-29 13:16:42 -07:00
Yury Semikhatsky a0416459e1 feat(assertions): support some regex flags, improve error messages (#676) 2021-10-28 18:37:15 -07:00
Yury Semikhatsky ddffc45e84 tests: use shorter timeout for in page evals (#675) 2021-10-28 16:51:58 -07:00
Yury Semikhatsky e85258908e fix(assertions): include property name into message (#674) 2021-10-28 15:43:57 -07:00
Max Schmitt c61d1da352 chore: drop support for Windows 32 bit (#673) 2021-10-28 08:26:50 -07:00
Yury Semikhatsky a60b0a9b78 fix(assertions): error message when not is used (#671) 2021-10-27 16:46:13 -07:00
Yury Semikhatsky 38bde7ad25 fix(assertions): set default timeout to 5s (#670) 2021-10-27 16:18:42 -07:00
Yury Semikhatsky a8e41b1ede fix(docker): add test-jar to the project dependencies (#668) 2021-10-27 15:03:57 -07:00
Yury Semikhatsky 45b141811b chore: extract common routines to base class (#666) 2021-10-26 17:53:09 -07:00
Yury Semikhatsky bd6ed7bc88 feat: locator assertions part 2 (#663) 2021-10-26 16:41:47 -07:00
Yury Semikhatsky d0e7ab1e58 feat: locator assertions (part 1) (#662) 2021-10-25 18:31:07 -07:00
Yury Semikhatsky 7ff7ee188b chore: disable interception when route.times==0 (#660) 2021-10-22 12:42:51 -07:00
Yury Semikhatsky d291a64e11 chore: driver-side waitForTimeout (#651) 2021-10-22 08:58:00 -07:00
Yury Semikhatsky 9f2b482084 chore: move common parts to parent pom, update module descriptions (#658) 2021-10-21 23:40:17 -07:00
Yury Semikhatsky b7319c629d feat: add web-first assertions for page (#657) 2021-10-21 18:22:00 -07:00
Yury Semikhatsky 38c5dc28a4 chore: bump development version (#656) 2021-10-21 17:54:52 -07:00
Andrey Lushnikov 1b9f7732fe chore: roll driver to 1.16 release (#653) 2021-10-21 00:00:39 -07:00
Yury Semikhatsky 1a4dec86cd chore: remove route handlers with times=0 (#652) 2021-10-20 18:47:52 -07:00
Yury Semikhatsky c802c87e52 chore: roll driver to 1.16.0-next-1634661437000 (#650) 2021-10-19 11:37:44 -07:00
Yury Semikhatsky ab81b542b8 chore: roll driver (#643) 2021-10-12 16:03:11 -07:00
Yury Semikhatsky 07e7cb85c7 chore: roll driver (#642) 2021-10-12 11:25:51 -07:00
Yury Semikhatsky 7af5405d38 feat: roll driver, implement locator.waitFor (#639) 2021-10-05 19:00:19 -07:00
Yury Semikhatsky 50ba2dacbb chore: bump example dependency version (#632) 2021-09-30 08:40:18 -07:00
Yury Semikhatsky 61f5e4dfdd chore: roll driver to 1.16.0-next-1632766475000 (#629) 2021-09-27 13:21:10 -07:00
Andrey Lushnikov 84344c9ff9 docs: put in instructions on how to update package version (#626) 2021-09-23 08:11:51 -07:00
Max Schmitt c0fd575fac chore(set_maven_version): do not generate version backups (#620) 2021-09-21 09:11:18 -07:00
Max Schmitt 81724c7c94 chore: mark 1.16.0-SNAPSHOT (#621) 2021-09-21 15:11:27 +02:00
Yury Semikhatsky 3eb88931bf test: stop using chunked encoding for responses (#617) 2021-09-20 17:13:39 -07:00
Max Schmitt 948dd1515a test: fix sizes test by using no chunked requests (#616) 2021-09-20 13:14:45 -07:00
Yury Semikhatsky dd57d5248d chore: switch to connect implementation in driver (#615) 2021-09-20 13:10:50 -07:00
82 changed files with 4281 additions and 535 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ on:
workflow_dispatch:
push:
branches:
- master
- main
jobs:
build:
timeout-minutes: 30
+1 -1
View File
@@ -3,7 +3,7 @@ name: "devrelease:docker"
on:
push:
branches:
- master
- main
jobs:
publish-canary-docker:
name: "publish to DockerHub"
+4 -4
View File
@@ -2,11 +2,11 @@ name: Build & Test
on:
push:
branches:
- master
- main
- release-*
pull_request:
branches:
- master
- main
- release-*
jobs:
dev:
@@ -36,7 +36,7 @@ jobs:
- name: Build with Maven
run: mvn -B package -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress
run: mvn test --no-transfer-progress --fail-at-end
env:
BROWSER: ${{ matrix.browser }}
- name: Test Spring Boot Starter
@@ -85,7 +85,7 @@ jobs:
- name: Build with Maven
run: mvn -B package -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test --no-transfer-progress
run: mvn test --no-transfer-progress --fail-at-end
env:
BROWSER: chromium
BROWSER_CHANNEL: ${{ matrix.browser-channel }}
+2 -4
View File
@@ -2,17 +2,15 @@ name: Test CLI
on:
push:
branches:
- master
- main
- release-*
pull_request:
branches:
- master
- main
- release-*
jobs:
verify:
timeout-minutes: 30
strategy:
fail-fast: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
+2 -2
View File
@@ -5,7 +5,7 @@ on:
- '.github/workflows/test_docker.yml'
- 'Dockerfile*'
branches:
- master
- main
- release-*
pull_request:
paths:
@@ -14,7 +14,7 @@ on:
- scripts/CLI_VERSION
- '**/pom.xml'
branches:
- master
- main
- release-*
jobs:
test:
+2 -4
View File
@@ -2,14 +2,14 @@ name: Verify API
on:
push:
branches:
- master
- main
- release-*
paths:
- 'scripts/*'
- 'api-generator/*'
pull_request:
branches:
- master
- main
- release-*
paths:
- 'scripts/**'
@@ -17,8 +17,6 @@ on:
jobs:
verify:
timeout-minutes: 30
strategy:
fail-fast: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
+5 -5
View File
@@ -11,9 +11,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->96.0.4641.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->15.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->92.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->98.0.4695.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->15.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->94.0.1<!-- 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/next/intro/#system-requirements) for details.
@@ -43,7 +43,7 @@ To run Playwright simply add following dependency to your Maven project:
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.14.1</version>
<version>1.16.0</version>
</dependency>
```
@@ -179,7 +179,7 @@ You can also browse [javadoc online](https://www.javadoc.io/doc/com.microsoft.pl
## Contributing
Follow [the instructions](https://github.com/microsoft/playwright-java/blob/master/CONTRIBUTING.md#getting-code) to build the project from source and install the driver.
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?
+6
View File
@@ -5,3 +5,9 @@
* set new driver version in `scripts/CLI_VERSION`
* regenerate API: `./scripts/download_driver_for_all_platforms.sh -f && ./scripts/generate_api.sh && ./scripts/update_readme.sh`
* commit & send PR with the roll
# Updating Version
```bash
./scripts/set_maven_version.sh 1.15.0
```
+65
View File
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.17.2</version>
</parent>
<artifactId>assertions</artifactId>
<name>Playwright - Assertions</name>
<description>
This module provides Playwright assertions that will wait until the expected condition is met.
</description>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<subpackages>com.microsoft.playwright.assertions</subpackages>
<excludePackageNames>com.microsoft.playwright.impl</excludePackageNames>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.opentest4j</groupId>
<artifactId>opentest4j</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,160 @@
/*
* 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.assertions;
import java.util.*;
import java.util.regex.Pattern;
import com.microsoft.playwright.Page;
/**
* The {@code PageAssertions} class provides assertion methods that can be used to make assertions about the {@code Page} state in the
* tests. A new instance of {@code LocatorAssertions} is created by calling {@link PlaywrightAssertions#assertThat
* PlaywrightAssertions.assertThat()}:
* <pre>{@code
* ...
* import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
*
* public class TestPage {
* ...
* @Test
* void navigatesToLoginPage() {
* ...
* page.click("#login");
* assertThat(page).hasURL(Pattern.compile(".*\/login"));
* }
* }
* }</pre>
*/
public interface PageAssertions {
class HasTitleOptions {
/**
* Time to retry the assertion for.
*/
public Double timeout;
/**
* Time to retry the assertion for.
*/
public HasTitleOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
class HasURLOptions {
/**
* Time to retry the assertion for.
*/
public Double timeout;
/**
* Time to retry the assertion for.
*/
public HasURLOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
/**
* Ensures the page has the given title.
* <pre>{@code
* assertThat(page).hasTitle("Playwright");
* }</pre>
*
* @param titleOrRegExp Expected title or RegExp.
*/
default void hasTitle(String titleOrRegExp) {
hasTitle(titleOrRegExp, null);
}
/**
* Ensures the page has the given title.
* <pre>{@code
* assertThat(page).hasTitle("Playwright");
* }</pre>
*
* @param titleOrRegExp Expected title or RegExp.
*/
void hasTitle(String titleOrRegExp, HasTitleOptions options);
/**
* Ensures the page has the given title.
* <pre>{@code
* assertThat(page).hasTitle("Playwright");
* }</pre>
*
* @param titleOrRegExp Expected title or RegExp.
*/
default void hasTitle(Pattern titleOrRegExp) {
hasTitle(titleOrRegExp, null);
}
/**
* Ensures the page has the given title.
* <pre>{@code
* assertThat(page).hasTitle("Playwright");
* }</pre>
*
* @param titleOrRegExp Expected title or RegExp.
*/
void hasTitle(Pattern titleOrRegExp, HasTitleOptions options);
/**
* Ensures the page is navigated to the given URL.
* <pre>{@code
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
*/
default void hasURL(String urlOrRegExp) {
hasURL(urlOrRegExp, null);
}
/**
* Ensures the page is navigated to the given URL.
* <pre>{@code
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
*/
void hasURL(String urlOrRegExp, HasURLOptions options);
/**
* Ensures the page is navigated to the given URL.
* <pre>{@code
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
*/
default void hasURL(Pattern urlOrRegExp) {
hasURL(urlOrRegExp, null);
}
/**
* Ensures the page is navigated to the given URL.
* <pre>{@code
* assertThat(page).hasURL(".com");
* }</pre>
*
* @param urlOrRegExp Expected substring or RegExp.
*/
void hasURL(Pattern urlOrRegExp, HasURLOptions options);
/**
* Makes the assertion check for the opposite condition. For example, this code tests that the page URL doesn't contain
* {@code "error"}:
* <pre>{@code
* assertThat(page).not().hasURL("error");
* }</pre>
*/
PageAssertions not();
}
@@ -0,0 +1,79 @@
/*
* 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.assertions;
import java.util.*;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.impl.LocatorAssertionsImpl;
import com.microsoft.playwright.impl.PageAssertionsImpl;
/**
* The {@code PlaywrightAssertions} class provides convenience methods for creating assertions that will wait until the expected
* condition is met.
*
* <p> Consider the following example:
* <pre>{@code
* ...
* import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
*
* public class TestExample {
* ...
* @Test
* void statusBecomesSubmitted() {
* ...
* page.click("#submit-button");
* assertThat(page.locator(".status")).hasText("Submitted");
* }
* }
* }</pre>
*
* <p> Playwright will be re-testing the node with the selector {@code .status} until fetched Node has the {@code "Submitted"} text. It
* will be re-fetching the node and checking it over and over, until the condition is met or until the timeout is reached.
* You can pass this timeout as an option.
*
* <p> By default, the timeout for assertions is set to 5 seconds.
*
* <p> To use Playwright assertions add the following dependency into the {@code pom.xml} of your Maven project:
*/
public interface PlaywrightAssertions {
/**
* Creates a {@code LocatorAssertions} object for the given {@code Locator}.
* <pre>{@code
* PlaywrightAssertions.assertThat(locator).isVisible();
* }</pre>
*
* @param locator {@code Locator} object to use for assertions.
*/
static LocatorAssertions assertThat(Locator locator) {
return new LocatorAssertionsImpl(locator);
}
/**
* Creates a {@code PageAssertions} object for the given {@code Page}.
* <pre>{@code
* PlaywrightAssertions.assertThat(page).hasTitle("News");
* }</pre>
*
* @param page {@code Page} object to use for assertions.
*/
static PageAssertions assertThat(Page page) {
return new PageAssertionsImpl(page);
}
}
@@ -0,0 +1,105 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import org.opentest4j.AssertionFailedError;
import org.opentest4j.ValueWrapper;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static java.util.Arrays.asList;
class AssertionsBase {
final LocatorImpl actualLocator;
final boolean isNot;
AssertionsBase(LocatorImpl actual, boolean isNot) {
this.actualLocator = actual;
this.isNot = isNot;
}
void expectImpl(String expression, ExpectedTextValue textValue, Object expected, String message, FrameExpectOptions options) {
expectImpl(expression, asList(textValue), expected, message, options);
}
void expectImpl(String expression, List<ExpectedTextValue> expectedText, Object expected, String message, FrameExpectOptions options) {
if (options == null) {
options = new FrameExpectOptions();
}
options.expectedText = expectedText;
options.isNot = isNot;
expectImpl(expression, options, expected, message);
}
void expectImpl(String expression, FrameExpectOptions expectOptions, Object expected, String message) {
if (expectOptions.timeout == null) {
expectOptions.timeout = 5_000.0;
}
if (expectOptions.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);
if (!log.isEmpty()) {
log = "\nCall log:\n" + log;
}
if (expected == null) {
throw new AssertionFailedError(message + log);
}
throw new AssertionFailedError(message + log, formatValue(expected), formatValue(actual));
}
}
private static ValueWrapper formatValue(Object value) {
if (value == null || !value.getClass().isArray()) {
return ValueWrapper.create(value);
}
Collection<String> values = asList((Object[]) value).stream().map(e -> e.toString()).collect(Collectors.toList());
String stringRepresentation = "[" + String.join(", ", values) + "]";
return ValueWrapper.create(value, stringRepresentation);
}
static ExpectedTextValue expectedRegex(Pattern pattern) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.regexSource = pattern.pattern();
if (pattern.flags() != 0) {
expected.regexFlags = "";
if ((pattern.flags() & Pattern.CASE_INSENSITIVE) != 0) {
// Case-insensitive search.
expected.regexFlags += "i";
}
if ((pattern.flags() & Pattern.DOTALL) != 0) {
// Allows . to match newline characters.
expected.regexFlags += "s";
}
if ((pattern.flags() & Pattern.MULTILINE) != 0) {
// Multi-line search.
expected.regexFlags += "m";
}
if ((pattern.flags() & ~(Pattern.MULTILINE | Pattern.CASE_INSENSITIVE | Pattern.DOTALL)) != 0) {
throw new PlaywrightException("Unexpected RegEx flag, only CASE_INSENSITIVE, DOTALL and MULTILINE are supported.");
}
}
return expected;
}
}
@@ -0,0 +1,302 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.assertions.LocatorAssertions;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Serialization.serializeArgument;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
public class LocatorAssertionsImpl extends AssertionsBase implements LocatorAssertions {
public LocatorAssertionsImpl(Locator locator) {
this(locator, false);
}
private LocatorAssertionsImpl(Locator locator, boolean isNot) {
super((LocatorImpl) locator, isNot);
}
@Override
public void containsText(String text, ContainsTextOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, text, "Locator expected to contain text", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void containsText(Pattern pattern, ContainsTextOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, pattern, "Locator expected to contain regex", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void containsText(String[] strings, ContainsTextOptions options) {
List<ExpectedTextValue> list = new ArrayList<>();
for (String text : strings) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
list.add(expected);
}
expectImpl("to.contain.text.array", list, strings, "Locator expected to contain text", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void containsText(Pattern[] patterns, ContainsTextOptions options) {
List<ExpectedTextValue> list = new ArrayList<>();
for (Pattern pattern : patterns) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
list.add(expected);
}
expectImpl("to.contain.text.array", list, patterns, "Locator expected to contain text", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasAttribute(String name, String text, HasAttributeOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
hasAttribute(name, expected, text, options);
}
@Override
public void hasAttribute(String name, Pattern pattern, HasAttributeOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
hasAttribute(name, expected, pattern, options);
}
private void hasAttribute(String name, ExpectedTextValue expectedText, Object expectedValue, HasAttributeOptions options) {
if (options == null) {
options = new HasAttributeOptions();
}
FrameExpectOptions commonOptions = convertViaJson(options, FrameExpectOptions.class);
commonOptions.expressionArg = name;
String message = "Locator expected to have attribute '" + name + "'";
if (expectedValue instanceof Pattern) {
message += " matching regex";
}
expectImpl("to.have.attribute", expectedText, expectedValue, message, commonOptions);
}
@Override
public void hasClass(String text, HasClassOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expectImpl("to.have.class", expected, text, "Locator expected to have class", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasClass(Pattern pattern, HasClassOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.class", expected, pattern, "Locator expected to have class matching regex", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasClass(String[] strings, HasClassOptions options) {
List<ExpectedTextValue> list = new ArrayList<>();
for (String text : strings) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
list.add(expected);
}
expectImpl("to.have.class.array", list, strings, "Locator expected to have class", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasClass(Pattern[] patterns, HasClassOptions options) {
List<ExpectedTextValue> list = new ArrayList<>();
for (Pattern pattern : patterns) {
ExpectedTextValue expected = expectedRegex(pattern);
list.add(expected);
}
expectImpl("to.have.class.array", list, patterns, "Locator expected to have class matching regex", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasCount(int count, HasCountOptions options) {
if (options == null) {
options = new HasCountOptions();
}
FrameExpectOptions commonOptions = convertViaJson(options, FrameExpectOptions.class);
commonOptions.expectedNumber = count;
List<ExpectedTextValue> expectedText = null;
expectImpl("to.have.count", expectedText, count, "Locator expected to have count", commonOptions);
}
@Override
public void hasCSS(String name, String value, HasCSSOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = value;
hasCSS(name, expected, value, options);
}
@Override
public void hasCSS(String name, Pattern pattern, HasCSSOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
hasCSS(name, expected, pattern, options);
}
private void hasCSS(String name, ExpectedTextValue expectedText, Object expectedValue, HasCSSOptions options) {
if (options == null) {
options = new HasCSSOptions();
}
FrameExpectOptions commonOptions = convertViaJson(options, FrameExpectOptions.class);
commonOptions.expressionArg = name;
String message = "Locator expected to have CSS property '" + name + "'";
if (expectedValue instanceof Pattern) {
message += " matching regex";
}
expectImpl("to.have.css", expectedText, expectedValue, message, commonOptions);
}
@Override
public void hasId(String id, HasIdOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = id;
expectImpl("to.have.id", expected, id, "Locator expected to have ID", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasJSProperty(String name, Object value, HasJSPropertyOptions options) {
if (options == null) {
options = new HasJSPropertyOptions();
}
FrameExpectOptions commonOptions = convertViaJson(options, FrameExpectOptions.class);
commonOptions.expressionArg = name;
commonOptions.expectedValue = serializeArgument(value);
List<ExpectedTextValue> list = null;
expectImpl("to.have.property", list, value, "Locator expected to have JavaScript property '" + name + "'", commonOptions);
}
@Override
public void hasText(String text, HasTextOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expected.matchSubstring = false;
expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, text, "Locator expected to have text", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasText(Pattern pattern, HasTextOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
// Just match substring, same as containsText.
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
expectImpl("to.have.text", expected, pattern, "Locator expected to have text matching regex", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasText(String[] strings, HasTextOptions options) {
List<ExpectedTextValue> list = new ArrayList<>();
for (String text : strings) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = text;
expected.matchSubstring = false;
expected.normalizeWhiteSpace = true;
list.add(expected);
}
expectImpl("to.have.text.array", list, strings, "Locator expected to have text", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasText(Pattern[] patterns, HasTextOptions options) {
List<ExpectedTextValue> list = new ArrayList<>();
for (Pattern pattern : patterns) {
ExpectedTextValue expected = expectedRegex(pattern);
expected.matchSubstring = true;
expected.normalizeWhiteSpace = true;
list.add(expected);
}
expectImpl("to.have.text.array", list, patterns, "Locator expected to have text matching regex", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasValue(String value, HasValueOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = value;
expectImpl("to.have.value", expected, value, "Locator expected to have value", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasValue(Pattern pattern, HasValueOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.value", expected, pattern, "Locator expected to have value matching regex", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void isChecked(IsCheckedOptions options) {
expectTrue("to.be.checked", "Locator expected to be checked", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void isDisabled(IsDisabledOptions options) {
expectTrue("to.be.disabled", "Locator expected to be disabled", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void isEditable(IsEditableOptions options) {
expectTrue("to.be.editable", "Locator expected to be editable", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void isEmpty(IsEmptyOptions options) {
expectTrue("to.be.empty", "Locator expected to be empty", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void isEnabled(IsEnabledOptions options) {
expectTrue("to.be.enabled", "Locator expected to be enabled", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void isFocused(IsFocusedOptions options) {
expectTrue("to.be.focused", "Locator expected to be focused", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void isHidden(IsHiddenOptions options) {
expectTrue("to.be.hidden", "Locator expected to be hidden", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void isVisible(IsVisibleOptions options) {
expectTrue("to.be.visible", "Locator expected to be visible", convertViaJson(options, FrameExpectOptions.class));
}
private void expectTrue(String expression, String message, FrameExpectOptions options) {
List<ExpectedTextValue> expectedText = null;
expectImpl(expression, expectedText, null, message, options);
}
@Override
public LocatorAssertions not() {
return new LocatorAssertionsImpl(actualLocator, !isNot);
}
}
@@ -0,0 +1,73 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.assertions.PageAssertions;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.UrlMatcher.resolveUrl;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
public class PageAssertionsImpl extends AssertionsBase implements PageAssertions {
private final PageImpl actualPage;
public PageAssertionsImpl(Page page) {
this(page, false);
}
private PageAssertionsImpl(Page page, boolean isNot) {
super((LocatorImpl) page.locator(":root"), isNot);
this.actualPage = (PageImpl) page;
}
@Override
public void hasTitle(String title, HasTitleOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
expected.string = title;
expectImpl("to.have.title", expected, title, "Page title expected to be", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasTitle(Pattern pattern, HasTitleOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.title", expected, pattern, "Page title expected to match regex", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasURL(String url, HasURLOptions options) {
ExpectedTextValue expected = new ExpectedTextValue();
if (actualPage.context().baseUrl != null) {
url = resolveUrl(actualPage.context().baseUrl, url);
}
expected.string = url;
expectImpl("to.have.url", expected, url, "Page URL expected to be", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public void hasURL(Pattern pattern, HasURLOptions options) {
ExpectedTextValue expected = expectedRegex(pattern);
expectImpl("to.have.url", expected, pattern, "Page URL expected to match regex", convertViaJson(options, FrameExpectOptions.class));
}
@Override
public PageAssertions not() {
return new PageAssertionsImpl(actualPage, !isNot);
}
}
@@ -0,0 +1,826 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.assertions.LocatorAssertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;
import java.util.regex.Pattern;
import static com.microsoft.playwright.Utils.mapOf;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
public class TestLocatorAssertions extends TestBase {
@Test
void containsTextWRegexPass() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).containsText(Pattern.compile("ex"));
// Should not normalize whitespace.
assertThat(locator).containsText(Pattern.compile("ext cont"));
}
@Test
void containsTextWRegexCaseInsensitivePass() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).containsText(Pattern.compile("text", Pattern.CASE_INSENSITIVE));
}
@Test
void containsTextWRegexMultilinePass() {
page.setContent("<div id=node>Text \nContent</div>");
Locator locator = page.locator("#node");
assertThat(locator).containsText(Pattern.compile("^Content", Pattern.MULTILINE));
}
@Test
void containsTextWRegexDotAllPass() {
page.setContent("<div id=node>foo\nbar</div>");
Locator locator = page.locator("#node");
assertThat(locator).containsText(Pattern.compile("foo.bar", Pattern.DOTALL));
}
@Test
void containsTextWRegexFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).containsText(Pattern.compile("ex2"), new LocatorAssertions.ContainsTextOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("ex2", e.getExpected().getStringRepresentation());
assertEquals("Text content", e.getActual().getValue());
assertTrue(e.getMessage().contains("Locator expected to contain regex"), e.getMessage());
}
}
@Test
void hasTextWRegexPass() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).hasText(Pattern.compile("Te.t"));
// Should not normalize whitespace.
assertThat(locator).hasText(Pattern.compile("Text.+content"));
}
@Test
void hasTextWRegexFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasText(Pattern.compile("Text 2"), new LocatorAssertions.HasTextOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("Text 2", e.getExpected().getStringRepresentation());
assertEquals("Text content", e.getActual().getValue());
assertTrue(e.getMessage().contains("Locator expected to have text matching regex"), e.getMessage());
}
}
@Test
void hasTextWTextPass() {
page.setContent("<div id=node><span></span>Text \ncontent&nbsp; </div>");
Locator locator = page.locator("#node");
// Should normalize whitespace.
assertThat(locator).hasText("Text content");
}
@Test
void hasTextWTextFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
// Should normalize whitespace.
try {
assertThat(locator).hasText("Text", new LocatorAssertions.HasTextOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("Text", e.getExpected().getStringRepresentation());
assertEquals("Text content", e.getActual().getValue());
assertTrue(e.getMessage().contains("Locator expected to have text"), e.getMessage());
}
}
@Test
void hasTextWTextArrayPass() {
page.setContent("<div>Text \n1</div><div>Text 2a</div>");
Locator locator = page.locator("div");
// Should normalize whitespace.
assertThat(locator).hasText(new String[] {"Text 1", "Text 2a"});
}
@Test
void hasTextWTextArrayPassEmpty() {
page.setContent("<div></div>");
Locator locator = page.locator("p");
// Should normalize whitespace.
assertThat(locator).hasText(new String[] {});
}
@Test
void hasTextWTextArrayPassNotEmpty() {
page.setContent("<div><p>Test</p></div>");
Locator locator = page.locator("div");
// Should normalize whitespace.
assertThat(locator).not().hasText(new String[] {});
}
@Test
void hasTextWTextArrayPassOnEmpty() {
page.setContent("<div></div>");
Locator locator = page.locator("p");
// Should normalize whitespace.
assertThat(locator).not().hasText(new String[] {"Test"});
}
@Test
void hasTextWTextArrayFailOnNotEmpty() {
page.setContent("<div></div>");
Locator locator = page.locator("p");
// Should normalize whitespace.
try {
assertThat(locator).not().hasText(new String[] {}, new LocatorAssertions.HasTextOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("[]", e.getExpected().getStringRepresentation());
assertEquals("null", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected not to have text"), e.getMessage());
}
}
@Test
void hasTextWTextArrayPassLazyPass() {
page.setContent("<div id=div></div>");
Locator locator = page.locator("p");
page.evaluate("setTimeout(() => {\n" +
" div.innerHTML = \"<p>Text 1</p><p>Text 2</p>\";\n" +
"}, 100);");
// Should normalize whitespace.
assertThat(locator).hasText(new String[] {"Text 1", "Text 2"}, new LocatorAssertions.HasTextOptions().setTimeout(1000));
}
@Test
void hasTextWTextArrayFail() {
page.setContent("<div>Text 1</div><div>Text 3</div>");
Locator locator = page.locator("div");
page.evaluate("setTimeout(() => {\n" +
" div.innerHTML = \"<p>Text 1</p><p>Text 2</p>\";\n" +
"}, 100);");
try {
// Should normalize whitespace.
assertThat(locator).hasText(new String[] {"Text 1", "Text 3", "Extra"}, new LocatorAssertions.HasTextOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("[Text 1, Text 3, Extra]", e.getExpected().getStringRepresentation());
assertEquals("[Text 1, Text 3]", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have text"), e.getMessage());
}
}
@Test
void hasTextWRegExArrayPass() {
page.setContent("<div>Text \n1</div><div>Text 2a</div>");
Locator locator = page.locator("div");
// Should normalize whitespace.
assertThat(locator).hasText(new Pattern[] {Pattern.compile( "Text \n1"), Pattern.compile("Text \\d+a")});
}
@Test
void hasTextWRegExArrayFail() {
page.setContent("<div>Text 1</div><div>Text 3</div>");
Locator locator = page.locator("div");
try {
// Should normalize whitespace.
assertThat(locator).hasText(new Pattern[] {Pattern.compile( "Text 1"), Pattern.compile("Text \\d"), Pattern.compile("Extra")}, new LocatorAssertions.HasTextOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("[Text 1, Text \\d, Extra]", e.getExpected().getStringRepresentation());
assertEquals("[Text 1, Text 3]", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have text"), e.getMessage());
}
}
@Test
void hasAttributeTextPass() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).hasAttribute("id", "node");
}
@Test
void hasAttributeTextFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasAttribute("id", "foo", new LocatorAssertions.HasAttributeOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("foo", e.getExpected().getStringRepresentation());
assertEquals("node", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have attribute 'id'"), e.getMessage());
}
}
@Test
void hasAttributeRegExpPass() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).hasAttribute("id", Pattern.compile("n..e"));
}
@Test
void hasAttributeRegExpFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasAttribute("id", Pattern.compile(".Nod.."), new LocatorAssertions.HasAttributeOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals(".Nod..", e.getExpected().getStringRepresentation());
assertEquals("node", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have attribute 'id' matching regex"), e.getMessage());
}
}
@Test
void hasClassTextPass() {
page.setContent("<div class=\"foo bar baz\"></div>");
Locator locator = page.locator("div");
assertThat(locator).hasClass("foo bar baz");
}
@Test
void hasClassTextFail() {
page.setContent("<div class=\"bar baz\"></div>");
Locator locator = page.locator("div");
try {
assertThat(locator).hasClass("foo bar baz", new LocatorAssertions.HasClassOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("foo bar baz", e.getExpected().getStringRepresentation());
assertEquals("bar baz", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have class"), e.getMessage());
}
}
@Test
void hasClassRegExpPass() {
page.setContent("<div class=\"foo bar baz\"></div>");
Locator locator = page.locator("div");
assertThat(locator).hasClass(Pattern.compile("foo.* baz"));
}
@Test
void hasClassRegExpFail() {
page.setContent("<div class=\"bar baz\"></div>");
Locator locator = page.locator("div");
try {
assertThat(locator).hasClass(Pattern.compile("foo Z.*"), new LocatorAssertions.HasClassOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("foo Z.*", e.getExpected().getStringRepresentation());
assertEquals("bar baz", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have class matching regex"), e.getMessage());
}
}
@Test
void hasClassTextArrayPass() {
page.setContent("<div class=\"foo\"></div><div class=\"bar\"></div><div class=\"baz\"></div>");
Locator locator = page.locator("div");
assertThat(locator).hasClass(new String[] {"foo", "bar", "baz"});
}
@Test
void hasClassTextArrayFail() {
page.setContent("<div class=\"foo\"></div><div class=\"bar\"></div><div class=\"baz\"></div>");
Locator locator = page.locator("div");
try {
assertThat(locator).hasClass(new String[] {"foo", "bar", "missing"}, new LocatorAssertions.HasClassOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("[foo, bar, missing]", e.getExpected().getStringRepresentation());
assertEquals("[foo, bar, baz]", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have class"), e.getMessage());
}
}
@Test
void hasClassRegExpArrayPass() {
page.setContent("<div class=\"foo\"></div><div class=\"bar\"></div><div class=\"baz\"></div>");
Locator locator = page.locator("div");
assertThat(locator).hasClass(new Pattern[] {Pattern.compile("fo.*"), Pattern.compile(".ar"), Pattern.compile("baz")});
}
@Test
void hasClassRegExpArrayFail() {
page.setContent("<div class=\"foo\"></div><div class=\"bar\"></div><div class=\"baz\"></div>");
Locator locator = page.locator("div");
try {
assertThat(locator).hasClass(new Pattern[] {Pattern.compile("fo.*"), Pattern.compile(".ar"), Pattern.compile("baz"), Pattern.compile("extra")}, new LocatorAssertions.HasClassOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("[fo.*, .ar, baz, extra]", e.getExpected().getStringRepresentation());
assertEquals("[foo, bar, baz]", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have class matching regex"), e.getMessage());
}
}
@Test
void hasCountPass() {
page.setContent("<select><option>One</option><option>Two</option></select>");
Locator locator = page.locator("option");
assertThat(locator).hasCount(2);
}
@Test
void hasCountFail() {
page.setContent("<select><option>One</option><option>Two</option></select>");
Locator locator = page.locator("option");
try {
assertThat(locator).hasCount(1, new LocatorAssertions.HasCountOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("1", e.getExpected().getStringRepresentation());
assertEquals("2", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have count"), e.getMessage());
}
}
@Test
void hasCountPassZero() {
page.setContent("<div></div>");
Locator locator = page.locator("span");
assertThat(locator).hasCount(0);
assertThat(locator).not().hasCount(1);
}
@Test
void hasCSSPass() {
page.setContent("<div id=node style='color: rgb(255, 0, 0)'>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).hasCSS("color", "rgb(255, 0, 0)");
}
@Test
void hasCSSFail() {
page.setContent("<div id=node style='color: rgb(255, 0, 0)'>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasCSS("color", "red", new LocatorAssertions.HasCSSOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("red", e.getExpected().getStringRepresentation());
assertEquals("rgb(255, 0, 0)", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have CSS property 'color'"), e.getMessage());
}
}
@Test
void hasCSSRegExPass() {
page.setContent("<div id=node style='color: rgb(255, 0, 0)'>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).hasCSS("color", Pattern.compile("rgb.*"));
}
@Test
void hasCSSRegExFail() {
page.setContent("<div id=node style='color: rgb(255, 0, 0)'>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasCSS("color", Pattern.compile("red"), new LocatorAssertions.HasCSSOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("red", e.getExpected().getStringRepresentation());
assertEquals("rgb(255, 0, 0)", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have CSS property 'color' matching regex"), e.getMessage());
}
}
@Test
void hasIdPass() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
assertThat(locator).hasId("node");
}
@Test
void hasIdFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasId("foo", new LocatorAssertions.HasIdOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("foo", e.getExpected().getStringRepresentation());
assertEquals("node", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have ID"), e.getMessage());
}
}
@Test
void hasJSPropertyPass() {
page.setContent("<div></div>");
page.evalOnSelector("div", "e => e.foo = { a: 1, b: 'string' }");
Locator locator = page.locator("div");
assertThat(locator).hasJSProperty("foo", mapOf("a", 1, "b", "string"));
}
@Test
void hasJSPropertyNumberFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
page.evalOnSelector("div", "e => e.foo = 2021");
try {
assertThat(locator).hasJSProperty("foo", 1, new LocatorAssertions.HasJSPropertyOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("1", e.getExpected().getStringRepresentation());
assertEquals("2021", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have JavaScript property 'foo'"), e.getMessage());
}
}
@Test
void hasJSPropertyObjectFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
page.evalOnSelector("div", "e => e.foo = { a: 1, b: 'string' }");
try {
assertThat(locator).hasJSProperty("foo", mapOf("a", 2), new LocatorAssertions.HasJSPropertyOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("{a=2}", e.getExpected().getStringRepresentation());
assertEquals("{a=1, b=string}", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have JavaScript property 'foo'"), e.getMessage());
}
}
@Test
void hasJSPropertyStringFail() {
page.setContent("<div id=node>Text content</div>");
Locator locator = page.locator("#node");
try {
assertThat(locator).hasJSProperty("id", "foo", new LocatorAssertions.HasJSPropertyOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("foo", e.getExpected().getStringRepresentation());
assertEquals("node", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have JavaScript property 'id'"), e.getMessage());
}
}
@Test
void hasValueTextPass() {
page.setContent("<input id=node></input>");
Locator locator = page.locator("#node");
locator.fill("Text content");
assertThat(locator).hasValue("Text content");
}
@Test
void hasValueTextFail() {
page.setContent("<input id=node></input>");
Locator locator = page.locator("#node");
locator.fill("Text content");
try {
assertThat(locator).hasValue("Text2", new LocatorAssertions.HasValueOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("Text2", e.getExpected().getStringRepresentation());
assertEquals("Text content", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have value"), e.getMessage());
}
}
@Test
void hasValueRegExpPass() {
page.setContent("<input id=node></input>");
Locator locator = page.locator("#node");
locator.fill("Text content");
assertThat(locator).hasValue(Pattern.compile("Text"));
}
@Test
void hasValueRegExpPassWithNot() {
page.setContent("<input id=node></input>");
Locator locator = page.locator("#node");
locator.fill("Text content");
assertThat(locator).not().hasValue(Pattern.compile("Text2"));
}
@Test
void hasValueRegExpFail() {
page.setContent("<input id=node></input>");
Locator locator = page.locator("#node");
locator.fill("Text content");
try {
assertThat(locator).hasValue(Pattern.compile("Text2"), new LocatorAssertions.HasValueOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("Text2", e.getExpected().getStringRepresentation());
assertEquals("Text content", e.getActual().getStringRepresentation());
assertTrue(e.getMessage().contains("Locator expected to have value matching regex"), e.getMessage());
}
}
@Test
void isCheckedPass() {
page.setContent("<input type=checkbox checked></input>");
Locator locator = page.locator("input");
assertThat(locator).isChecked();
}
@Test
void isCheckedFail() {
page.setContent("<input type=checkbox></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).isChecked(new LocatorAssertions.IsCheckedOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be checked"), e.getMessage());
}
}
@Test
void notIsCheckedFail() {
page.setContent("<input type=checkbox checked></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).not().isChecked(new LocatorAssertions.IsCheckedOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be checked"), e.getMessage());
}
}
@Test
void isDisabledPass() {
page.setContent("<button disabled>Text</button>");
Locator locator = page.locator("button");
assertThat(locator).isDisabled();
}
@Test
void isDisabledFail() {
page.setContent("<button>Text</button>");
Locator locator = page.locator("button");
try {
assertThat(locator).isDisabled(new LocatorAssertions.IsDisabledOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be disabled"), e.getMessage());
}
}
@Test
void notIsDisabledFail() {
page.setContent("<button disabled>Text</button>");
Locator locator = page.locator("button");
try {
assertThat(locator).not().isDisabled(new LocatorAssertions.IsDisabledOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be disabled"), e.getMessage());
}
}
@Test
void isEditablePass() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
assertThat(locator).isEditable();
}
@Test
void isEditableFail() {
page.setContent("<input disabled></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).isEditable(new LocatorAssertions.IsEditableOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be editable"), e.getMessage());
}
}
@Test
void notIsEditableFail() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).not().isEditable(new LocatorAssertions.IsEditableOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be editable"), e.getMessage());
}
}
@Test
void isEmptyPass() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
assertThat(locator).isEmpty();
}
@Test
void isEmptyFail() {
page.setContent("<input value=text></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).isEmpty(new LocatorAssertions.IsEmptyOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be empty"), e.getMessage());
}
}
@Test
void notIsEmptyFail() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).not().isEmpty(new LocatorAssertions.IsEmptyOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be empty"), e.getMessage());
}
}
@Test
void isEnabledPass() {
page.setContent("<button>Text</button>");
Locator locator = page.locator("button");
assertThat(locator).isEnabled();
}
@Test
void isEnabledFail() {
page.setContent("<button disabled>Text</button>");
Locator locator = page.locator("button");
try {
assertThat(locator).isEnabled(new LocatorAssertions.IsEnabledOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be enabled"), e.getMessage());
}
}
@Test
void notIsEnabledFail() {
page.setContent("<button>Text</button>");
Locator locator = page.locator("button");
try {
assertThat(locator).not().isEnabled(new LocatorAssertions.IsEnabledOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be enabled"), e.getMessage());
}
}
@Test
void isFocusedPass() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
locator.focus();
assertThat(locator).isFocused();
}
@Test
void isFocusedFail() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).isFocused(new LocatorAssertions.IsFocusedOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be focused"), e.getMessage());
}
}
@Test
void notIsFocusedFail() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
locator.focus();
try {
assertThat(locator).not().isFocused(new LocatorAssertions.IsFocusedOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be focused"), e.getMessage());
}
}
@Test
void isHiddenPass() {
page.setContent("<button style='display: none'></button>");
Locator locator = page.locator("button");
assertThat(locator).isHidden();
}
@Test
void isHiddenFail() {
page.setContent("<button></button>");
Locator locator = page.locator("button");
try {
assertThat(locator).isHidden(new LocatorAssertions.IsHiddenOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be hidden"), e.getMessage());
}
}
@Test
void notIsHiddenFail() {
page.setContent("<button style='display: none'></button>");
Locator locator = page.locator("button");
try {
assertThat(locator).not().isHidden(new LocatorAssertions.IsHiddenOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be hidden"), e.getMessage());
}
}
@Test
void isVisiblePass() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
assertThat(locator).isVisible();
}
@Test
void isVisibleFail() {
page.setContent("<input style='display: none'></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).isVisible(new LocatorAssertions.IsVisibleOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected to be visible"), e.getMessage());
}
}
@Test
void notIsVisibleFail() {
page.setContent("<input></input>");
Locator locator = page.locator("input");
try {
assertThat(locator).not().isVisible(new LocatorAssertions.IsVisibleOptions().setTimeout(1000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertNull(e.getExpected());
assertNull(e.getActual());
assertTrue(e.getMessage().contains("Locator expected not to be visible"), e.getMessage());
}
}
}
@@ -0,0 +1,254 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import org.junit.jupiter.api.Test;
import java.net.MalformedURLException;
import java.net.URL;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
public class TestLocatorFrame extends TestBase {
private static void routeIframe(Page page) {
page.route("**/empty.html", route -> route.fulfill(new Route.FulfillOptions()
.setBody("<iframe src='iframe.html'></iframe>").setContentType("text/html")));
page.route("**/iframe.html", route -> {
route.fulfill(new Route.FulfillOptions().setBody("<html>\n" +
" <div>\n" +
" <button>Hello iframe</button>\n" +
" <iframe src='iframe-2.html'></iframe>\n" +
" </div>\n" +
" <span>1</span>\n" +
" <span>2</span>\n" +
" </html>").setContentType("text/html"));
});
page.route("**/iframe-2.html", route -> {
route.fulfill(new Route.FulfillOptions().setBody("<html><button>Hello nested iframe</button></html>").setContentType("text/html"));
});
}
private static void routeAmbiguous(Page page) {
page.route("**/empty.html", route -> {
route.fulfill(new Route.FulfillOptions()
.setBody("<iframe src='iframe-1.html'></iframe>\n" +
"<iframe src='iframe-2.html'></iframe>\n" +
"<iframe src='iframe-3.html'></iframe>")
.setContentType("text/html"));
});
page.route("**/iframe-*", route -> {
try {
String path = new URL(route.request().url()).getPath().substring(1);
route.fulfill(new Route.FulfillOptions()
.setBody("<html><button>Hello from " + path + "</button></html>")
.setContentType("text/html"));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
});
}
@Test
void shouldWorkForIframe() {
routeIframe(page);
page.navigate(server.EMPTY_PAGE);
Locator button = page.frameLocator("iframe").locator("button");
button.waitFor();
assertEquals("Hello iframe", button.innerText());
assertThat(button).hasText("Hello iframe");
button.click();
}
@Test
void shouldWorkForNestedIframe() {
routeIframe(page);
page.navigate(server.EMPTY_PAGE);
Locator button = page.frameLocator("iframe").frameLocator("iframe").locator("button");
button.waitFor();
assertEquals("Hello nested iframe", button.innerText());
assertThat(button).hasText("Hello nested iframe");
button.click();
}
@Test
void shouldWorkForAnd() {
routeIframe(page);
page.navigate(server.EMPTY_PAGE);
Locator locator = page.frameLocator("iframe").locator("button");
assertThat(locator).hasText("Hello iframe");
assertEquals("Hello iframe", locator.innerText());
Locator spans = page.frameLocator("iframe").locator("span");
assertThat(spans).hasCount(2);
}
@Test
void shouldWaitForFrame() {
page.navigate(server.EMPTY_PAGE);
try {
page.frameLocator("iframe").locator("span").click(new Locator.ClickOptions().setTimeout(300));
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("waiting for frame \"iframe\""), e.getMessage());
}
}
@Test
void shouldWaitForFrame2() {
routeIframe(page);
page.evaluate("url => setTimeout(() => location.href = url, 300)", server.EMPTY_PAGE);
page.frameLocator("iframe").locator("button").click();
}
void shouldWaitForFrameToGo() {
}
@Test
void shouldNotWaitForFrame() {
page.navigate(server.EMPTY_PAGE);
assertThat(page.frameLocator("iframe").locator("span")).isHidden();
}
@Test
void shouldNotWaitForFrame2() {
page.navigate(server.EMPTY_PAGE);
assertThat(page.frameLocator("iframe").locator("span")).not().isVisible();
}
@Test
void shouldNotWaitForFrame3() {
page.navigate(server.EMPTY_PAGE);
assertThat(page.frameLocator("iframe").locator("span")).hasCount(0);
}
@Test
void shouldClickInLazyIframe() {
page.route("**/iframe.html", route -> {
route.fulfill(new Route.FulfillOptions().setBody("<html><button>Hello iframe</button></html>").setContentType("text/html"));
});
// empty pge
page.navigate(server.EMPTY_PAGE);
// add blank iframe
page.evaluate("setTimeout(() => {\n" +
" const iframe = document.createElement('iframe');\n" +
" document.body.appendChild(iframe);\n" +
" // navigate iframe\n" +
" setTimeout(() => iframe.src = 'iframe.html', 500);\n" +
" }, 500);");
// Click in iframe
Locator button = page.frameLocator("iframe").locator("button");
button.click();
assertThat(button).hasText("Hello iframe");
assertEquals("Hello iframe", button.innerText());
}
@Test
void waitForShouldSurviveFrameReattach() {
routeIframe(page);
page.navigate(server.EMPTY_PAGE);
Locator button = page.frameLocator("iframe").locator("button:has-text('Hello nested iframe')");
page.evaluate("setTimeout(() => {\n" +
" document.querySelector('iframe').remove();\n" +
" setTimeout(() => {\n" +
" const iframe = document.createElement('iframe');\n" +
" iframe.src = 'iframe-2.html';\n" +
" document.body.appendChild(iframe);\n" +
" }, 500);\n" +
" }, 500);");
button.waitFor();
}
@Test
void clickShouldSurviveFrameReattach() {
routeIframe(page);
page.navigate(server.EMPTY_PAGE);
Locator button = page.frameLocator("iframe").locator("button:has-text('Hello nested iframe')");
page.evaluate("setTimeout(() => {\n" +
" document.querySelector('iframe').remove();\n" +
" setTimeout(() => {\n" +
" const iframe = document.createElement('iframe');\n" +
" iframe.src = 'iframe-2.html';\n" +
" document.body.appendChild(iframe);\n" +
" }, 500);\n" +
" }, 500);");
button.click();
}
@Test
void clickShouldSurviveIframeNavigation() {
routeIframe(page);
page.navigate(server.EMPTY_PAGE);
Locator button = page.frameLocator("iframe").locator("button:has-text('Hello nested iframe')");
page.evaluate("setTimeout(() => {\n" +
" document.querySelector('iframe').src = 'iframe-2.html';\n" +
" }, 500);");
button.click();
}
@Test
void shouldNonWorkForNonFrame() {
routeIframe(page);
page.setContent("<div></div>");
Locator button = page.frameLocator("div").locator("button");
try {
button.waitFor();
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("<div></div>"), e.getMessage());
assertTrue(e.getMessage().contains("<iframe> was expected"), e.getMessage());
}
}
@Test
void locatorFrameLocatorShouldWorkForIframe() {
routeIframe(page);
page.navigate(server.EMPTY_PAGE);
Locator button = page.locator("body").frameLocator("iframe").locator("button");
button.waitFor();
assertThat(button).hasText("Hello iframe");
assertEquals("Hello iframe", button.innerText());
button.click();
}
@Test
void locatorFrameLocatorShouldThrowOnAmbiguity() {
routeAmbiguous(page);
page.navigate(server.EMPTY_PAGE);
Locator button = page.locator("body").frameLocator("iframe").locator("button");
try {
button.waitFor();
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Error: strict mode violation: \"body >> iframe\" resolved to 3 elements"), e.getMessage());
}
}
@Test
void locatorFrameLocatorShouldNotThrowOnFirstLastNth() {
routeAmbiguous(page);
page.navigate(server.EMPTY_PAGE);
Locator button1 = page.locator("body").frameLocator("iframe").first().locator("button");
assertThat(button1).hasText("Hello from iframe-1.html");
Locator button2 = page.locator("body").frameLocator("iframe").nth(1).locator("button");
assertThat(button2).hasText("Hello from iframe-2.html");
Locator button3 = page.locator("body").frameLocator("iframe").last().locator("button");
assertThat(button3).hasText("Hello from iframe-3.html");
}
}
@@ -0,0 +1,137 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.assertions.PageAssertions;
import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;
import java.util.regex.Pattern;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
public class TestPageAssertions extends TestBase {
@Test
void hasURLTextPass() {
page.navigate("data:text/html,<div>A</div>");
assertThat(page).hasURL("data:text/html,<div>A</div>");
}
@Test
void hasURLTextFail() {
page.navigate("data:text/html,<div>B</div>");
try {
assertThat(page).hasURL("foo", new PageAssertions.HasURLOptions().setTimeout(1_000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("foo", e.getExpected().getValue());
assertEquals("data:text/html,<div>B</div>", e.getActual().getValue());
assertTrue(e.getMessage().contains("Page URL expected to be"), e.getMessage());
}
}
@Test
void shouldSupportHasUrlWithBaseUrl() {
try (BrowserContext context = browser.newContext(new Browser.NewContextOptions().setBaseURL(server.PREFIX))) {
Page page = context.newPage();
page.navigate(server.EMPTY_PAGE);
assertThat(page).hasURL("/empty.html", new PageAssertions.HasURLOptions().setTimeout(1_000));
}
}
@Test
void notHasUrlText() {
page.navigate("data:text/html,<div>B</div>");
assertThat(page).not().hasURL("about:blank", new PageAssertions.HasURLOptions().setTimeout(1000));
}
@Test
void hasURLRegexPass() {
page.navigate("data:text/html,<div>A</div>");
assertThat(page).hasURL(Pattern.compile("text"));
}
@Test
void hasURLRegexFail() {
page.navigate(server.EMPTY_PAGE);
try {
assertThat(page).hasURL(Pattern.compile(".*foo.*"), new PageAssertions.HasURLOptions().setTimeout(1_000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals(".*foo.*", e.getExpected().getStringRepresentation());
assertEquals(server.EMPTY_PAGE, e.getActual().getValue());
assertTrue(e.getMessage().contains("Page URL expected to match regex"), e.getMessage());
}
}
@Test
void notHasUrlRegEx() {
page.navigate("data:text/html,<div>B</div>");
assertThat(page).not().hasURL(Pattern.compile("about"), new PageAssertions.HasURLOptions().setTimeout(1000));
}
@Test
void hasTitleTextPass() {
page.navigate(server.PREFIX + "/title.html");
assertThat(page).hasTitle("Woof-Woof", new PageAssertions.HasTitleOptions().setTimeout(1_000));
}
@Test
void hasTitleTextFail() {
page.navigate(server.PREFIX + "/title.html");
try {
assertThat(page).hasTitle("foo", new PageAssertions.HasTitleOptions().setTimeout(1_000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("foo", e.getExpected().getValue());
assertEquals("Woof-Woof", e.getActual().getValue());
assertTrue(e.getMessage().contains("Page title expected to be"), e.getMessage());
}
}
@Test
void hasTitleRegexPass() {
page.navigate(server.PREFIX + "/title.html");
assertThat(page).hasTitle(Pattern.compile("^.oof.+oof$"));
}
@Test
void hasTitleRegexFail() {
page.navigate(server.PREFIX + "/title.html");
try {
assertThat(page).hasTitle(Pattern.compile("^foo[AB]"), new PageAssertions.HasTitleOptions().setTimeout(1_000));
fail("did not throw");
} catch (AssertionFailedError e) {
assertEquals("^foo[AB]", e.getExpected().getStringRepresentation());
assertEquals("Woof-Woof", e.getActual().getValue());
assertTrue(e.getMessage().contains("Page title expected to match regex"), e.getMessage());
}
}
@Test
void notHasTitleRegEx() {
page.navigate(server.PREFIX + "/title.html");
assertThat(page).not().hasTitle(Pattern.compile("ab.ut"));
}
@Test
void hasTitleRegExCaseInsensitivePass() {
page.navigate(server.PREFIX + "/title.html");
assertThat(page).hasTitle(Pattern.compile("woof-woof", Pattern.CASE_INSENSITIVE));
}
}
+2 -17
View File
@@ -6,13 +6,13 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.15.0-SNAPSHOT</version>
<version>1.17.2</version>
</parent>
<artifactId>driver-bundle</artifactId>
<name>Playwright - Drivers For All Platforms</name>
<description>
This module includes playwright-cli binary and related utilities for all supported platforms.
This module includes Playwright driver and related utilities for all supported platforms.
It is intended to be used on the systems where Playwright driver is not preinstalled.
</description>
@@ -28,25 +28,10 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<source>8</source>
<failOnError>false</failOnError>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration>
</plugin>
</plugins>
</build>
@@ -131,7 +131,7 @@ public class DriverJar extends Driver {
private static String platformDir() {
String name = System.getProperty("os.name").toLowerCase();
if (name.contains("windows")) {
return System.getProperty("os.arch").equals("amd64") ? "win32_x64" : "win32";
return "win32_x64";
}
if (name.contains("linux")) {
return "linux";
+2 -17
View File
@@ -6,13 +6,13 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.15.0-SNAPSHOT</version>
<version>1.17.2</version>
</parent>
<artifactId>driver</artifactId>
<name>Playwright - Driver</name>
<description>
This module provides API for discovery and launching of playwright-cli binary.
This module provides API for discovery and launching of Playwright driver.
</description>
<build>
@@ -24,25 +24,10 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<source>8</source>
<failOnError>false</failOnError>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration>
</plugin>
</plugins>
</build>
+2 -2
View File
@@ -6,7 +6,7 @@
<groupId>org.example</groupId>
<artifactId>examples</artifactId>
<version>1.15.0-SNAPSHOT</version>
<version>1.17.2</version>
<name>Playwright Client Examples</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -15,7 +15,7 @@
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.11.1</version>
<version>1.16.0</version>
</dependency>
</dependencies>
<build>
+19 -11
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.15.0-SNAPSHOT</version>
<version>1.17.2</version>
</parent>
<artifactId>playwright</artifactId>
@@ -31,17 +31,7 @@
<configuration>
<subpackages>com.microsoft.playwright</subpackages>
<excludePackageNames>com.microsoft.playwright.impl</excludePackageNames>
<additionalOptions>--allow-script-in-comments</additionalOptions>
<failOnError>false</failOnError>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -51,7 +41,25 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<targetPath>resources</targetPath>
</testResource>
</testResources>
</build>
<dependencies>
<dependency>
@@ -86,7 +86,7 @@ public interface Browser extends AutoCloseable {
*/
public Double deviceScaleFactor;
/**
* An object containing additional HTTP headers to be sent with every request. All header values must be strings.
* An object containing additional HTTP headers to be sent with every request.
*/
public Map<String, String> extraHTTPHeaders;
/**
@@ -107,7 +107,7 @@ public interface Browser extends AutoCloseable {
*/
public HttpCredentials httpCredentials;
/**
* Whether to ignore HTTPS errors during navigation. Defaults to {@code false}.
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
*/
public Boolean ignoreHTTPSErrors;
/**
@@ -249,7 +249,7 @@ public interface Browser extends AutoCloseable {
return this;
}
/**
* An object containing additional HTTP headers to be sent with every request. All header values must be strings.
* An object containing additional HTTP headers to be sent with every request.
*/
public NewContextOptions setExtraHTTPHeaders(Map<String, String> extraHTTPHeaders) {
this.extraHTTPHeaders = extraHTTPHeaders;
@@ -294,7 +294,7 @@ public interface Browser extends AutoCloseable {
return this;
}
/**
* Whether to ignore HTTPS errors during navigation. Defaults to {@code false}.
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
*/
public NewContextOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) {
this.ignoreHTTPSErrors = ignoreHTTPSErrors;
@@ -510,7 +510,7 @@ public interface Browser extends AutoCloseable {
*/
public Double deviceScaleFactor;
/**
* An object containing additional HTTP headers to be sent with every request. All header values must be strings.
* An object containing additional HTTP headers to be sent with every request.
*/
public Map<String, String> extraHTTPHeaders;
/**
@@ -531,7 +531,7 @@ public interface Browser extends AutoCloseable {
*/
public HttpCredentials httpCredentials;
/**
* Whether to ignore HTTPS errors during navigation. Defaults to {@code false}.
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
*/
public Boolean ignoreHTTPSErrors;
/**
@@ -673,7 +673,7 @@ public interface Browser extends AutoCloseable {
return this;
}
/**
* An object containing additional HTTP headers to be sent with every request. All header values must be strings.
* An object containing additional HTTP headers to be sent with every request.
*/
public NewPageOptions setExtraHTTPHeaders(Map<String, String> extraHTTPHeaders) {
this.extraHTTPHeaders = extraHTTPHeaders;
@@ -718,7 +718,7 @@ public interface Browser extends AutoCloseable {
return this;
}
/**
* Whether to ignore HTTPS errors during navigation. Defaults to {@code false}.
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
*/
public NewPageOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) {
this.ignoreHTTPSErrors = ignoreHTTPSErrors;
@@ -29,8 +29,8 @@ import java.util.regex.Pattern;
* <p> If a page opens another page, e.g. with a {@code window.open} call, the popup will belong to the parent page's browser
* context.
*
* <p> Playwright allows creation of "incognito" browser contexts with {@code browser.newContext()} method. "Incognito" browser
* contexts don't write any browsing data to disk.
* <p> Playwright allows creating "incognito" browser contexts with {@link Browser#newContext Browser.newContext()} method.
* "Incognito" browser contexts don't write any browsing data to disk.
* <pre>{@code
* // Create a new incognito browser context
* BrowserContext context = browser.newContext();
@@ -435,7 +435,7 @@ public interface BrowserType {
*/
public Path executablePath;
/**
* An object containing additional HTTP headers to be sent with every request. All header values must be strings.
* An object containing additional HTTP headers to be sent with every request.
*/
public Map<String, String> extraHTTPHeaders;
/**
@@ -485,7 +485,7 @@ public interface BrowserType {
*/
public List<String> ignoreDefaultArgs;
/**
* Whether to ignore HTTPS errors during navigation. Defaults to {@code false}.
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
*/
public Boolean ignoreHTTPSErrors;
/**
@@ -692,7 +692,7 @@ public interface BrowserType {
return this;
}
/**
* An object containing additional HTTP headers to be sent with every request. All header values must be strings.
* An object containing additional HTTP headers to be sent with every request.
*/
public LaunchPersistentContextOptions setExtraHTTPHeaders(Map<String, String> extraHTTPHeaders) {
this.extraHTTPHeaders = extraHTTPHeaders;
@@ -784,7 +784,7 @@ public interface BrowserType {
return this;
}
/**
* Whether to ignore HTTPS errors during navigation. Defaults to {@code false}.
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
*/
public LaunchPersistentContextOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) {
this.ignoreHTTPSErrors = ignoreHTTPSErrors;
@@ -1079,7 +1079,8 @@ public interface BrowserType {
* @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. 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}.
* 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.
*/
default BrowserContext launchPersistentContext(Path userDataDir) {
return launchPersistentContext(userDataDir, null);
@@ -1093,7 +1094,8 @@ public interface BrowserType {
* @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. 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}.
* 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.
*/
BrowserContext launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options);
/**
@@ -23,6 +23,8 @@ import java.util.*;
/**
* ElementHandle represents an in-page DOM element. ElementHandles can be created with the {@link Page#querySelector
* Page.querySelector()} method.
*
* <p> <strong>NOTE:</strong> The use of ElementHandle is discouraged, use {@code Locator} objects and web-first assertions instead.
* <pre>{@code
* ElementHandle hrefElement = page.querySelector("a");
* hrefElement.click();
@@ -34,10 +36,6 @@ import java.util.*;
* <p> ElementHandle instances can be used as an argument in {@link Page#evalOnSelector Page.evalOnSelector()} and {@link
* Page#evaluate Page.evaluate()} methods.
*
* <p> <strong>NOTE:</strong> In most cases, you would want to use the {@code Locator} object instead. You should only use {@code ElementHandle} if you want to
* retain a handle to a particular DOM Node that you intend to pass into {@link Page#evaluate Page.evaluate()} as an
* argument.
*
* <p> The difference between the {@code Locator} and ElementHandle is that the ElementHandle points to a particular element, while
* {@code Locator} captures the logic of how to retrieve an element.
*
@@ -813,6 +813,7 @@ public interface Frame {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitUntilState waitUntil;
@@ -841,6 +842,7 @@ public interface Frame {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public NavigateOptions setWaitUntil(WaitUntilState waitUntil) {
@@ -1455,6 +1457,7 @@ public interface Frame {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitUntilState waitUntil;
@@ -1475,6 +1478,7 @@ public interface Frame {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public SetContentOptions setWaitUntil(WaitUntilState waitUntil) {
@@ -1875,7 +1879,9 @@ public interface Frame {
*/
public Double timeout;
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
public Object url;
/**
@@ -1884,6 +1890,7 @@ public interface Frame {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitUntilState waitUntil;
@@ -1899,21 +1906,27 @@ public interface Frame {
return this;
}
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
public WaitForNavigationOptions setUrl(String url) {
this.url = url;
return this;
}
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
public WaitForNavigationOptions setUrl(Pattern url) {
this.url = url;
return this;
}
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
public WaitForNavigationOptions setUrl(Predicate<String> url) {
this.url = url;
@@ -1925,6 +1938,7 @@ public interface Frame {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitForNavigationOptions setWaitUntil(WaitUntilState waitUntil) {
@@ -2004,6 +2018,7 @@ public interface Frame {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitUntilState waitUntil;
@@ -2024,6 +2039,7 @@ public interface Frame {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitForURLOptions setWaitUntil(WaitUntilState waitUntil) {
@@ -2313,6 +2329,9 @@ public interface Frame {
/**
* Returns the return value of {@code expression}.
*
* <p> <strong>NOTE:</strong> This method does not wait for the element to pass actionability checks and therefore can lead to the flaky tests. Use
* {@link Locator#evaluate Locator.evaluate()}, other {@code Locator} helper methods or web-first assertions instead.
*
* <p> The method finds an element matching the specified selector within the frame and passes it as a first argument to
* {@code expression}. See <a href="https://playwright.dev/java/docs/selectors/">Working with selectors</a> for more details. If
* no elements match the selector, the method throws an error.
@@ -2340,6 +2359,9 @@ public interface Frame {
/**
* Returns the return value of {@code expression}.
*
* <p> <strong>NOTE:</strong> This method does not wait for the element to pass actionability checks and therefore can lead to the flaky tests. Use
* {@link Locator#evaluate Locator.evaluate()}, other {@code Locator} helper methods or web-first assertions instead.
*
* <p> The method finds an element matching the specified selector within the frame and passes it as a first argument to
* {@code expression}. See <a href="https://playwright.dev/java/docs/selectors/">Working with selectors</a> for more details. If
* no elements match the selector, the method throws an error.
@@ -2366,6 +2388,9 @@ public interface Frame {
/**
* Returns the return value of {@code expression}.
*
* <p> <strong>NOTE:</strong> This method does not wait for the element to pass actionability checks and therefore can lead to the flaky tests. Use
* {@link Locator#evaluate Locator.evaluate()}, other {@code Locator} helper methods or web-first assertions instead.
*
* <p> The method finds an element matching the specified selector within the frame and passes it as a first argument to
* {@code expression}. See <a href="https://playwright.dev/java/docs/selectors/">Working with selectors</a> for more details. If
* no elements match the selector, the method throws an error.
@@ -2391,6 +2416,9 @@ public interface Frame {
/**
* Returns the return value of {@code expression}.
*
* <p> <strong>NOTE:</strong> In most cases, {@link Locator#evaluateAll Locator.evaluateAll()}, other {@code Locator} helper methods and web-first
* assertions do a better job.
*
* <p> The method finds all elements matching the specified selector within the frame and passes an array of matched elements
* as a first argument to {@code expression}. See <a href="https://playwright.dev/java/docs/selectors/">Working with
* selectors</a> for more details.
@@ -2415,6 +2443,9 @@ public interface Frame {
/**
* Returns the return value of {@code expression}.
*
* <p> <strong>NOTE:</strong> In most cases, {@link Locator#evaluateAll Locator.evaluateAll()}, other {@code Locator} helper methods and web-first
* assertions do a better job.
*
* <p> The method finds all elements matching the specified selector within the frame and passes an array of matched elements
* as a first argument to {@code expression}. See <a href="https://playwright.dev/java/docs/selectors/">Working with
* selectors</a> for more details.
@@ -2459,7 +2490,7 @@ public interface Frame {
*
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link Frame#evaluate Frame.evaluate()}:
* <pre>{@code
* ElementHandle bodyHandle = frame.querySelector("body");
* ElementHandle bodyHandle = frame.evaluate("document.body");
* String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -2494,7 +2525,7 @@ public interface Frame {
*
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link Frame#evaluate Frame.evaluate()}:
* <pre>{@code
* ElementHandle bodyHandle = frame.querySelector("body");
* ElementHandle bodyHandle = frame.evaluate("document.body");
* String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -2637,6 +2668,19 @@ public interface Frame {
* }</pre>
*/
ElementHandle frameElement();
/**
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
* that iframe. Following snippet locates element with text "Submit" in the iframe with id {@code my-frame}, like {@code <iframe
* id="my-frame">}:
* <pre>{@code
* Locator locator = frame.frameLocator("#my-iframe").locator("text=Submit");
* locator.click();
* }</pre>
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors/">working with
* selectors</a> for more details.
*/
FrameLocator frameLocator(String selector);
/**
* Returns element attribute value.
*
@@ -2981,6 +3025,8 @@ public interface Frame {
/**
* Returns the ElementHandle pointing to the frame element.
*
* <p> <strong>NOTE:</strong> The use of {@code ElementHandle} is discouraged, use {@code Locator} objects and web-first assertions instead.
*
* <p> The method finds an element matching the specified selector within the frame. See <a
* href="https://playwright.dev/java/docs/selectors/">Working with selectors</a> for more details. If no elements match the
* selector, returns {@code null}.
@@ -2994,6 +3040,8 @@ public interface Frame {
/**
* Returns the ElementHandle pointing to the frame element.
*
* <p> <strong>NOTE:</strong> The use of {@code ElementHandle} is discouraged, use {@code Locator} objects and web-first assertions instead.
*
* <p> The method finds an element matching the specified selector within the frame. See <a
* href="https://playwright.dev/java/docs/selectors/">Working with selectors</a> for more details. If no elements match the
* selector, returns {@code null}.
@@ -3005,6 +3053,8 @@ public interface Frame {
/**
* Returns the ElementHandles pointing to the frame elements.
*
* <p> <strong>NOTE:</strong> The use of {@code ElementHandle} is discouraged, use {@code Locator} objects instead.
*
* <p> The method finds all elements matching the specified selector within the frame. See <a
* href="https://playwright.dev/java/docs/selectors/">Working with selectors</a> for more details. If no elements match the
* selector, returns empty array.
@@ -3875,6 +3925,9 @@ public interface Frame {
* Returns when element specified by selector satisfies {@code state} option. Returns {@code null} if waiting for {@code hidden} or
* {@code detached}.
*
* <p> <strong>NOTE:</strong> Playwright automatically waits for element to be ready before performing an action. Using {@code Locator} objects and
* web-first assertions make the code wait-for-selector-free.
*
* <p> Wait for the {@code selector} to satisfy {@code state} option (either appear/disappear from dom, or become visible/hidden). If at
* the moment of calling the method {@code selector} already satisfies the condition, the method will return immediately. If the
* selector doesn't satisfy the condition for the {@code timeout} milliseconds, the function will throw.
@@ -3910,6 +3963,9 @@ public interface Frame {
* Returns when element specified by selector satisfies {@code state} option. Returns {@code null} if waiting for {@code hidden} or
* {@code detached}.
*
* <p> <strong>NOTE:</strong> Playwright automatically waits for element to be ready before performing an action. Using {@code Locator} objects and
* web-first assertions make the code wait-for-selector-free.
*
* <p> Wait for the {@code selector} to satisfy {@code state} option (either appear/disappear from dom, or become visible/hidden). If at
* the moment of calling the method {@code selector} already satisfies the condition, the method will return immediately. If the
* selector doesn't satisfy the condition for the {@code timeout} milliseconds, the function will throw.
@@ -3955,7 +4011,9 @@ public interface Frame {
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
default void waitForURL(String url) {
waitForURL(url, null);
@@ -3967,7 +4025,9 @@ public interface Frame {
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
void waitForURL(String url, WaitForURLOptions options);
/**
@@ -3977,7 +4037,9 @@ public interface Frame {
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
default void waitForURL(Pattern url) {
waitForURL(url, null);
@@ -3989,7 +4051,9 @@ public interface Frame {
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
void waitForURL(Pattern url, WaitForURLOptions options);
/**
@@ -3999,7 +4063,9 @@ public interface Frame {
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
default void waitForURL(Predicate<String> url) {
waitForURL(url, null);
@@ -4011,7 +4077,9 @@ public interface Frame {
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
void waitForURL(Predicate<String> url, WaitForURLOptions options);
}
@@ -0,0 +1,71 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import java.util.*;
/**
* FrameLocator represents a view to the {@code iframe} on the page. It captures the logic sufficient to retrieve the {@code iframe}
* and locate elements in that iframe. FrameLocator can be created with either {@link Page#frameLocator
* Page.frameLocator()} or {@link Locator#frameLocator Locator.frameLocator()} method.
* <pre>{@code
* Locator locator = page.frameLocator("#my-frame").locator("text=Submit");
* locator.click();
* }</pre>
*
* <p> **Strictness**
*
* <p> Frame locators are strict. This means that all operations on frame locators will throw if more than one element matches
* given selector.
* <pre>{@code
* // Throws if there are several frames in DOM:
* page.frame_locator(".result-frame").locator("button").click();
*
* // Works because we explicitly tell locator to pick the first frame:
* page.frame_locator(".result-frame").first().locator("button").click();
* }</pre>
*/
public interface FrameLocator {
/**
* Returns locator to the first matching frame.
*/
FrameLocator first();
/**
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
* that iframe.
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors/">working with
* selectors</a> for more details.
*/
FrameLocator frameLocator(String selector);
/**
* Returns locator to the last matching frame.
*/
FrameLocator last();
/**
* The method finds an element matching the specified selector in the FrameLocator's subtree.
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors/">working with
* selectors</a> for more details.
*/
Locator locator(String selector);
/**
* Returns locator to the n-th matching frame.
*/
FrameLocator nth(int index);
}
@@ -1355,6 +1355,51 @@ public interface Locator {
return this;
}
}
class WaitForOptions {
/**
* Defaults to {@code "visible"}. Can be either:
* <ul>
* <li> {@code "attached"} - wait for element to be present in DOM.</li>
* <li> {@code "detached"} - wait for element to not be present in DOM.</li>
* <li> {@code "visible"} - wait for element to have non-empty bounding box and no {@code visibility:hidden}. Note that element without any
* content or with {@code display:none} has an empty bounding box and is not considered visible.</li>
* <li> {@code "hidden"} - wait for element to be either detached from DOM, or have an empty bounding box or {@code visibility:hidden}. This
* is opposite to the {@code "visible"} option.</li>
* </ul>
*/
public WaitForSelectorState state;
/**
* Maximum time in milliseconds, defaults to 30 seconds, pass {@code 0} to disable timeout. The default value can be changed by
* using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()} or {@link Page#setDefaultTimeout
* Page.setDefaultTimeout()} methods.
*/
public Double timeout;
/**
* Defaults to {@code "visible"}. Can be either:
* <ul>
* <li> {@code "attached"} - wait for element to be present in DOM.</li>
* <li> {@code "detached"} - wait for element to not be present in DOM.</li>
* <li> {@code "visible"} - wait for element to have non-empty bounding box and no {@code visibility:hidden}. Note that element without any
* content or with {@code display:none} has an empty bounding box and is not considered visible.</li>
* <li> {@code "hidden"} - wait for element to be either detached from DOM, or have an empty bounding box or {@code visibility:hidden}. This
* is opposite to the {@code "visible"} option.</li>
* </ul>
*/
public WaitForOptions setState(WaitForSelectorState state) {
this.state = state;
return this;
}
/**
* Maximum time in milliseconds, defaults to 30 seconds, pass {@code 0} to disable timeout. The default value can be changed by
* using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()} or {@link Page#setDefaultTimeout
* Page.setDefaultTimeout()} methods.
*/
public WaitForOptions setTimeout(double timeout) {
this.timeout = timeout;
return this;
}
}
/**
* Returns an array of {@code node.innerText} values for all matching nodes.
*/
@@ -1853,6 +1898,18 @@ public interface Locator {
* Calls <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus">focus</a> on the element.
*/
void focus(FocusOptions options);
/**
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
* that iframe:
* <pre>{@code
* Locator locator = page.frameLocator("iframe").locator("text=Submit");
* locator.click();
* }</pre>
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors/">working with
* selectors</a> for more details.
*/
FrameLocator frameLocator(String selector);
/**
* Returns element attribute value.
*
@@ -2000,8 +2057,7 @@ public interface Locator {
*/
Locator last();
/**
* The method finds an element matching the specified selector in the {@code Locator}'s subtree. See <a
* href="https://playwright.dev/java/docs/selectors/">Working with selectors</a> for more details.
* The method finds an element matching the specified selector in the {@code Locator}'s subtree.
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors/">working with
* selectors</a> for more details.
@@ -2667,5 +2723,29 @@ public interface Locator {
* zero timeout disables this.
*/
void uncheck(UncheckOptions options);
/**
* Returns when element specified by locator satisfies the {@code state} option.
*
* <p> If target element already satisfies the condition, the method returns immediately. Otherwise, waits for up to {@code timeout}
* milliseconds until the condition is met.
* <pre>{@code
* Locator orderSent = page.locator("#order-sent");
* orderSent.waitFor();
* }</pre>
*/
default void waitFor() {
waitFor(null);
}
/**
* Returns when element specified by locator satisfies the {@code state} option.
*
* <p> If target element already satisfies the condition, the method returns immediately. Otherwise, waits for up to {@code timeout}
* milliseconds until the condition is met.
* <pre>{@code
* Locator orderSent = page.locator("#order-sent");
* orderSent.waitFor();
* }</pre>
*/
void waitFor(WaitForOptions options);
}
@@ -1147,6 +1147,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitUntilState waitUntil;
@@ -1167,6 +1168,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public GoBackOptions setWaitUntil(WaitUntilState waitUntil) {
@@ -1188,6 +1190,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitUntilState waitUntil;
@@ -1208,6 +1211,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public GoForwardOptions setWaitUntil(WaitUntilState waitUntil) {
@@ -1234,6 +1238,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitUntilState waitUntil;
@@ -1262,6 +1267,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public NavigateOptions setWaitUntil(WaitUntilState waitUntil) {
@@ -1889,6 +1895,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitUntilState waitUntil;
@@ -1909,6 +1916,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public ReloadOptions setWaitUntil(WaitUntilState waitUntil) {
@@ -2195,6 +2203,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitUntilState waitUntil;
@@ -2215,6 +2224,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public SetContentOptions setWaitUntil(WaitUntilState waitUntil) {
@@ -2712,7 +2722,9 @@ public interface Page extends AutoCloseable {
*/
public Double timeout;
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
public Object url;
/**
@@ -2721,6 +2733,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitUntilState waitUntil;
@@ -2736,21 +2749,27 @@ public interface Page extends AutoCloseable {
return this;
}
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
public WaitForNavigationOptions setUrl(String url) {
this.url = url;
return this;
}
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
public WaitForNavigationOptions setUrl(Pattern url) {
this.url = url;
return this;
}
/**
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
public WaitForNavigationOptions setUrl(Predicate<String> url) {
this.url = url;
@@ -2762,6 +2781,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitForNavigationOptions setWaitUntil(WaitUntilState waitUntil) {
@@ -2929,6 +2949,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitUntilState waitUntil;
@@ -2949,6 +2970,7 @@ public interface Page extends AutoCloseable {
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* <li> {@code "commit"} - consider operation to be finished when network response is received and the document started loading.</li>
* </ul>
*/
public WaitForURLOptions setWaitUntil(WaitUntilState waitUntil) {
@@ -3445,7 +3467,10 @@ public interface Page extends AutoCloseable {
*/
void emulateMedia(EmulateMediaOptions options);
/**
* The method finds an element matching the specified selector within the page and passes it as a first argument to
* <strong>NOTE:</strong> This method does not wait for the element to pass actionability checks and therefore can lead to the flaky tests. Use
* {@link Locator#evaluate Locator.evaluate()}, other {@code Locator} helper methods or web-first assertions instead.
*
* <p> The method finds an element matching the specified selector within the page and passes it as a first argument to
* {@code expression}. If no elements match the selector, the method throws an error. Returns the value of {@code expression}.
*
* <p> If {@code expression} returns a <a
@@ -3471,7 +3496,10 @@ public interface Page extends AutoCloseable {
return evalOnSelector(selector, expression, arg, null);
}
/**
* The method finds an element matching the specified selector within the page and passes it as a first argument to
* <strong>NOTE:</strong> This method does not wait for the element to pass actionability checks and therefore can lead to the flaky tests. Use
* {@link Locator#evaluate Locator.evaluate()}, other {@code Locator} helper methods or web-first assertions instead.
*
* <p> The method finds an element matching the specified selector within the page and passes it as a first argument to
* {@code expression}. If no elements match the selector, the method throws an error. Returns the value of {@code expression}.
*
* <p> If {@code expression} returns a <a
@@ -3496,7 +3524,10 @@ public interface Page extends AutoCloseable {
return evalOnSelector(selector, expression, null);
}
/**
* The method finds an element matching the specified selector within the page and passes it as a first argument to
* <strong>NOTE:</strong> This method does not wait for the element to pass actionability checks and therefore can lead to the flaky tests. Use
* {@link Locator#evaluate Locator.evaluate()}, other {@code Locator} helper methods or web-first assertions instead.
*
* <p> The method finds an element matching the specified selector within the page and passes it as a first argument to
* {@code expression}. If no elements match the selector, the method throws an error. Returns the value of {@code expression}.
*
* <p> If {@code expression} returns a <a
@@ -3520,7 +3551,10 @@ public interface Page extends AutoCloseable {
*/
Object evalOnSelector(String selector, String expression, Object arg, EvalOnSelectorOptions options);
/**
* The method finds all elements matching the specified selector within the page and passes an array of matched elements as
* <strong>NOTE:</strong> In most cases, {@link Locator#evaluateAll Locator.evaluateAll()}, other {@code Locator} helper methods and web-first
* assertions do a better job.
*
* <p> The method finds all elements matching the specified selector within the page and passes an array of matched elements as
* a first argument to {@code expression}. Returns the result of {@code expression} invocation.
*
* <p> If {@code expression} returns a <a
@@ -3541,7 +3575,10 @@ public interface Page extends AutoCloseable {
return evalOnSelectorAll(selector, expression, null);
}
/**
* The method finds all elements matching the specified selector within the page and passes an array of matched elements as
* <strong>NOTE:</strong> In most cases, {@link Locator#evaluateAll Locator.evaluateAll()}, other {@code Locator} helper methods and web-first
* assertions do a better job.
*
* <p> The method finds all elements matching the specified selector within the page and passes an array of matched elements as
* a first argument to {@code expression}. Returns the result of {@code expression} invocation.
*
* <p> If {@code expression} returns a <a
@@ -3586,7 +3623,7 @@ public interface Page extends AutoCloseable {
*
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link Page#evaluate Page.evaluate()}:
* <pre>{@code
* ElementHandle bodyHandle = page.querySelector("body");
* ElementHandle bodyHandle = page.evaluate("document.body");
* String html = (String) page.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -3625,7 +3662,7 @@ public interface Page extends AutoCloseable {
*
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link Page#evaluate Page.evaluate()}:
* <pre>{@code
* ElementHandle bodyHandle = page.querySelector("body");
* ElementHandle bodyHandle = page.evaluate("document.body");
* String html = (String) page.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -3971,6 +4008,19 @@ public interface Page extends AutoCloseable {
* @param url A glob pattern, regex pattern or predicate receiving frame's {@code url} as a [URL] object.
*/
Frame frameByUrl(Predicate<String> url);
/**
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
* that iframe. Following snippet locates element with text "Submit" in the iframe with id {@code my-frame}, like {@code <iframe
* id="my-frame">}:
* <pre>{@code
* Locator locator = page.frameLocator("#my-iframe").locator("text=Submit");
* locator.click();
* }</pre>
*
* @param selector A selector to use when resolving DOM element. See <a href="https://playwright.dev/java/docs/selectors/">working with
* selectors</a> for more details.
*/
FrameLocator frameLocator(String selector);
/**
* An array of all frames attached to the page.
*/
@@ -4500,9 +4550,10 @@ public interface Page extends AutoCloseable {
*/
void press(String selector, String key, PressOptions options);
/**
* The method finds an element matching the specified selector within the page. If no elements match the selector, the
* return value resolves to {@code null}. To wait for an element on the page, use {@link Page#waitForSelector
* Page.waitForSelector()}.
* <strong>NOTE:</strong> The use of {@code ElementHandle} is discouraged, use {@code Locator} objects and web-first assertions instead.
*
* <p> The method finds an element matching the specified selector within the page. If no elements match the selector, the
* return value resolves to {@code null}. To wait for an element on the page, use {@link Locator#waitFor Locator.waitFor()}.
*
* <p> Shortcut for main frame's {@link Frame#querySelector Frame.querySelector()}.
*
@@ -4513,9 +4564,10 @@ public interface Page extends AutoCloseable {
return querySelector(selector, null);
}
/**
* The method finds an element matching the specified selector within the page. If no elements match the selector, the
* return value resolves to {@code null}. To wait for an element on the page, use {@link Page#waitForSelector
* Page.waitForSelector()}.
* <strong>NOTE:</strong> The use of {@code ElementHandle} is discouraged, use {@code Locator} objects and web-first assertions instead.
*
* <p> The method finds an element matching the specified selector within the page. If no elements match the selector, the
* return value resolves to {@code null}. To wait for an element on the page, use {@link Locator#waitFor Locator.waitFor()}.
*
* <p> Shortcut for main frame's {@link Frame#querySelector Frame.querySelector()}.
*
@@ -4524,7 +4576,9 @@ public interface Page extends AutoCloseable {
*/
ElementHandle querySelector(String selector, QuerySelectorOptions options);
/**
* The method finds all elements matching the specified selector within the page. If no elements match the selector, the
* <strong>NOTE:</strong> The use of {@code ElementHandle} is discouraged, use {@code Locator} objects and web-first assertions instead.
*
* <p> The method finds all elements matching the specified selector within the page. If no elements match the selector, the
* return value resolves to {@code []}.
*
* <p> Shortcut for main frame's {@link Frame#querySelectorAll Frame.querySelectorAll()}.
@@ -4534,15 +4588,15 @@ public interface Page extends AutoCloseable {
*/
List<ElementHandle> querySelectorAll(String selector);
/**
* Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the
* last redirect.
* This method reloads the current page, in the same way as if the user had triggered a browser refresh. Returns the main
* resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect.
*/
default Response reload() {
return reload(null);
}
/**
* Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the
* last redirect.
* This method reloads the current page, in the same way as if the user had triggered a browser refresh. Returns the main
* resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect.
*/
Response reload(ReloadOptions options);
/**
@@ -5454,7 +5508,9 @@ public interface Page extends AutoCloseable {
* Browser#newContext Browser.newContext()} allows to set viewport size (and more) for all pages in the context at once.
*
* <p> {@code page.setViewportSize} will resize the page. A lot of websites don't expect phones to change size, so you should set the
* viewport size before navigating to the page.
* viewport size before navigating to the page. {@link Page#setViewportSize Page.setViewportSize()} will also reset
* {@code screen} size, use {@link Browser#newContext Browser.newContext()} with {@code screen} and {@code viewport} parameters if you need
* better control of these properties.
* <pre>{@code
* Page page = browser.newPage();
* page.setViewportSize(640, 480);
@@ -6306,6 +6362,9 @@ public interface Page extends AutoCloseable {
* Returns when element specified by selector satisfies {@code state} option. Returns {@code null} if waiting for {@code hidden} or
* {@code detached}.
*
* <p> <strong>NOTE:</strong> Playwright automatically waits for element to be ready before performing an action. Using {@code Locator} objects and
* web-first assertions make the code wait-for-selector-free.
*
* <p> Wait for the {@code selector} to satisfy {@code state} option (either appear/disappear from dom, or become visible/hidden). If at
* the moment of calling the method {@code selector} already satisfies the condition, the method will return immediately. If the
* selector doesn't satisfy the condition for the {@code timeout} milliseconds, the function will throw.
@@ -6341,6 +6400,9 @@ public interface Page extends AutoCloseable {
* Returns when element specified by selector satisfies {@code state} option. Returns {@code null} if waiting for {@code hidden} or
* {@code detached}.
*
* <p> <strong>NOTE:</strong> Playwright automatically waits for element to be ready before performing an action. Using {@code Locator} objects and
* web-first assertions make the code wait-for-selector-free.
*
* <p> Wait for the {@code selector} to satisfy {@code state} option (either appear/disappear from dom, or become visible/hidden). If at
* the moment of calling the method {@code selector} already satisfies the condition, the method will return immediately. If the
* selector doesn't satisfy the condition for the {@code timeout} milliseconds, the function will throw.
@@ -6394,7 +6456,9 @@ public interface Page extends AutoCloseable {
*
* <p> Shortcut for main frame's {@link Frame#waitForURL Frame.waitForURL()}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
default void waitForURL(String url) {
waitForURL(url, null);
@@ -6408,7 +6472,9 @@ public interface Page extends AutoCloseable {
*
* <p> Shortcut for main frame's {@link Frame#waitForURL Frame.waitForURL()}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
void waitForURL(String url, WaitForURLOptions options);
/**
@@ -6420,7 +6486,9 @@ public interface Page extends AutoCloseable {
*
* <p> Shortcut for main frame's {@link Frame#waitForURL Frame.waitForURL()}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
default void waitForURL(Pattern url) {
waitForURL(url, null);
@@ -6434,7 +6502,9 @@ public interface Page extends AutoCloseable {
*
* <p> Shortcut for main frame's {@link Frame#waitForURL Frame.waitForURL()}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
void waitForURL(Pattern url, WaitForURLOptions options);
/**
@@ -6446,7 +6516,9 @@ public interface Page extends AutoCloseable {
*
* <p> Shortcut for main frame's {@link Frame#waitForURL Frame.waitForURL()}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
default void waitForURL(Predicate<String> url) {
waitForURL(url, null);
@@ -6460,7 +6532,9 @@ public interface Page extends AutoCloseable {
*
* <p> Shortcut for main frame's {@link Frame#waitForURL Frame.waitForURL()}.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the
* parameter is a string without wilcard characters, the method will wait for navigation to URL that is exactly equal to
* the string.
*/
void waitForURL(Predicate<String> url, WaitForURLOptions options);
/**
@@ -62,11 +62,11 @@ public interface Selectors {
* Page page = browser.newPage();
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* ElementHandle button = page.querySelector("tag=button");
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* int buttonCount = (int) page.locator("tag=button").count();
* browser.close();
* }</pre>
*
@@ -97,11 +97,11 @@ public interface Selectors {
* Page page = browser.newPage();
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* ElementHandle button = page.querySelector("tag=button");
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* int buttonCount = (int) page.locator("tag=button").count();
* browser.close();
* }</pre>
*
@@ -130,11 +130,11 @@ public interface Selectors {
* Page page = browser.newPage();
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* ElementHandle button = page.querySelector("tag=button");
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* int buttonCount = (int) page.locator("tag=button").count();
* browser.close();
* }</pre>
*
@@ -165,11 +165,11 @@ public interface Selectors {
* Page page = browser.newPage();
* page.setContent("<div><button>Click me</button></div>");
* // Use the selector prefixed with its name.
* ElementHandle button = page.querySelector("tag=button");
* Locator button = page.locator("tag=button");
* // Combine it with other selector engines.
* page.click("tag=div >> text=\"Click me\"");
* // Can use it in any methods supporting selectors.
* int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length");
* int buttonCount = (int) page.locator("tag=button").count();
* browser.close();
* }</pre>
*
@@ -51,6 +51,10 @@ public interface Tracing {
* Whether to capture DOM snapshot on every action.
*/
public Boolean snapshots;
/**
* Trace name to be shown in the Trace Viewer.
*/
public String title;
/**
* If specified, the trace is going to be saved into the file with the given name inside the {@code tracesDir} folder specified
@@ -74,6 +78,27 @@ public interface Tracing {
this.snapshots = snapshots;
return this;
}
/**
* Trace name to be shown in the Trace Viewer.
*/
public StartOptions setTitle(String title) {
this.title = title;
return this;
}
}
class StartChunkOptions {
/**
* Trace name to be shown in the Trace Viewer.
*/
public String title;
/**
* Trace name to be shown in the Trace Viewer.
*/
public StartChunkOptions setTitle(String title) {
this.title = title;
return this;
}
}
class StopOptions {
/**
@@ -157,7 +182,34 @@ public interface Tracing {
* .setPath(Paths.get("trace2.zip")));
* }</pre>
*/
void startChunk();
default void startChunk() {
startChunk(null);
}
/**
* Start a new trace chunk. If you'd like to record multiple traces on the same {@code BrowserContext}, use {@link Tracing#start
* Tracing.start()} once, and then create multiple trace chunks with {@link Tracing#startChunk Tracing.startChunk()} and
* {@link Tracing#stopChunk Tracing.stopChunk()}.
* <pre>{@code
* context.tracing().start(new Tracing.StartOptions()
* .setScreenshots(true)
* .setSnapshots(true));
* Page page = context.newPage();
* page.navigate("https://playwright.dev");
*
* context.tracing().startChunk();
* page.click("text=Get Started");
* // Everything between startChunk and stopChunk will be recorded in the trace.
* context.tracing().stopChunk(new Tracing.StopChunkOptions()
* .setPath(Paths.get("trace1.zip")));
*
* context.tracing().startChunk();
* page.navigate("http://example.com");
* // Save a second trace file with different actions.
* context.tracing().stopChunk(new Tracing.StopChunkOptions()
* .setPath(Paths.get("trace2.zip")));
* }</pre>
*/
void startChunk(StartChunkOptions options);
/**
* Stop tracing.
*/
@@ -457,14 +457,27 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
private void unroute(UrlMatcher matcher, Consumer<Route> handler) {
withLogging("BrowserContext.unroute", () -> {
routes.remove(matcher, handler);
if (routes.size() == 0) {
JsonObject params = new JsonObject();
params.addProperty("enabled", false);
sendMessage("setNetworkInterceptionEnabled", params);
}
maybeDisableNetworkInterception();
});
}
private void maybeDisableNetworkInterception() {
if (routes.size() == 0) {
JsonObject params = new JsonObject();
params.addProperty("enabled", false);
sendMessage("setNetworkInterceptionEnabled", params);
}
}
void handleRoute(Route route) {
boolean handled = routes.handle(route);
if (handled) {
maybeDisableNetworkInterception();
} else {
route.resume();
}
}
void pause() {
sendMessage("pause");
}
@@ -473,10 +486,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
protected void handleEvent(String event, JsonObject params) {
if ("route".equals(event)) {
Route route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
boolean handled = routes.handle(route);
if (!handled) {
route.resume();
}
handleRoute(route);
} else if ("page".equals(event)) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
pages.add(page);
@@ -71,7 +71,6 @@ class BrowserImpl extends ChannelOwner implements Browser {
} catch (IOException e) {
throw new PlaywrightException("Failed to close browser connection", e);
}
notifyRemoteClosed();
return;
}
try {
@@ -16,6 +16,7 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Browser;
@@ -23,12 +24,7 @@ import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.PlaywrightException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
import static com.microsoft.playwright.impl.Serialization.gson;
@@ -58,51 +54,40 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
}
private Browser connectImpl(String wsEndpoint, ConnectOptions options) {
try {
Duration timeout = Duration.ofDays(1);
Map<String, String> headers = Collections.emptyMap();
Duration slowMo = null;
if (options != null) {
if (options.timeout != null) {
timeout = Duration.ofMillis(Math.round(options.timeout));
}
if (options.headers != null) {
headers = options.headers;
}
if (options.slowMo != null) {
slowMo = Duration.ofMillis(options.slowMo.intValue());
}
}
WebSocketTransport transport = new WebSocketTransport(new URI(wsEndpoint), headers, timeout, slowMo);
Connection connection = new Connection(transport);
PlaywrightImpl playwright = connection.initializePlaywright();
if (!playwright.initializer.has("preLaunchedBrowser")) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace(System.err);
}
throw new PlaywrightException("Malformed endpoint. Did you use launchServer method?");
}
playwright.initSharedSelectors(this.connection.getExistingObject("Playwright"));
BrowserImpl browser = connection.getExistingObject(playwright.initializer.getAsJsonObject("preLaunchedBrowser").get("guid").getAsString());
browser.isRemote = true;
browser.isConnectedOverWebSocket = true;
Consumer<WebSocketTransport> connectionCloseListener = t -> browser.notifyRemoteClosed();
transport.onClose(connectionCloseListener);
browser.onDisconnected(b -> {
playwright.unregisterSelectors();
transport.offClose(connectionCloseListener);
try {
connection.close();
} catch (IOException e) {
e.printStackTrace(System.err);
}
});
return browser;
} catch (URISyntaxException e) {
throw new PlaywrightException("Failed to connect", e);
if (options == null) {
options = new ConnectOptions();
}
// We don't use gson() here as the headers map should be serialized to a json object.
JsonObject params = new Gson().toJsonTree(options).getAsJsonObject();
params.addProperty("wsEndpoint", wsEndpoint);
JsonObject json = sendMessage("connect", params).getAsJsonObject();
JsonPipe pipe = connection.getExistingObject(json.getAsJsonObject("pipe").get("guid").getAsString());
Connection connection = new Connection(pipe);
PlaywrightImpl playwright = connection.initializePlaywright();
if (!playwright.initializer.has("preLaunchedBrowser")) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace(System.err);
}
throw new PlaywrightException("Malformed endpoint. Did you use launchServer method?");
}
playwright.initSharedSelectors(this.connection.getExistingObject("Playwright"));
BrowserImpl browser = connection.getExistingObject(playwright.initializer.getAsJsonObject("preLaunchedBrowser").get("guid").getAsString());
browser.isRemote = true;
browser.isConnectedOverWebSocket = true;
Consumer<JsonPipe> connectionCloseListener = t -> browser.notifyRemoteClosed();
pipe.onClose(connectionCloseListener);
browser.onDisconnected(b -> {
playwright.unregisterSelectors();
pipe.offClose(connectionCloseListener);
try {
connection.close();
} catch (IOException e) {
e.printStackTrace(System.err);
}
});
return browser;
}
@Override
@@ -85,6 +85,9 @@ public class Connection {
}
Connection(Transport transport) {
if (isLogging) {
transport = new TransportLogger(transport);
}
this.transport = transport;
root = new Root(this);
String srcRoot = System.getenv("PLAYWRIGHT_JAVA_SRC");
@@ -172,11 +175,7 @@ public class Connection {
metadata.addProperty("apiName", apiName);
}
message.add("metadata", metadata);
String messageString = gson().toJson(message);
if (isLogging) {
logWithTimestamp("SEND ► " + messageString);
}
transport.send(messageString);
transport.send(message);
return result;
}
@@ -200,16 +199,13 @@ public class Connection {
}
void processOneMessage() {
String messageString = transport.poll(Duration.ofMillis(10));
if (messageString == null) {
JsonObject message = transport.poll(Duration.ofMillis(10));
if (message == null) {
return;
}
if (isLogging) {
logWithTimestamp("◀ RECV " + messageString);
}
Gson gson = gson();
Message message = gson.fromJson(messageString, Message.class);
dispatch(message);
Message messageObj = gson.fromJson(message, Message.class);
dispatch(messageObj);
}
private void dispatch(Message message) {
@@ -305,7 +301,7 @@ public class Connection {
case "ElementHandle":
result = new ElementHandleImpl(parent, type, guid, initializer);
break;
case "FetchRequest":
case "APIRequestContext":
// Create fake object as this API is experimental an only exposed in Node.js.
result = new ChannelOwner(parent, type, guid, initializer);
break;
@@ -315,6 +311,9 @@ public class Connection {
case "JSHandle":
result = new JSHandleImpl(parent, type, guid, initializer);
break;
case "JsonPipe":
result = new JsonPipe(parent, type, guid, initializer);
break;
case "Page":
result = new PageImpl(parent, type, guid, initializer);
break;
@@ -31,7 +31,7 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.options.LoadState.*;
import static com.microsoft.playwright.options.WaitUntilState.*;
import static com.microsoft.playwright.impl.Serialization.*;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
@@ -40,7 +40,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
private String url;
FrameImpl parentFrame;
Set<FrameImpl> childFrames = new LinkedHashSet<>();
private final Set<LoadState> loadStates = new HashSet<>();
private final Set<WaitUntilState> loadStates = new HashSet<>();
enum InternalEventType { NAVIGATED, LOADSTATE }
private final ListenerCollection<InternalEventType> internalListeners = new ListenerCollection<>();
@@ -61,11 +61,12 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
}
private static LoadState loadStateFromProtocol(String value) {
private static WaitUntilState loadStateFromProtocol(String value) {
switch (value) {
case "load": return LOAD;
case "domcontentloaded": return DOMCONTENTLOADED;
case "networkidle": return NETWORKIDLE;
case "commit": return COMMIT;
default: throw new PlaywrightException("Unexpected value: " + value);
}
}
@@ -357,6 +358,11 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public FrameLocator frameLocator(String selector) {
return new FrameLocatorImpl(this, selector);
}
ElementHandle frameElementImpl() {
JsonObject json = sendMessage("frameElement").getAsJsonObject();
return connection.getExistingObject(json.getAsJsonObject("element").get("guid").getAsString());
@@ -560,7 +566,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public Locator locator(String selector) {
public LocatorImpl locator(String selector) {
return new LocatorImpl(this, selector);
}
@@ -801,6 +807,10 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
void waitForLoadStateImpl(LoadState state, WaitForLoadStateOptions options) {
waitForLoadStateImpl(convertViaJson(state, WaitUntilState.class), options);
}
private void waitForLoadStateImpl(WaitUntilState state, WaitForLoadStateOptions options) {
if (options == null) {
options = new WaitForLoadStateOptions();
}
@@ -815,11 +825,11 @@ public class FrameImpl extends ChannelOwner implements Frame {
runUntil(() -> {}, new WaitableRace<>(waitables));
}
private class WaitForLoadStateHelper implements Waitable<Void>, Consumer<LoadState> {
private final LoadState expectedState;
private class WaitForLoadStateHelper implements Waitable<Void>, Consumer<WaitUntilState> {
private final WaitUntilState expectedState;
private boolean isDone;
WaitForLoadStateHelper(LoadState state) {
WaitForLoadStateHelper(WaitUntilState state) {
expectedState = state;
isDone = loadStates.contains(state);
if (!isDone) {
@@ -828,7 +838,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public void accept(LoadState state) {
public void accept(WaitUntilState state) {
if (expectedState.equals(state)) {
isDone = true;
dispose();
@@ -851,13 +861,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
private class WaitForNavigationHelper implements Waitable<Response>, Consumer<JsonObject> {
private final UrlMatcher matcher;
private final LoadState expectedLoadState;
private final WaitUntilState expectedLoadState;
private WaitForLoadStateHelper loadStateHelper;
private RequestImpl request;
private RuntimeException exception;
WaitForNavigationHelper(UrlMatcher matcher, LoadState expectedLoadState) {
WaitForNavigationHelper(UrlMatcher matcher, WaitUntilState expectedLoadState) {
this.matcher = matcher;
this.expectedLoadState = expectedLoadState;
internalListeners.add(InternalEventType.NAVIGATED, this);
@@ -935,7 +945,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
if (matcher == null) {
matcher = UrlMatcher.forOneOf(page.context().baseUrl, options.url);
}
waitables.add(new WaitForNavigationHelper(matcher, convertViaJson(options.waitUntil, LoadState.class)));
waitables.add(new WaitForNavigationHelper(matcher, options.waitUntil));
waitables.add(page.createWaitForCloseHelper());
waitables.add(page.createWaitableFrameDetach(this));
waitables.add(page.createWaitableNavigationTimeout(options.timeout));
@@ -948,11 +958,16 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
ElementHandle waitForSelectorImpl(String selector, WaitForSelectorOptions options) {
return waitForSelectorImpl(selector, options, false);
}
ElementHandle waitForSelectorImpl(String selector, WaitForSelectorOptions options, boolean omitReturnValue) {
if (options == null) {
options = new WaitForSelectorOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("selector", selector);
params.addProperty("omitReturnValue", omitReturnValue);
JsonElement json = sendMessage("waitForSelector", params);
JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
if (element == null) {
@@ -967,13 +982,9 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
void waitForTimeoutImpl(double timeout) {
runUntil(() -> {}, new WaitableTimeout<Void>(timeout) {
@Override
public Void get() {
// Override to not throw.
return null;
}
});
JsonObject params = new JsonObject();
params.addProperty("timeout", timeout);
sendMessage("waitForTimeout", params);
}
@Override
@@ -1000,8 +1011,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
options = new WaitForURLOptions();
}
if (matcher.test(url())) {
waitForLoadStateImpl(convertViaJson(options.waitUntil, LoadState.class),
convertViaJson(options, WaitForLoadStateOptions.class));
waitForLoadStateImpl(options.waitUntil, convertViaJson(options, WaitForLoadStateOptions.class));
return;
}
waitForNavigationImpl(() -> {}, convertViaJson(options, WaitForNavigationOptions.class), matcher);
@@ -1011,7 +1021,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
if ("loadstate".equals(event)) {
JsonElement add = params.get("add");
if (add != null) {
LoadState state = loadStateFromProtocol(add.getAsString());
WaitUntilState state = loadStateFromProtocol(add.getAsString());
loadStates.add(state);
internalListeners.notify(InternalEventType.LOADSTATE, state);
}
@@ -0,0 +1,54 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.FrameLocator;
class FrameLocatorImpl implements FrameLocator {
private final FrameImpl frame;
private final String frameSelector;
FrameLocatorImpl(FrameImpl frame, String selector) {
this.frame = frame;
this.frameSelector = selector;
}
@Override
public FrameLocator first() {
return new FrameLocatorImpl(frame, frameSelector + " >> nth=0");
}
@Override
public FrameLocatorImpl frameLocator(String selector) {
return new FrameLocatorImpl(frame, frameSelector + " >> control=enter-frame >> " + selector);
}
@Override
public FrameLocator last() {
return new FrameLocatorImpl(frame, frameSelector + " >> nth=-1");
}
@Override
public LocatorImpl locator(String selector) {
return new LocatorImpl(frame, frameSelector + " >> control=enter-frame >> " + selector);
}
@Override
public FrameLocator nth(int index) {
return new FrameLocatorImpl(frame, frameSelector + " >> nth=" + index);
}
}
@@ -0,0 +1,109 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.LinkedList;
import java.util.Queue;
import java.util.function.Consumer;
import static com.microsoft.playwright.impl.Serialization.gson;
class JsonPipe extends ChannelOwner implements Transport {
private final Queue<JsonObject> incoming = new LinkedList<>();
private ListenerCollection<EventType> listeners = new ListenerCollection<>();
private enum EventType { CLOSE }
private boolean isClosed;
JsonPipe(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@Override
public void send(JsonObject message) {
checkIfClosed();
JsonObject params = new JsonObject();
params.add("message", message);
sendMessage("send", params);
}
@Override
public JsonObject poll(Duration timeout) {
Instant start = Instant.now();
return runUntil(() -> {}, new Waitable<JsonObject>() {
JsonObject message;
@Override
public boolean isDone() {
if (!incoming.isEmpty()) {
message = incoming.remove();
return true;
}
checkIfClosed();
if (Duration.between(start, Instant.now()).compareTo(timeout) > 0) {
return true;
}
return false;
}
@Override
public JsonObject get() {
return message;
}
@Override
public void dispose() {
}
});
}
@Override
public void close() throws IOException {
if (!isClosed) {
sendMessage("close");
}
}
void onClose(Consumer<JsonPipe> handler) {
listeners.add(EventType.CLOSE, handler);
}
void offClose(Consumer<JsonPipe> handler) {
listeners.remove(EventType.CLOSE, handler);
}
@Override
protected void handleEvent(String event, JsonObject params) {
if ("message".equals(event)) {
incoming.add(params.get("message").getAsJsonObject());
} else if ("closed".equals(event)) {
isClosed = true;
listeners.notify(EventType.CLOSE, this);
}
}
private void checkIfClosed() {
if (isClosed) {
throw new PlaywrightException("Browser has been closed");
}
}
}
@@ -1,9 +1,8 @@
package com.microsoft.playwright.impl;
import com.microsoft.playwright.ElementHandle;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.JSHandle;
import com.microsoft.playwright.Locator;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.BoundingBox;
import com.microsoft.playwright.options.FilePayload;
import com.microsoft.playwright.options.SelectOption;
@@ -13,6 +12,8 @@ import java.nio.file.Path;
import java.util.List;
import java.util.function.BiFunction;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Serialization.serializeArgument;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
class LocatorImpl implements Locator {
@@ -147,6 +148,11 @@ class LocatorImpl implements Locator {
frame.focus(selector, convertViaJson(options, Frame.FocusOptions.class).setStrict(true));
}
@Override
public FrameLocatorImpl frameLocator(String selector) {
return new FrameLocatorImpl(frame, this.selector + " >> " + selector);
}
@Override
public String getAttribute(String name, GetAttributeOptions options) {
if (options == null) {
@@ -399,8 +405,36 @@ class LocatorImpl implements Locator {
frame.uncheck(selector, convertViaJson(options, Frame.UncheckOptions.class).setStrict(true));
}
@Override
public void waitFor(WaitForOptions options) {
if (options == null) {
options = new WaitForOptions();
}
waitForImpl(options);
}
private void waitForImpl(WaitForOptions options) {
frame.withLogging("Locator.waitFor", () -> frame.waitForSelectorImpl(selector, convertViaJson(options, Frame.WaitForSelectorOptions.class).setStrict(true), true));
}
@Override
public String toString() {
return "Locator@" + selector;
}
FrameExpectResult expect(String expression, FrameExpectOptions options) {
return frame.withLogging("Locator.expect", () -> expectImpl(expression, options));
}
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);
JsonElement json = frame.sendMessage("expect", params);
FrameExpectResult result = gson().fromJson(json, FrameExpectResult.class);
return result;
}
}
@@ -198,11 +198,10 @@ public class PageImpl extends ChannelOwner implements Page {
} else if ("route".equals(event)) {
Route route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
boolean handled = routes.handle(route);
if (!handled) {
handled = browserContext.routes.handle(route);
}
if (!handled) {
route.resume();
if (handled) {
maybeDisableNetworkInterception();
} else {
browserContext.handleRoute(route);
}
} else if ("video".equals(event)) {
String artifactGuid = params.getAsJsonObject("artifact").get("guid").getAsString();
@@ -738,6 +737,11 @@ public class PageImpl extends ChannelOwner implements Page {
return frameFor(new UrlMatcher(predicate));
}
@Override
public FrameLocator frameLocator(String selector) {
return mainFrame.frameLocator(selector);
}
private Frame frameFor(UrlMatcher matcher) {
for (Frame frame : frames) {
if (matcher.test(frame.url())) {
@@ -875,7 +879,7 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public Locator locator(String selector) {
public LocatorImpl locator(String selector) {
return mainFrame.locator(selector);
}
@@ -1181,14 +1185,18 @@ public class PageImpl extends ChannelOwner implements Page {
private void unroute(UrlMatcher matcher, Consumer<Route> handler) {
withLogging("Page.unroute", () -> {
routes.remove(matcher, handler);
if (routes.size() == 0) {
JsonObject params = new JsonObject();
params.addProperty("enabled", false);
sendMessage("setNetworkInterceptionEnabled", params);
}
maybeDisableNetworkInterception();
});
}
private void maybeDisableNetworkInterception() {
if (routes.size() == 0) {
JsonObject params = new JsonObject();
params.addProperty("enabled", false);
sendMessage("setNetworkInterceptionEnabled", params);
}
}
@Override
public String url() {
return mainFrame.url();
@@ -15,6 +15,7 @@
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import java.io.*;
@@ -24,8 +25,10 @@ import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import static com.microsoft.playwright.impl.Serialization.gson;
public class PipeTransport implements Transport {
private final BlockingQueue<String> incoming = new ArrayBlockingQueue<>(1000);
private final BlockingQueue<JsonObject> incoming = new ArrayBlockingQueue<>(1000);
private final BlockingQueue<String> outgoing= new ArrayBlockingQueue<>(1000);
private final ReaderThread readerThread;
@@ -42,24 +45,27 @@ public class PipeTransport implements Transport {
}
@Override
public void send(String message) {
public void send(JsonObject message) {
if (isClosed) {
throw new PlaywrightException("Playwright connection closed");
}
try {
outgoing.put(message);
// We could serialize the message on the IO thread but there is no guarantee
// that the message object won't be modified on this thread after it's added
// to the queue.
outgoing.put(gson().toJson(message));
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to send message", e);
}
}
@Override
public String poll(Duration timeout) {
public JsonObject poll(Duration timeout) {
if (isClosed) {
throw new PlaywrightException("Playwright connection closed");
}
try {
String message = incoming.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
JsonObject message = incoming.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
if (message == null && readerThread.exception != null) {
try {
close();
@@ -91,7 +97,7 @@ public class PipeTransport implements Transport {
class ReaderThread extends Thread {
private final DataInputStream in;
private final BlockingQueue<String> queue;
private final BlockingQueue<JsonObject> queue;
volatile boolean isClosing;
volatile Exception exception;
@@ -107,7 +113,7 @@ class ReaderThread extends Thread {
}
}
ReaderThread(DataInputStream in, BlockingQueue<String> queue) {
ReaderThread(DataInputStream in, BlockingQueue<JsonObject> queue) {
this.in = in;
this.queue = queue;
}
@@ -116,7 +122,8 @@ class ReaderThread extends Thread {
public void run() {
while (!isInterrupted()) {
try {
queue.put(readMessage());
JsonObject message = gson().fromJson(readMessage(), JsonObject.class);
queue.put(message);
} catch (IOException e) {
if (!isInterrupted() && !isClosing) {
exception = e;
@@ -18,18 +18,12 @@
package com.microsoft.playwright.impl;
class Binary {
}
import java.util.List;
class Channel {
String guid;
}
class Metadata{
String stack;
}
class SerializedValue{
Number n;
Boolean b;
@@ -51,45 +45,11 @@ class SerializedValue{
Number h;
}
class SerializedArgument{
SerializedValue value;
Channel[] handles;
}
class AXNode{
String role;
String name;
String valueString;
Number valueNumber;
String description;
String keyshortcuts;
String roledescription;
String valuetext;
Boolean disabled;
Boolean expanded;
Boolean focused;
Boolean modal;
Boolean multiline;
Boolean multiselectable;
Boolean readonly;
Boolean required;
Boolean selected;
// Possible values: { 'checked, 'unchecked, 'mixed }
String checked;
// Possible values: { 'pressed, 'released, 'mixed }
String pressed;
Number level;
Number valuemin;
Number valuemax;
String autocomplete;
String haspopup;
String invalid;
String orientation;
AXNode[] children;
}
class SerializedError{
public static class Error {
String message;
@@ -119,3 +79,28 @@ class SerializedError{
}
}
class ExpectedTextValue {
String string;
String regexSource;
String regexFlags;
Boolean matchSubstring;
Boolean normalizeWhiteSpace;
}
class FrameExpectOptions {
Object expressionArg;
List<ExpectedTextValue> expectedText;
Integer expectedNumber;
SerializedArgument expectedValue;
Boolean useInnerText;
boolean isNot;
Double timeout;
}
class FrameExpectResult {
boolean matches;
SerializedValue received;
List<String> log;
}
@@ -167,13 +167,8 @@ public class RequestImpl extends ChannelOwner implements Request {
if (rawHeaders != null) {
return rawHeaders;
}
ResponseImpl response = response();
// there is no response, so should we return the headers we have now?
if (response == null) {
return headers;
}
JsonArray rawHeadersJson = response.withLogging("Request.allHeaders", () -> {
JsonObject result = response.sendMessage("rawRequestHeaders").getAsJsonObject();
JsonArray rawHeadersJson = withLogging("Request.allHeaders", () -> {
JsonObject result = sendMessage("rawRequestHeaders").getAsJsonObject();
return result.getAsJsonArray("headers");
});
@@ -38,7 +38,7 @@ public class RouteImpl extends ChannelOwner implements Route {
withLogging("Route.abort", () -> {
JsonObject params = new JsonObject();
params.addProperty("errorCode", errorCode);
sendMessage("abort", params);
sendMessageAsync("abort", params);
});
}
@@ -73,7 +73,7 @@ public class RouteImpl extends ChannelOwner implements Route {
String base64 = Base64.getEncoder().encodeToString(bytes);
params.addProperty("postData", base64);
}
sendMessage("continue", params);
sendMessageAsync("continue", params);
}
@Override
@@ -128,7 +128,7 @@ public class RouteImpl extends ChannelOwner implements Route {
params.add("headers", Serialization.toProtocol(headers));
params.addProperty("isBase64", isBase64);
params.addProperty("body", body);
sendMessage("fulfill", params);
sendMessageAsync("fulfill", params);
}
@Override
@@ -50,6 +50,10 @@ class Router {
handler.accept(route);
return true;
}
boolean isDone() {
return times != null && times <= 0;
}
}
void add(UrlMatcher matcher, Consumer<Route> handler, Integer times) {
@@ -69,6 +73,9 @@ class Router {
boolean handle(Route route) {
for (RouteInfo info : routes) {
if (info.handle(route)) {
if (info.isDone()) {
routes.remove(info);
}
return true;
}
}
@@ -33,29 +33,26 @@ import java.nio.file.Path;
import java.util.*;
class Serialization {
private static Gson gson;
private static Gson gson = new GsonBuilder()
.registerTypeAdapter(SameSiteAttribute.class, new SameSiteAdapter().nullSafe())
.registerTypeAdapter(BrowserChannel.class, new ToLowerCaseAndDashSerializer<BrowserChannel>())
.registerTypeAdapter(ColorScheme.class, new ToLowerCaseAndDashSerializer<ColorScheme>())
.registerTypeAdapter(Media.class, new ToLowerCaseSerializer<Media>())
.registerTypeAdapter(ForcedColors.class, new ToLowerCaseSerializer<ForcedColors>())
.registerTypeAdapter(ReducedMotion.class, new ToLowerCaseAndDashSerializer<ReducedMotion>())
.registerTypeAdapter(ScreenshotType.class, new ToLowerCaseSerializer<ScreenshotType>())
.registerTypeAdapter(MouseButton.class, new ToLowerCaseSerializer<MouseButton>())
.registerTypeAdapter(LoadState.class, new ToLowerCaseSerializer<LoadState>())
.registerTypeAdapter(WaitUntilState.class, new ToLowerCaseSerializer<WaitUntilState>())
.registerTypeAdapter(WaitForSelectorState.class, new ToLowerCaseSerializer<WaitForSelectorState>())
.registerTypeAdapter((new TypeToken<List<KeyboardModifier>>(){}).getType(), new KeyboardModifiersSerializer())
.registerTypeAdapter(Optional.class, new OptionalSerializer())
.registerTypeHierarchyAdapter(JSHandleImpl.class, new HandleSerializer())
.registerTypeAdapter((new TypeToken<Map<String, String>>(){}).getType(), new StringMapSerializer())
.registerTypeAdapter((new TypeToken<Map<String, Object>>(){}).getType(), new FirefoxUserPrefsSerializer())
.registerTypeHierarchyAdapter(Path.class, new PathSerializer()).create();;
static Gson gson() {
if (gson == null) {
gson = new GsonBuilder()
.registerTypeAdapter(SameSiteAttribute.class, new SameSiteAdapter().nullSafe())
.registerTypeAdapter(BrowserChannel.class, new ToLowerCaseAndDashSerializer<BrowserChannel>())
.registerTypeAdapter(ColorScheme.class, new ToLowerCaseAndDashSerializer<ColorScheme>())
.registerTypeAdapter(Media.class, new ToLowerCaseSerializer<Media>())
.registerTypeAdapter(ForcedColors.class, new ToLowerCaseSerializer<ForcedColors>())
.registerTypeAdapter(ReducedMotion.class, new ToLowerCaseAndDashSerializer<ReducedMotion>())
.registerTypeAdapter(ScreenshotType.class, new ToLowerCaseSerializer<ScreenshotType>())
.registerTypeAdapter(MouseButton.class, new ToLowerCaseSerializer<MouseButton>())
.registerTypeAdapter(LoadState.class, new ToLowerCaseSerializer<LoadState>())
.registerTypeAdapter(WaitUntilState.class, new ToLowerCaseSerializer<WaitUntilState>())
.registerTypeAdapter(WaitForSelectorState.class, new ToLowerCaseSerializer<WaitForSelectorState>())
.registerTypeAdapter((new TypeToken<List<KeyboardModifier>>(){}).getType(), new KeyboardModifiersSerializer())
.registerTypeAdapter(Optional.class, new OptionalSerializer())
.registerTypeHierarchyAdapter(JSHandleImpl.class, new HandleSerializer())
.registerTypeAdapter((new TypeToken<Map<String, String>>(){}).getType(), new StringMapSerializer())
.registerTypeAdapter((new TypeToken<Map<String, Object>>(){}).getType(), new FirefoxUserPrefsSerializer())
.registerTypeHierarchyAdapter(Path.class, new PathSerializer()).create();
}
return gson;
}
@@ -34,6 +34,7 @@ class TracingImpl implements Tracing {
private void stopChunkImpl(Path path) {
JsonObject params = new JsonObject();
params.addProperty("save", path != null);
params.addProperty("skipCompress", false);
JsonObject json = context.sendMessage("tracingStopChunk", params).getAsJsonObject();
if (!json.has("artifact")) {
return;
@@ -54,12 +55,20 @@ class TracingImpl implements Tracing {
}
@Override
public void startChunk() {
public void startChunk(StartChunkOptions options) {
context.withLogging("Tracing.startChunk", () -> {
context.sendMessage("tracingStartChunk");
startChunkImpl(options);
});
}
private void startChunkImpl(StartChunkOptions options) {
if (options == null) {
options = new StartChunkOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
context.sendMessage("tracingStartChunk", params);
}
private void startImpl(StartOptions options) {
if (options == null) {
options = new StartOptions();
@@ -16,11 +16,13 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.time.Duration;
public interface Transport {
void send(String message);
String poll(Duration timeout);
void send(JsonObject message);
JsonObject poll(Duration timeout);
void close() throws IOException;
}
@@ -0,0 +1,39 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.time.Duration;
import static com.microsoft.playwright.impl.LoggingSupport.logWithTimestamp;
import static com.microsoft.playwright.impl.Serialization.gson;
class TransportLogger implements Transport {
private final Transport transport;
TransportLogger(Transport transport) {
this.transport = transport;
}
@Override
public void send(JsonObject message) {
String messageString = gson().toJson(message);
logWithTimestamp("SEND ► " + messageString);
transport.send(message);
}
@Override
public JsonObject poll(Duration timeout) {
JsonObject message = transport.poll(timeout);
if (message != null) {
String messageString = gson().toJson(message);
logWithTimestamp("◀ RECV " + messageString);
}
return message;
}
@Override
public void close() throws IOException {
transport.close();
}
}
@@ -54,7 +54,7 @@ class UrlMatcher {
throw new PlaywrightException("Url must be String, Pattern or Predicate<String>, found: " + object.getClass().getTypeName());
}
private static String resolveUrl(URL baseUrl, String spec) {
static String resolveUrl(URL baseUrl, String spec) {
if (baseUrl == null) {
return spec;
}
@@ -1,129 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.microsoft.playwright.PlaywrightException;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
class WebSocketTransport implements Transport {
private final BlockingQueue<String> incoming = new LinkedBlockingQueue<>();
private final ClientConnection clientConnection;
private final Duration slowMo;
private boolean isClosed;
private volatile Exception lastError;
ListenerCollection<EventType> listeners = new ListenerCollection<>();
private enum EventType { CLOSE }
private class ClientConnection extends WebSocketClient {
ClientConnection(URI serverUri) {
super(serverUri);
}
@Override
public void onOpen(ServerHandshake handshakedata) {
}
@Override
public void onMessage(String message) {
incoming.add(message);
}
@Override
public void onClose(int code, String reason, boolean remote) {
}
@Override
public void onError(Exception ex) {
lastError = ex;
}
}
WebSocketTransport(URI uri, Map<String, String> headers, Duration timeout, Duration slowMo) {
clientConnection = new ClientConnection(uri);
for (Map.Entry<String, String> entry : headers.entrySet()) {
clientConnection.addHeader(entry.getKey(), entry.getValue());
}
try {
if (!clientConnection.connectBlocking(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
throw new PlaywrightException("Failed to connect", lastError);
}
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to connect", e);
}
this.slowMo = slowMo;
}
@Override
public void send(String message) {
checkIfClosed();
clientConnection.send(message);
}
@Override
public String poll(Duration timeout) {
checkIfClosed();
try {
String message = incoming.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
if (slowMo != null && message != null) {
Thread.sleep(slowMo.toMillis());
}
return message;
} catch (InterruptedException e) {
throw new PlaywrightException("Failed to read message", e);
}
}
@Override
public void close() throws IOException {
if (isClosed) {
return;
}
isClosed = true;
clientConnection.close();
}
void onClose(Consumer<WebSocketTransport> handler) {
listeners.add(EventType.CLOSE, handler);
}
void offClose(Consumer<WebSocketTransport> handler) {
listeners.remove(EventType.CLOSE, handler);
}
private void checkIfClosed() {
if (isClosed) {
throw new PlaywrightException("Playwright connection closed");
}
if (clientConnection.isClosed()) {
isClosed = true;
listeners.notify(EventType.CLOSE, this);
throw new PlaywrightException("Playwright connection closed");
}
}
}
@@ -23,7 +23,7 @@ public class Proxy {
*/
public String server;
/**
* Optional coma-separated domains to bypass proxy, for example {@code ".com, chromium.org, .domain.com"}.
* Optional comma-separated domains to bypass proxy, for example {@code ".com, chromium.org, .domain.com"}.
*/
public String bypass;
/**
@@ -39,7 +39,7 @@ public class Proxy {
this.server = server;
}
/**
* Optional coma-separated domains to bypass proxy, for example {@code ".com, chromium.org, .domain.com"}.
* Optional comma-separated domains to bypass proxy, for example {@code ".com, chromium.org, .domain.com"}.
*/
public Proxy setBypass(String bypass) {
this.bypass = bypass;
@@ -19,5 +19,6 @@ package com.microsoft.playwright.options;
public enum WaitUntilState {
LOAD,
DOMCONTENTLOADED,
NETWORKIDLE
NETWORKIDLE,
COMMIT
}
@@ -23,7 +23,6 @@ import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import java.security.KeyStore;
class HttpsConfiguratorImpl extends HttpsConfigurator {
@@ -52,8 +51,7 @@ class HttpsConfiguratorImpl extends HttpsConfigurator {
String password = "password";
// Generated via
// keytool -genkey -keyalg RSA -validity 36500 -keysize 4096 -dname cn=Playwright,ou=Playwright,o=Playwright,c=US -keystore keystore.jks -storepass password -keypass password
ks.load(new FileInputStream("src/test/resources/keys/keystore.jks"), password.toCharArray());
ks.load(HttpsConfiguratorImpl.class.getClassLoader().getResourceAsStream("resources/keys/keystore.jks"), password.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, password.toCharArray());
@@ -35,7 +35,6 @@ public class Server implements HttpHandler {
public final String CROSS_PROCESS_PREFIX;
public final int PORT;
public final String EMPTY_PAGE;
private final File resourcesDir;
private final Map<String, CompletableFuture<Request>> requestSubscribers = Collections.synchronizedMap(new HashMap<>());
private final Map<String, Auth> auths = Collections.synchronizedMap(new HashMap<>());
@@ -79,7 +78,6 @@ public class Server implements HttpHandler {
server.setExecutor(null); // creates a default executor
File cwd = FileSystems.getDefault().getPath(".").toFile();
resourcesDir = new File(cwd, "src/test/resources");
server.start();
}
@@ -192,32 +190,42 @@ public class Server implements HttpHandler {
if ("/".equals(path)) {
path = "/index.html";
}
File file = new File(resourcesDir, path.substring(1));
if (!file.exists()) {
// Resources from "src/test/resources/" are copied to "resources/" directory in the jar.
String resourcePath = "resources" + path;
InputStream resource = getClass().getClassLoader().getResourceAsStream(resourcePath);
if (resource == null) {
exchange.sendResponseHeaders(404, 0);
try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("File not found: " + file.getCanonicalPath());
writer.write("File not found: " + resourcePath);
}
return;
}
exchange.getResponseHeaders().add("Content-Type", mimeType(file));
OutputStream output = exchange.getResponseBody();
exchange.getResponseHeaders().add("Content-Type", mimeType(new File(resourcePath)));
ByteArrayOutputStream body = new ByteArrayOutputStream();
OutputStream output = body;
if (gzipRoutes.contains(path)) {
exchange.getResponseHeaders().add("Content-Encoding", "gzip");
}
try (FileInputStream input = new FileInputStream(file)) {
exchange.sendResponseHeaders(200, 0);
try (InputStream input = resource) {
if (gzipRoutes.contains(path)) {
output = new GZIPOutputStream(output);
}
copy(input, output);
output.close();
} catch (IOException e) {
try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) {
body.reset();
try (Writer writer = new OutputStreamWriter(output)) {
writer.write("Exception: " + e);
}
return;
}
output.close();
long contentLength = body.size();
// -1 means no body, 0 means chunked encoding.
exchange.sendResponseHeaders(200, contentLength == 0 ? -1 : contentLength);
if (contentLength > 0) {
exchange.getResponseBody().write(body.toByteArray());
}
exchange.getResponseBody().close();
}
private static String mimeType(File file) {
@@ -226,11 +226,20 @@ public class TestBrowserTypeConnect extends TestBase {
BrowserServer server = launchBrowserServer(browserType);
Browser remote = browserType.connect(server.wsEndpoint);
Page page = remote.newPage();
boolean[] disconnected = {false};
remote.onDisconnected(b -> disconnected[0] = true);
server.kill();
while (!disconnected[0]) {
try {
page.waitForTimeout(10);
} catch (PlaywrightException e) {
}
}
assertFalse(remote.isConnected());
try {
page.evaluate("1 + 1");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Playwright connection closed"));
assertTrue(e.getMessage().contains("Browser has been closed"), e.getMessage());
}
assertFalse(remote.isConnected());
}
@@ -248,7 +257,6 @@ public class TestBrowserTypeConnect extends TestBase {
try {
page.waitForTimeout(10);
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Playwright connection closed"));
}
}
assertFalse(browser.isConnected());
@@ -256,7 +264,7 @@ public class TestBrowserTypeConnect extends TestBase {
page.waitForNavigation(() -> {});
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Playwright connection closed"));
assertTrue(e.getMessage().contains("Page closed") || e.getMessage().contains("Browser has been closed"), e.getMessage());
}
}
@@ -271,7 +279,7 @@ public class TestBrowserTypeConnect extends TestBase {
page.navigate(server.PREFIX + "/one-style.html", new Page.NavigateOptions().setTimeout(60000));
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Playwright connection closed"));
assertTrue(e.getMessage().contains("Browser has been closed"));
}
}
@@ -61,14 +61,14 @@ public class TestElementHandleConvenience extends TestBase {
page.inputValue("#inner");
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Node is not an HTMLInputElement or HTMLTextAreaElement"), e.getMessage());
assertTrue(e.getMessage().contains("Node is not an <input>, <textarea> or <select> element"), e.getMessage());
}
ElementHandle handle2 = page.querySelector("#inner");
try {
handle2.inputValue();
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Node is not an HTMLInputElement or HTMLTextAreaElement"), e.getMessage());
assertTrue(e.getMessage().contains("Node is not an <input>, <textarea> or <select> element"), e.getMessage());
}
}
@@ -95,14 +95,14 @@ public class TestElementHandleConvenience extends TestBase {
page.innerText("svg");
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Not an HTMLElement"));
assertTrue(e.getMessage().contains("Node is not an HTMLElement"), e.getMessage());
}
ElementHandle handle = page.querySelector("svg");
try {
handle.innerText();
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Not an HTMLElement"));
assertTrue(e.getMessage().contains("Node is not an HTMLElement"), e.getMessage());
}
}
@@ -65,14 +65,14 @@ public class TestLocatorConvenience extends TestBase {
page.inputValue("#inner");
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Node is not an HTMLInputElement or HTMLTextAreaElement or HTMLSelectElement"));
assertTrue(e.getMessage().contains("Node is not an <input>, <textarea> or <select> element"), e.getMessage());
}
try {
Locator locator2 = page.locator("#inner");
locator2.inputValue();
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Node is not an HTMLInputElement or HTMLTextAreaElement or HTMLSelectElement"));
assertTrue(e.getMessage().contains("Node is not an <input>, <textarea> or <select> element"), e.getMessage());
}
}
@@ -99,14 +99,14 @@ public class TestLocatorConvenience extends TestBase {
page.innerText("svg");
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Not an HTMLElement"));
assertTrue(e.getMessage().contains("Node is not an HTMLElement"), e.getMessage());
}
Locator locator = page.locator("svg");
try {
locator.innerText();
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Not an HTMLElement"));
assertTrue(e.getMessage().contains("Node is not an HTMLElement"), e.getMessage());
}
}
@@ -16,9 +16,11 @@
package com.microsoft.playwright;
import com.microsoft.playwright.options.WaitForSelectorState;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestLocatorMisc extends TestBase{
@Test
@@ -30,4 +32,21 @@ public class TestLocatorMisc extends TestBase{
input.setChecked(false);
assertEquals(false, page.evaluate("checkbox.checked"));
}
@Test
void shouldWaitFor() {
page.setContent("<div></div>");
Locator locator = page.locator("span");
page.evalOnSelector("div", "div => setTimeout(() => div.innerHTML = '<span>target</span>', 500)");
locator.waitFor();
assertTrue(locator.textContent().contains("target"));
}
@Test
void shouldWaitForHidden() {
page.setContent("<div><span>target</span></div>");
Locator locator = page.locator("span");
page.evalOnSelector("div", "div => setTimeout(() => div.innerHTML = '', 500)");
locator.waitFor(new Locator.WaitForOptions().setState(WaitForSelectorState.HIDDEN));
}
}
@@ -73,6 +73,18 @@ public class TestNetworkRequest extends TestBase {
assertEquals(server.PREFIX + "/empty.html", requests.get(1).url());
}
@Test
void shouldWorkAllHeadersInsideRoute() {
List<Request> requests = new ArrayList<>();
page.route("**", route -> {
assertTrue(route.request().allHeaders().get("accept").length() > 5);
requests.add(route.request());
route.resume();
});
page.navigate(server.PREFIX + "/empty.html");
assertEquals(1, requests.size());
}
// https://github.com/microsoft/playwright/issues/3993
@Test
void shouldNotWorkForARedirectAndInterception() {
@@ -53,13 +53,12 @@ public class TestPageNetworkSizes extends TestBase {
}
@Test
@Disabled("responseBodySize == 16")
void shouldSetBodySizeHeadersSizeAndTransferSize() throws ExecutionException, InterruptedException {
server.setRoute("/get", exchange -> {
// In Firefox, |fetch| will be hanging until it receives |Content-Type| header
// from server.
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(200, 0);
exchange.sendResponseHeaders(200, 6);
try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("abc134");
}
@@ -70,21 +69,20 @@ public class TestPageNetworkSizes extends TestBase {
() -> page.evaluate("async () => fetch('./get').then(r => r.text())"));
request.get();
Sizes sizes = response.request().sizes();
assertEquals("abc134", response.text());
assertEquals(6, sizes.responseBodySize);
assertTrue(sizes.responseHeadersSize >= 100);
assertTrue(sizes.responseHeadersSize > 10);
}
@Test
@Disabled("responseBodySize == 5")
void shouldSetBodySizeTo0WhenThereWasNoResponseBody() {
Response response = page.navigate(server.EMPTY_PAGE);
Sizes sizes = response.request().sizes();
assertEquals(0, sizes.responseBodySize);
assertTrue(sizes.responseHeadersSize >= 100, "" + sizes.responseHeadersSize);
assertTrue(sizes.responseHeadersSize > 10, "" + sizes.responseHeadersSize);
}
@Test
@Disabled("responseBodySize == 0")
void shouldHaveTheCorrectResponseBodySize() throws IOException {
Response response = page.navigate(server.PREFIX + "/simplezip.json");
Sizes sizes = response.request().sizes();
@@ -141,7 +141,7 @@ public class TestPageSelectOption extends TestBase {
page.selectOption("body", "");
fail("did not throw");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Element is not a <select> element."));
assertTrue(e.getMessage().contains("Element is not a <select> element"), e.getMessage());
}
}
@@ -0,0 +1,124 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.WaitUntilState;
import org.junit.jupiter.api.Test;
import static com.microsoft.playwright.options.WaitUntilState.COMMIT;
import static com.microsoft.playwright.options.WaitUntilState.DOMCONTENTLOADED;
import static org.junit.jupiter.api.Assertions.*;
public class TestPageSetContent extends TestBase {
private static final String expectedOutput = "<html><head></head><body><div>hello</div></body></html>";
@Test
void shouldWork() {
page.setContent("<div>hello</div>");
Object result = page.content();
assertEquals(expectedOutput, result);
}
@Test
void shouldWorkWithDomcontentloaded() {
page.setContent("<div>hello</div>", new Page.SetContentOptions().setWaitUntil(DOMCONTENTLOADED));
Object result = page.content();
assertEquals(expectedOutput, result);
}
@Test
void shouldWorkWithCommit() {
page.setContent("<div>hello</div>", new Page.SetContentOptions().setWaitUntil(COMMIT));
Object result = page.content();
assertEquals(expectedOutput, result);
}
@Test
void shouldWorkWithDoctype() {
String doctype = "<!DOCTYPE html>";
page.setContent(doctype + "<div>hello</div>");
Object result = page.content();
assertEquals(doctype + expectedOutput, result);
}
@Test
void shouldWorkWithHTML4Doctype() {
String doctype = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" " +
"\"http://www.w3.org/TR/html4/strict.dtd\">";
page.setContent(doctype + "<div>hello</div>");
Object result = page.content();
assertEquals(doctype + expectedOutput, result);
}
@Test
void shouldRespectTimeout() {
String imgPath = "/img.png";
// stall for image
server.setRoute(imgPath, exchange -> {});
try {
page.setContent("<img src='" + server.PREFIX + imgPath + "'></img>", new Page.SetContentOptions().setTimeout(100));
fail("did not throw");
} catch (TimeoutError e) {
}
}
@Test
void shouldRespectDefaultNavigationTimeout() {
page.setDefaultNavigationTimeout(100);
String imgPath = "/img.png";
// stall for image
server.setRoute(imgPath, exchange -> {});
try {
page.setContent("<img src='" + server.PREFIX + imgPath + "'></img>");
fail("did not throw");
} catch (TimeoutError e) {
assertTrue(e.getMessage().contains("Timeout 100ms exceeded."), e.getMessage());
}
}
@Test
void shouldWorkFastEnough() {
for (int i = 0; i < 20; ++i) {
page.setContent("<div>yo</div>");
}
}
@Test
void shouldWorkWithTrickyContent() {
page.setContent("<div>hello world</div>" + "\\x7F");
assertEquals("hello world", page.evalOnSelector("div", "div => div.textContent"));
}
@Test
void shouldWorkWithAccents() {
page.setContent("<div>aberración</div>");
assertEquals("aberración", page.evalOnSelector("div", "div => div.textContent"));
}
@Test
void shouldWorkWithEmojis() {
page.setContent("<div>🐥</div>");
assertEquals("🐥", page.evalOnSelector("div", "div => div.textContent"));
}
@Test
void shouldWorkWithNewline() {
page.setContent("<div>\n</div>");
assertEquals("\n", page.evalOnSelector("div", "div => div.textContent"));
}
}
@@ -19,12 +19,14 @@ package com.microsoft.playwright;
import com.microsoft.playwright.options.WaitUntilState;
import org.junit.jupiter.api.Test;
import java.io.OutputStreamWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.regex.Pattern;
import static com.microsoft.playwright.Utils.expectedSSLError;
import static java.util.Collections.nCopies;
import static org.junit.jupiter.api.Assertions.*;
public class TestPageWaitForNavigation extends TestBase {
@@ -47,8 +49,6 @@ public class TestPageWaitForNavigation extends TestBase {
fail("did not throw");
} catch (TimeoutError e) {
assertTrue(e.getMessage().contains("Timeout 5000ms exceeded"));
// assertTrue(e.getMessage().contains("waiting for navigation to '**/frame.html' until 'load'"));
// assertTrue(e.getMessage().contains("navigated to '${server.EMPTY_PAGE}'"));
}
}
@@ -56,6 +56,23 @@ public class TestPageWaitForNavigation extends TestBase {
void shouldWorkWithBothDomcontentloadedAndLoad() {
}
@Test
void shouldWorkWithCommit() {
server.setRoute("/empty.html", exchange -> {
exchange.getResponseHeaders().add("Content-Type", "text/html");
exchange.sendResponseHeaders(200, 8192);
OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody());
writer.write("<title>" + String.join("", nCopies(4100, "A")));
writer.flush();
});
page.waitForNavigation(new Page.WaitForNavigationOptions().setWaitUntil(WaitUntilState.COMMIT), () -> {
try {
page.navigate(server.EMPTY_PAGE, new Page.NavigateOptions().setTimeout(100));
} catch (TimeoutError e) {
}
});
}
@Test
void shouldWorkWithClickingOnAnchorLinks() {
page.navigate(server.EMPTY_PAGE);
@@ -19,6 +19,11 @@ package com.microsoft.playwright;
import com.microsoft.playwright.options.WaitUntilState;
import org.junit.jupiter.api.Test;
import java.io.OutputStreamWriter;
import java.util.Collections;
import java.util.concurrent.Semaphore;
import static java.util.Collections.nCopies;
import static org.junit.jupiter.api.Assertions.*;
public class TestPageWaitForUrl extends TestBase {
@@ -47,6 +52,27 @@ public class TestPageWaitForUrl extends TestBase {
page.waitForURL("**/one-style.html", new Page.WaitForURLOptions().setWaitUntil(WaitUntilState.LOAD));
}
@Test
void shouldWorkWithCommit() {
server.setRoute("/empty.html", exchange -> {
exchange.getResponseHeaders().add("Content-Type", "text/html");
exchange.sendResponseHeaders(200, 8192);
OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody());
writer.write("<title>" + String.join("", nCopies(4100, "A")));
writer.flush();
});
try {
page.navigate(server.EMPTY_PAGE, new Page.NavigateOptions().setTimeout(100));
} catch (TimeoutError e) {
}
page.waitForURL("**/empty.html", new Page.WaitForURLOptions().setWaitUntil(WaitUntilState.COMMIT));
}
@Test
void shouldWorkWithCommitAndAboutBlank() {
page.waitForURL("about:blank", new Page.WaitForURLOptions().setWaitUntil(WaitUntilState.COMMIT));
}
@Test
void shouldWorkWithClickingOnAnchorLinks() {
page.navigate(server.EMPTY_PAGE);
@@ -16,6 +16,7 @@
package com.microsoft.playwright;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
@@ -72,6 +73,7 @@ public class TestRequestContinue extends TestBase {
}
@Test
@Disabled("resume() method is now asynchronous")
void shouldNotAllowChangingProtocolWhenOverridingUrl() {
PlaywrightException[] error = {null};
page.route("**/*", route -> {
@@ -41,8 +41,8 @@ public class TestWheel extends TestBase {
"shiftKey", false,
"altKey", false,
"metaKey", false);
assertEquals(expected, page.evaluate("window.lastEvent"));
page.waitForFunction("window.scrollY === 100");
assertEquals(expected, page.evaluate("window.lastEvent"));
}
@Test
@@ -51,7 +51,7 @@ public class TestWorkers extends TestBase {
try {
workerThisObj.getProperty("self");
} catch (PlaywrightException e) {
assertTrue(e.getMessage().contains("Target closed"), e.getMessage());
assertTrue(e.getMessage().contains("Target closed") || e.getMessage().contains("Worker was closed"), e.getMessage());
}
}
+22 -4
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>parent-pom</artifactId>
<version>1.15.0-SNAPSHOT</version>
<version>1.17.2</version>
<packaging>pom</packaging>
<name>Playwright Parent Project</name>
<description>Java library to automate Chromium, Firefox and WebKit with a single API.
@@ -39,6 +39,7 @@
<module>driver</module>
<module>driver-bundle</module>
<module>playwright</module>
<module>assertions</module>
</modules>
<properties>
@@ -47,6 +48,7 @@
<junit.version>5.7.0</junit.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<websocket.version>1.5.1</websocket.version>
<opentest4j.version>1.2.0</opentest4j.version>
</properties>
<dependencyManagement>
@@ -66,6 +68,11 @@
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<dependency>
<groupId>org.opentest4j</groupId>
<artifactId>opentest4j</artifactId>
<version>${opentest4j.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
@@ -76,6 +83,7 @@
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>${websocket.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
@@ -108,6 +116,18 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<additionalOptions>--allow-script-in-comments</additionalOptions>
<failOnError>false</failOnError>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -123,9 +143,7 @@
junit.jupiter.execution.parallel.config.dynamic.factor=0.5
</configurationParameters>
</properties>
<failIfNoTests>
false
</failIfNoTests>
<failIfNoTests>false</failIfNoTests>
</configuration>
</plugin>
<plugin>
+1 -1
View File
@@ -1 +1 @@
1.15.0-1631797286000
1.17.1
+2 -2
View File
@@ -33,7 +33,7 @@ fi
mkdir -p driver
cd driver
for PLATFORM in mac linux win32 win32_x64
for PLATFORM in mac linux win32_x64
do
FILE_NAME=$FILE_PREFIX-$PLATFORM.zip
if [[ -d $PLATFORM ]]; then
@@ -45,7 +45,7 @@ do
echo "Downloading driver for $PLATFORM to $(pwd)"
URL=https://playwright.azureedge.net/builds/driver
if ! [[ $CLI_VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
if [[ "$CLI_VERSION" == *-alpha* || "$CLI_VERSION" == *-beta* || "$CLI_VERSION" == *-next* ]]; then
URL=$URL/next
fi
URL=$URL/$FILE_NAME
-3
View File
@@ -14,9 +14,6 @@ Darwin)
Linux)
PLAYWRIGHT_CLI=./driver-bundle/src/main/resources/driver/linux/playwright.sh
;;
MINGW32*)
PLAYWRIGHT_CLI=./driver-bundle/src/main/resources/driver/win32/playwright.cmd
;;
MINGW64*)
PLAYWRIGHT_CLI=./driver-bundle/src/main/resources/driver/win32_x64/playwright.cmd
;;
-4
View File
@@ -26,10 +26,6 @@ Linux)
PLATFORM=linux
echo "Downloading driver for Linux"
;;
MINGW32*)
PLATFORM=win32
echo "Downloading driver for Win32"
;;
MINGW64*)
PLATFORM=win32_x64
echo "Downloading driver for Win64"
+1 -1
View File
@@ -25,5 +25,5 @@ POM_FILES=(
for name in ${POM_FILES[*]};
do
mvn versions:set -D newVersion=$VERSION -f $name
mvn versions:set -D generateBackupPoms=false -D newVersion=$VERSION -f $name
done
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>api-generator</artifactId>
<version>1.15.0-SNAPSHOT</version>
<version>1.17.2</version>
<name>Playwright - API Generator</name>
<description>
This is an internal module used to generate Java API from the upstream Playwright
@@ -24,6 +24,7 @@ import com.google.gson.JsonObject;
import java.io.*;
import java.nio.file.FileSystems;
import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -627,6 +628,24 @@ class Method extends Element {
output.add(offset + "}");
return;
}
if ("PlaywrightAssertions.assertThat".equals(jsonPath)) {
writeJavadoc(params, output, offset);
String originalName = jsonElement.getAsJsonObject().get("originalName").getAsString();
if ("assertThatPage".equals(originalName)) {
output.add(offset + "static PageAssertions assertThat(Page page) {");
output.add(offset + " return new PageAssertionsImpl(page);");
output.add(offset + "}");
output.add("");
} else if ("assertThatLocator".equals(originalName)) {
output.add(offset + "static LocatorAssertions assertThat(Locator locator) {");
output.add(offset + " return new LocatorAssertionsImpl(locator);");
output.add(offset + "}");
output.add("");
} else {
throw new RuntimeException("Unexpected originalName: " + originalName);
}
return;
}
int numOverloads = 1;
for (int i = 0; i < params.size(); i++) {
if (params.get(i).type.isTypeUnion()) {
@@ -844,9 +863,7 @@ class Interface extends TypeDefinition {
" * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
" * See the License for the specific language governing permissions and\n" +
" * limitations under the License.\n" +
" */\n" +
"\n" +
"package com.microsoft.playwright;\n";
" */\n";
private static Set<String> allowedBaseInterfaces = new HashSet<>(asList("Browser", "JSHandle", "BrowserContext"));
private static Set<String> autoCloseableInterfaces = new HashSet<>(asList("Playwright", "Browser", "BrowserContext", "Page"));
@@ -877,7 +894,6 @@ class Interface extends TypeDefinition {
}
void writeTo(List<String> output, String offset) {
output.add(header);
if ("Playwright".equals(jsonName)) {
output.add("import com.microsoft.playwright.impl.PlaywrightImpl;");
}
@@ -900,9 +916,21 @@ class Interface extends TypeDefinition {
if (asList("Page", "Frame", "BrowserContext", "WebSocket").contains(jsonName)) {
output.add("import java.util.function.Predicate;");
}
if (asList("Page", "Frame", "BrowserContext").contains(jsonName)) {
if (asList("Page", "Frame", "BrowserContext", "PageAssertions", "LocatorAssertions").contains(jsonName)) {
output.add("import java.util.regex.Pattern;");
}
if ("PlaywrightAssertions".equals(jsonName)) {
output.add("import com.microsoft.playwright.Locator;");
output.add("import com.microsoft.playwright.Page;");
output.add("import com.microsoft.playwright.impl.LocatorAssertionsImpl;");
output.add("import com.microsoft.playwright.impl.PageAssertionsImpl;");
}
if ("PageAssertions".equals(jsonName)) {
output.add("import com.microsoft.playwright.Page;");
}
if ("LocatorAssertions".equals(jsonName)) {
output.add("import com.microsoft.playwright.Locator;");
}
output.add("");
List<String> superInterfaces = new ArrayList<>();
@@ -1062,10 +1090,22 @@ public class ApiGenerator {
ApiGenerator(Reader reader) throws IOException {
JsonArray api = new Gson().fromJson(reader, JsonArray.class);
File cwd = FileSystems.getDefault().getPath(".").toFile();
filterOtherLangs(api, new Stack<>());
File dir = new File(cwd, "playwright/src/main/java/com/microsoft/playwright");
System.out.println("Writing files to: " + dir.getCanonicalPath());
Stack<String> path = new Stack<>();
filterOtherLangs(api, path);
generate(api, dir, "com.microsoft.playwright", isAssertion().negate());
File assertionsDir = new File(cwd,"assertions/src/main/java/com/microsoft/playwright/assertions");
System.out.println("Writing assertion files to: " + dir.getCanonicalPath());
generate(api, assertionsDir, "com.microsoft.playwright.assertions", isAssertion());
}
private static Predicate<String> isAssertion() {
return className -> className.toLowerCase().contains("assert");
}
private void generate(JsonArray api, File dir, String packageName, Predicate<String> classFilter) throws IOException {
Map<String, TypeDefinition> topLevelTypes = new HashMap<>();
for (JsonElement entry: api) {
String name = entry.getAsJsonObject().get("name").getAsString();
@@ -1073,17 +1113,26 @@ public class ApiGenerator {
if (asList("PlaywrightException", "TimeoutError").contains(name)) {
continue;
}
if (!classFilter.test(name)) {
continue;
}
List<String> lines = new ArrayList<>();
lines.add(Interface.header);
lines.add("package " + packageName + ";");
lines.add("");
new Interface(entry.getAsJsonObject(), topLevelTypes).writeTo(lines, "");
String text = String.join("\n", lines);
try (FileWriter writer = new FileWriter(new File(dir, name + ".java"))) {
writer.write(text);
}
}
dir = new File(dir, "options");
for (TypeDefinition e : topLevelTypes.values()) {
List<String> lines = new ArrayList<>();
lines.add(Interface.header.replace("package com.microsoft.playwright;", "package com.microsoft.playwright.options;"));
lines.add(Interface.header);
lines.add("package " + packageName + ".options;");
lines.add("");
e.writeTo(lines, "");
String text = String.join("\n", lines);
try (FileWriter writer = new FileWriter(new File(dir, e.name() + ".java"))) {
@@ -1117,6 +1166,8 @@ public class ApiGenerator {
List<String> aliasPath = new ArrayList<>(path);
aliasPath.set(aliasPath.size() - 1, alias);
aliases.put(String.join(".", path), String.join(".", aliasPath));
// Save original name.
object.addProperty("originalName", object.get("name").getAsString());
// Rename in place.
object.addProperty("name", alias);
}
@@ -1189,7 +1240,6 @@ public class ApiGenerator {
public static void main(String[] args) throws IOException {
File cwd = FileSystems.getDefault().getPath(".").toFile();
System.out.println(cwd.getCanonicalPath());
File file = new File(cwd, "tools/api-generator/src/main/resources/api.json");
System.out.println("Reading from: " + file.getCanonicalPath());
new ApiGenerator(new FileReader(file));
@@ -10,6 +10,7 @@ cd "$(dirname $0)"
PROJECT_DIR=$(mktemp -d)
echo "Creating project in $PROJECT_DIR"
cp -R . $PROJECT_DIR
cp -R ../../assertions/src/test/ $PROJECT_DIR/src/
cp -R ../../driver-bundle/src/test/ $PROJECT_DIR/src/
cp -R ../../playwright/src/test/ $PROJECT_DIR/src/
cd $PROJECT_DIR
+13 -4
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-local-installation</artifactId>
<version>1.15.0-SNAPSHOT</version>
<version>1.17.2</version>
<name>Test local installation</name>
<description>Runs Playwright test suite (copied from playwright module) against locally cached Playwright</description>
<properties>
@@ -20,6 +20,18 @@
<artifactId>playwright</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>assertions</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
@@ -52,9 +64,6 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration>
</plugin>
</plugins>
</build>
+1 -1
View File
@@ -9,7 +9,7 @@
</parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-spring-boot-starter</artifactId>
<version>1.15.0-SNAPSHOT</version>
<version>1.17.2</version>
<name>Test Playwright With Spring Boot</name>
<properties>
<spring.version>2.4.3</spring.version>
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.microsoft.playwright</groupId>
<artifactId>update-version</artifactId>
<version>1.15.0-SNAPSHOT</version>
<version>1.17.2</version>
<name>Playwright - Update Version in Documentation</name>
<description>
This is an internal module used to update versions in the documentation based on