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, Consumerhandler); + /** + * 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, Consumerhandler); + /** + * 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(Predicateurl, 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. + * + ** + * @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. + * + *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, Consumerhandler); + /** + * 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, Consumerhandler); + /** + * 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(Predicateurl, 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"); * } 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
"); + 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