diff --git a/README.md b/README.md index 5ff12a170..676b4b9f4 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 134.0.6998.35 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 136.0.7103.25 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 18.4 | ✅ | ✅ | ✅ | -| Firefox 135.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 137.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | ## Documentation diff --git a/playwright/src/main/java/com/microsoft/playwright/APIRequest.java b/playwright/src/main/java/com/microsoft/playwright/APIRequest.java index 85833f084..ba3090b79 100644 --- a/playwright/src/main/java/com/microsoft/playwright/APIRequest.java +++ b/playwright/src/main/java/com/microsoft/playwright/APIRequest.java @@ -72,6 +72,12 @@ class NewContextOptions { * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. */ public Boolean ignoreHTTPSErrors; + /** + * Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is + * exceeded. Defaults to {@code 20}. Pass {@code 0} to not follow redirects. This can be overwritten for each request + * individually. + */ + public Integer maxRedirects; /** * Network proxy settings. */ @@ -171,6 +177,15 @@ public NewContextOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) { this.ignoreHTTPSErrors = ignoreHTTPSErrors; return this; } + /** + * Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is + * exceeded. Defaults to {@code 20}. Pass {@code 0} to not follow redirects. This can be overwritten for each request + * individually. + */ + public NewContextOptions setMaxRedirects(int maxRedirects) { + this.maxRedirects = maxRedirects; + return this; + } /** * Network proxy settings. */ diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java index be6b18654..97b46508b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java @@ -411,8 +411,6 @@ class StorageStateOptions { * Set to {@code true} to include IndexedDB in * the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase * Authentication, enable this. - * - *

NOTE: IndexedDBs with typed arrays are currently not supported. */ public Boolean indexedDB; /** @@ -425,8 +423,6 @@ class StorageStateOptions { * Set to {@code true} to include IndexedDB in * the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase * Authentication, enable this. - * - *

NOTE: IndexedDBs with typed arrays are currently not supported. */ public StorageStateOptions setIndexedDB(boolean indexedDB) { this.indexedDB = indexedDB; @@ -996,8 +992,8 @@ default void grantPermissions(List permissions) { * *

NOTE: Enabling routing disables http cache. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the - * context options was provided and the passed URL is a path, it gets merged via the {@code new URL()} constructor. * @param handler handler function to route the request. * @since v1.8 @@ -1052,8 +1048,8 @@ default void route(String url, Consumer handler) { * *

NOTE: Enabling routing disables http cache. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the - * context options was provided and the passed URL is a path, it gets merged via the {@code new URL()} constructor. * @param handler handler function to route the request. * @since v1.8 @@ -1106,8 +1102,8 @@ default void route(String url, Consumer handler) { * *

NOTE: Enabling routing disables http cache. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the - * context options was provided and the passed URL is a path, it gets merged via the {@code new URL()} constructor. * @param handler handler function to route the request. * @since v1.8 @@ -1162,8 +1158,8 @@ default void route(Pattern url, Consumer handler) { * *

NOTE: Enabling routing disables http cache. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the - * context options was provided and the passed URL is a path, it gets merged via the {@code new URL()} constructor. * @param handler handler function to route the request. * @since v1.8 @@ -1216,8 +1212,8 @@ default void route(Pattern url, Consumer handler) { * *

NOTE: Enabling routing disables http cache. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the - * context options was provided and the passed URL is a path, it gets merged via the {@code new URL()} constructor. * @param handler handler function to route the request. * @since v1.8 @@ -1272,8 +1268,8 @@ default void route(Predicate url, Consumer handler) { * *

NOTE: Enabling routing disables http cache. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the - * context options was provided and the passed URL is a path, it gets merged via the {@code new URL()} constructor. * @param handler handler function to route the request. * @since v1.8 diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java index b100387de..6f6681fc1 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java @@ -226,8 +226,8 @@ class LaunchOptions { /** * Whether to run browser in headless mode. More details for Chromium and Firefox. Defaults to {@code true} - * unless the {@code devtools} option is {@code true}. + * href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox. Defaults to {@code true} unless + * the {@code devtools} option is {@code true}. */ public Boolean headless; /** @@ -368,8 +368,8 @@ public LaunchOptions setHandleSIGTERM(boolean handleSIGTERM) { /** * Whether to run browser in headless mode. More details for Chromium and Firefox. Defaults to {@code true} - * unless the {@code devtools} option is {@code true}. + * href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox. Defaults to {@code true} unless + * the {@code devtools} option is {@code true}. */ public LaunchOptions setHeadless(boolean headless) { this.headless = headless; @@ -564,8 +564,8 @@ class LaunchPersistentContextOptions { /** * Whether to run browser in headless mode. More details for Chromium and Firefox. Defaults to {@code true} - * unless the {@code devtools} option is {@code true}. + * href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox. Defaults to {@code true} unless + * the {@code devtools} option is {@code true}. */ public Boolean headless; /** @@ -934,8 +934,8 @@ public LaunchPersistentContextOptions setHasTouch(boolean hasTouch) { /** * Whether to run browser in headless mode. More details for Chromium and Firefox. Defaults to {@code true} - * unless the {@code devtools} option is {@code true}. + * href="https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/">Firefox. Defaults to {@code true} unless + * the {@code devtools} option is {@code true}. */ public LaunchPersistentContextOptions setHeadless(boolean headless) { this.headless = headless; @@ -1365,11 +1365,15 @@ default Browser launch() { *

Launches browser that uses persistent storage located at {@code userDataDir} and returns the only context. Closing this * context will automatically close the browser. * - * @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for More details for Chromium and Firefox. Note that - * Chromium's user data directory is the **parent** directory of the "Profile Path" seen at {@code chrome://version}. Pass - * an empty string to use a temporary directory instead. + * href="https://wiki.mozilla.org/Firefox/CommandLineOptions#User_profile">Firefox. Chromium's user data directory is + * the **parent** directory of the "Profile Path" seen at {@code chrome://version}. + * + *

Note that browsers do not allow launching multiple instances with the same User Data Directory. * @since v1.8 */ default BrowserContext launchPersistentContext(Path userDataDir) { @@ -1381,11 +1385,15 @@ default BrowserContext launchPersistentContext(Path userDataDir) { *

Launches browser that uses persistent storage located at {@code userDataDir} and returns the only context. Closing this * context will automatically close the browser. * - * @param userDataDir Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for More details for Chromium and Firefox. Note that - * Chromium's user data directory is the **parent** directory of the "Profile Path" seen at {@code chrome://version}. Pass - * an empty string to use a temporary directory instead. + * href="https://wiki.mozilla.org/Firefox/CommandLineOptions#User_profile">Firefox. Chromium's user data directory is + * the **parent** directory of the "Profile Path" seen at {@code chrome://version}. + * + *

Note that browsers do not allow launching multiple instances with the same User Data Directory. * @since v1.8 */ BrowserContext launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options); diff --git a/playwright/src/main/java/com/microsoft/playwright/Locator.java b/playwright/src/main/java/com/microsoft/playwright/Locator.java index 1fdaea343..0027ea4fc 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Locator.java +++ b/playwright/src/main/java/com/microsoft/playwright/Locator.java @@ -30,6 +30,11 @@ */ public interface Locator { class AriaSnapshotOptions { + /** + * Generate symbolic reference for each element. One can use {@code aria-ref=} locator immediately after capturing the + * snapshot to perform actions on the element. + */ + public Boolean ref; /** * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout @@ -38,6 +43,14 @@ class AriaSnapshotOptions { */ public Double timeout; + /** + * Generate symbolic reference for each element. One can use {@code aria-ref=} locator immediately after capturing the + * snapshot to perform actions on the element. + */ + public AriaSnapshotOptions setRef(boolean ref) { + this.ref = ref; + return this; + } /** * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout @@ -600,18 +613,14 @@ public ElementHandleOptions setTimeout(double timeout) { } class EvaluateOptions { /** - * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default - * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout - * BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()} - * methods. + * Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation + * itself is not limited by the timeout. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. */ public Double timeout; /** - * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default - * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout - * BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()} - * methods. + * Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation + * itself is not limited by the timeout. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. */ public EvaluateOptions setTimeout(double timeout) { this.timeout = timeout; @@ -620,18 +629,14 @@ public EvaluateOptions setTimeout(double timeout) { } class EvaluateHandleOptions { /** - * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default - * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout - * BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()} - * methods. + * Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation + * itself is not limited by the timeout. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. */ public Double timeout; /** - * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default - * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout - * BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()} - * methods. + * Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation + * itself is not limited by the timeout. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. */ public EvaluateHandleOptions setTimeout(double timeout) { this.timeout = timeout; diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java index 4a3cd7314..3f7158283 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Page.java +++ b/playwright/src/main/java/com/microsoft/playwright/Page.java @@ -6317,8 +6317,8 @@ default Response reload() { * *

NOTE: Enabling routing disables http cache. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the - * context options was provided and the passed URL is a path, it gets merged via the {@code new URL()} constructor. * @param handler handler function to route the request. * @since v1.8 @@ -6376,8 +6376,8 @@ default void route(String url, Consumer handler) { * *

NOTE: Enabling routing disables http cache. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the - * context options was provided and the passed URL is a path, it gets merged via the {@code new URL()} constructor. * @param handler handler function to route the request. * @since v1.8 @@ -6433,8 +6433,8 @@ default void route(String url, Consumer handler) { * *

NOTE: Enabling routing disables http cache. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the - * context options was provided and the passed URL is a path, it gets merged via the {@code new URL()} constructor. * @param handler handler function to route the request. * @since v1.8 @@ -6492,8 +6492,8 @@ default void route(Pattern url, Consumer handler) { * *

NOTE: Enabling routing disables http cache. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the - * context options was provided and the passed URL is a path, it gets merged via the {@code new URL()} constructor. * @param handler handler function to route the request. * @since v1.8 @@ -6549,8 +6549,8 @@ default void route(Pattern url, Consumer handler) { * *

NOTE: Enabling routing disables http cache. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the - * context options was provided and the passed URL is a path, it gets merged via the {@code new URL()} constructor. * @param handler handler function to route the request. * @since v1.8 @@ -6608,8 +6608,8 @@ default void route(Predicate url, Consumer handler) { * *

NOTE: Enabling routing disables http cache. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a {@code baseURL} via the - * context options was provided and the passed URL is a path, it gets merged via the {@code new URL()} constructor. * @param handler handler function to route the request. * @since v1.8 diff --git a/playwright/src/main/java/com/microsoft/playwright/Route.java b/playwright/src/main/java/com/microsoft/playwright/Route.java index ae05c816c..e9bc75a7f 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Route.java +++ b/playwright/src/main/java/com/microsoft/playwright/Route.java @@ -370,6 +370,10 @@ default void abort() { * matching handlers won't be invoked. Use {@link com.microsoft.playwright.Route#fallback Route.fallback()} If you want * next matching handler in the chain to be invoked. * + *

NOTE: The {@code Cookie} header cannot be overridden using this method. If a value is provided, it will be ignored, and the + * cookie will be loaded from the browser's cookie store. To set custom cookies, use {@link + * com.microsoft.playwright.BrowserContext#addCookies BrowserContext.addCookies()}. + * * @since v1.8 */ default void resume() { @@ -398,6 +402,10 @@ default void resume() { * matching handlers won't be invoked. Use {@link com.microsoft.playwright.Route#fallback Route.fallback()} If you want * next matching handler in the chain to be invoked. * + *

NOTE: The {@code Cookie} header cannot be overridden using this method. If a value is provided, it will be ignored, and the + * cookie will be loaded from the browser's cookie store. To set custom cookies, use {@link + * com.microsoft.playwright.BrowserContext#addCookies BrowserContext.addCookies()}. + * * @since v1.8 */ void resume(ResumeOptions options); diff --git a/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java b/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java index fc021796e..a8607fdfd 100644 --- a/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java +++ b/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java @@ -16,6 +16,7 @@ package com.microsoft.playwright.assertions; +import java.util.*; import java.util.regex.Pattern; import com.microsoft.playwright.options.AriaRole; @@ -237,6 +238,20 @@ public IsVisibleOptions setVisible(boolean visible) { return this; } } + class ContainsClassOptions { + /** + * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. + */ + public Double timeout; + + /** + * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. + */ + public ContainsClassOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } class ContainsTextOptions { /** * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular @@ -855,6 +870,98 @@ default void isVisible() { * @since v1.20 */ void isVisible(IsVisibleOptions options); + /** + * Ensures the {@code Locator} points to an element with given CSS classes. All classes from the asserted value, separated + * by spaces, must be present in the Element.classList in any order. + * + *

Usage + *

{@code
+   * assertThat(page.locator("#component")).containsClass("middle selected row");
+   * assertThat(page.locator("#component")).containsClass("selected");
+   * assertThat(page.locator("#component")).containsClass("row middle");
+   * }
+ * + *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected + * class lists. Each element's class attribute is matched against the corresponding class in the array: + *

{@code
+   * assertThat(page.locator("list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
+   * }
+ * + * @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements. + * @since v1.52 + */ + default void containsClass(String expected) { + containsClass(expected, null); + } + /** + * Ensures the {@code Locator} points to an element with given CSS classes. All classes from the asserted value, separated + * by spaces, must be present in the Element.classList in any order. + * + *

Usage + *

{@code
+   * assertThat(page.locator("#component")).containsClass("middle selected row");
+   * assertThat(page.locator("#component")).containsClass("selected");
+   * assertThat(page.locator("#component")).containsClass("row middle");
+   * }
+ * + *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected + * class lists. Each element's class attribute is matched against the corresponding class in the array: + *

{@code
+   * assertThat(page.locator("list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
+   * }
+ * + * @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements. + * @since v1.52 + */ + void containsClass(String expected, ContainsClassOptions options); + /** + * Ensures the {@code Locator} points to an element with given CSS classes. All classes from the asserted value, separated + * by spaces, must be present in the Element.classList in any order. + * + *

Usage + *

{@code
+   * assertThat(page.locator("#component")).containsClass("middle selected row");
+   * assertThat(page.locator("#component")).containsClass("selected");
+   * assertThat(page.locator("#component")).containsClass("row middle");
+   * }
+ * + *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected + * class lists. Each element's class attribute is matched against the corresponding class in the array: + *

{@code
+   * assertThat(page.locator("list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
+   * }
+ * + * @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements. + * @since v1.52 + */ + default void containsClass(List expected) { + containsClass(expected, null); + } + /** + * Ensures the {@code Locator} points to an element with given CSS classes. All classes from the asserted value, separated + * by spaces, must be present in the Element.classList in any order. + * + *

Usage + *

{@code
+   * assertThat(page.locator("#component")).containsClass("middle selected row");
+   * assertThat(page.locator("#component")).containsClass("selected");
+   * assertThat(page.locator("#component")).containsClass("row middle");
+   * }
+ * + *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected + * class lists. Each element's class attribute is matched against the corresponding class in the array: + *

{@code
+   * assertThat(page.locator("list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
+   * }
+ * + * @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements. + * @since v1.52 + */ + void containsClass(List expected, ContainsClassOptions options); /** * Ensures the {@code Locator} points to an element that contains the given text. All nested elements will be considered * when computing the text content of the element. You can use regular expressions for the value as well. @@ -1445,12 +1552,13 @@ default void hasAttribute(String name, Pattern value) { void hasAttribute(String name, Pattern value, HasAttributeOptions options); /** * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match - * the element's {@code class} attribute. To match individual classes or perform partial matches, use a regular expression: + * the element's {@code class} attribute. To match individual classes use {@link + * com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}. * *

Usage *

{@code
-   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * assertThat(page.locator("#component")).hasClass("middle selected row");
+   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * }
* *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected @@ -1468,12 +1576,13 @@ default void hasClass(String expected) { } /** * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match - * the element's {@code class} attribute. To match individual classes or perform partial matches, use a regular expression: + * the element's {@code class} attribute. To match individual classes use {@link + * com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}. * *

Usage *

{@code
-   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * assertThat(page.locator("#component")).hasClass("middle selected row");
+   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * }
* *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected @@ -1489,12 +1598,13 @@ default void hasClass(String expected) { void hasClass(String expected, HasClassOptions options); /** * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match - * the element's {@code class} attribute. To match individual classes or perform partial matches, use a regular expression: + * the element's {@code class} attribute. To match individual classes use {@link + * com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}. * *

Usage *

{@code
-   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * assertThat(page.locator("#component")).hasClass("middle selected row");
+   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * }
* *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected @@ -1512,12 +1622,13 @@ default void hasClass(Pattern expected) { } /** * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match - * the element's {@code class} attribute. To match individual classes or perform partial matches, use a regular expression: + * the element's {@code class} attribute. To match individual classes use {@link + * com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}. * *

Usage *

{@code
-   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * assertThat(page.locator("#component")).hasClass("middle selected row");
+   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * }
* *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected @@ -1533,12 +1644,13 @@ default void hasClass(Pattern expected) { void hasClass(Pattern expected, HasClassOptions options); /** * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match - * the element's {@code class} attribute. To match individual classes or perform partial matches, use a regular expression: + * the element's {@code class} attribute. To match individual classes use {@link + * com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}. * *

Usage *

{@code
-   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * assertThat(page.locator("#component")).hasClass("middle selected row");
+   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * }
* *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected @@ -1556,12 +1668,13 @@ default void hasClass(String[] expected) { } /** * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match - * the element's {@code class} attribute. To match individual classes or perform partial matches, use a regular expression: + * the element's {@code class} attribute. To match individual classes use {@link + * com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}. * *

Usage *

{@code
-   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * assertThat(page.locator("#component")).hasClass("middle selected row");
+   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * }
* *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected @@ -1577,12 +1690,13 @@ default void hasClass(String[] expected) { void hasClass(String[] expected, HasClassOptions options); /** * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match - * the element's {@code class} attribute. To match individual classes or perform partial matches, use a regular expression: + * the element's {@code class} attribute. To match individual classes use {@link + * com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}. * *

Usage *

{@code
-   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * assertThat(page.locator("#component")).hasClass("middle selected row");
+   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * }
* *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected @@ -1600,12 +1714,13 @@ default void hasClass(Pattern[] expected) { } /** * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match - * the element's {@code class} attribute. To match individual classes or perform partial matches, use a regular expression: + * the element's {@code class} attribute. To match individual classes use {@link + * com.microsoft.playwright.assertions.LocatorAssertions#containsClass LocatorAssertions.containsClass()}. * *

Usage *

{@code
-   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * assertThat(page.locator("#component")).hasClass("middle selected row");
+   * assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
    * }
* *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java index c1bb3dd72..4d0f84025 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -480,7 +480,7 @@ public APIRequestContextImpl request() { @Override public void route(String url, Consumer handler, RouteOptions options) { - route(new UrlMatcher(baseUrl, url), handler, options); + route(UrlMatcher.forGlob(baseUrl, url, this.connection.localUtils, false), handler, options); } @Override @@ -502,7 +502,7 @@ public void routeFromHAR(Path har, RouteFromHAROptions options) { recordIntoHar(null, har, options); return; } - UrlMatcher matcher = UrlMatcher.forOneOf(baseUrl, options.url); + UrlMatcher matcher = UrlMatcher.forOneOf(baseUrl, options.url, this.connection.localUtils, false); HARRouter harRouter = new HARRouter(connection.localUtils, har, options.notFound); onClose(context -> harRouter.dispose()); route(matcher, route -> harRouter.handle(route), null); @@ -517,7 +517,7 @@ private void route(UrlMatcher matcher, Consumer handler, RouteOptions opt @Override public void routeWebSocket(String url, Consumer handler) { - routeWebSocketImpl(new UrlMatcher(baseUrl, url), handler); + routeWebSocketImpl(UrlMatcher.forGlob(baseUrl, url, this.connection.localUtils, true), handler); } @Override @@ -656,7 +656,7 @@ public void unrouteAll() { @Override public void unroute(String url, Consumer handler) { - unroute(new UrlMatcher(this.baseUrl, url), handler); + unroute(UrlMatcher.forGlob(this.baseUrl, url, this.connection.localUtils, false), handler); } @Override diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java index b1f06f9b4..e74eb5412 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java @@ -1031,7 +1031,7 @@ private Response waitForNavigationImpl(Logger logger, Runnable code, WaitForNavi List> waitables = new ArrayList<>(); if (matcher == null) { - matcher = UrlMatcher.forOneOf(page.context().baseUrl, options.url); + matcher = UrlMatcher.forOneOf(page.context().baseUrl, options.url, this.connection.localUtils, false); } logger.log("waiting for navigation " + matcher); waitables.add(new WaitForNavigationHelper(matcher, options.waitUntil, logger)); @@ -1078,7 +1078,7 @@ void waitForTimeoutImpl(double timeout) { @Override public void waitForURL(String url, WaitForURLOptions options) { - waitForURL(new UrlMatcher(page.context().baseUrl, url), options); + waitForURL(UrlMatcher.forGlob(page.context().baseUrl, url, this.connection.localUtils, false), options); } @Override diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/LocalUtils.java b/playwright/src/main/java/com/microsoft/playwright/impl/LocalUtils.java index 666a91e9e..b4f36c03f 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/LocalUtils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/LocalUtils.java @@ -21,10 +21,11 @@ import java.nio.file.Path; import java.util.List; +import java.util.regex.Pattern; import static com.microsoft.playwright.impl.Serialization.gson; -class LocalUtils extends ChannelOwner { +public class LocalUtils extends ChannelOwner { LocalUtils(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); markAsInternalType(); @@ -59,4 +60,16 @@ String tracingStarted(String tracesDir, String traceName) { JsonObject json = connection.localUtils().sendMessage("tracingStarted", params).getAsJsonObject(); return json.get("stacksId").getAsString(); } + + public Pattern globToRegex(String glob, String baseURL, boolean webSocketUrl) { + JsonObject params = new JsonObject(); + params.addProperty("glob", glob); + if (baseURL != null) { + params.addProperty("baseURL", baseURL); + } + params.addProperty("webSocketUrl", webSocketUrl); + JsonObject json = connection.localUtils().sendMessage("globToRegex", params).getAsJsonObject(); + String regex = json.get("regex").getAsString(); + return Pattern.compile(regex); + } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorAssertionsImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorAssertionsImpl.java index 337ecda86..ab9448b44 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorAssertionsImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorAssertionsImpl.java @@ -39,6 +39,25 @@ private LocatorAssertionsImpl(Locator locator, boolean isNot) { super((LocatorImpl) locator, isNot); } + + @Override + public void containsClass(String classname, ContainsClassOptions options) { + ExpectedTextValue expected = new ExpectedTextValue(); + expected.string = classname; + expectImpl("to.contain.class", expected, classname, "Locator expected to contain class", convertType(options, FrameExpectOptions.class)); + } + + @Override + public void containsClass(List classnames, ContainsClassOptions options) { + List list = new ArrayList<>(); + for (String text : classnames) { + ExpectedTextValue expected = new ExpectedTextValue(); + expected.string = text; + list.add(expected); + } + expectImpl("to.contain.class.array", list, classnames, "Locator expected to contain classes", convertType(options, FrameExpectOptions.class)); + } + @Override public void containsText(String text, ContainsTextOptions options) { ExpectedTextValue expected = new ExpectedTextValue(); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java index c0a98e4b7..2eff38309 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -785,7 +785,7 @@ public Frame frame(String name) { @Override public Frame frameByUrl(String glob) { - return frameFor(new UrlMatcher(browserContext.baseUrl, glob)); + return frameFor(UrlMatcher.forGlob(browserContext.baseUrl, glob, this.connection.localUtils, false)); } @Override @@ -1105,7 +1105,7 @@ private Response reloadImpl(ReloadOptions options) { @Override public void route(String url, Consumer handler, RouteOptions options) { - route(new UrlMatcher(browserContext.baseUrl, url), handler, options); + route(UrlMatcher.forGlob(browserContext.baseUrl, url, this.connection.localUtils, false), handler, options); } @Override @@ -1127,7 +1127,7 @@ public void routeFromHAR(Path har, RouteFromHAROptions options) { browserContext.recordIntoHar(this, har, convertType(options, BrowserContext.RouteFromHAROptions.class)); return; } - UrlMatcher matcher = UrlMatcher.forOneOf(browserContext.baseUrl, options.url); + UrlMatcher matcher = UrlMatcher.forOneOf(browserContext.baseUrl, options.url, this.connection.localUtils, false); HARRouter harRouter = new HARRouter(connection.localUtils, har, options.notFound); onClose(context -> harRouter.dispose()); route(matcher, route -> harRouter.handle(route), null); @@ -1142,7 +1142,7 @@ private void route(UrlMatcher matcher, Consumer handler, RouteOptions opt @Override public void routeWebSocket(String url, Consumer handler) { - routeWebSocketImpl(new UrlMatcher(browserContext.baseUrl, url), handler); + routeWebSocketImpl(UrlMatcher.forGlob(browserContext.baseUrl, url, this.connection.localUtils, true), handler); } @Override @@ -1365,7 +1365,7 @@ public void unrouteAll() { @Override public void unroute(String url, Consumer handler) { - unroute(new UrlMatcher(browserContext.baseUrl, url), handler); + unroute(UrlMatcher.forGlob(browserContext.baseUrl, url, this.connection.localUtils, false), handler); } @Override @@ -1508,7 +1508,7 @@ public T get() { @Override public Request waitForRequest(String urlGlob, WaitForRequestOptions options, Runnable code) { - return waitForRequest(new UrlMatcher(browserContext.baseUrl, urlGlob), null, options, code); + return waitForRequest(UrlMatcher.forGlob(browserContext.baseUrl, urlGlob, this.connection.localUtils, false), null, options, code); } @Override @@ -1553,7 +1553,7 @@ private Request waitForRequestFinishedImpl(WaitForRequestFinishedOptions options @Override public Response waitForResponse(String urlGlob, WaitForResponseOptions options, Runnable code) { - return waitForResponse(new UrlMatcher(browserContext.baseUrl, urlGlob), null, options, code); + return waitForResponse(UrlMatcher.forGlob(browserContext.baseUrl, urlGlob, this.connection.localUtils, false), null, options, code); } @Override @@ -1606,7 +1606,7 @@ public void waitForTimeout(double timeout) { @Override public void waitForURL(String url, WaitForURLOptions options) { - waitForURL(new UrlMatcher(browserContext.baseUrl, url), options); + waitForURL(UrlMatcher.forGlob(browserContext.baseUrl, url, this.connection.localUtils, false), options); } @Override diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java index 32963edbf..38863c8e8 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java @@ -90,8 +90,12 @@ void unregisterSelectors() { sharedSelectors.removeChannel(selectors); } + public LocalUtils localUtils() { + return connection.localUtils; + } + public JsonArray deviceDescriptors() { - return connection.localUtils.deviceDescriptors(); + return localUtils().deviceDescriptors(); } @Override diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java b/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java index 0129146dd..c19b08a84 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java @@ -21,25 +21,22 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.util.Arrays; import java.util.function.Predicate; import java.util.regex.Pattern; -import static com.microsoft.playwright.impl.Utils.globToRegex; import static com.microsoft.playwright.impl.Utils.toJsRegexFlags; class UrlMatcher { - private final String baseURL; public final String glob; public final Pattern pattern; public final Predicate predicate; - static UrlMatcher forOneOf(URL baseUrl, Object object) { + static UrlMatcher forOneOf(URL baseUrl, Object object, LocalUtils localUtils, boolean isWebSocketUrl) { if (object == null) { - return new UrlMatcher(null, null, null, null); + return new UrlMatcher(null, null, null); } if (object instanceof String) { - return new UrlMatcher(baseUrl, (String) object); + return UrlMatcher.forGlob(baseUrl, (String) object, localUtils, isWebSocketUrl); } if (object instanceof Pattern) { return new UrlMatcher((Pattern) object); @@ -66,61 +63,32 @@ private static String resolveUrl(String baseUrl, String spec) { } } - private static String normaliseUrl(String spec) { - try { - // Align with the Node.js URL parser which automatically adds a slash to the path if it is empty. - URI url = new URI(spec); - if (url.getScheme() != null && - Arrays.asList("http", "https", "ws", "wss").contains(url.getScheme()) && - url.getPath().isEmpty()) { - return new URI(url.getScheme(), url.getAuthority(), "/", url.getQuery(), url.getFragment()).toString(); - } - return url.toString(); - } catch (URISyntaxException e) { - return spec; - } - } - - UrlMatcher(URL baseURL, String glob) { - this(baseURL, glob, null, null); + static UrlMatcher forGlob(URL baseURL, String glob, LocalUtils localUtils, boolean isWebSocketUrl) { + Pattern pattern = localUtils.globToRegex(glob, baseURL != null ? baseURL.toString() : null, isWebSocketUrl); + return new UrlMatcher(glob, pattern, null); } UrlMatcher(Pattern pattern) { - this(null, null, pattern, null); + this(null, pattern, null); } UrlMatcher(Predicate predicate) { - this(null, null, null, predicate); + this(null, null, predicate); } - private UrlMatcher(URL baseURL, String glob, Pattern pattern, Predicate predicate) { - this.baseURL = baseURL != null ? baseURL.toString() : null; + private UrlMatcher(String glob, Pattern pattern, Predicate predicate) { this.glob = glob; this.pattern = pattern; this.predicate = predicate; } boolean test(String value) { - return testImpl(baseURL, pattern, predicate, glob, value); - } - - private static boolean testImpl(String baseURL, Pattern pattern, Predicate predicate, String glob, String value) { if (pattern != null) { return pattern.matcher(value).find(); } if (predicate != null) { return predicate.test(value); } - if (glob != null) { - if (!glob.startsWith("*")) { - // Allow http(s) baseURL to match ws(s) urls. - if (baseURL != null && Pattern.compile("^https?://").matcher(baseURL).find() && Pattern.compile("^wss?://").matcher(value).find()) { - baseURL = baseURL.replaceFirst("^http", "ws"); - } - glob = normaliseUrl(resolveUrl(baseURL, glob)); - } - return Pattern.compile(globToRegex(glob)).matcher(value).find(); - } return true; } @@ -157,10 +125,12 @@ public int hashCode() { @Override public String toString() { + if (glob != null) + return String.format("", glob); if (pattern != null) return String.format("", pattern.pattern(), toJsRegexFlags(pattern)); if (this.predicate != null) return ""; - return String.format("", glob); + return ""; } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java index 85239e40b..26d6db846 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java @@ -17,7 +17,6 @@ package com.microsoft.playwright.impl; import com.google.gson.JsonArray; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.options.ClientCertificate; @@ -32,7 +31,6 @@ import java.lang.reflect.Modifier; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.attribute.FileTime; import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -91,79 +89,6 @@ public static T clone(T f) { return convertType(f, (Class) f.getClass()); } - - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping - static Set escapeGlobChars = new HashSet<>(Arrays.asList('$', '^', '+', '.', '*', '(', ')', '|', '\\', '?', '{', '}', '[', ']')); - - static String globToRegex(String glob) { - StringBuilder tokens = new StringBuilder(); - tokens.append('^'); - boolean inGroup = false; - for (int i = 0; i < glob.length(); ++i) { - char c = glob.charAt(i); - if (c == '\\' && i + 1 < glob.length()) { - char nextChar = glob.charAt(++i); - if (escapeGlobChars.contains(nextChar)) { - tokens.append('\\'); - } - tokens.append(nextChar); - continue; - } - if (c == '*') { - boolean beforeDeep = i < 1 || glob.charAt(i - 1) == '/'; - int starCount = 1; - while (i + 1 < glob.length() && glob.charAt(i + 1) == '*') { - starCount++; - i++; - } - boolean afterDeep = i + 1 >= glob.length() || glob.charAt(i + 1) == '/'; - boolean isDeep = starCount > 1 && beforeDeep && afterDeep; - if (isDeep) { - tokens.append("((?:[^/]*(?:\\/|$))*)"); - i++; - } else { - tokens.append("([^/]*)"); - } - continue; - } - - switch (c) { - case '?': - tokens.append('.'); - break; - case '[': - tokens.append('['); - break; - case ']': - tokens.append(']'); - break; - case '{': - inGroup = true; - tokens.append('('); - break; - case '}': - inGroup = false; - tokens.append(')'); - break; - case ',': - if (inGroup) { - tokens.append('|'); - break; - } - tokens.append("\\").append(c); - break; - default: - if (escapeGlobChars.contains(c)) { - tokens.append('\\'); - } - tokens.append(c); - break; - } - } - tokens.append('$'); - return tokens.toString(); - } - static String mimeType(Path path) { String mimeType; try { diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextStorageState.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextStorageState.java index fba305d6e..4d62fec4f 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextStorageState.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextStorageState.java @@ -195,14 +195,18 @@ void shouldSupportIndexedDB() { " \"keyPath\": \"taskTitle\",\n" + " \"records\": [\n" + " {\n" + - " \"value\": {\n" + - " \"day\": \"01\",\n" + - " \"hours\": \"1\",\n" + - " \"minutes\": \"1\",\n" + - " \"month\": \"January\",\n" + - " \"notified\": \"no\",\n" + - " \"taskTitle\": \"Pet the cat\",\n" + - " \"year\": \"2025\"\n" + + " \"valueEncoded\": {\n" + + " \"id\": 1,\n" + + " \"o\": [\n" + + " {\"k\": \"taskTitle\", \"v\": \"Pet the cat\"},\n" + + " {\"k\": \"hours\", \"v\": \"1\"},\n" + + " {\"k\": \"minutes\", \"v\": \"1\"},\n" + + " {\"k\": \"day\", \"v\": \"01\"},\n" + + " {\"k\": \"month\", \"v\": \"January\"},\n" + + " {\"k\": \"year\", \"v\": \"2025\"},\n" + + " {\"k\": \"notified\", \"v\": \"no\"},\n" + + " {\"k\": \"signature\", \"v\": { \"ta\": {\"b\":\"c2lnbmVkIGJ5IHNpbW9u\",\"k\":\"ui8\"}}}\n" + + " ]\n" + " }\n" + " }\n" + " ],\n" + diff --git a/playwright/src/test/java/com/microsoft/playwright/TestElementHandleSelectText.java b/playwright/src/test/java/com/microsoft/playwright/TestElementHandleSelectText.java index dc4f3b8c4..7972d6771 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestElementHandleSelectText.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestElementHandleSelectText.java @@ -28,7 +28,7 @@ void shouldSelectTextarea() { ElementHandle textarea = page.querySelector("textarea"); textarea.evaluate("textarea => textarea.value = 'some value'"); textarea.selectText(); - if (isFirefox() || isWebKit()) { + if (isFirefox()) { assertEquals(0, textarea.evaluate("el => el.selectionStart")); assertEquals(10, textarea.evaluate("el => el.selectionEnd")); } else { @@ -42,7 +42,7 @@ void shouldSelectInput() { ElementHandle input = page.querySelector("input"); input.evaluate("input => input.value = 'some value'"); input.selectText(); - if (isFirefox() || isWebKit()) { + if (isFirefox()) { assertEquals(0, input.evaluate("el => el.selectionStart")); assertEquals(10, input.evaluate("el => el.selectionEnd")); } else { diff --git a/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java b/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java index e681d70a6..f11e39f22 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java @@ -17,6 +17,7 @@ package com.microsoft.playwright; import com.google.gson.Gson; +import com.microsoft.playwright.APIRequest.NewContextOptions; import com.microsoft.playwright.options.HttpCredentials; import com.microsoft.playwright.options.HttpCredentialsSend; import com.microsoft.playwright.options.HttpHeader; @@ -37,6 +38,8 @@ import static org.junit.jupiter.api.Assertions.*; public class TestGlobalFetch extends TestBase { + private static final List HTTP_METHODS = asList("GET", "PUT", "POST", "OPTIONS", "HEAD", "PATCH"); + @Test void shouldHaveJavaInDefaultUesrAgent() throws ExecutionException, InterruptedException { APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions()); @@ -358,7 +361,7 @@ void shouldThrowAnErrorWhenMaxRedirectsIsExceeded() { server.setRedirect("/b/c/redirect4", "/simple.json"); APIRequestContext request = playwright.request().newContext(); - for (String method : new String[] {"GET", "PUT", "POST", "OPTIONS", "HEAD", "PATCH"}) { + for (String method : HTTP_METHODS) { for (int maxRedirects = 1; maxRedirects < 4; maxRedirects++) { int currMaxRedirects = maxRedirects; PlaywrightException exception = assertThrows(PlaywrightException.class, @@ -370,13 +373,69 @@ void shouldThrowAnErrorWhenMaxRedirectsIsExceeded() { request.dispose(); } + @Test + void shouldUseMaxRedirectsFromFetchWhenProvidedOverridingNewContext() { + server.setRedirect("/a/redirect1", "/b/c/redirect2"); + server.setRedirect("/b/c/redirect2", "/b/c/redirect3"); + server.setRedirect("/b/c/redirect3", "/b/c/redirect4"); + server.setRedirect("/b/c/redirect4", "/simple.json"); + + APIRequestContext request = playwright.request().newContext(new NewContextOptions().setMaxRedirects(1)); + for (String method : HTTP_METHODS) { + APIResponse response = request.fetch(server.PREFIX + "/a/redirect1", + RequestOptions.create().setMethod(method).setMaxRedirects(4)); + assertEquals(200, response.status()); + } + request.dispose(); + } + + @Test + void shouldFollowRedirectsUpToMaxRedirectsLimitSetInNewContext() { + server.setRedirect("/a/redirect1", "/b/c/redirect2"); + server.setRedirect("/b/c/redirect2", "/b/c/redirect3"); + server.setRedirect("/b/c/redirect3", "/b/c/redirect4"); + server.setRedirect("/b/c/redirect4", "/simple.json"); + + for (String method : HTTP_METHODS) { + for (int maxRedirects = 1; maxRedirects <= 4; maxRedirects++) { + int currMaxRedirects = maxRedirects; + APIRequestContext request = playwright.request().newContext(new NewContextOptions().setMaxRedirects(currMaxRedirects)); + if (maxRedirects < 4) { + PlaywrightException exception = assertThrows(PlaywrightException.class, + () -> request.fetch(server.PREFIX + "/a/redirect1", + RequestOptions.create().setMethod(method))); + assertTrue(exception.getMessage().contains("Max redirect count exceeded"), exception.getMessage()); + } else { + APIResponse response = request.fetch(server.PREFIX + "/a/redirect1", RequestOptions.create().setMethod(method)); + assertEquals(200, response.status()); + } + request.dispose(); + } + } + } + + @Test + void shouldNotFollowRedirectsWhenMaxRedirectsIsSetTo0InNewContext() { + server.setRedirect("/a/redirect1", "/b/c/redirect2"); + server.setRedirect("/b/c/redirect2", "/simple.json"); + + APIRequestContext request = playwright.request().newContext(new NewContextOptions().setMaxRedirects(0)); + for (String method : HTTP_METHODS) { + APIResponse response = request.fetch(server.PREFIX + "/a/redirect1", + RequestOptions.create().setMethod(method)); + assertEquals("/b/c/redirect2", response.headers().get("location")); + assertEquals(302, response.status()); + } + request.dispose(); + } + @Test void shouldNotFollowRedirectsWhenMaxRedirectsIsSetTo0() { server.setRedirect("/a/redirect1", "/b/c/redirect2"); server.setRedirect("/b/c/redirect2", "/simple.json"); APIRequestContext request = playwright.request().newContext(); - for (String method : new String[] {"GET", "PUT", "POST", "OPTIONS", "HEAD", "PATCH"}) { + for (String method : HTTP_METHODS) { APIResponse response = request.fetch(server.PREFIX + "/a/redirect1", RequestOptions.create().setMethod(method).setMaxRedirects(0)); assertEquals("/b/c/redirect2", response.headers().get("location")); @@ -391,7 +450,7 @@ void shouldThrowAnErrorWhenMaxRedirectsIsLessThan0() { server.setRedirect("/b/c/redirect2", "/simple.json"); APIRequestContext request = playwright.request().newContext(); - for (String method : new String[] {"GET", "PUT", "POST", "OPTIONS", "HEAD", "PATCH"}) { + for (String method : HTTP_METHODS) { PlaywrightException exception = assertThrows(PlaywrightException.class, () -> request.fetch(server.PREFIX + "/a/redirect1", RequestOptions.create().setMethod(method).setMaxRedirects(-1))); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestLocatorAssertions.java b/playwright/src/test/java/com/microsoft/playwright/TestLocatorAssertions.java index ff7b7c714..e275483cd 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestLocatorAssertions.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestLocatorAssertions.java @@ -18,10 +18,12 @@ import com.microsoft.playwright.assertions.LocatorAssertions; import com.microsoft.playwright.assertions.PlaywrightAssertions; +import com.microsoft.playwright.assertions.LocatorAssertions.ContainsClassOptions; + import org.junit.jupiter.api.Test; import org.opentest4j.AssertionFailedError; -import org.opentest4j.ValueWrapper; +import static java.util.Arrays.asList; import java.util.regex.Pattern; import static com.microsoft.playwright.Utils.mapOf; @@ -1092,4 +1094,49 @@ void defaultTimeoutZeroHasTextPass() { // Restore default. PlaywrightAssertions.setDefaultAssertionTimeout(5_000); } + + @Test + void containsClassPass() { + page.setContent("

"); + Locator locator = page.locator("div"); + assertThat(locator).containsClass(""); + assertThat(locator).containsClass("bar"); + assertThat(locator).containsClass("baz bar"); + assertThat(locator).containsClass(" bar foo "); + assertThat(locator).not().containsClass(" baz not-matching"); + } + + @Test + void containsClassPassWithSvgs() { + page.setContent(""); + assertThat(page.locator("svg")).containsClass("c1"); + assertThat(page.locator("svg")).containsClass("c2 c1"); + } + + @Test + void containsClassFail() { + page.setContent("
"); + AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> { + assertThat(page.locator("div")).containsClass("does-not-exist", new ContainsClassOptions().setTimeout(1000)); + }); + assertTrue(e.getMessage().contains("Locator.expect with timeout 1000ms"), e.getMessage()); + } + + @Test + void containsClassPassWithArray() { + page.setContent("
"); + Locator locator = page.locator("div"); + assertThat(locator).containsClass(asList("foo", "hello", "baz")); + assertThat(locator).not().hasClass(new String[]{"not-there", "hello", "baz"}); // Class not there + assertThat(locator).not().hasClass(new String[]{"foo", "hello"}); // length mismatch + } + + @Test + void containsClassFailWithArray() { + page.setContent("
"); + AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> { + assertThat(page.locator("div")).containsClass(asList("foo", "bar", "baz"), new ContainsClassOptions().setTimeout(1000)); + }); + assertTrue(e.getMessage().contains("Locator.expect with timeout 1000ms"), e.getMessage()); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshot.java b/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshot.java index 8dc68e166..726c05be4 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshot.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshot.java @@ -1,5 +1,6 @@ package com.microsoft.playwright; +import com.microsoft.playwright.Locator.AriaSnapshotOptions; import com.microsoft.playwright.junit.FixtureTest; import com.microsoft.playwright.junit.UsePlaywright; import org.junit.jupiter.api.Test; @@ -12,6 +13,8 @@ import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; @FixtureTest @UsePlaywright @@ -67,7 +70,13 @@ void shouldSnapshotListWithAccessibleName(Page page) { @Test void shouldSnapshotComplex(Page page) { page.setContent(""); - checkAndMatchSnapshot(page.locator("body"), "- list:\n - listitem:\n - link \"link\""); + checkAndMatchSnapshot(page.locator("body"), "- list:\n - listitem:\n - link \"link\":\n - /url: about:blank"); + } + + @Test + void shouldSnapshotRef(Page page) { + page.setContent(""); + assertEquals(unshift("- list [ref=s1e3]:\n - listitem [ref=s1e4]: foo"), page.locator("body").ariaSnapshot(new AriaSnapshotOptions().setRef(true))); } @Test @@ -83,4 +92,23 @@ void shouldSnapshotDetailsVisibility(Page page) { page.setContent("
Summary
Details
"); checkAndMatchSnapshot(page.locator("body"), "- group: Summary"); } + + @Test + void shouldSnapshotChildren(Page page) { + page.setContent(""); + assertThat(page.locator("body")).matchesAriaSnapshot("- list:\n - /children: equal\n - listitem\n - listitem: Two\n - listitem: Three"); + assertThat(page.locator("body")).not().matchesAriaSnapshot("- list:\n - /children: equal\n - listitem\n - listitem: Two"); + + assertThat(page.locator("body")).matchesAriaSnapshot("- list:\n - /children: deep-equal\n - listitem:\n - img\n - text: One\n - listitem: Two\n - listitem: Three"); + assertThat(page.locator("body")).not().matchesAriaSnapshot("- list:\n - /children: deep-equal\n - listitem:\n - text: One\n - listitem: Two\n - listitem: Three"); + assertThat(page.locator("body")).matchesAriaSnapshot("- list:\n - /children: deep-equal\n - listitem:\n - /children: contain\n - text: One\n - listitem: Two\n - listitem: Three"); + } + + @Test + void shouldMatchUrl(Page page) { + page.setContent("Link"); + assertThat(page.locator("body")).matchesAriaSnapshot("" + + "- link:\n" + + " - /url: /.*example.com/"); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageClock.java b/playwright/src/test/java/com/microsoft/playwright/TestPageClock.java index bbdbb2d29..d00d365d6 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageClock.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageClock.java @@ -391,8 +391,8 @@ void whileRunningShouldPause(Page page) { page.clock().install(new Clock.InstallOptions().setTime(0)); page.navigate("data:text/html,"); page.clock().pauseAt(1000); - page.waitForTimeout(1000); - page.clock().resume(); + // Internally wait to make sure the clock is paused and not running. + page.waitForTimeout(1111); int now = (int) page.evaluate("() => Date.now()"); assertTrue(now >= 0 && now <= 1000); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageInterception.java b/playwright/src/test/java/com/microsoft/playwright/TestPageInterception.java index a398ede58..a6462ff22 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageInterception.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageInterception.java @@ -18,11 +18,12 @@ import org.junit.jupiter.api.Test; -import java.io.OutputStreamWriter; -import java.io.Writer; +import com.microsoft.playwright.impl.PlaywrightImpl; + import java.util.HashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.*; @@ -141,13 +142,82 @@ void shouldNotFollowRedirectsWhenMaxRedirectsIsSetTo0InRouteFetch() { } @Test - void shouldProperlyHandleCharacterSetsInGlobs() { - page.route("**/[a-z]*.html", route -> { - APIResponse response = route.fetch(new Route.FetchOptions().setUrl(server.PREFIX + "/one-style.html")); - route.fulfill(new Route.FulfillOptions().setResponse(response)); - }); - Response response = page.navigate(server.PREFIX + "/empty.html"); - assertEquals(200, response.status()); - assertTrue(response.text().contains("one-style.css"), response.text()); + void shouldWorkWithGlob() { + assertTrue(globToRegex("**/*.js").matcher("https://localhost:8080/foo.js").find()); + assertFalse(globToRegex("**/*.css").matcher("https://localhost:8080/foo.js").find()); + assertFalse(globToRegex("*.js").matcher("https://localhost:8080/foo.js").find()); + assertTrue(globToRegex("https://**/*.js").matcher("https://localhost:8080/foo.js").find()); + assertTrue(globToRegex("http://localhost:8080/simple/path.js").matcher("http://localhost:8080/simple/path.js").find()); + assertTrue(globToRegex("**/{a,b}.js").matcher("https://localhost:8080/a.js").find()); + assertTrue(globToRegex("**/{a,b}.js").matcher("https://localhost:8080/b.js").find()); + assertFalse(globToRegex("**/{a,b}.js").matcher("https://localhost:8080/c.js").find()); + + assertTrue(globToRegex("**/*.{png,jpg,jpeg}").matcher("https://localhost:8080/c.jpg").find()); + assertTrue(globToRegex("**/*.{png,jpg,jpeg}").matcher("https://localhost:8080/c.jpeg").find()); + assertTrue(globToRegex("**/*.{png,jpg,jpeg}").matcher("https://localhost:8080/c.png").find()); + assertFalse(globToRegex("**/*.{png,jpg,jpeg}").matcher("https://localhost:8080/c.css").find()); + assertTrue(globToRegex("foo*").matcher("foo.js").find()); + assertFalse(globToRegex("foo*").matcher("foo/bar.js").find()); + assertFalse(globToRegex("http://localhost:3000/signin-oidc*").matcher("http://localhost:3000/signin-oidc/foo").find()); + assertTrue(globToRegex("http://localhost:3000/signin-oidc*").matcher("http://localhost:3000/signin-oidcnice").find()); + + // range [] is NOT supported + assertTrue(globToRegex("**/api/v[0-9]").matcher("http://example.com/api/v[0-9]").find()); + assertFalse(globToRegex("**/api/v[0-9]").matcher("http://example.com/api/version").find()); + + // query params + assertTrue(globToRegex("**/api\\?param").matcher("http://example.com/api?param").find()); + assertFalse(globToRegex("**/api\\?param").matcher("http://example.com/api-param").find()); + assertTrue(globToRegex("**/three-columns/settings.html\\?**id=settings-**").matcher("http://mydomain:8080/blah/blah/three-columns/settings.html?id=settings-e3c58efe-02e9-44b0-97ac-dd138100cf7c&blah").find()); + + assertEquals("^\\?$", globToRegex("\\?").pattern()); + assertEquals("^\\\\$", globToRegex("\\").pattern()); + assertEquals("^\\\\$", globToRegex("\\\\").pattern()); + assertEquals("^\\[$", globToRegex("\\[").pattern()); + assertEquals("^\\[a-z\\]$", globToRegex("[a-z]").pattern()); + assertEquals("^\\$\\^\\+\\.\\*\\(\\)\\|\\?\\{\\}\\[\\]$", globToRegex("$^+.\\*()|\\?\\{\\}\\[\\]").pattern()); + + + assertTrue(urlMatches(null, "http://playwright.dev/", "http://playwright.dev")); + assertTrue(urlMatches(null, "http://playwright.dev/?a=b", "http://playwright.dev?a=b")); + assertTrue(urlMatches(null, "http://playwright.dev/", "h*://playwright.dev")); + assertTrue(urlMatches(null, "http://api.playwright.dev/?x=y", "http://*.playwright.dev?x=y")); + assertTrue(urlMatches(null, "http://playwright.dev/foo/bar", "**/foo/**")); + assertTrue(urlMatches("http://playwright.dev", "http://playwright.dev/?x=y", "?x=y")); + assertTrue(urlMatches("http://playwright.dev/foo/", "http://playwright.dev/foo/bar?x=y", "./bar?x=y")); + + // This is not supported, we treat ? as a query separator. + assertFalse(urlMatches(null, "http://localhost:8080/Simple/path.js", "http://localhost:8080/?imple/path.js")); + assertFalse(urlMatches(null, "http://playwright.dev/", "http://playwright.?ev")); + assertTrue(urlMatches(null, "http://playwright./?ev", "http://playwright.?ev")); + assertFalse(urlMatches(null, "http://playwright.dev/foo", "http://playwright.dev/f??")); + assertTrue(urlMatches(null, "http://playwright.dev/f??", "http://playwright.dev/f??")); + assertTrue(urlMatches(null, "http://playwright.dev/?x=y", "http://playwright.dev\\\\?x=y")); + assertTrue(urlMatches(null, "http://playwright.dev/?x=y", "http://playwright.dev/\\\\?x=y")); + assertTrue(urlMatches("http://playwright.dev/foo", "http://playwright.dev/foo?bar", "?bar")); + assertTrue(urlMatches("http://playwright.dev/foo", "http://playwright.dev/foo?bar", "\\\\?bar")); + assertTrue(urlMatches("http://first.host/", "http://second.host/foo", "**/foo")); + assertTrue(urlMatches("http://playwright.dev/", "http://localhost/", "*//localhost/")); + } + + Pattern globToRegex(String glob) { + return globToRegex(glob, null, false); + } + + Pattern globToRegex(String glob, String baseURL, boolean webSocketUrl) { + return ((PlaywrightImpl) playwright).localUtils().globToRegex(glob, baseURL, webSocketUrl); + } + + boolean urlMatches(String baseURL, String urlString, String match) { + if (match == null) { + return true; + } + + String glob = (String) match; + if (glob.isEmpty()) { + return true; + } + + return globToRegex(glob, baseURL, false).matcher(urlString).find(); } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageRequestContinue.java b/playwright/src/test/java/com/microsoft/playwright/TestPageRequestContinue.java index 404371102..f715dd13a 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageRequestContinue.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageRequestContinue.java @@ -16,9 +16,12 @@ package com.microsoft.playwright; +import com.microsoft.playwright.options.HttpHeader; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; import java.io.OutputStreamWriter; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -77,4 +80,83 @@ void shouldNotThrowWhenContinuingAfterPageIsClosed() { e.getMessage().contains("frame was detached"), e.getMessage()); assertTrue(done[0]); } + + @Test + @DisabledIf(value = "com.microsoft.playwright.TestBase#isFirefox", disabledReason = "We currently clear all headers during interception in firefox") + void continueShouldNotPropagateCookieOverrideToRedirects() throws ExecutionException, InterruptedException { + // https://github.com/microsoft/playwright/issues/35168 + server.setRoute("/set-cookie", exchange -> { + exchange.getResponseHeaders().add("Set-Cookie", "foo=bar;"); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + }); + page.navigate(server.PREFIX + "/set-cookie"); + assertEquals("foo=bar", page.evaluate("() => document.cookie")); + + server.setRedirect("/redirect", server.PREFIX + "/empty.html"); + page.route("**/redirect", route -> { + Map headers = new HashMap<>(route.request().allHeaders()); + headers.put("cookie", "override"); + route.resume(new Route.ResumeOptions().setHeaders(headers)); + }); + + Future serverRequest = server.futureRequest("/empty.html"); + page.navigate(server.PREFIX + "/redirect"); + assertEquals(asList("foo=bar"), serverRequest.get().headers.get("cookie")); + } + + @Test + @DisabledIf(value = "com.microsoft.playwright.TestBase#isFirefox", disabledReason = "We currently clear all headers during interception in firefox") + void continueShouldNotOverrideCookie() throws ExecutionException, InterruptedException { + // https://github.com/microsoft/playwright/issues/35168 + server.setRoute("/set-cookie", exchange -> { + exchange.getResponseHeaders().add("Set-Cookie", "foo=bar;"); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + }); + page.navigate(server.PREFIX + "/set-cookie"); + assertEquals("foo=bar", page.evaluate("() => document.cookie")); + + page.route("**", route -> { + Map headers = new HashMap<>(route.request().allHeaders()); + headers.put("cookie", "override"); + headers.put("custom", "value"); + route.resume(new Route.ResumeOptions().setHeaders(headers)); + }); + + Future serverRequest = server.futureRequest("/empty.html"); + page.navigate(server.EMPTY_PAGE); + + // Original cookie from the browser's cookie jar should be sent. + assertEquals(asList("foo=bar"), serverRequest.get().headers.get("cookie")); + assertEquals(asList("value"), serverRequest.get().headers.get("custom")); + } + + @Test + void redirectAfterContinueShouldBeAbleToDeleteCookie() throws ExecutionException, InterruptedException { + // https://github.com/microsoft/playwright/issues/35168 + server.setRoute("/set-cookie", exchange -> { + exchange.getResponseHeaders().add("Set-Cookie", "foo=bar;"); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + }); + page.navigate(server.PREFIX + "/set-cookie"); + assertEquals("foo=bar", page.evaluate("() => document.cookie")); + + server.setRoute("/delete-cookie", exchange -> { + exchange.getResponseHeaders().add("Set-Cookie", "foo=bar; expires=Thu, 01 Jan 1970 00:00:00 GMT"); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + }); + server.setRedirect("/redirect", "/delete-cookie"); + page.route("**/redirect", route -> { + // Pass original headers explicitly when continuing. + route.resume(new Route.ResumeOptions().setHeaders(route.request().allHeaders())); + }); + page.navigate(server.PREFIX + "/redirect"); + + Future serverRequest = server.futureRequest("/empty.html"); + page.navigate(server.EMPTY_PAGE); + assertNull(serverRequest.get().headers.get("cookie")); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java b/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java index 6812c7d4d..842664dbd 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java @@ -16,7 +16,7 @@ package com.microsoft.playwright; -import com.microsoft.playwright.options.Cookie; +import com.microsoft.playwright.options.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; @@ -110,7 +110,7 @@ void shouldUnrouteNonExistentPatternHandler() { } @Test - void shouldSupportQuestionMarkInGlobPattern() { + void shouldNotSupportQuestionMarkInGlobPattern() { server.setRoute("/index", exchange -> { exchange.sendResponseHeaders(200, 0); try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) { @@ -123,6 +123,18 @@ void shouldSupportQuestionMarkInGlobPattern() { writer.write("index123hello"); } }); + server.setRoute("/index?hello", exchange -> { + exchange.sendResponseHeaders(200, 0); + try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write("index?hello"); + } + }); + server.setRoute("/index1hello", exchange -> { + exchange.sendResponseHeaders(200, 0); + try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write("index1hello"); + } + }); page.route("**/index?hello", route -> { route.fulfill(new Route.FulfillOptions().setBody("intercepted any character")); @@ -139,7 +151,8 @@ void shouldSupportQuestionMarkInGlobPattern() { assertTrue(page.content().contains("index-no-hello"), page.content()); page.navigate(server.PREFIX + "/index1hello"); - assertTrue(page.content().contains("intercepted any character"), page.content()); + assertFalse(page.content().contains("intercepted any character"), page.content()); + assertTrue(page.content().contains("index1hello"), page.content()); page.navigate(server.PREFIX + "/index123hello"); assertTrue(page.content().contains("index123hello"), page.content()); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageSelectOption.java b/playwright/src/test/java/com/microsoft/playwright/TestPageSelectOption.java index 59fe16e2e..5250f548c 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageSelectOption.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageSelectOption.java @@ -25,6 +25,7 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static org.junit.jupiter.api.Assertions.*; +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; public class TestPageSelectOption extends TestBase { @Test @@ -238,4 +239,64 @@ void shouldWorkWhenReDefiningTopLevelEventClass() { assertEquals(asList("blue"), page.evaluate("() => window['result'].onChange")); } + @Test + void shouldWaitForOptionToBeEnabled() { + page.setContent( + "\n" + + "\n" + + ""); + + page.evaluate("() => setTimeout(hydrate, 1000)"); + page.locator("select").selectOption("two"); + + assertEquals("two", page.evaluate("window['result']")); + assertThat(page.locator("select")).hasValue("two"); + } + + @Test + void shouldWaitForSelectToBeSwapped() { + page.setContent( + "\n" + + "\n" + + ""); + + page.evaluate("() => setTimeout(window.hydrate, 1000)"); + page.locator("select").selectOption("two"); + + assertThat(page.locator("select")).hasValue("two"); + assertEquals("two", page.evaluate("window['result']")); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageSetInputFiles.java b/playwright/src/test/java/com/microsoft/playwright/TestPageSetInputFiles.java index ef2b6b710..63fe51178 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageSetInputFiles.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageSetInputFiles.java @@ -123,7 +123,7 @@ void shouldUploadMultipleLargeFiles(@TempDir Path tmpDir) throws IOException, Ex } FileChooser fileChooser = page.waitForFileChooser(() -> input.click()); fileChooser.setFiles(uploadFiles.toArray(new Path[0])); - Object filesLen = page.getByRole(AriaRole.TEXTBOX).evaluate("e => e.files.length"); + Object filesLen = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Choose File")).evaluate("e => e.files.length"); assertTrue(fileChooser.isMultiple()); assertEquals(filesCount, filesLen); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestSelectorsRole.java b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsRole.java index 587c091e5..24f109948 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestSelectorsRole.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsRole.java @@ -18,6 +18,7 @@ import com.microsoft.playwright.options.AriaRole; import org.junit.jupiter.api.Test; +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; import java.util.regex.Pattern; @@ -241,6 +242,66 @@ void shouldSupportDisabled() { ), page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setDisabled(false)).evaluateAll("els => els.map(e => e.outerHTML)")); } + @Test + void shouldInheritDisabledFromTheAncestor() { + page.setContent( + "\n" + + " \n" + + ""); + assertThat(page.locator("button")).isDisabled(); + + page.setContent( + "\n" + + "

Heading

\n" + + "
"); + // Non-control roles do not inherit disabled state + assertThat(page.locator("h1")).isEnabled(); + } + + @Test + void shouldSupportDisabledFieldset() { + page.setContent( + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + "\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
"); + + assertThat(page.getByTestId("inside-legend-element")).isEnabled(); + assertThat(page.getByTestId("nested-inside-legend-element")).isEnabled(); + assertThat(page.getByTestId("first-legend-element")).isEnabled(); + // Only the first legend is exempt from disabled fieldset + assertThat(page.getByTestId("second-legend-element")).isDisabled(); + // Nested fieldsets inherit disabled state + assertThat(page.getByTestId("deep-button")).isDisabled(); + } + @Test void shouldSupportLevel() { page.setContent("

Hello

\n" + @@ -357,15 +418,6 @@ void shouldSupportName() { "
" ), page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("^H[ae]llo$"))).evaluateAll("els => els.map(e => e.outerHTML)")); - assertEquals(asList( - "
", - "
" - ), page.locator("role=button[name=/h.*o/i]").evaluateAll("els => els.map(e => e.outerHTML)")); - assertEquals(asList( - "
", - "
" - ), page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("h.*o", Pattern.CASE_INSENSITIVE))).evaluateAll("els => els.map(e => e.outerHTML)")); - assertEquals(asList( "
", "
" diff --git a/playwright/src/test/resources/to-do-notifications/scripts/todo.js b/playwright/src/test/resources/to-do-notifications/scripts/todo.js index 2e3cfae21..40e4bbbb0 100644 --- a/playwright/src/test/resources/to-do-notifications/scripts/todo.js +++ b/playwright/src/test/resources/to-do-notifications/scripts/todo.js @@ -140,7 +140,7 @@ window.onload = () => { // Grab the values entered into the form fields and store them in an object ready for being inserted into the IndexedDB const newItem = [ - { taskTitle: title.value, hours: hours.value, minutes: minutes.value, day: day.value, month: month.value, year: year.value, notified: 'no' }, + { taskTitle: title.value, hours: hours.value, minutes: minutes.value, day: day.value, month: month.value, year: year.value, notified: 'no', signature: new TextEncoder().encode("signed by simon") }, ]; // Open a read/write DB transaction, ready for adding the data diff --git a/scripts/DRIVER_VERSION b/scripts/DRIVER_VERSION index c73f500ec..a63cb35e6 100644 --- a/scripts/DRIVER_VERSION +++ b/scripts/DRIVER_VERSION @@ -1 +1 @@ -1.51.1 +1.52.0 diff --git a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java index c0b299240..fc088237d 100644 --- a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java +++ b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java @@ -998,7 +998,7 @@ void writeTo(List output, String offset) { if ("Clock".equals(jsonName)) { output.add("import java.util.Date;"); } - if (asList("Page", "Frame", "ElementHandle", "Locator", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright").contains(jsonName)) { + if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright").contains(jsonName)) { output.add("import java.util.*;"); } if (asList("WebSocketRoute").contains(jsonName)) {