diff --git a/README.md b/README.md index 11a4d5cf..c4525c26 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 129.0.6668.29 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 130.0.6723.19 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 18.0 | ✅ | ✅ | ✅ | | Firefox 130.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/playwright/pom.xml b/playwright/pom.xml index 27bbab62..2fe9e5e1 100644 --- a/playwright/pom.xml +++ b/playwright/pom.xml @@ -61,6 +61,10 @@ org.junit.jupiter junit-jupiter-engine + + org.junit.jupiter + junit-jupiter-params + org.opentest4j opentest4j diff --git a/playwright/src/main/java/com/microsoft/playwright/APIRequest.java b/playwright/src/main/java/com/microsoft/playwright/APIRequest.java index 691a615d..48b52264 100644 --- a/playwright/src/main/java/com/microsoft/playwright/APIRequest.java +++ b/playwright/src/main/java/com/microsoft/playwright/APIRequest.java @@ -51,8 +51,6 @@ class NewContextOptions { * {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code * origin} property should be provided with an exact match to the request origin that the certificate is valid for. * - *

NOTE: Using Client Certificates in combination with Proxy Servers is not supported. - * *

NOTE: When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by * replacing {@code localhost} with {@code local.playwright}. */ @@ -126,8 +124,6 @@ public NewContextOptions setBaseURL(String baseURL) { * {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code * origin} property should be provided with an exact match to the request origin that the certificate is valid for. * - *

NOTE: Using Client Certificates in combination with Proxy Servers is not supported. - * *

NOTE: When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by * replacing {@code localhost} with {@code local.playwright}. */ diff --git a/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java b/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java index b8774c0a..a2f0d743 100644 --- a/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java @@ -129,8 +129,7 @@ default void dispose() { * } * *

The common way to send file(s) in the body of a request is to upload them as form fields with {@code - * multipart/form-data} encoding. Use {@code FormData} to construct request body and pass it to the request as {@code - * multipart} parameter: + * multipart/form-data} encoding, by specifiying the {@code multipart} parameter: *

{@code
    * // Pass file path to the form data constructor:
    * Path file = Paths.get("team.csv");
@@ -167,8 +166,7 @@ default APIResponse fetch(String urlOrRequest) {
    * }
* *

The common way to send file(s) in the body of a request is to upload them as form fields with {@code - * multipart/form-data} encoding. Use {@code FormData} to construct request body and pass it to the request as {@code - * multipart} parameter: + * multipart/form-data} encoding, by specifiying the {@code multipart} parameter: *

{@code
    * // Pass file path to the form data constructor:
    * Path file = Paths.get("team.csv");
@@ -204,8 +202,7 @@ default APIResponse fetch(String urlOrRequest) {
    * }
* *

The common way to send file(s) in the body of a request is to upload them as form fields with {@code - * multipart/form-data} encoding. Use {@code FormData} to construct request body and pass it to the request as {@code - * multipart} parameter: + * multipart/form-data} encoding, by specifiying the {@code multipart} parameter: *

{@code
    * // Pass file path to the form data constructor:
    * Path file = Paths.get("team.csv");
@@ -242,8 +239,7 @@ default APIResponse fetch(Request urlOrRequest) {
    * }
* *

The common way to send file(s) in the body of a request is to upload them as form fields with {@code - * multipart/form-data} encoding. Use {@code FormData} to construct request body and pass it to the request as {@code - * multipart} parameter: + * multipart/form-data} encoding, by specifiying the {@code multipart} parameter: *

{@code
    * // Pass file path to the form data constructor:
    * Path file = Paths.get("team.csv");
diff --git a/playwright/src/main/java/com/microsoft/playwright/Browser.java b/playwright/src/main/java/com/microsoft/playwright/Browser.java
index e3d1529d..34f58217 100644
--- a/playwright/src/main/java/com/microsoft/playwright/Browser.java
+++ b/playwright/src/main/java/com/microsoft/playwright/Browser.java
@@ -106,8 +106,6 @@ class NewContextOptions {
      * {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code
      * origin} property should be provided with an exact match to the request origin that the certificate is valid for.
      *
-     * 

NOTE: Using Client Certificates in combination with Proxy Servers is not supported. - * *

NOTE: When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by * replacing {@code localhost} with {@code local.playwright}. */ @@ -317,8 +315,6 @@ public NewContextOptions setBypassCSP(boolean bypassCSP) { * {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code * origin} property should be provided with an exact match to the request origin that the certificate is valid for. * - *

NOTE: Using Client Certificates in combination with Proxy Servers is not supported. - * *

NOTE: When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by * replacing {@code localhost} with {@code local.playwright}. */ @@ -659,8 +655,6 @@ class NewPageOptions { * {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code * origin} property should be provided with an exact match to the request origin that the certificate is valid for. * - *

NOTE: Using Client Certificates in combination with Proxy Servers is not supported. - * *

NOTE: When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by * replacing {@code localhost} with {@code local.playwright}. */ @@ -870,8 +864,6 @@ public NewPageOptions setBypassCSP(boolean bypassCSP) { * {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code * origin} property should be provided with an exact match to the request origin that the certificate is valid for. * - *

NOTE: Using Client Certificates in combination with Proxy Servers is not supported. - * *

NOTE: When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by * replacing {@code localhost} with {@code local.playwright}. */ diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java index 4dfd7246..005345c6 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java @@ -927,7 +927,7 @@ default void grantPermissions(List permissions) { * *

NOTE: {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by * Service Worker. See this issue. We recommend disabling - * Service Workers when using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * Service Workers when using request interception by setting {@code serviceWorkers} to {@code "block"}. * *

Usage * @@ -983,7 +983,7 @@ default void route(String url, Consumer handler) { * *

NOTE: {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by * Service Worker. See this issue. We recommend disabling - * Service Workers when using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * Service Workers when using request interception by setting {@code serviceWorkers} to {@code "block"}. * *

Usage * @@ -1037,7 +1037,7 @@ default void route(String url, Consumer handler) { * *

NOTE: {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by * Service Worker. See this issue. We recommend disabling - * Service Workers when using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * Service Workers when using request interception by setting {@code serviceWorkers} to {@code "block"}. * *

Usage * @@ -1093,7 +1093,7 @@ default void route(Pattern url, Consumer handler) { * *

NOTE: {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by * Service Worker. See this issue. We recommend disabling - * Service Workers when using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * Service Workers when using request interception by setting {@code serviceWorkers} to {@code "block"}. * *

Usage * @@ -1147,7 +1147,7 @@ default void route(Pattern url, Consumer handler) { * *

NOTE: {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by * Service Worker. See this issue. We recommend disabling - * Service Workers when using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * Service Workers when using request interception by setting {@code serviceWorkers} to {@code "block"}. * *

Usage * @@ -1203,7 +1203,7 @@ default void route(Predicate url, Consumer handler) { * *

NOTE: {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} will not intercept requests intercepted by * Service Worker. See this issue. We recommend disabling - * Service Workers when using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * Service Workers when using request interception by setting {@code serviceWorkers} to {@code "block"}. * *

Usage * @@ -1257,7 +1257,7 @@ default void route(Predicate url, Consumer handler) { * *

Playwright will not serve requests intercepted by Service Worker from the HAR file. See this issue. We recommend disabling Service Workers when - * using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * using request interception by setting {@code serviceWorkers} to {@code "block"}. * * @param har Path to a HAR file with prerecorded network data. If {@code * path} is a relative path, then it is resolved relative to the current working directory. @@ -1272,13 +1272,94 @@ default void routeFromHAR(Path har) { * *

Playwright will not serve requests intercepted by Service Worker from the HAR file. See this issue. We recommend disabling Service Workers when - * using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * using request interception by setting {@code serviceWorkers} to {@code "block"}. * * @param har Path to a HAR file with prerecorded network data. If {@code * path} is a relative path, then it is resolved relative to the current working directory. * @since v1.23 */ void routeFromHAR(Path har, RouteFromHAROptions options); + /** + * This method allows to modify websocket connections that are made by any page in the browser context. + * + *

Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this + * method before creating any pages. + * + *

Usage + * + *

Below is an example of a simple handler that blocks some websocket messages. See {@code WebSocketRoute} for more details + * and examples. + *

{@code
+   * context.routeWebSocket("/ws", ws -> {
+   *   ws.routeSend(message -> {
+   *     if ("to-be-blocked".equals(message))
+   *       return;
+   *     ws.send(message);
+   *   });
+   *   ws.connect();
+   * });
+   * }
+ * + * @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code + * baseURL} context option. + * @param handler Handler function to route the WebSocket. + * @since v1.48 + */ + void routeWebSocket(String url, Consumer handler); + /** + * This method allows to modify websocket connections that are made by any page in the browser context. + * + *

Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this + * method before creating any pages. + * + *

Usage + * + *

Below is an example of a simple handler that blocks some websocket messages. See {@code WebSocketRoute} for more details + * and examples. + *

{@code
+   * context.routeWebSocket("/ws", ws -> {
+   *   ws.routeSend(message -> {
+   *     if ("to-be-blocked".equals(message))
+   *       return;
+   *     ws.send(message);
+   *   });
+   *   ws.connect();
+   * });
+   * }
+ * + * @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code + * baseURL} context option. + * @param handler Handler function to route the WebSocket. + * @since v1.48 + */ + void routeWebSocket(Pattern url, Consumer handler); + /** + * This method allows to modify websocket connections that are made by any page in the browser context. + * + *

Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this + * method before creating any pages. + * + *

Usage + * + *

Below is an example of a simple handler that blocks some websocket messages. See {@code WebSocketRoute} for more details + * and examples. + *

{@code
+   * context.routeWebSocket("/ws", ws -> {
+   *   ws.routeSend(message -> {
+   *     if ("to-be-blocked".equals(message))
+   *       return;
+   *     ws.send(message);
+   *   });
+   *   ws.connect();
+   * });
+   * }
+ * + * @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code + * baseURL} context option. + * @param handler Handler function to route the WebSocket. + * @since v1.48 + */ + void routeWebSocket(Predicate url, Consumer handler); /** * This setting will change the default maximum navigation time for the following methods and related shortcuts: *
    diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java index 8ab6e59a..827d4868 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java @@ -465,8 +465,6 @@ class LaunchPersistentContextOptions { * {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code * origin} property should be provided with an exact match to the request origin that the certificate is valid for. * - *

    NOTE: Using Client Certificates in combination with Proxy Servers is not supported. - * *

    NOTE: When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by * replacing {@code localhost} with {@code local.playwright}. */ @@ -768,8 +766,6 @@ public LaunchPersistentContextOptions setChromiumSandbox(boolean chromiumSandbox * {@code pfx}). Optionally, {@code passphrase} property should be provided if the certificate is encrypted. The {@code * origin} property should be provided with an exact match to the request origin that the certificate is valid for. * - *

    NOTE: Using Client Certificates in combination with Proxy Servers is not supported. - * *

    NOTE: When using WebKit on macOS, accessing {@code localhost} will not pick up client certificates. You can make it work by * replacing {@code localhost} with {@code local.playwright}. */ diff --git a/playwright/src/main/java/com/microsoft/playwright/Frame.java b/playwright/src/main/java/com/microsoft/playwright/Frame.java index 000fc31d..d732b829 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Frame.java +++ b/playwright/src/main/java/com/microsoft/playwright/Frame.java @@ -290,7 +290,8 @@ class ClickOptions { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public Boolean trial; @@ -375,7 +376,8 @@ public ClickOptions setTimeout(double timeout) { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public ClickOptions setTrial(boolean trial) { this.trial = trial; @@ -426,7 +428,8 @@ class DblclickOptions { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public Boolean trial; @@ -504,7 +507,8 @@ public DblclickOptions setTimeout(double timeout) { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public DblclickOptions setTrial(boolean trial) { this.trial = trial; @@ -1127,7 +1131,8 @@ class HoverOptions { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public Boolean trial; @@ -1191,7 +1196,8 @@ public HoverOptions setTimeout(double timeout) { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public HoverOptions setTrial(boolean trial) { this.trial = trial; @@ -1932,7 +1938,8 @@ class TapOptions { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public Boolean trial; @@ -1996,7 +2003,8 @@ public TapOptions setTimeout(double timeout) { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public TapOptions setTrial(boolean trial) { this.trial = trial; diff --git a/playwright/src/main/java/com/microsoft/playwright/FrameLocator.java b/playwright/src/main/java/com/microsoft/playwright/FrameLocator.java index dade2420..0fe625a3 100644 --- a/playwright/src/main/java/com/microsoft/playwright/FrameLocator.java +++ b/playwright/src/main/java/com/microsoft/playwright/FrameLocator.java @@ -22,10 +22,10 @@ /** * 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 - * com.microsoft.playwright.Page#frameLocator Page.frameLocator()} or {@link com.microsoft.playwright.Locator#frameLocator - * Locator.frameLocator()} method. + * com.microsoft.playwright.Locator#contentFrame Locator.contentFrame()}, {@link com.microsoft.playwright.Page#frameLocator + * Page.frameLocator()} or {@link com.microsoft.playwright.Locator#frameLocator Locator.frameLocator()} method. *

    {@code
    - * Locator locator = page.frameLocator("#my-frame").getByText("Submit");
    + * Locator locator = page.locator("#my-frame").contentFrame().getByText("Submit");
      * locator.click();
      * }
    * @@ -35,10 +35,10 @@ * a given selector. *
    {@code
      * // Throws if there are several frames in DOM:
    - * page.frame_locator(".result-frame").getByRole(AriaRole.BUTTON).click();
    + * page.locator(".result-frame").contentFrame().getByRole(AriaRole.BUTTON).click();
      *
      * // Works because we explicitly tell locator to pick the first frame:
    - * page.frame_locator(".result-frame").first().getByRole(AriaRole.BUTTON).click();
    + * page.locator(".result-frame").first().contentFrame().getByRole(AriaRole.BUTTON).click();
      * }
    * *

    Converting Locator to FrameLocator @@ -383,7 +383,8 @@ public LocatorOptions setHasText(Pattern hasText) { } } /** - * Returns locator to the first matching frame. + * @deprecated Use {@link com.microsoft.playwright.Locator#first Locator.first()} followed by {@link + * com.microsoft.playwright.Locator#contentFrame Locator.contentFrame()} instead. * * @since v1.17 */ @@ -953,7 +954,8 @@ default Locator getByTitle(Pattern text) { */ Locator getByTitle(Pattern text, GetByTitleOptions options); /** - * Returns locator to the last matching frame. + * @deprecated Use {@link com.microsoft.playwright.Locator#last Locator.last()} followed by {@link + * com.microsoft.playwright.Locator#contentFrame Locator.contentFrame()} instead. * * @since v1.17 */ @@ -1003,7 +1005,8 @@ default Locator locator(Locator selectorOrLocator) { */ Locator locator(Locator selectorOrLocator, LocatorOptions options); /** - * Returns locator to the n-th matching frame. It's zero based, {@code nth(0)} selects the first frame. + * @deprecated Use {@link com.microsoft.playwright.Locator#nth Locator.nth()} followed by {@link + * com.microsoft.playwright.Locator#contentFrame Locator.contentFrame()} instead. * * @since v1.17 */ @@ -1018,7 +1021,7 @@ default Locator locator(Locator selectorOrLocator) { * *

    Usage *

    {@code
    -   * FrameLocator frameLocator = page.frameLocator("iframe[name=\"embedded\"]");
    +   * FrameLocator frameLocator = page.locator("iframe[name=\"embedded\"]").contentFrame();
        * // ...
        * Locator locator = frameLocator.owner();
        * assertThat(locator).isVisible();
    diff --git a/playwright/src/main/java/com/microsoft/playwright/Locator.java b/playwright/src/main/java/com/microsoft/playwright/Locator.java
    index 9d8021b6..99161071 100644
    --- a/playwright/src/main/java/com/microsoft/playwright/Locator.java
    +++ b/playwright/src/main/java/com/microsoft/playwright/Locator.java
    @@ -235,7 +235,8 @@ class ClickOptions {
         /**
          * When set, this method only performs the actionability
          * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
    -     * performing it.
    +     * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
    +     * elements which are only visible when those keys are pressed.
          */
         public Boolean trial;
     
    @@ -312,7 +313,8 @@ public ClickOptions setTimeout(double timeout) {
         /**
          * When set, this method only performs the actionability
          * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
    -     * performing it.
    +     * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
    +     * elements which are only visible when those keys are pressed.
          */
         public ClickOptions setTrial(boolean trial) {
           this.trial = trial;
    @@ -358,7 +360,8 @@ class DblclickOptions {
         /**
          * When set, this method only performs the actionability
          * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
    -     * performing it.
    +     * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
    +     * elements which are only visible when those keys are pressed.
          */
         public Boolean trial;
     
    @@ -428,7 +431,8 @@ public DblclickOptions setTimeout(double timeout) {
         /**
          * When set, this method only performs the actionability
          * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
    -     * performing it.
    +     * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
    +     * elements which are only visible when those keys are pressed.
          */
         public DblclickOptions setTrial(boolean trial) {
           this.trial = trial;
    @@ -1059,7 +1063,8 @@ class HoverOptions {
         /**
          * When set, this method only performs the actionability
          * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
    -     * performing it.
    +     * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
    +     * elements which are only visible when those keys are pressed.
          */
         public Boolean trial;
     
    @@ -1115,7 +1120,8 @@ public HoverOptions setTimeout(double timeout) {
         /**
          * When set, this method only performs the actionability
          * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
    -     * performing it.
    +     * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
    +     * elements which are only visible when those keys are pressed.
          */
         public HoverOptions setTrial(boolean trial) {
           this.trial = trial;
    @@ -1882,7 +1888,8 @@ class TapOptions {
         /**
          * When set, this method only performs the actionability
          * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
    -     * performing it.
    +     * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
    +     * elements which are only visible when those keys are pressed.
          */
         public Boolean trial;
     
    @@ -1938,7 +1945,8 @@ public TapOptions setTimeout(double timeout) {
         /**
          * When set, this method only performs the actionability
          * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without
    -     * performing it.
    +     * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing
    +     * elements which are only visible when those keys are pressed.
          */
         public TapOptions setTrial(boolean trial) {
           this.trial = trial;
    @@ -2137,10 +2145,9 @@ public WaitForOptions setTimeout(double timeout) {
        * When the locator points to a list of elements, this returns an array of locators, pointing to their respective elements.
        *
        * 

    NOTE: {@link com.microsoft.playwright.Locator#all Locator.all()} does not wait for elements to match the locator, and instead - * immediately returns whatever is present in the page. When the list of elements changes dynamically, {@link - * com.microsoft.playwright.Locator#all Locator.all()} will produce unpredictable and flaky results. When the list of - * elements is stable, but loaded dynamically, wait for the full list to finish loading before calling {@link - * com.microsoft.playwright.Locator#all Locator.all()}. + * immediately returns whatever is present in the page.When the list of elements changes dynamically, {@link com.microsoft.playwright.Locator#all Locator.all()} will produce + * unpredictable and flaky results.When the list of elements is stable, but loaded dynamically, wait for the full list to finish loading before calling + * {@link com.microsoft.playwright.Locator#all Locator.all()}. * *

    Usage *

    {@code
    @@ -4077,7 +4084,10 @@ default Locator locator(Locator selectorOrLocator) {
        */
       Locator nth(int index);
       /**
    -   * Creates a locator that matches either of the two locators.
    +   * Creates a locator matching all elements that match one or both of the two locators.
    +   *
    +   * 

    Note that when both locators match something, the resulting locator will have multiple matches and violate locator strictness guidelines. * *

    Usage * diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java index 096812a2..14e788a4 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Page.java +++ b/playwright/src/main/java/com/microsoft/playwright/Page.java @@ -560,7 +560,8 @@ class ClickOptions { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public Boolean trial; @@ -645,7 +646,8 @@ public ClickOptions setTimeout(double timeout) { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public ClickOptions setTrial(boolean trial) { this.trial = trial; @@ -723,7 +725,8 @@ class DblclickOptions { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public Boolean trial; @@ -801,7 +804,8 @@ public DblclickOptions setTimeout(double timeout) { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public DblclickOptions setTrial(boolean trial) { this.trial = trial; @@ -1591,7 +1595,8 @@ class HoverOptions { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public Boolean trial; @@ -1655,7 +1660,8 @@ public HoverOptions setTimeout(double timeout) { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public HoverOptions setTrial(boolean trial) { this.trial = trial; @@ -2962,7 +2968,8 @@ class TapOptions { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public Boolean trial; @@ -3026,7 +3033,8 @@ public TapOptions setTimeout(double timeout) { /** * When set, this method only performs the actionability * checks and skips the action. Defaults to {@code false}. Useful to wait until the element is ready for the action without - * performing it. + * performing it. Note that keyboard {@code modifiers} will be pressed regardless of {@code trial} to allow testing + * elements which are only visible when those keys are pressed. */ public TapOptions setTrial(boolean trial) { this.trial = trial; @@ -5414,6 +5422,25 @@ default Response goForward() { * @since v1.8 */ Response goForward(GoForwardOptions options); + /** + * Request the page to perform garbage collection. Note that there is no guarantee that all unreachable objects will be + * collected. + * + *

    This is useful to help detect memory leaks. For example, if your page has a large object {@code "suspect"} that might be + * leaked, you can check that it does not leak by using a {@code WeakRef}. + *

    {@code
    +   * // 1. In your page, save a WeakRef for the "suspect".
    +   * page.evaluate("globalThis.suspectWeakRef = new WeakRef(suspect)");
    +   * // 2. Request garbage collection.
    +   * page.requestGC();
    +   * // 3. Check that weak ref does not deref to the original object.
    +   * assertTrue(page.evaluate("!globalThis.suspectWeakRef.deref()"));
    +   * }
    + * + * @since v1.48 + */ + void requestGC(); /** * Returns the main resource response. In case of multiple redirects, the navigation will resolve with the first * non-redirect response. @@ -5773,8 +5800,7 @@ default Locator locator(String selector) { *

    User can inspect selectors or perform manual steps while paused. Resume will continue running the original script from * the place it was paused. * - *

    NOTE: This method requires Playwright to be started in a headed mode, with a falsy {@code headless} value in the {@link - * com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. + *

    NOTE: This method requires Playwright to be started in a headed mode, with a falsy {@code headless} option. * * @since v1.9 */ @@ -6041,15 +6067,13 @@ default ElementHandle querySelector(String selector) { * *

    NOTE: Running the handler will alter your page state mid-test. For example it will change the currently focused element and * move the mouse. Make sure that actions that run after the handler are self-contained and do not rely on the focus and - * mouse state being unchanged.

    For example, consider a test that calls {@link - * com.microsoft.playwright.Locator#focus Locator.focus()} followed by {@link com.microsoft.playwright.Keyboard#press - * Keyboard.press()}. If your handler clicks a button between these two actions, the focused element most likely will be - * wrong, and key press will happen on the unexpected element. Use {@link com.microsoft.playwright.Locator#press - * Locator.press()} instead to avoid this problem.

    Another example is a series of mouse actions, where {@link - * com.microsoft.playwright.Mouse#move Mouse.move()} is followed by {@link com.microsoft.playwright.Mouse#down - * Mouse.down()}. Again, when the handler runs between these two actions, the mouse position will be wrong during the mouse - * down. Prefer self-contained actions like {@link com.microsoft.playwright.Locator#click Locator.click()} that do not rely - * on the state being unchanged by a handler. + * mouse state being unchanged.For example, consider a test that calls {@link com.microsoft.playwright.Locator#focus Locator.focus()} followed by + * {@link com.microsoft.playwright.Keyboard#press Keyboard.press()}. If your handler clicks a button between these two + * actions, the focused element most likely will be wrong, and key press will happen on the unexpected element. Use {@link + * com.microsoft.playwright.Locator#press Locator.press()} instead to avoid this problem.Another example is a series of mouse actions, where {@link com.microsoft.playwright.Mouse#move Mouse.move()} is followed + * by {@link com.microsoft.playwright.Mouse#down Mouse.down()}. Again, when the handler runs between these two actions, the + * mouse position will be wrong during the mouse down. Prefer self-contained actions like {@link + * com.microsoft.playwright.Locator#click Locator.click()} that do not rely on the state being unchanged by a handler. * *

    Usage * @@ -6135,15 +6159,13 @@ default void addLocatorHandler(Locator locator, Consumer handler) { * *

    NOTE: Running the handler will alter your page state mid-test. For example it will change the currently focused element and * move the mouse. Make sure that actions that run after the handler are self-contained and do not rely on the focus and - * mouse state being unchanged.

    For example, consider a test that calls {@link - * com.microsoft.playwright.Locator#focus Locator.focus()} followed by {@link com.microsoft.playwright.Keyboard#press - * Keyboard.press()}. If your handler clicks a button between these two actions, the focused element most likely will be - * wrong, and key press will happen on the unexpected element. Use {@link com.microsoft.playwright.Locator#press - * Locator.press()} instead to avoid this problem.

    Another example is a series of mouse actions, where {@link - * com.microsoft.playwright.Mouse#move Mouse.move()} is followed by {@link com.microsoft.playwright.Mouse#down - * Mouse.down()}. Again, when the handler runs between these two actions, the mouse position will be wrong during the mouse - * down. Prefer self-contained actions like {@link com.microsoft.playwright.Locator#click Locator.click()} that do not rely - * on the state being unchanged by a handler. + * mouse state being unchanged.For example, consider a test that calls {@link com.microsoft.playwright.Locator#focus Locator.focus()} followed by + * {@link com.microsoft.playwright.Keyboard#press Keyboard.press()}. If your handler clicks a button between these two + * actions, the focused element most likely will be wrong, and key press will happen on the unexpected element. Use {@link + * com.microsoft.playwright.Locator#press Locator.press()} instead to avoid this problem.Another example is a series of mouse actions, where {@link com.microsoft.playwright.Mouse#move Mouse.move()} is followed + * by {@link com.microsoft.playwright.Mouse#down Mouse.down()}. Again, when the handler runs between these two actions, the + * mouse position will be wrong during the mouse down. Prefer self-contained actions like {@link + * com.microsoft.playwright.Locator#click Locator.click()} that do not rely on the state being unchanged by a handler. * *

    Usage * @@ -6240,7 +6262,7 @@ default Response reload() { * *

    NOTE: {@link com.microsoft.playwright.Page#route Page.route()} will not intercept requests intercepted by Service Worker. See * this issue. We recommend disabling Service Workers - * when using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * when using request interception by setting {@code serviceWorkers} to {@code "block"}. * *

    NOTE: {@link com.microsoft.playwright.Page#route Page.route()} will not intercept the first request of a popup page. Use * {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} instead. @@ -6299,7 +6321,7 @@ default void route(String url, Consumer handler) { * *

    NOTE: {@link com.microsoft.playwright.Page#route Page.route()} will not intercept requests intercepted by Service Worker. See * this issue. We recommend disabling Service Workers - * when using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * when using request interception by setting {@code serviceWorkers} to {@code "block"}. * *

    NOTE: {@link com.microsoft.playwright.Page#route Page.route()} will not intercept the first request of a popup page. Use * {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} instead. @@ -6356,7 +6378,7 @@ default void route(String url, Consumer handler) { * *

    NOTE: {@link com.microsoft.playwright.Page#route Page.route()} will not intercept requests intercepted by Service Worker. See * this issue. We recommend disabling Service Workers - * when using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * when using request interception by setting {@code serviceWorkers} to {@code "block"}. * *

    NOTE: {@link com.microsoft.playwright.Page#route Page.route()} will not intercept the first request of a popup page. Use * {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} instead. @@ -6415,7 +6437,7 @@ default void route(Pattern url, Consumer handler) { * *

    NOTE: {@link com.microsoft.playwright.Page#route Page.route()} will not intercept requests intercepted by Service Worker. See * this issue. We recommend disabling Service Workers - * when using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * when using request interception by setting {@code serviceWorkers} to {@code "block"}. * *

    NOTE: {@link com.microsoft.playwright.Page#route Page.route()} will not intercept the first request of a popup page. Use * {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} instead. @@ -6472,7 +6494,7 @@ default void route(Pattern url, Consumer handler) { * *

    NOTE: {@link com.microsoft.playwright.Page#route Page.route()} will not intercept requests intercepted by Service Worker. See * this issue. We recommend disabling Service Workers - * when using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * when using request interception by setting {@code serviceWorkers} to {@code "block"}. * *

    NOTE: {@link com.microsoft.playwright.Page#route Page.route()} will not intercept the first request of a popup page. Use * {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} instead. @@ -6531,7 +6553,7 @@ default void route(Predicate url, Consumer handler) { * *

    NOTE: {@link com.microsoft.playwright.Page#route Page.route()} will not intercept requests intercepted by Service Worker. See * this issue. We recommend disabling Service Workers - * when using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * when using request interception by setting {@code serviceWorkers} to {@code "block"}. * *

    NOTE: {@link com.microsoft.playwright.Page#route Page.route()} will not intercept the first request of a popup page. Use * {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()} instead. @@ -6585,7 +6607,7 @@ default void route(Predicate url, Consumer handler) { * *

    Playwright will not serve requests intercepted by Service Worker from the HAR file. See this issue. We recommend disabling Service Workers when - * using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * using request interception by setting {@code serviceWorkers} to {@code "block"}. * * @param har Path to a HAR file with prerecorded network data. If {@code * path} is a relative path, then it is resolved relative to the current working directory. @@ -6600,13 +6622,88 @@ default void routeFromHAR(Path har) { * *

    Playwright will not serve requests intercepted by Service Worker from the HAR file. See this issue. We recommend disabling Service Workers when - * using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}. + * using request interception by setting {@code serviceWorkers} to {@code "block"}. * * @param har Path to a HAR file with prerecorded network data. If {@code * path} is a relative path, then it is resolved relative to the current working directory. * @since v1.23 */ void routeFromHAR(Path har, RouteFromHAROptions options); + /** + * This method allows to modify websocket connections that are made by the page. + * + *

    Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this + * method before navigating the page. + * + *

    Usage + * + *

    Below is an example of a simple mock that responds to a single message. See {@code WebSocketRoute} for more details and + * examples. + *

    {@code
    +   * page.routeWebSocket("/ws", ws -> {
    +   *   ws.onMessage(message -> {
    +   *     if ("request".equals(message))
    +   *       ws.send("response");
    +   *   });
    +   * });
    +   * }
    + * + * @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code + * baseURL} context option. + * @param handler Handler function to route the WebSocket. + * @since v1.48 + */ + void routeWebSocket(String url, Consumer handler); + /** + * This method allows to modify websocket connections that are made by the page. + * + *

    Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this + * method before navigating the page. + * + *

    Usage + * + *

    Below is an example of a simple mock that responds to a single message. See {@code WebSocketRoute} for more details and + * examples. + *

    {@code
    +   * page.routeWebSocket("/ws", ws -> {
    +   *   ws.onMessage(message -> {
    +   *     if ("request".equals(message))
    +   *       ws.send("response");
    +   *   });
    +   * });
    +   * }
    + * + * @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code + * baseURL} context option. + * @param handler Handler function to route the WebSocket. + * @since v1.48 + */ + void routeWebSocket(Pattern url, Consumer handler); + /** + * This method allows to modify websocket connections that are made by the page. + * + *

    Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this + * method before navigating the page. + * + *

    Usage + * + *

    Below is an example of a simple mock that responds to a single message. See {@code WebSocketRoute} for more details and + * examples. + *

    {@code
    +   * page.routeWebSocket("/ws", ws -> {
    +   *   ws.onMessage(message -> {
    +   *     if ("request".equals(message))
    +   *       ws.send("response");
    +   *   });
    +   * });
    +   * }
    + * + * @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code + * baseURL} context option. + * @param handler Handler function to route the WebSocket. + * @since v1.48 + */ + void routeWebSocket(Predicate url, Consumer handler); /** * Returns the buffer with the captured screenshot. * @@ -7256,6 +7353,8 @@ default void setInputFiles(String selector, FilePayload[] files) { * page.navigate("https://example.com"); * }
    * + * @param width Page width in pixels. + * @param height Page height in pixels. * @since v1.8 */ void setViewportSize(int width, int height); diff --git a/playwright/src/main/java/com/microsoft/playwright/Tracing.java b/playwright/src/main/java/com/microsoft/playwright/Tracing.java index 961db33e..82ea8d49 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Tracing.java +++ b/playwright/src/main/java/com/microsoft/playwright/Tracing.java @@ -39,8 +39,8 @@ public interface Tracing { class StartOptions { /** * If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code - * tracesDir} folder specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify the - * final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stop + * tracesDir} directory specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify + * the final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stop * Tracing.stop()} instead. */ public String name; @@ -69,8 +69,8 @@ class StartOptions { /** * If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code - * tracesDir} folder specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify the - * final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stop + * tracesDir} directory specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify + * the final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stop * Tracing.stop()} instead. */ public StartOptions setName(String name) { @@ -115,8 +115,8 @@ public StartOptions setTitle(String title) { class StartChunkOptions { /** * If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code - * tracesDir} folder specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify the - * final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stopChunk + * tracesDir} directory specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify + * the final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stopChunk * Tracing.stopChunk()} instead. */ public String name; @@ -127,8 +127,8 @@ class StartChunkOptions { /** * If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code - * tracesDir} folder specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify the - * final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stopChunk + * tracesDir} directory specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify + * the final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stopChunk * Tracing.stopChunk()} instead. */ public StartChunkOptions setName(String name) { diff --git a/playwright/src/main/java/com/microsoft/playwright/WebSocketRoute.java b/playwright/src/main/java/com/microsoft/playwright/WebSocketRoute.java new file mode 100644 index 00000000..4e56b5bb --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/WebSocketRoute.java @@ -0,0 +1,209 @@ +/* + * 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.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Whenever a {@code WebSocket} route is set up + * with {@link com.microsoft.playwright.Page#routeWebSocket Page.routeWebSocket()} or {@link + * com.microsoft.playwright.BrowserContext#routeWebSocket BrowserContext.routeWebSocket()}, the {@code WebSocketRoute} + * object allows to handle the WebSocket, like an actual server would do. + * + *

    Mocking + * + *

    By default, the routed WebSocket will not connect to the server. This way, you can mock entire communcation over the + * WebSocket. Here is an example that responds to a {@code "request"} with a {@code "response"}. + *

    {@code
    + * page.routeWebSocket("/ws", ws -> {
    + *   ws.onMessage(message -> {
    + *     if ("request".equals(message))
    + *       ws.send("response");
    + *   });
    + * });
    + * }
    + * + *

    Since we do not call {@link com.microsoft.playwright.WebSocketRoute#connectToServer WebSocketRoute.connectToServer()} + * inside the WebSocket route handler, Playwright assumes that WebSocket will be mocked, and opens the WebSocket inside the + * page automatically. + * + *

    Intercepting + * + *

    Alternatively, you may want to connect to the actual server, but intercept messages in-between and modify or block them. + * Calling {@link com.microsoft.playwright.WebSocketRoute#connectToServer WebSocketRoute.connectToServer()} returns a + * server-side {@code WebSocketRoute} instance that you can send messages to, or handle incoming messages. + * + *

    Below is an example that modifies some messages sent by the page to the server. Messages sent from the server to the + * page are left intact, relying on the default forwarding. + *

    {@code
    + * page.routeWebSocket("/ws", ws -> {
    + *   WebSocketRoute server = ws.connectToServer();
    + *   ws.onMessage(message -> {
    + *     if ("request".equals(message))
    + *       server.send("request2");
    + *     else
    + *       server.send(message);
    + *   });
    + * });
    + * }
    + * + *

    After connecting to the server, all **messages are forwarded** between the page and the server by default. + * + *

    However, if you call {@link com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} on the + * original route, messages from the page to the server **will not be forwarded** anymore, but should instead be handled by + * the {@code handler}. + * + *

    Similarly, calling {@link com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} on the + * server-side WebSocket will **stop forwarding messages** from the server to the page, and {@code handler} should take + * care of them. + * + *

    The following example blocks some messages in both directions. Since it calls {@link + * com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} in both directions, there is no automatic + * forwarding at all. + *

    {@code
    + * page.routeWebSocket("/ws", ws -> {
    + *   WebSocketRoute server = ws.connectToServer();
    + *   ws.onMessage(message -> {
    + *     if (!"blocked-from-the-page".equals(message))
    + *       server.send(message);
    + *   });
    + *   server.onMessage(message -> {
    + *     if (!"blocked-from-the-server".equals(message))
    + *       ws.send(message);
    + *   });
    + * });
    + * }
    + */ +public interface WebSocketRoute { + class CloseOptions { + /** + * Optional close code. + */ + public Integer code; + /** + * Optional close reason. + */ + public String reason; + + /** + * Optional close code. + */ + public CloseOptions setCode(int code) { + this.code = code; + return this; + } + /** + * Optional close reason. + */ + public CloseOptions setReason(String reason) { + this.reason = reason; + return this; + } + } + /** + * Closes one side of the WebSocket connection. + * + * @since v1.48 + */ + default void close() { + close(null); + } + /** + * Closes one side of the WebSocket connection. + * + * @since v1.48 + */ + void close(CloseOptions options); + /** + * By default, routed WebSocket does not connect to the server, so you can mock entire WebSocket communication. This method + * connects to the actual WebSocket server, and returns the server-side {@code WebSocketRoute} instance, giving the ability + * to send and receive messages from the server. + * + *

    Once connected to the server: + *

      + *
    • Messages received from the server will be **automatically forwarded** to the WebSocket in the page, unless {@link + * com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} is called on the server-side {@code + * WebSocketRoute}.
    • + *
    • Messages sent by the {@code + * WebSocket.send()} call in the page will be **automatically forwarded** to the server, unless {@link + * com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} is called on the original {@code + * WebSocketRoute}.
    • + *
    + * + *

    See examples at the top for more details. + * + * @since v1.48 + */ + WebSocketRoute connectToServer(); + /** + * Allows to handle {@code WebSocket.close}. + * + *

    By default, closing one side of the connection, either in the page or on the server, will close the other side. However, + * when {@link com.microsoft.playwright.WebSocketRoute#onClose WebSocketRoute.onClose()} handler is set up, the default + * forwarding of closure is disabled, and handler should take care of it. + * + * @param handler Function that will handle WebSocket closure. Received an optional close code and an optional close reason. + * @since v1.48 + */ + void onClose(BiConsumer handler); + /** + * This method allows to handle messages that are sent by the WebSocket, either from the page or from the server. + * + *

    When called on the original WebSocket route, this method handles messages sent from the page. You can handle this + * messages by responding to them with {@link com.microsoft.playwright.WebSocketRoute#send WebSocketRoute.send()}, + * forwarding them to the server-side connection returned by {@link com.microsoft.playwright.WebSocketRoute#connectToServer + * WebSocketRoute.connectToServer()} or do something else. + * + *

    Once this method is called, messages are not automatically forwarded to the server or to the page - you should do that + * manually by calling {@link com.microsoft.playwright.WebSocketRoute#send WebSocketRoute.send()}. See examples at the top + * for more details. + * + *

    Calling this method again will override the handler with a new one. + * + * @param handler Function that will handle messages. + * @since v1.48 + */ + void onMessage(Consumer handler); + /** + * Sends a message to the WebSocket. When called on the original WebSocket, sends the message to the page. When called on + * the result of {@link com.microsoft.playwright.WebSocketRoute#connectToServer WebSocketRoute.connectToServer()}, sends + * the message to the server. See examples at the top for more details. + * + * @param message Message to send. + * @since v1.48 + */ + void send(String message); + /** + * Sends a message to the WebSocket. When called on the original WebSocket, sends the message to the page. When called on + * the result of {@link com.microsoft.playwright.WebSocketRoute#connectToServer WebSocketRoute.connectToServer()}, sends + * the message to the server. See examples at the top for more details. + * + * @param message Message to send. + * @since v1.48 + */ + void send(byte[] message); + /** + * URL of the WebSocket created in the page. + * + * @since v1.48 + */ + String url(); +} + 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 735fd1ba..5cf39fe9 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -50,6 +50,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { final List backgroundPages = new ArrayList<>(); final Router routes = new Router(); + final WebSocketRouter webSocketRoutes = new WebSocketRouter(); private boolean closeWasCalled; private final WaitableEvent closePromise; final Map bindings = new HashMap<>(); @@ -514,6 +515,28 @@ private void route(UrlMatcher matcher, Consumer handler, RouteOptions opt }); } + @Override + public void routeWebSocket(String url, Consumer handler) { + routeWebSocketImpl(new UrlMatcher(baseUrl, url), handler); + } + + @Override + public void routeWebSocket(Pattern pattern, Consumer handler) { + routeWebSocketImpl(new UrlMatcher(pattern), handler); + } + + @Override + public void routeWebSocket(Predicate predicate, Consumer handler) { + routeWebSocketImpl(new UrlMatcher(predicate), handler); + } + + private void routeWebSocketImpl(UrlMatcher matcher, Consumer handler) { + withLogging("BrowserContext.routeWebSocket", () -> { + webSocketRoutes.add(matcher, handler); + updateWebSocketInterceptionPatterns(); + }); + } + void recordIntoHar(PageImpl page, Path har, RouteFromHAROptions options) { JsonObject params = new JsonObject(); if (page != null) { @@ -681,6 +704,10 @@ private void updateInterceptionPatterns() { sendMessage("setNetworkInterceptionPatterns", routes.interceptionPatterns()); } + private void updateWebSocketInterceptionPatterns() { + sendMessage("setWebSocketInterceptionPatterns", webSocketRoutes.interceptionPatterns()); + } + void handleRoute(RouteImpl route) { Router.HandleResult handled = routes.handle(route); if (handled != Router.HandleResult.NoMatchingHandler) { @@ -691,6 +718,12 @@ void handleRoute(RouteImpl route) { } } + void handleWebSocketRoute(WebSocketRouteImpl route) { + if (!webSocketRoutes.handle(route)) { + route.connectToServer(); + } + } + WaitableResult pause() { return sendMessageAsync("pause", new JsonObject()); } @@ -730,6 +763,9 @@ protected void handleEvent(String event, JsonObject params) { RouteImpl route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString()); route.browserContext = this; handleRoute(route); + } else if ("webSocketRoute".equals(event)) { + WebSocketRouteImpl route = connection.getExistingObject(params.getAsJsonObject("webSocketRoute").get("guid").getAsString()); + handleWebSocketRoute(route); } else if ("page".equals(event)) { PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString()); pages.add(page); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ChannelOwner.java b/playwright/src/main/java/com/microsoft/playwright/impl/ChannelOwner.java index 776b0f9d..b5f6edd3 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/ChannelOwner.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ChannelOwner.java @@ -35,6 +35,7 @@ class ChannelOwner extends LoggingSupport { final String guid; final JsonObject initializer; private boolean wasCollected; + private boolean isInternalType; protected ChannelOwner(ChannelOwner parent, String type, String guid, JsonObject initializer) { this(parent.connection, parent, type, guid, initializer); @@ -58,6 +59,10 @@ private ChannelOwner(Connection connection, ChannelOwner parent, String type, St } } + void markAsInternalType() { + isInternalType = true; + } + void disposeChannelOwner(boolean wasGarbageCollected) { // Clean up from parent and connection. if (parent != null) { @@ -84,6 +89,9 @@ T withWaitLogging(String apiName, Function code) { @Override T withLogging(String apiName, Supplier code) { + if (isInternalType) { + apiName = null; + } String previousApiName = connection.setApiName(apiName); try { return super.withLogging(apiName, code); @@ -92,6 +100,10 @@ T withLogging(String apiName, Supplier code) { } } + WaitableResult sendMessageAsync(String method) { + return sendMessageAsync(method, new JsonObject()); + } + WaitableResult sendMessageAsync(String method, JsonObject params) { checkNotCollected(); return connection.sendMessageAsync(guid, method, params); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java index 710ab2b2..f0d3ca5f 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java @@ -384,6 +384,9 @@ private ChannelOwner createRemoteObject(String parentGuid, JsonObject params) { case "WebSocket": result = new WebSocketImpl(parent, type, guid, initializer); break; + case "WebSocketRoute": + result = new WebSocketRouteImpl(parent, type, guid, initializer); + break; case "Worker": result = new WorkerImpl(parent, type, guid, initializer); break; 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 376ab314..666a91e9 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/LocalUtils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/LocalUtils.java @@ -27,6 +27,7 @@ class LocalUtils extends ChannelOwner { LocalUtils(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); + markAsInternalType(); } JsonArray deviceDescriptors() { 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 456cd8f1..c0a98e4b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -48,6 +48,7 @@ public class PageImpl extends ChannelOwner implements Page { final Waitable waitableClosedOrCrashed; private ViewportSize viewport; private final Router routes = new Router(); + private final WebSocketRouter webSocketRoutes = new WebSocketRouter(); private final Set frames = new LinkedHashSet<>(); private final Map locatorHandlers = new HashMap<>(); @@ -212,6 +213,11 @@ protected void handleEvent(String event, JsonObject params) { if (handled == Router.HandleResult.NoMatchingHandler || handled == Router.HandleResult.Fallback) { browserContext.handleRoute(route); } + } else if ("webSocketRoute".equals(event)) { + WebSocketRouteImpl route = connection.getExistingObject(params.getAsJsonObject("webSocketRoute").get("guid").getAsString()); + if (!webSocketRoutes.handle(route)) { + browserContext.handleWebSocketRoute(route); + } } else if ("video".equals(event)) { String artifactGuid = params.getAsJsonObject("artifact").get("guid").getAsString(); ArtifactImpl artifact = connection.getExistingObject(artifactGuid); @@ -927,6 +933,11 @@ Response goForwardImpl(GoForwardOptions options) { return null; } + @Override + public void requestGC() { + withLogging("Page.requestGC", () -> sendMessage("requestGC")); + } + @Override public ResponseImpl navigate(String url, NavigateOptions options) { return withLogging("Page.navigate", () -> mainFrame.navigateImpl(url, convertType(options, Frame.NavigateOptions.class))); @@ -1129,6 +1140,28 @@ private void route(UrlMatcher matcher, Consumer handler, RouteOptions opt }); } + @Override + public void routeWebSocket(String url, Consumer handler) { + routeWebSocketImpl(new UrlMatcher(browserContext.baseUrl, url), handler); + } + + @Override + public void routeWebSocket(Pattern pattern, Consumer handler) { + routeWebSocketImpl(new UrlMatcher(pattern), handler); + } + + @Override + public void routeWebSocket(Predicate predicate, Consumer handler) { + routeWebSocketImpl(new UrlMatcher(predicate), handler); + } + + private void routeWebSocketImpl(UrlMatcher matcher, Consumer handler) { + withLogging("Page.routeWebSocket", () -> { + webSocketRoutes.add(matcher, handler); + updateWebSocketInterceptionPatterns(); + }); + } + @Override public byte[] screenshot(ScreenshotOptions options) { return withLogging("Page.screenshot", () -> screenshotImpl(options)); @@ -1356,6 +1389,10 @@ private void updateInterceptionPatterns() { sendMessage("setNetworkInterceptionPatterns", routes.interceptionPatterns()); } + private void updateWebSocketInterceptionPatterns() { + sendMessage("setWebSocketInterceptionPatterns", webSocketRoutes.interceptionPatterns()); + } + @Override public String url() { return mainFrame.url(); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/RouteImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/RouteImpl.java index bbbe7197..7a96aac3 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/RouteImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/RouteImpl.java @@ -38,6 +38,7 @@ public class RouteImpl extends ChannelOwner implements Route { public RouteImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); + markAsInternalType(); request = connection.getExistingObject(initializer.getAsJsonObject("request").get("guid").getAsString()); } @@ -47,7 +48,6 @@ public void abort(String errorCode) { withLogging("Route.abort", () -> { JsonObject params = new JsonObject(); params.addProperty("errorCode", errorCode); - params.addProperty("requestUrl", request.initializer.get("url").getAsString()); sendMessageAsync("abort", params); }); } @@ -135,7 +135,6 @@ private void resumeImpl(RequestImpl.FallbackOverrides options, boolean isFallbac params.addProperty("postData", base64); } } - params.addProperty("requestUrl", request.initializer.get("url").getAsString()); params.addProperty("isFallback", isFallback); sendMessageAsync("continue", params); } @@ -231,7 +230,6 @@ private void fulfillImpl(FulfillOptions options) { if (fetchResponseUid != null) { params.addProperty("fetchResponseUid", fetchResponseUid); } - params.addProperty("requestUrl", request.initializer.get("url").getAsString()); sendMessageAsync("fulfill", params); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Router.java b/playwright/src/main/java/com/microsoft/playwright/impl/Router.java index 87c274a3..78d7e10f 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Router.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Router.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.function.Consumer; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -99,27 +100,7 @@ HandleResult handle(RouteImpl route) { } JsonObject interceptionPatterns() { - JsonArray jsonPatterns = new JsonArray(); - for (RouteInfo route : routes) { - JsonObject jsonPattern = new JsonObject(); - Object urlFilter = route.matcher.rawSource; - if (urlFilter instanceof String) { - jsonPattern.addProperty("glob", (String) urlFilter); - } else if (urlFilter instanceof Pattern) { - Pattern pattern = (Pattern) urlFilter; - jsonPattern.addProperty("regexSource", pattern.pattern()); - jsonPattern.addProperty("regexFlags", toJsRegexFlags(pattern)); - } else { - // Match all requests. - jsonPattern.addProperty("glob", "**/*"); - jsonPatterns = new JsonArray(); - jsonPatterns.add(jsonPattern); - break; - } - jsonPatterns.add(jsonPattern); - } - JsonObject result = new JsonObject(); - result.add("patterns", jsonPatterns); - return result; + List matchers = routes.stream().map(r -> r.matcher).collect(Collectors.toList()); + return Utils.interceptionPatterns(matchers); } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java index e5c25a94..f88d2130 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java @@ -33,6 +33,7 @@ class TracingImpl extends ChannelOwner implements Tracing { TracingImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); + markAsInternalType(); } private void stopChunkImpl(Path path) { 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 09960ef2..69153ce7 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java @@ -16,6 +16,8 @@ package com.microsoft.playwright.impl; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; import com.microsoft.playwright.PlaywrightException; import java.net.MalformedURLException; @@ -25,6 +27,7 @@ import java.util.regex.Pattern; import static com.microsoft.playwright.impl.Utils.globToRegex; +import static com.microsoft.playwright.impl.Utils.toJsRegexFlags; class UrlMatcher { final Object rawSource; 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 eeef923e..61df059a 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java @@ -457,4 +457,29 @@ private static String base64Buffer(byte[] bytes, Path path) throws IOException { } return Base64.getEncoder().encodeToString(bytes); } + + static JsonObject interceptionPatterns(List matchers) { + JsonArray jsonPatterns = new JsonArray(); + for (UrlMatcher matcher: matchers) { + JsonObject jsonPattern = new JsonObject(); + Object urlFilter = matcher.rawSource; + if (urlFilter instanceof String) { + jsonPattern.addProperty("glob", (String) urlFilter); + } else if (urlFilter instanceof Pattern) { + Pattern pattern = (Pattern) urlFilter; + jsonPattern.addProperty("regexSource", pattern.pattern()); + jsonPattern.addProperty("regexFlags", toJsRegexFlags(pattern)); + } else { + // Match all requests. + jsonPattern.addProperty("glob", "**/*"); + jsonPatterns = new JsonArray(); + jsonPatterns.add(jsonPattern); + break; + } + jsonPatterns.add(jsonPattern); + } + JsonObject result = new JsonObject(); + result.add("patterns", jsonPatterns); + return result; + } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketFrameImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketFrameImpl.java new file mode 100644 index 00000000..546a0253 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketFrameImpl.java @@ -0,0 +1,29 @@ +package com.microsoft.playwright.impl; + +import com.microsoft.playwright.WebSocketFrame; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +class WebSocketFrameImpl implements WebSocketFrame { + private byte[] bytes; + private String text; + + WebSocketFrameImpl(String payload, boolean isBase64) { + if (isBase64) { + bytes = Base64.getDecoder().decode(payload); + } else { + text = payload; + } + } + + @Override + public byte[] binary() { + return bytes; + } + + @Override + public String text() { + return text; + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketImpl.java index 432a8a2d..262e5f20 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketImpl.java @@ -21,9 +21,7 @@ import com.microsoft.playwright.WebSocket; import com.microsoft.playwright.WebSocketFrame; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Base64; import java.util.List; import java.util.function.Consumer; import java.util.function.Predicate; @@ -151,28 +149,6 @@ private WebSocketFrame waitForEventWithTimeout(EventType eventType, Runnable cod return runUntil(code, new WaitableRace<>(waitables)); } - private static class WebSocketFrameImpl implements WebSocketFrame { - private final byte[] bytes; - - WebSocketFrameImpl(String payload, boolean isBase64) { - if (isBase64) { - bytes = Base64.getDecoder().decode(payload); - } else { - bytes = payload.getBytes(); - } - } - - @Override - public byte[] binary() { - return bytes; - } - - @Override - public String text() { - return new String(bytes, StandardCharsets.UTF_8); - } - } - @Override void handleEvent(String event, JsonObject parameters) { switch (event) { diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketRouteImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketRouteImpl.java new file mode 100644 index 00000000..53479fcf --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketRouteImpl.java @@ -0,0 +1,186 @@ +package com.microsoft.playwright.impl; + +import com.google.gson.JsonObject; +import com.microsoft.playwright.PlaywrightException; +import com.microsoft.playwright.WebSocketFrame; +import com.microsoft.playwright.WebSocketRoute; + +import java.util.Base64; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import static com.microsoft.playwright.impl.Serialization.gson; + +class WebSocketRouteImpl extends ChannelOwner implements WebSocketRoute { + + private Consumer onPageMessage; + private BiConsumer onPageClose; + private Consumer onServerMessage; + private BiConsumer onServerClose; + private boolean connected; + private WebSocketRoute server = new WebSocketRoute() { + @Override + public void close(CloseOptions options) { + if (options == null) { + options = new CloseOptions(); + } + JsonObject params = gson().toJsonTree(options).getAsJsonObject(); + params.addProperty("wasClean", true); + sendMessageAsync("closeServer", params); + } + + @Override + public WebSocketRoute connectToServer() { + throw new PlaywrightException("connectToServer must be called on the page-side WebSocketRoute"); + } + + @Override + public void onClose(BiConsumer handler) { + onServerClose = handler; + } + + @Override + public void onMessage(Consumer handler) { + onServerMessage = handler; + } + + @Override + public void send(String message) { + JsonObject params = new JsonObject(); + params.addProperty("message", message); + params.addProperty("isBase64", false); + sendMessageAsync("sendToServer", params); + } + + @Override + public void send(byte[] message) { + JsonObject params = new JsonObject(); + String base64 = Base64.getEncoder().encodeToString(message); + params.addProperty("message", base64); + params.addProperty("isBase64", true); + sendMessageAsync("sendToServer", params); + } + + @Override + public String url() { + return initializer.get("url").getAsString(); + } + }; + + WebSocketRouteImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { + super(parent, type, guid, initializer); + } + + @Override + public void close(CloseOptions options) { + if (options == null) { + options = new CloseOptions(); + } + JsonObject params = gson().toJsonTree(options).getAsJsonObject(); + params.addProperty("wasClean", true); + sendMessageAsync("closePage", params); + } + + @Override + public WebSocketRoute connectToServer() { + if (connected) { + throw new PlaywrightException("Already connected to the server"); + } + connected = true; + sendMessageAsync("connect"); + return server; + } + + @Override + public void onClose(BiConsumer handler) { + onPageClose = handler; + } + + @Override + public void onMessage(Consumer handler) { + onPageMessage = handler; + } + + @Override + public void send(String message) { + JsonObject params = new JsonObject(); + params.addProperty("message", message); + params.addProperty("isBase64", false); + sendMessageAsync("sendToPage", params); + } + + @Override + public void send(byte[] message) { + JsonObject params = new JsonObject(); + String base64 = Base64.getEncoder().encodeToString(message); + params.addProperty("message", base64); + params.addProperty("isBase64", true); + sendMessageAsync("sendToPage", params); + } + + @Override + public String url() { + return initializer.get("url").getAsString(); + } + + void afterHandle() { + if (this.connected) { + return; + } + // Ensure that websocket is "open" and can send messages without an actual server connection. + sendMessageAsync("ensureOpened"); + } + + @Override + protected void handleEvent(String event, JsonObject params) { + if ("messageFromPage".equals(event)) { + String message = params.get("message").getAsString(); + boolean isBase64 = params.get("isBase64").getAsBoolean(); + if (onPageMessage != null) { + onPageMessage.accept(new WebSocketFrameImpl(message, isBase64)); + } else if (connected) { + JsonObject messageParams = new JsonObject(); + messageParams.addProperty("message", message); + messageParams.addProperty("isBase64", isBase64); + sendMessageAsync("sendToServer", messageParams); + } + } else if ("messageFromServer".equals(event)) { + String message = params.get("message").getAsString(); + boolean isBase64 = params.get("isBase64").getAsBoolean(); + if (onServerMessage != null) { + onServerMessage.accept(new WebSocketFrameImpl(message, isBase64)); + } else { + JsonObject messageParams = new JsonObject(); + messageParams.addProperty("message", message); + messageParams.addProperty("isBase64", isBase64); + sendMessageAsync("sendToPage", messageParams); + } + } else if ("closePage".equals(event)) { + int code = params.get("code").getAsInt(); + String reason = params.get("reason").getAsString(); + boolean wasClean = params.get("wasClean").getAsBoolean(); + if (onPageClose != null) { + onPageClose.accept(code, reason); + } else { + JsonObject closeParams = new JsonObject(); + closeParams.addProperty("code", code); + closeParams.addProperty("reason", reason); + closeParams.addProperty("wasClean", wasClean); + sendMessageAsync("closeServer", closeParams); + } + } else if ("closeServer".equals(event)) { + int code = params.get("code").getAsInt(); + String reason = params.get("reason").getAsString(); + boolean wasClean = params.get("wasClean").getAsBoolean(); + if (onServerClose != null) { + onServerClose.accept(code, reason); + } else { + JsonObject closeParams = new JsonObject(); + closeParams.addProperty("code", code); + closeParams.addProperty("reason", reason); + closeParams.addProperty("wasClean", wasClean); + sendMessageAsync("closePage", closeParams); + } + } + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketRouter.java b/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketRouter.java new file mode 100644 index 00000000..f43869f9 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketRouter.java @@ -0,0 +1,47 @@ +package com.microsoft.playwright.impl; + +import com.google.gson.JsonObject; +import com.microsoft.playwright.WebSocketRoute; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class WebSocketRouter { + private List routes = new ArrayList<>(); + + private static class RouteInfo { + final UrlMatcher matcher; + private final Consumer handler; + + RouteInfo(UrlMatcher matcher, Consumer handler) { + this.matcher = matcher; + this.handler = handler; + } + + void handle(WebSocketRouteImpl route) { + handler.accept(route); + route.afterHandle(); + } + } + + void add(UrlMatcher matcher, Consumer handler) { + routes.add(0, new RouteInfo(matcher, handler)); + } + + boolean handle(WebSocketRouteImpl route) { + for (RouteInfo routeInfo: routes) { + if (routeInfo.matcher.test(route.url())) { + routeInfo.handle(route); + return true; + } + } + return false; + } + + JsonObject interceptionPatterns() { + List matchers = routes.stream().map(r -> r.matcher).collect(Collectors.toList()); + return Utils.interceptionPatterns(matchers); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestDefaultBrowserContext2.java b/playwright/src/test/java/com/microsoft/playwright/TestDefaultBrowserContext2.java index 48642d01..fd69326d 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestDefaultBrowserContext2.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestDefaultBrowserContext2.java @@ -168,35 +168,6 @@ void shouldRestoreStateFromUserDataDir() throws IOException { browserContext3.close(); } - @Test - void shouldRestoreCookiesFromUserDataDir() throws IOException { -// TODO: test.flaky(browserName === "chromium"); - Path userDataDir = tempDir.resolve("user-data-dir"); - BrowserType.LaunchPersistentContextOptions browserOptions = null; - BrowserContext browserContext = browserType.launchPersistentContext(userDataDir, browserOptions); - Page page = browserContext.newPage(); - page.navigate(server.EMPTY_PAGE); - Object documentCookie = page.evaluate("() => {\n" + - " document.cookie = 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT';\n" + - " return document.cookie;\n" + - " }"); - assertEquals("doSomethingOnlyOnce=true", documentCookie); - browserContext.close(); - - BrowserContext browserContext2 = browserType.launchPersistentContext(userDataDir, browserOptions); - Page page2 = browserContext2.newPage(); - page2.navigate(server.EMPTY_PAGE); - assertEquals("doSomethingOnlyOnce=true", page2.evaluate("() => document.cookie")); - browserContext2.close(); - - Path userDataDir2 = tempDir.resolve("user-data-dir-2"); - BrowserContext browserContext3 = browserType.launchPersistentContext(userDataDir2, browserOptions); - Page page3 = browserContext3.newPage(); - page3.navigate(server.EMPTY_PAGE); - assertNotEquals("doSomethingOnlyOnce=true", page3.evaluate("() => document.cookie")); - browserContext3.close(); - } - @Test void shouldHaveDefaultURLWhenLaunchingBrowser() { launchPersistent(); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestLocatorFrame.java b/playwright/src/test/java/com/microsoft/playwright/TestLocatorFrame.java index ac8aab13..e9c039d6 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestLocatorFrame.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestLocatorFrame.java @@ -104,7 +104,7 @@ void shouldWaitForFrame() { PlaywrightException e = assertThrows(PlaywrightException.class, () -> { page.frameLocator("iframe").locator("span").click(new Locator.ClickOptions().setTimeout(300)); }); - assertTrue(e.getMessage().contains("waiting for frameLocator(\"iframe\")"), e.getMessage()); + assertTrue(e.getMessage().contains("waiting for locator(\"iframe\").contentFrame()"), e.getMessage()); } @Test diff --git a/playwright/src/test/java/com/microsoft/playwright/TestRequestGC.java b/playwright/src/test/java/com/microsoft/playwright/TestRequestGC.java new file mode 100644 index 00000000..9e31d14d --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestRequestGC.java @@ -0,0 +1,33 @@ +package com.microsoft.playwright; + +import com.microsoft.playwright.junit.FixtureTest; +import com.microsoft.playwright.junit.UsePlaywright; +import org.junit.jupiter.api.Test; + +import static com.microsoft.playwright.Utils.mapOf; +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@FixtureTest +@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class) +public class TestRequestGC { + + @Test + void shouldWork(Page page) { + page.evaluate("() => {\n" + + " globalThis.objectToDestroy = { hello: 'world' };\n" + + " globalThis.weakRef = new WeakRef(globalThis.objectToDestroy);\n" + + " }"); + + page.requestGC(); + assertEquals(mapOf("hello", "world"), page.evaluate("() => globalThis.weakRef.deref()")); + + page.requestGC(); + assertEquals(mapOf("hello", "world"), page.evaluate("() => globalThis.weakRef.deref()")); + + page.evaluate("() => globalThis.objectToDestroy = null"); + page.requestGC(); + assertNull(page.evaluate("() => globalThis.weakRef.deref()")); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java b/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java new file mode 100644 index 00000000..6f202318 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java @@ -0,0 +1,320 @@ +package com.microsoft.playwright; + +import com.microsoft.playwright.junit.FixtureTest; +import com.microsoft.playwright.junit.UsePlaywright; +import org.java_websocket.WebSocket; +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.regex.Pattern; + +import static com.microsoft.playwright.Utils.mapOf; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@FixtureTest +@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class TestRouteWebSocket { + private WebSocketServerImpl webSocketServer; + + @BeforeAll + void startWebSockerServer() throws InterruptedException { + webSocketServer = WebSocketServerImpl.create(); + } + + @AfterAll + void stopWebSockerServer() throws IOException, InterruptedException { + webSocketServer.stop(); + } + + @AfterEach + void resetWebSocketServer() { + webSocketServer.reset(); + } + + private void setupWS(Page target, int port, String binaryType) { + setupWS(target.mainFrame(), port, binaryType); + } + private void setupWS(Frame target, int port, String binaryType) { + target.navigate("about:blank"); + target.evaluate("({ port, binaryType }) => {\n" + + " window.log = [];\n" + + " window.ws = new WebSocket('ws://localhost:' + port + '/ws');\n" + + " window.ws.binaryType = binaryType;\n" + + " window.ws.addEventListener('open', () => window.log.push('open'));\n" + + " window.ws.addEventListener('close', event => window.log.push(`close code=${event.code} reason=${event.reason}`));\n" + + " window.ws.addEventListener('error', event => window.log.push(`error`));\n" + + " window.ws.addEventListener('message', async event => {\n" + + " let data;\n" + + " if (typeof event.data === 'string')\n" + + " data = event.data;\n" + + " else if (event.data instanceof Blob)\n" + + " data = 'blob:' + await event.data.text();\n" + + " else\n" + + " data = 'arraybuffer:' + await (new Blob([event.data])).text();\n" + + " window.log.push(`message: data=${data} origin=${event.origin} lastEventId=${event.lastEventId}`);\n" + + " });\n" + + " window.wsOpened = new Promise(f => window.ws.addEventListener('open', () => f()));\n" + + " }", mapOf("port", port, "binaryType", binaryType)); + } + + private void setupRoute(Page page, String mock) { + if ("no-match".equals(mock)) { + page.routeWebSocket(Pattern.compile("/zzz/"), ws -> {}); + } else if ("pass-through".equals(mock)) { + page.routeWebSocket(Pattern.compile("/.*/"), ws -> { + WebSocketRoute server = ws.connectToServer(); + ws.onMessage(message -> { + if (message.text() != null) { + server.send(message.text()); + } else { + server.send(message.binary()); + } + }); + server.onMessage(message -> { + if (message.text() != null) { + ws.send(message.text()); + } else { + ws.send(message.binary()); + } + }); + }); + } + } + + @ParameterizedTest + @ValueSource(strings = {"no-mock", "no-match", "pass-through"}) + public void shouldWorkWithTextMessage(String mock, Page page) throws Exception { + setupRoute(page, mock); + Future wsPromise = webSocketServer.waitForWebSocket(); + setupWS(page, webSocketServer.getPort(), "blob"); + + page.waitForCondition(() -> { + Boolean result = (Boolean) page.evaluate("() => window.log.length >= 1"); + return result; + }); + assertEquals(asList("open"), page.evaluate("window.log")); + + org.java_websocket.WebSocket ws = wsPromise.get(); + ws.send("hello"); + page.waitForCondition(() -> { + Boolean result = (Boolean) page.evaluate("() => window.log.length >= 2"); + return result; + }); + + assertEquals( + asList("open", "message: data=hello origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId="), + page.evaluate("window.log")); + + assertEquals(1, page.evaluate("window.ws.readyState")); + + Future messagePromise = webSocketServer.waitForMessage(); + page.evaluate("() => window.ws.send('hi')"); + assertEquals("hi", messagePromise.get()); + ws.close(1008, "oops"); + page.waitForCondition(() -> { + Integer result = (Integer) page.evaluate("window.ws.readyState"); + return result == 3; + }); + + assertEquals( + asList("open", "message: data=hello origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId=", + "close code=1008 reason=oops"), + page.evaluate("window.log")); + } + + + @ParameterizedTest + @ValueSource(strings = {"no-mock", "no-match", "pass-through"}) + public void shouldWorkWithBinaryTypeBlob(String mock, Page page) throws Exception { + setupRoute(page, mock); + Future wsPromise = webSocketServer.waitForWebSocket(); + setupWS(page, webSocketServer.getPort(), "blob"); + org.java_websocket.WebSocket ws = wsPromise.get(); + ws.send("hi".getBytes(StandardCharsets.UTF_8)); + page.waitForCondition(() -> { + Boolean result = (Boolean) page.evaluate("() => window.log.length >= 2"); + return result; + }); + + assertEquals( + asList("open", "message: data=blob:hi origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId="), + page.evaluate("window.log")); + Future messagePromise = webSocketServer.waitForMessage(); + page.evaluate("() => window.ws.send(new Blob([new Uint8Array(['h'.charCodeAt(0), 'i'.charCodeAt(0)])]))"); + // Without this the blob message is not sent in pass-through! + assertEquals(1, page.evaluate("window.ws.readyState")); + assertEquals("hi", messagePromise.get()); + } + + @ParameterizedTest + @ValueSource(strings = {"no-mock", "no-match", "pass-through"}) + public void shouldWorkWithBinaryTypeArrayBuffer(String mock, Page page) throws Exception { + setupRoute(page, mock); + Future wsPromise = webSocketServer.waitForWebSocket(); + setupWS(page, webSocketServer.getPort(), "arraybuffer"); + org.java_websocket.WebSocket ws = wsPromise.get(); + ws.send("hi".getBytes(StandardCharsets.UTF_8)); + page.waitForCondition(() -> { + Boolean result = (Boolean) page.evaluate("() => window.log.length >= 2"); + return result; + }); + + assertEquals( + asList("open", "message: data=arraybuffer:hi origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId="), + page.evaluate("window.log")); + Future messagePromise = webSocketServer.waitForMessage(); + page.evaluate("() => window.ws.send(new Blob([new Uint8Array(['h'.charCodeAt(0), 'i'.charCodeAt(0)])]))"); + // Without this the blob message is not sent in pass-through! + assertEquals(1, page.evaluate("window.ws.readyState")); + assertEquals("hi", messagePromise.get()); + } + + @Test + public void shouldWorkWithServer(Page page) throws ExecutionException, InterruptedException { + WebSocketRoute[] wsRoute = new WebSocketRoute[]{null}; + page.routeWebSocket(Pattern.compile("/.*/"), ws -> { + WebSocketRoute server = ws.connectToServer(); + ws.onMessage(frame -> { + String message = frame.text(); + switch (message) { + case "to-respond": + ws.send("response"); + break; + case "to-block": + break; + case "to-modify": + server.send("modified"); + break; + default: + server.send(message); + } + }); + server.onMessage(frame -> { + String message = frame.text(); + switch (message) { + case "to-block": + break; + case "to-modify": + ws.send("modified"); + break; + default: + ws.send(message); + } + }); + server.send("fake"); + wsRoute[0] = ws; + }); + + Future ws = webSocketServer.waitForWebSocket(); + setupWS(page, webSocketServer.getPort(), "blob"); + page.waitForCondition(() -> webSocketServer.logCopy().size() >= 1); + assertEquals( + asList("message: fake"), + webSocketServer.logCopy()); + + ws.get().send("to-modify"); + ws.get().send("to-block"); + ws.get().send("pass-server"); + page.waitForCondition(() -> (Boolean) page.evaluate("() => window.log.length >= 3")); + assertEquals( + asList( + "open", + "message: data=modified origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId=", + "message: data=pass-server origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId="), + page.evaluate("window.log")); + + page.evaluate("async () => {\n" + + " window.ws.send('to-respond');\n" + + " window.ws.send('to-modify');\n" + + " window.ws.send('to-block');\n" + + " window.ws.send('pass-client');\n" + + " }"); + page.waitForCondition(() -> webSocketServer.logCopy().size() >= 3); + assertEquals( + asList("message: fake", "message: modified", "message: pass-client"), + webSocketServer.logCopy()); + + page.waitForCondition(() -> (Boolean) page.evaluate("() => window.log.length >= 4")); + assertEquals( + asList( + "open", + "message: data=modified origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId=", + "message: data=pass-server origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId=", + "message: data=response origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId="), + page.evaluate("window.log")); + + wsRoute[0].send("another"); + page.waitForCondition(() -> (Boolean) page.evaluate("() => window.log.length >= 5")); + assertEquals( + asList( + "open", + "message: data=modified origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId=", + "message: data=pass-server origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId=", + "message: data=response origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId=", + "message: data=another origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId="), + page.evaluate("window.log")); + + page.evaluate("window.ws.send('pass-client-2');"); + page.waitForCondition(() -> webSocketServer.logCopy().size() >= 4); + assertEquals( + asList("message: fake", "message: modified", "message: pass-client", "message: pass-client-2"), + webSocketServer.logCopy()); + + page.evaluate("window.ws.close(3009, 'problem');"); + page.waitForCondition(() -> webSocketServer.logCopy().size() >= 5); + assertEquals( + asList("message: fake", "message: modified", "message: pass-client", "message: pass-client-2", "close: code=3009 reason=problem"), + webSocketServer.logCopy()); + } + + @Test + public void shouldWorkWithoutServer(Page page) { + WebSocketRoute[] wsRoute = new WebSocketRoute[]{ null }; + page.routeWebSocket(Pattern.compile("/.*/"), ws -> { + ws.onMessage(frame -> { + String message = frame.text(); + if ("to-respond".equals(message)) { + ws.send("response"); + } + }); + wsRoute[0] = ws; + }); + setupWS(page, webSocketServer.getPort(), "blob"); + + page.evaluate("async () => {\n" + + " await window.wsOpened;\n" + + " window.ws.send('to-respond');\n" + + " window.ws.send('to-block');\n" + + " window.ws.send('to-respond');\n" + + " }"); + + page.waitForCondition(() -> (Boolean) page.evaluate("() => window.log.length >= 3")); + assertEquals( + asList( + "open", + "message: data=response origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId=", + "message: data=response origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId="), + page.evaluate("window.log")); + + + wsRoute[0].send("another"); + wsRoute[0].close(new WebSocketRoute.CloseOptions().setCode(3008).setReason("oops")); + + page.waitForCondition(() -> (Boolean) page.evaluate("() => window.log.length >= 5")); + assertEquals( + asList( + "open", + "message: data=response origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId=", + "message: data=response origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId=", + "message: data=another origin=ws://localhost:" + webSocketServer.getPort() + " lastEventId=", + "close code=3008 reason=oops"), + page.evaluate("window.log")); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestWorkers.java b/playwright/src/test/java/com/microsoft/playwright/TestWorkers.java index b1bf63cd..9cb3b98f 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestWorkers.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestWorkers.java @@ -35,6 +35,7 @@ private int browserMajorVersion() { } @Test + @DisabledIf(value="com.microsoft.playwright.TestBase#isChromium", disabledReason="Started failing since last driver roll, flaky upstream") void pageWorkers() { Worker worker = page.waitForWorker(() -> page.navigate(server.PREFIX + "/worker/worker.html")); assertTrue(worker.url().contains("worker.js")); @@ -150,6 +151,7 @@ void shouldAttributeNetworkActivityForWorkerInsideIframeToTheIframe() { } @Test + @DisabledIf(value="com.microsoft.playwright.TestBase#isChromium", disabledReason="Started failing since last driver roll, flaky upstream") void shouldReportNetworkActivity() { Assumptions.assumeFalse(isFirefox() && browserMajorVersion() < 114); Worker worker = page.waitForWorker(() -> page.navigate(server.PREFIX + "/worker/worker.html")); diff --git a/playwright/src/test/java/com/microsoft/playwright/WebSocketServerImpl.java b/playwright/src/test/java/com/microsoft/playwright/WebSocketServerImpl.java index 3c90928a..33abb774 100644 --- a/playwright/src/test/java/com/microsoft/playwright/WebSocketServerImpl.java +++ b/playwright/src/test/java/com/microsoft/playwright/WebSocketServerImpl.java @@ -21,15 +21,25 @@ import org.java_websocket.server.WebSocketServer; import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicInteger; import static com.microsoft.playwright.Utils.nextFreePort; class WebSocketServerImpl extends WebSocketServer implements AutoCloseable { volatile ClientHandshake lastClientHandshake; + private volatile CompletableFuture futureWebSocket; + private volatile CompletableFuture futureMessage; private final Semaphore startSemaphore = new Semaphore(0); + private List log = new ArrayList<>(); + static WebSocketServerImpl create() throws InterruptedException { // FIXME: WebSocketServer.stop() doesn't release socket immediately and starting another server // fails with "Address already in use", so we just allocate new port. @@ -44,6 +54,40 @@ private WebSocketServerImpl(InetSocketAddress address) { super(address, 1); } + synchronized void reset() { + futureMessage = null; + futureWebSocket = null; + synchronized (log) { + log.clear(); + } + } + + Future waitForWebSocket() { + if (futureWebSocket == null) { + futureWebSocket = new CompletableFuture<>(); + } + return futureWebSocket; + } + + Future waitForMessage() { + if (futureMessage == null) { + futureMessage = new CompletableFuture<>(); + } + return futureMessage; + } + + List logCopy() { + synchronized (log) { + return new ArrayList<>(log); + } + } + + private void addLog(String line) { + synchronized (log) { + log.add(line); + } + } + @Override public void close() throws Exception { this.stop(); @@ -52,19 +96,41 @@ public void close() throws Exception { @Override public void onOpen(org.java_websocket.WebSocket webSocket, ClientHandshake clientHandshake) { lastClientHandshake = clientHandshake; + if (futureWebSocket != null) { + futureWebSocket.complete(webSocket); + futureWebSocket = null; + return; + } webSocket.send("incoming"); } @Override - public void onClose(org.java_websocket.WebSocket webSocket, int i, String s, boolean b) { + public void onClose(org.java_websocket.WebSocket webSocket, int code, String reason, boolean remote) { + addLog("close: code=" + code + " reason=" + reason); } @Override public void onMessage(org.java_websocket.WebSocket webSocket, String s) { + addLog("message: " + s); + if (futureMessage != null) { + futureMessage.complete(s); + futureMessage = null; + } } + public void onMessage(WebSocket conn, ByteBuffer message) { + String text = new String(message.array(), StandardCharsets.UTF_8); + addLog("message: " + text); + if (futureMessage != null) { + futureMessage.complete(text); + futureMessage = null; + } + } + + @Override public void onError(WebSocket webSocket, Exception e) { + addLog("error: " + e.toString()); e.printStackTrace(); startSemaphore.release(); } diff --git a/pom.xml b/pom.xml index e7dd2b98..160bb069 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,12 @@ ${junit.version} test + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + org.java-websocket Java-WebSocket diff --git a/scripts/DRIVER_VERSION b/scripts/DRIVER_VERSION index 4159cb56..a13bcd81 100644 --- a/scripts/DRIVER_VERSION +++ b/scripts/DRIVER_VERSION @@ -1 +1 @@ -1.47.0-beta-1726138322000 +1.48.0-beta-1727692967000 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 ebe277e5..f6d81169 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 @@ -134,6 +134,18 @@ private static String formatSpec(JsonArray spec) { out.add(line.getAsString()); } out.add("}

    "); + } else if ("note".equals(type)) { + StringBuilder paragraph = new StringBuilder(); + if (!out.isEmpty()) + paragraph.append("\n

    "); + paragraph.append("NOTE: "); + for (JsonElement text : node.getAsJsonArray("children")) { + if (!"text".equals(text.getAsJsonObject().get("type").getAsString())) { + continue; + } + paragraph.append(beautify(text.getAsJsonObject().get("text").getAsString())); + } + out.add(paragraph.toString()); } else { String paragraph = node.get("text").getAsString(); Matcher matcher = Pattern.compile("^\\*\\*(.+)\\*\\*$").matcher(paragraph); @@ -143,9 +155,6 @@ private static String formatSpec(JsonArray spec) { paragraph = "" + title + ""; } else { paragraph = beautify(paragraph); - if ("note".equals(type)) { - paragraph = "NOTE: " + paragraph; - } } if (!out.isEmpty()) paragraph = "\n

    " + paragraph; @@ -532,6 +541,9 @@ private String convertBuiltinType(JsonObject jsonType) { throw new RuntimeException("Missing mapping for " + jsonPath); } } + if ("WebSocketRoute.onClose.handler".equals(jsonPath)) { + return "BiConsumer"; + } if (jsonType.getAsJsonArray("args").size() == 1) { String paramType = convertBuiltinType(jsonType.getAsJsonArray("args").get(0).getAsJsonObject()); if (!jsonType.has("returnType") || jsonType.get("returnType").isJsonNull()) { @@ -989,7 +1001,10 @@ void writeTo(List output, String offset) { if (asList("Page", "Frame", "ElementHandle", "Locator", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright").contains(jsonName)) { output.add("import java.util.*;"); } - if (asList("Page", "Browser", "BrowserContext", "WebSocket", "Worker", "CDPSession").contains(jsonName)) { + if (asList("WebSocketRoute").contains(jsonName)) { + output.add("import java.util.function.BiConsumer;"); + } + if (asList("Page", "Browser", "BrowserContext", "WebSocket", "Worker", "CDPSession", "WebSocketRoute").contains(jsonName)) { output.add("import java.util.function.Consumer;"); } if (asList("Page", "BrowserContext").contains(jsonName)) { diff --git a/tools/test-local-installation/pom.xml b/tools/test-local-installation/pom.xml index d46ca2fe..70db7923 100644 --- a/tools/test-local-installation/pom.xml +++ b/tools/test-local-installation/pom.xml @@ -9,10 +9,10 @@ Runs Playwright test suite (copied from playwright module) against locally cached Playwright 1.8 - 2.10.1 - 5.10.2 + 2.11.0 + 5.11.0 UTF-8 - 1.5.6 + 1.5.7 @@ -38,6 +38,12 @@ ${junit.version} test + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + org.java-websocket Java-WebSocket