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 interface APIRequest { * {@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 @@ public interface APIRequestContext { * } * *
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 @@ public interface APIRequestContext {
* }
*
* 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 @@ public interface APIRequestContext {
* }
*
* 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 @@ public interface APIRequestContext {
* }
*
* 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 @@ public interface Browser extends AutoCloseable {
* {@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 interface Browser extends AutoCloseable {
* {@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 @@ public interface Browser extends AutoCloseable {
* {@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 interface Browser extends AutoCloseable {
* {@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 @@ public interface BrowserContext extends AutoCloseable {
*
*
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 @@ public interface BrowserContext extends AutoCloseable {
*
*
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 @@ public interface BrowserContext extends AutoCloseable {
*
*
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 @@ public interface BrowserContext extends AutoCloseable {
*
*
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 @@ public interface BrowserContext extends AutoCloseable {
*
*
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 @@ public interface BrowserContext extends AutoCloseable {
*
*
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 @@ public interface BrowserContext extends AutoCloseable {
*
*
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 @@ public interface BrowserContext extends AutoCloseable {
*
*
Playwright will not serve requests intercepted by Service Worker from the HAR file. See this issue. We recommend disabling Service Workers when
- * using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
+ * using request interception by setting {@code serviceWorkers} to {@code "block"}.
*
* @param har Path to a HAR file with prerecorded network data. If {@code
* path} is a relative path, then it is resolved relative to the current working directory.
* @since v1.23
*/
void routeFromHAR(Path har, RouteFromHAROptions options);
+ /**
+ * This method allows to modify websocket connections that are made by any page in the browser context.
+ *
+ *
Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this
+ * method before creating any pages.
+ *
+ *
Usage
+ *
+ *
Below is an example of a simple handler that blocks some websocket messages. See {@code WebSocketRoute} for more details
+ * and examples.
+ *
{@code
+ * context.routeWebSocket("/ws", ws -> {
+ * ws.routeSend(message -> {
+ * if ("to-be-blocked".equals(message))
+ * return;
+ * ws.send(message);
+ * });
+ * ws.connect();
+ * });
+ * }
+ *
+ * @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code
+ * baseURL} context option.
+ * @param handler Handler function to route the WebSocket.
+ * @since v1.48
+ */
+ void routeWebSocket(String url, Consumer handler);
+ /**
+ * This method allows to modify websocket connections that are made by any page in the browser context.
+ *
+ * Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this
+ * method before creating any pages.
+ *
+ *
Usage
+ *
+ *
Below is an example of a simple handler that blocks some websocket messages. See {@code WebSocketRoute} for more details
+ * and examples.
+ *
{@code
+ * context.routeWebSocket("/ws", ws -> {
+ * ws.routeSend(message -> {
+ * if ("to-be-blocked".equals(message))
+ * return;
+ * ws.send(message);
+ * });
+ * ws.connect();
+ * });
+ * }
+ *
+ * @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code
+ * baseURL} context option.
+ * @param handler Handler function to route the WebSocket.
+ * @since v1.48
+ */
+ void routeWebSocket(Pattern url, Consumer handler);
+ /**
+ * This method allows to modify websocket connections that are made by any page in the browser context.
+ *
+ * Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this
+ * method before creating any pages.
+ *
+ *
Usage
+ *
+ *
Below is an example of a simple handler that blocks some websocket messages. See {@code WebSocketRoute} for more details
+ * and examples.
+ *
{@code
+ * context.routeWebSocket("/ws", ws -> {
+ * ws.routeSend(message -> {
+ * if ("to-be-blocked".equals(message))
+ * return;
+ * ws.send(message);
+ * });
+ * ws.connect();
+ * });
+ * }
+ *
+ * @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code
+ * baseURL} context option.
+ * @param handler Handler function to route the WebSocket.
+ * @since v1.48
+ */
+ void routeWebSocket(Predicate url, Consumer handler);
/**
* This setting will change the default maximum navigation time for the following methods and related shortcuts:
*
diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java
index 8ab6e59a..827d4868 100644
--- a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java
+++ b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java
@@ -465,8 +465,6 @@ public interface BrowserType {
* {@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 interface BrowserType {
* {@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 @@ public interface Frame {
/**
* 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 interface Frame {
/**
* 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 @@ public interface Frame {
/**
* 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 interface Frame {
/**
* 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 @@ public interface Frame {
/**
* 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 interface Frame {
/**
* 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 @@ public interface Frame {
/**
* 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 interface Frame {
/**
* 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 @@ import java.util.regex.Pattern;
/**
* 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 @@ import java.util.regex.Pattern;
* 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 interface FrameLocator {
}
}
/**
- * 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 @@ public interface FrameLocator {
*/
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 @@ public interface FrameLocator {
*/
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 @@ public interface FrameLocator {
*
*
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 @@ public interface Locator {
/**
* 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 interface Locator {
/**
* 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 @@ public interface Locator {
/**
* 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 interface Locator {
/**
* 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 @@ public interface Locator {
/**
* 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 interface Locator {
/**
* 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 @@ public interface Locator {
/**
* 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 interface Locator {
/**
* 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 interface Locator {
* 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 @@ public interface Locator {
*/
Locator nth(int index);
/**
- * Creates a locator that matches either of the two locators.
+ * Creates a locator matching all elements that match one or both of the two locators.
+ *
+ * Note that when both locators match something, the resulting locator will have multiple matches and violate locator strictness guidelines.
*
*
Usage
*
diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java
index 096812a2..14e788a4 100644
--- a/playwright/src/main/java/com/microsoft/playwright/Page.java
+++ b/playwright/src/main/java/com/microsoft/playwright/Page.java
@@ -560,7 +560,8 @@ public interface Page extends AutoCloseable {
/**
* 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 interface Page extends AutoCloseable {
/**
* 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 @@ public interface Page extends AutoCloseable {
/**
* 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 interface Page extends AutoCloseable {
/**
* 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 @@ public interface Page extends AutoCloseable {
/**
* 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 interface Page extends AutoCloseable {
/**
* 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 @@ public interface Page extends AutoCloseable {
/**
* 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 interface Page extends AutoCloseable {
/**
* 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 @@ public interface Page extends AutoCloseable {
* @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 @@ public interface Page extends AutoCloseable {
* 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 @@ public interface Page extends AutoCloseable {
*
*
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 @@ public interface Page extends AutoCloseable {
*
*
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 @@ public interface Page extends AutoCloseable {
*
*
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 @@ public interface Page extends AutoCloseable {
*
*
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 @@ public interface Page extends AutoCloseable {
*
*
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 @@ public interface Page extends AutoCloseable {
*
*
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 @@ public interface Page extends AutoCloseable {
*
*
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 @@ public interface Page extends AutoCloseable {
*
*
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 @@ public interface Page extends AutoCloseable {
*
*
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 @@ public interface Page extends AutoCloseable {
*
*
Playwright will not serve requests intercepted by Service Worker from the HAR file. See this issue. We recommend disabling Service Workers when
- * using request interception by setting {@code Browser.newContext.serviceWorkers} to {@code "block"}.
+ * using request interception by setting {@code serviceWorkers} to {@code "block"}.
*
* @param har Path to a HAR file with prerecorded network data. If {@code
* path} is a relative path, then it is resolved relative to the current working directory.
* @since v1.23
*/
void routeFromHAR(Path har, RouteFromHAROptions options);
+ /**
+ * This method allows to modify websocket connections that are made by the page.
+ *
+ *
Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this
+ * method before navigating the page.
+ *
+ *
Usage
+ *
+ *
Below is an example of a simple mock that responds to a single message. See {@code WebSocketRoute} for more details and
+ * examples.
+ *
{@code
+ * page.routeWebSocket("/ws", ws -> {
+ * ws.onMessage(message -> {
+ * if ("request".equals(message))
+ * ws.send("response");
+ * });
+ * });
+ * }
+ *
+ * @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code
+ * baseURL} context option.
+ * @param handler Handler function to route the WebSocket.
+ * @since v1.48
+ */
+ void routeWebSocket(String url, Consumer handler);
+ /**
+ * This method allows to modify websocket connections that are made by the page.
+ *
+ * Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this
+ * method before navigating the page.
+ *
+ *
Usage
+ *
+ *
Below is an example of a simple mock that responds to a single message. See {@code WebSocketRoute} for more details and
+ * examples.
+ *
{@code
+ * page.routeWebSocket("/ws", ws -> {
+ * ws.onMessage(message -> {
+ * if ("request".equals(message))
+ * ws.send("response");
+ * });
+ * });
+ * }
+ *
+ * @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code
+ * baseURL} context option.
+ * @param handler Handler function to route the WebSocket.
+ * @since v1.48
+ */
+ void routeWebSocket(Pattern url, Consumer handler);
+ /**
+ * This method allows to modify websocket connections that are made by the page.
+ *
+ * Note that only {@code WebSocket}s created after this method was called will be routed. It is recommended to call this
+ * method before navigating the page.
+ *
+ *
Usage
+ *
+ *
Below is an example of a simple mock that responds to a single message. See {@code WebSocketRoute} for more details and
+ * examples.
+ *
{@code
+ * page.routeWebSocket("/ws", ws -> {
+ * ws.onMessage(message -> {
+ * if ("request".equals(message))
+ * ws.send("response");
+ * });
+ * });
+ * }
+ *
+ * @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the {@code
+ * baseURL} context option.
+ * @param handler Handler function to route the WebSocket.
+ * @since v1.48
+ */
+ void routeWebSocket(Predicate url, Consumer handler);
/**
* Returns the buffer with the captured screenshot.
*
@@ -7256,6 +7353,8 @@ public interface Page extends AutoCloseable {
* page.navigate("https://example.com");
* }
*
+ * @param width Page width in pixels.
+ * @param height Page height in pixels.
* @since v1.8
*/
void setViewportSize(int width, int height);
diff --git a/playwright/src/main/java/com/microsoft/playwright/Tracing.java b/playwright/src/main/java/com/microsoft/playwright/Tracing.java
index 961db33e..82ea8d49 100644
--- a/playwright/src/main/java/com/microsoft/playwright/Tracing.java
+++ b/playwright/src/main/java/com/microsoft/playwright/Tracing.java
@@ -39,8 +39,8 @@ public interface Tracing {
class StartOptions {
/**
* If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code
- * tracesDir} folder specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify the
- * final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stop
+ * tracesDir} directory specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify
+ * the final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stop
* Tracing.stop()} instead.
*/
public String name;
@@ -69,8 +69,8 @@ public interface Tracing {
/**
* 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 interface Tracing {
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 @@ public interface Tracing {
/**
* If specified, intermediate trace files are going to be saved into the files with the given name prefix inside the {@code
- * tracesDir} folder specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify the
- * final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stopChunk
+ * tracesDir} directory specified in {@link com.microsoft.playwright.BrowserType#launch BrowserType.launch()}. To specify
+ * the final trace zip file name, you need to pass {@code path} option to {@link com.microsoft.playwright.Tracing#stopChunk
* Tracing.stopChunk()} instead.
*/
public StartChunkOptions setName(String name) {
diff --git a/playwright/src/main/java/com/microsoft/playwright/WebSocketRoute.java b/playwright/src/main/java/com/microsoft/playwright/WebSocketRoute.java
new file mode 100644
index 00000000..4e56b5bb
--- /dev/null
+++ b/playwright/src/main/java/com/microsoft/playwright/WebSocketRoute.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.microsoft.playwright;
+
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * Whenever a {@code WebSocket} route is set up
+ * with {@link com.microsoft.playwright.Page#routeWebSocket Page.routeWebSocket()} or {@link
+ * com.microsoft.playwright.BrowserContext#routeWebSocket BrowserContext.routeWebSocket()}, the {@code WebSocketRoute}
+ * object allows to handle the WebSocket, like an actual server would do.
+ *
+ * Mocking
+ *
+ *
By default, the routed WebSocket will not connect to the server. This way, you can mock entire communcation over the
+ * WebSocket. Here is an example that responds to a {@code "request"} with a {@code "response"}.
+ *
{@code
+ * page.routeWebSocket("/ws", ws -> {
+ * ws.onMessage(message -> {
+ * if ("request".equals(message))
+ * ws.send("response");
+ * });
+ * });
+ * }
+ *
+ * Since we do not call {@link com.microsoft.playwright.WebSocketRoute#connectToServer WebSocketRoute.connectToServer()}
+ * inside the WebSocket route handler, Playwright assumes that WebSocket will be mocked, and opens the WebSocket inside the
+ * page automatically.
+ *
+ *
Intercepting
+ *
+ *
Alternatively, you may want to connect to the actual server, but intercept messages in-between and modify or block them.
+ * Calling {@link com.microsoft.playwright.WebSocketRoute#connectToServer WebSocketRoute.connectToServer()} returns a
+ * server-side {@code WebSocketRoute} instance that you can send messages to, or handle incoming messages.
+ *
+ *
Below is an example that modifies some messages sent by the page to the server. Messages sent from the server to the
+ * page are left intact, relying on the default forwarding.
+ *
{@code
+ * page.routeWebSocket("/ws", ws -> {
+ * WebSocketRoute server = ws.connectToServer();
+ * ws.onMessage(message -> {
+ * if ("request".equals(message))
+ * server.send("request2");
+ * else
+ * server.send(message);
+ * });
+ * });
+ * }
+ *
+ * After connecting to the server, all **messages are forwarded** between the page and the server by default.
+ *
+ *
However, if you call {@link com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} on the
+ * original route, messages from the page to the server **will not be forwarded** anymore, but should instead be handled by
+ * the {@code handler}.
+ *
+ *
Similarly, calling {@link com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} on the
+ * server-side WebSocket will **stop forwarding messages** from the server to the page, and {@code handler} should take
+ * care of them.
+ *
+ *
The following example blocks some messages in both directions. Since it calls {@link
+ * com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} in both directions, there is no automatic
+ * forwarding at all.
+ *
{@code
+ * page.routeWebSocket("/ws", ws -> {
+ * WebSocketRoute server = ws.connectToServer();
+ * ws.onMessage(message -> {
+ * if (!"blocked-from-the-page".equals(message))
+ * server.send(message);
+ * });
+ * server.onMessage(message -> {
+ * if (!"blocked-from-the-server".equals(message))
+ * ws.send(message);
+ * });
+ * });
+ * }
+ */
+public interface WebSocketRoute {
+ class CloseOptions {
+ /**
+ * Optional close code.
+ */
+ public Integer code;
+ /**
+ * Optional close reason.
+ */
+ public String reason;
+
+ /**
+ * Optional close code.
+ */
+ public CloseOptions setCode(int code) {
+ this.code = code;
+ return this;
+ }
+ /**
+ * Optional close reason.
+ */
+ public CloseOptions setReason(String reason) {
+ this.reason = reason;
+ return this;
+ }
+ }
+ /**
+ * Closes one side of the WebSocket connection.
+ *
+ * @since v1.48
+ */
+ default void close() {
+ close(null);
+ }
+ /**
+ * Closes one side of the WebSocket connection.
+ *
+ * @since v1.48
+ */
+ void close(CloseOptions options);
+ /**
+ * By default, routed WebSocket does not connect to the server, so you can mock entire WebSocket communication. This method
+ * connects to the actual WebSocket server, and returns the server-side {@code WebSocketRoute} instance, giving the ability
+ * to send and receive messages from the server.
+ *
+ * Once connected to the server:
+ *
+ * - Messages received from the server will be **automatically forwarded** to the WebSocket in the page, unless {@link
+ * com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} is called on the server-side {@code
+ * WebSocketRoute}.
+ * - Messages sent by the {@code
+ * WebSocket.send()} call in the page will be **automatically forwarded** to the server, unless {@link
+ * com.microsoft.playwright.WebSocketRoute#onMessage WebSocketRoute.onMessage()} is called on the original {@code
+ * WebSocketRoute}.
+ *
+ *
+ * See examples at the top for more details.
+ *
+ * @since v1.48
+ */
+ WebSocketRoute connectToServer();
+ /**
+ * Allows to handle {@code WebSocket.close}.
+ *
+ *
By default, closing one side of the connection, either in the page or on the server, will close the other side. However,
+ * when {@link com.microsoft.playwright.WebSocketRoute#onClose WebSocketRoute.onClose()} handler is set up, the default
+ * forwarding of closure is disabled, and handler should take care of it.
+ *
+ * @param handler Function that will handle WebSocket closure. Received an optional close code and an optional close reason.
+ * @since v1.48
+ */
+ void onClose(BiConsumer handler);
+ /**
+ * This method allows to handle messages that are sent by the WebSocket, either from the page or from the server.
+ *
+ * When called on the original WebSocket route, this method handles messages sent from the page. You can handle this
+ * messages by responding to them with {@link com.microsoft.playwright.WebSocketRoute#send WebSocketRoute.send()},
+ * forwarding them to the server-side connection returned by {@link com.microsoft.playwright.WebSocketRoute#connectToServer
+ * WebSocketRoute.connectToServer()} or do something else.
+ *
+ *
Once this method is called, messages are not automatically forwarded to the server or to the page - you should do that
+ * manually by calling {@link com.microsoft.playwright.WebSocketRoute#send WebSocketRoute.send()}. See examples at the top
+ * for more details.
+ *
+ *
Calling this method again will override the handler with a new one.
+ *
+ * @param handler Function that will handle messages.
+ * @since v1.48
+ */
+ void onMessage(Consumer handler);
+ /**
+ * Sends a message to the WebSocket. When called on the original WebSocket, sends the message to the page. When called on
+ * the result of {@link com.microsoft.playwright.WebSocketRoute#connectToServer WebSocketRoute.connectToServer()}, sends
+ * the message to the server. See examples at the top for more details.
+ *
+ * @param message Message to send.
+ * @since v1.48
+ */
+ void send(String message);
+ /**
+ * Sends a message to the WebSocket. When called on the original WebSocket, sends the message to the page. When called on
+ * the result of {@link com.microsoft.playwright.WebSocketRoute#connectToServer WebSocketRoute.connectToServer()}, sends
+ * the message to the server. See examples at the top for more details.
+ *
+ * @param message Message to send.
+ * @since v1.48
+ */
+ void send(byte[] message);
+ /**
+ * URL of the WebSocket created in the page.
+ *
+ * @since v1.48
+ */
+ String url();
+}
+
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java
index 735fd1ba..5cf39fe9 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java
@@ -50,6 +50,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
final List backgroundPages = new ArrayList<>();
final Router routes = new Router();
+ final WebSocketRouter webSocketRoutes = new WebSocketRouter();
private boolean closeWasCalled;
private final WaitableEvent closePromise;
final Map bindings = new HashMap<>();
@@ -514,6 +515,28 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
});
}
+ @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 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
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 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
}
+ void handleWebSocketRoute(WebSocketRouteImpl route) {
+ if (!webSocketRoutes.handle(route)) {
+ route.connectToServer();
+ }
+ }
+
WaitableResult pause() {
return sendMessageAsync("pause", new JsonObject());
}
@@ -730,6 +763,9 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
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 @@ class ChannelOwner extends LoggingSupport {
}
}
+ void markAsInternalType() {
+ isInternalType = true;
+ }
+
void disposeChannelOwner(boolean wasGarbageCollected) {
// Clean up from parent and connection.
if (parent != null) {
@@ -84,6 +89,9 @@ class ChannelOwner extends LoggingSupport {
@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 @@ class ChannelOwner extends LoggingSupport {
}
}
+ 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 @@ public class Connection {
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 @@ import static com.microsoft.playwright.impl.Serialization.gson;
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 @@ public class PageImpl extends ChannelOwner implements Page {
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 @@ public class PageImpl extends ChannelOwner implements Page {
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 @@ public class PageImpl extends ChannelOwner implements Page {
});
}
+ @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 @@ public class PageImpl extends ChannelOwner implements Page {
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 class RouteImpl extends ChannelOwner implements Route {
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 @@ public class RouteImpl extends ChannelOwner implements Route {
params.addProperty("postData", base64);
}
}
- params.addProperty("requestUrl", request.initializer.get("url").getAsString());
params.addProperty("isFallback", isFallback);
sendMessageAsync("continue", params);
}
@@ -231,7 +230,6 @@ public class RouteImpl extends ChannelOwner implements Route {
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 com.microsoft.playwright.Route;
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 @@ class Router {
}
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.function.Predicate;
import java.util.regex.Pattern;
import static com.microsoft.playwright.impl.Utils.globToRegex;
+import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
class UrlMatcher {
final Object rawSource;
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 @@ public class Utils {
}
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.PlaywrightException;
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 @@ class WebSocketImpl extends ChannelOwner implements WebSocket {
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 @@ public class TestDefaultBrowserContext2 extends TestBase {
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 @@ public class TestLocatorFrame extends TestBase {
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 @@ public class TestWorkers extends TestBase {
}
@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 @@ public class TestWorkers extends TestBase {
}
@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.handshake.ClientHandshake;
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 @@ class WebSocketServerImpl extends WebSocketServer implements AutoCloseable {
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 @@ class WebSocketServerImpl extends WebSocketServer implements AutoCloseable {
@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 @@ abstract class Element { paragraph = "" + title + ""; } else { paragraph = beautify(paragraph); - if ("note".equals(type)) { - paragraph = "NOTE: " + paragraph; - } } if (!out.isEmpty()) paragraph = "\n
" + paragraph;
@@ -532,6 +541,9 @@ class TypeRef extends Element {
throw new RuntimeException("Missing mapping for " + jsonPath);
}
}
+ if ("WebSocketRoute.onClose.handler".equals(jsonPath)) {
+ return "BiConsumer