From 35cc5771d3f8831717b721dadaab212177511cda Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 5 May 2026 18:09:16 -0700 Subject: [PATCH 1/2] chore: roll to 1.60.0-alpha-1778025033000 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the following upstream client-side PRs: - microsoft/playwright#39767 - feat: Add `location()` to `WebError` - microsoft/playwright#39950 - feat: page version of toMatchAriaSnapshot - microsoft/playwright#40083 - feat(locator): add `description` option to getByRole - microsoft/playwright#40092 - feat(expect): support pseudo-element in toHaveCSS - microsoft/playwright#40159 - feat(tracing): add tracing.startHar / tracing.stopHar - microsoft/playwright#40196 - chore: remove deprecated `handle` option from exposeBinding - microsoft/playwright#40215 - feat(api): Locator.highlight({ style }) - microsoft/playwright#40219 - feat(api): Page.hideHighlight, Locator.hideHighlight, highlight() returns AutoCloseable - microsoft/playwright#40283 - feat(locator): add drop API for files and clipboard-like data - microsoft/playwright#40315 - feat(browser): Browser.onContext / offContext - microsoft/playwright#40341 - feat(browsercontext): mirror Page lifecycle events on BrowserContext - microsoft/playwright#40377 - feat(route): expose WebSocket subprotocols on WebSocketRoute - microsoft/playwright#40606 - feat(api): unify WebError and ConsoleMessage location - microsoft/playwright#40651 - docs: alias drop-payload Object types Protocol shape change for FrameExpectResult.received (now `{ value, ariaSnapshot }` instead of a bare SerializedValue) — updated AssertionsBase to unwrap it. Generator updates: added new types to relevant import lists; share topLevelTypes between the assertion and non-assertion generate runs so PseudoElement gets emitted into options/. --- README.md | 4 +- examples/pom.xml | 2 +- .../playwright/APIRequestContext.java | 33 ++-- .../com/microsoft/playwright/Browser.java | 9 + .../microsoft/playwright/BrowserContext.java | 126 ++++++------- .../com/microsoft/playwright/BrowserType.java | 21 +++ .../java/com/microsoft/playwright/Frame.java | 35 +++- .../microsoft/playwright/FrameLocator.java | 35 +++- .../com/microsoft/playwright/Locator.java | 171 +++++++++++++++++- .../java/com/microsoft/playwright/Page.java | 125 ++++++------- .../com/microsoft/playwright/Tracing.java | 103 +++++++++++ .../com/microsoft/playwright/WebError.java | 7 + .../microsoft/playwright/WebSocketRoute.java | 22 +++ .../assertions/LocatorAssertions.java | 12 ++ .../playwright/assertions/PageAssertions.java | 48 +++++ .../impl/APIRequestContextImpl.java | 5 + .../playwright/impl/AssertionsBase.java | 9 +- .../playwright/impl/BrowserContextImpl.java | 159 ++++++++++------ .../playwright/impl/BrowserImpl.java | 12 ++ .../microsoft/playwright/impl/FrameImpl.java | 49 ++++- .../playwright/impl/LocatorImpl.java | 16 +- .../playwright/impl/LocatorUtils.java | 4 + .../playwright/impl/PageAssertionsImpl.java | 10 + .../microsoft/playwright/impl/PageImpl.java | 23 ++- .../microsoft/playwright/impl/Protocol.java | 6 +- .../playwright/impl/TracingImpl.java | 122 +++++++++++++ .../playwright/impl/WebErrorImpl.java | 10 +- .../playwright/impl/WebSocketRouteImpl.java | 24 +++ .../playwright/options/DropPayload.java | 33 ++++ .../playwright/options/PseudoElement.java | 22 +++ .../playwright/options/WebErrorLocation.java | 33 ++++ .../microsoft/playwright/TestBrowser1.java | 9 + .../playwright/TestBrowserContextEvents.java | 86 +++++++++ .../TestBrowserContextExposeFunction.java | 15 -- .../playwright/TestLocatorHighlight.java | 14 ++ .../playwright/TestPageAriaSnapshot.java | 10 + .../microsoft/playwright/TestPageDrop.java | 125 +++++++++++++ .../playwright/TestPageExposeFunction.java | 51 ------ .../playwright/TestRouteWebSocket.java | 25 +++ .../playwright/TestSelectorsRole.java | 2 +- .../com/microsoft/playwright/TestTracing.java | 12 ++ .../microsoft/playwright/TraceViewerPage.java | 2 +- scripts/DRIVER_VERSION | 2 +- .../playwright/tools/ApiGenerator.java | 53 +++--- 44 files changed, 1370 insertions(+), 326 deletions(-) create mode 100644 playwright/src/main/java/com/microsoft/playwright/options/DropPayload.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/options/PseudoElement.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/options/WebErrorLocation.java create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestPageDrop.java diff --git a/README.md b/README.md index 82bf24d25..9827c347e 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 147.0.7727.15 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 148.0.7778.96 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 26.4 | ✅ | ✅ | ✅ | -| Firefox 148.0.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 150.0.1 | :white_check_mark: | :white_check_mark: | :white_check_mark: | ## Documentation diff --git a/examples/pom.xml b/examples/pom.xml index 13f953919..dd612f2c6 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -10,7 +10,7 @@ Playwright Client Examples UTF-8 - 1.59.0 + 1.60.0 diff --git a/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java b/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java index 34b25000d..e7449fe26 100644 --- a/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java @@ -23,24 +23,23 @@ * This API is used for the Web API testing. You can use it to trigger API endpoints, configure micro-services, prepare * environment or the service to your e2e test. * - *

Each Playwright browser context has associated with it {@code APIRequestContext} instance which shares cookie storage - * with the browser context and can be accessed via {@link com.microsoft.playwright.BrowserContext#request - * BrowserContext.request()} or {@link com.microsoft.playwright.Page#request Page.request()}. It is also possible to create - * a new APIRequestContext instance manually by calling {@link com.microsoft.playwright.APIRequest#newContext - * APIRequest.newContext()}. + *

Each Playwright browser context has an associated {@code APIRequestContext}, accessible via {@link + * com.microsoft.playwright.BrowserContext#request BrowserContext.request()} or {@link + * com.microsoft.playwright.Page#request Page.request()} (these return the + * + *

**same instance** — {@code page.request} is a shortcut for {@code page.context().request}). You can also create a + * standalone, isolated instance with {@link com.microsoft.playwright.APIRequest#newContext APIRequest.newContext()}. * *

Cookie management * - *

{@code APIRequestContext} returned by {@link com.microsoft.playwright.BrowserContext#request BrowserContext.request()} - * and {@link com.microsoft.playwright.Page#request Page.request()} shares cookie storage with the corresponding {@code - * BrowserContext}. Each API request will have {@code Cookie} header populated with the values from the browser context. If - * the API response contains {@code Set-Cookie} header it will automatically update {@code BrowserContext} cookies and - * requests made from the page will pick them up. This means that if you log in using this API, your e2e test will be - * logged in and vice versa. + *

The {@code APIRequestContext} returned by {@link com.microsoft.playwright.BrowserContext#request + * BrowserContext.request()} and + * + *

{@link com.microsoft.playwright.Page#request Page.request()} uses the same cookie jar as its {@code BrowserContext}: * - *

If you want API requests to not interfere with the browser cookies you should create a new {@code APIRequestContext} by - * calling {@link com.microsoft.playwright.APIRequest#newContext APIRequest.newContext()}. Such {@code APIRequestContext} - * object will have its own isolated cookie storage. + *

If you want API requests that do **not** share cookies with the browser, create an isolated context via {@link + * com.microsoft.playwright.APIRequest#newContext APIRequest.newContext()}. Such {@code APIRequestContext} object will have + * its own isolated cookie storage. */ public interface APIRequestContext { class DisposeOptions { @@ -484,5 +483,11 @@ default String storageState() { * @since v1.16 */ String storageState(StorageStateOptions options); + /** + * + * + * @since v1.60 + */ + Tracing tracing(); } diff --git a/playwright/src/main/java/com/microsoft/playwright/Browser.java b/playwright/src/main/java/com/microsoft/playwright/Browser.java index c87963e03..f3d5a6a03 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Browser.java +++ b/playwright/src/main/java/com/microsoft/playwright/Browser.java @@ -43,6 +43,15 @@ */ public interface Browser extends AutoCloseable { + /** + * Emitted when a new browser context is created. + */ + void onContext(Consumer handler); + /** + * Removes handler that was previously added with {@link #onContext onContext(handler)}. + */ + void offContext(Consumer handler); + /** * Emitted when Browser gets disconnected from the browser application. This might happen because of one of the following: *

    diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java index 7b3c4a3c3..02f76f61b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java @@ -114,6 +114,48 @@ public interface BrowserContext extends AutoCloseable { */ void offDialog(Consumer handler); + /** + * Emitted when attachment download started in any page belonging to this context. User can access basic file operations on + * downloaded content via the passed {@code Download} instance. See also {@link com.microsoft.playwright.Page#onDownload + * Page.onDownload()} to receive events about a specific page. + */ + void onDownload(Consumer handler); + /** + * Removes handler that was previously added with {@link #onDownload onDownload(handler)}. + */ + void offDownload(Consumer handler); + + /** + * Emitted when a frame is attached in any page belonging to this context. See also {@link + * com.microsoft.playwright.Page#onFrameAttached Page.onFrameAttached()} to receive events about a specific page. + */ + void onFrameAttached(Consumer handler); + /** + * Removes handler that was previously added with {@link #onFrameAttached onFrameAttached(handler)}. + */ + void offFrameAttached(Consumer handler); + + /** + * Emitted when a frame is detached in any page belonging to this context. See also {@link + * com.microsoft.playwright.Page#onFrameDetached Page.onFrameDetached()} to receive events about a specific page. + */ + void onFrameDetached(Consumer handler); + /** + * Removes handler that was previously added with {@link #onFrameDetached onFrameDetached(handler)}. + */ + void offFrameDetached(Consumer handler); + + /** + * Emitted when a frame is navigated to a new url in any page belonging to this context. See also {@link + * com.microsoft.playwright.Page#onFrameNavigated Page.onFrameNavigated()} to receive events about navigations in a + * specific page. + */ + void onFrameNavigated(Consumer handler); + /** + * Removes handler that was previously added with {@link #onFrameNavigated onFrameNavigated(handler)}. + */ + void offFrameNavigated(Consumer handler); + /** * The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event will * also fire for popup pages. See also {@link com.microsoft.playwright.Page#onPopup Page.onPopup()} to receive events about @@ -141,6 +183,27 @@ public interface BrowserContext extends AutoCloseable { */ void offPage(Consumer handler); + /** + * Emitted when a page in this context is closed. See also {@link com.microsoft.playwright.Page#onClose Page.onClose()} to + * receive events about a specific page. + */ + void onPageClose(Consumer handler); + /** + * Removes handler that was previously added with {@link #onPageClose onPageClose(handler)}. + */ + void offPageClose(Consumer handler); + + /** + * Emitted when the JavaScript {@code load} event is + * dispatched in any page belonging to this context. See also {@link com.microsoft.playwright.Page#onLoad Page.onLoad()} to + * receive events about a specific page. + */ + void onPageLoad(Consumer handler); + /** + * Removes handler that was previously added with {@link #onPageLoad onPageLoad(handler)}. + */ + void offPageLoad(Consumer handler); + /** * Emitted when exception is unhandled in any of the pages in this context. To listen for errors from a particular page, * use {@link com.microsoft.playwright.Page#onPageError Page.onPageError()} instead. @@ -271,20 +334,6 @@ public CloseOptions setReason(String reason) { return this; } } - class ExposeBindingOptions { - /** - * @deprecated This option will be removed in the future. - */ - public Boolean handle; - - /** - * @deprecated This option will be removed in the future. - */ - public ExposeBindingOptions setHandle(boolean handle) { - this.handle = handle; - return this; - } - } class GrantPermissionsOptions { /** * The [origin] to grant permissions to, e.g. "https://example.com". @@ -736,54 +785,7 @@ default List cookies() { * @param callback Callback function that will be called in the Playwright's context. * @since v1.8 */ - default AutoCloseable exposeBinding(String name, BindingCallback callback) { - return exposeBinding(name, callback, null); - } - /** - * The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context. - * When called, the function executes {@code callback} and returns a Promise which - * resolves to the return value of {@code callback}. If the {@code callback} returns a Promise, it will be - * awaited. - * - *

    The first argument of the {@code callback} function contains information about the caller: {@code { browserContext: - * BrowserContext, page: Page, frame: Frame }}. - * - *

    See {@link com.microsoft.playwright.Page#exposeBinding Page.exposeBinding()} for page-only version. - * - *

    Usage - * - *

    An example of exposing page URL to all frames in all pages in the context: - *

    {@code
    -   * import com.microsoft.playwright.*;
    -   *
    -   * public class Example {
    -   *   public static void main(String[] args) {
    -   *     try (Playwright playwright = Playwright.create()) {
    -   *       BrowserType webkit = playwright.webkit();
    -   *       Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
    -   *       BrowserContext context = browser.newContext();
    -   *       context.exposeBinding("pageURL", (source, args) -> source.page().url());
    -   *       Page page = context.newPage();
    -   *       page.setContent("\n" +
    -   *         "\n" +
    -   *         "
    "); - * page.getByRole(AriaRole.BUTTON).click(); - * } - * } - * } - * }
    - * - * @param name Name of the function on the window object. - * @param callback Callback function that will be called in the Playwright's context. - * @since v1.8 - */ - AutoCloseable exposeBinding(String name, BindingCallback callback, ExposeBindingOptions options); + AutoCloseable exposeBinding(String name, BindingCallback callback); /** * The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context. * When called, the function executes {@code callback} and returns a Learn more about {@code aria-checked}. */ public Boolean checked; + /** + * Option to match the accessible description. By + * default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior. + * + *

    Learn more about accessible description. + */ + public Object description; /** * An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * @@ -875,8 +882,8 @@ class GetByRoleOptions { */ public Boolean disabled; /** - * Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} - * is a regular expression. Note that exact match still trims whitespace. + * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false. + * Ignored when the value is a regular expression. Note that exact match still trims whitespace. */ public Boolean exact; /** @@ -928,6 +935,26 @@ public GetByRoleOptions setChecked(boolean checked) { this.checked = checked; return this; } + /** + * Option to match the accessible description. By + * default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior. + * + *

    Learn more about accessible description. + */ + public GetByRoleOptions setDescription(String description) { + this.description = description; + return this; + } + /** + * Option to match the accessible description. By + * default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior. + * + *

    Learn more about accessible description. + */ + public GetByRoleOptions setDescription(Pattern description) { + this.description = description; + return this; + } /** * An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * @@ -939,8 +966,8 @@ public GetByRoleOptions setDisabled(boolean disabled) { return this; } /** - * Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} - * is a regular expression. Note that exact match still trims whitespace. + * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false. + * Ignored when the value is a regular expression. Note that exact match still trims whitespace. */ public GetByRoleOptions setExact(boolean exact) { this.exact = exact; diff --git a/playwright/src/main/java/com/microsoft/playwright/FrameLocator.java b/playwright/src/main/java/com/microsoft/playwright/FrameLocator.java index dc386fbd2..ad11d9130 100644 --- a/playwright/src/main/java/com/microsoft/playwright/FrameLocator.java +++ b/playwright/src/main/java/com/microsoft/playwright/FrameLocator.java @@ -107,6 +107,13 @@ class GetByRoleOptions { *

    Learn more about {@code aria-checked}. */ public Boolean checked; + /** + * Option to match the accessible description. By + * default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior. + * + *

    Learn more about accessible description. + */ + public Object description; /** * An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * @@ -115,8 +122,8 @@ class GetByRoleOptions { */ public Boolean disabled; /** - * Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} - * is a regular expression. Note that exact match still trims whitespace. + * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false. + * Ignored when the value is a regular expression. Note that exact match still trims whitespace. */ public Boolean exact; /** @@ -168,6 +175,26 @@ public GetByRoleOptions setChecked(boolean checked) { this.checked = checked; return this; } + /** + * Option to match the accessible description. By + * default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior. + * + *

    Learn more about accessible description. + */ + public GetByRoleOptions setDescription(String description) { + this.description = description; + return this; + } + /** + * Option to match the accessible description. By + * default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior. + * + *

    Learn more about accessible description. + */ + public GetByRoleOptions setDescription(Pattern description) { + this.description = description; + return this; + } /** * An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * @@ -179,8 +206,8 @@ public GetByRoleOptions setDisabled(boolean disabled) { return this; } /** - * Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} - * is a regular expression. Note that exact match still trims whitespace. + * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false. + * Ignored when the value is a regular expression. Note that exact match still trims whitespace. */ public GetByRoleOptions setExact(boolean exact) { this.exact = exact; diff --git a/playwright/src/main/java/com/microsoft/playwright/Locator.java b/playwright/src/main/java/com/microsoft/playwright/Locator.java index 9deec253c..b0eb27f08 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Locator.java +++ b/playwright/src/main/java/com/microsoft/playwright/Locator.java @@ -30,6 +30,13 @@ */ public interface Locator { class AriaSnapshotOptions { + /** + * When {@code true}, appends each element's bounding box as {@code [box=x,y,width,height]} to the snapshot. Coordinates + * are relative to the viewport, in CSS pixels, as returned by {@code + * Element.getBoundingClientRect()}. Defaults to {@code false}. + */ + public Boolean boxes; /** * When specified, limits the depth of the snapshot. */ @@ -47,6 +54,16 @@ class AriaSnapshotOptions { */ public Double timeout; + /** + * When {@code true}, appends each element's bounding box as {@code [box=x,y,width,height]} to the snapshot. Coordinates + * are relative to the viewport, in CSS pixels, as returned by {@code + * Element.getBoundingClientRect()}. Defaults to {@code false}. + */ + public AriaSnapshotOptions setBoxes(boolean boxes) { + this.boxes = boxes; + return this; + } /** * When specified, limits the depth of the snapshot. */ @@ -645,6 +662,46 @@ public DragToOptions setTrial(boolean trial) { return this; } } + class DropOptions { + /** + * A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the + * element. + */ + public Position position; + /** + * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default + * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout + * BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()} + * methods. + */ + public Double timeout; + + /** + * A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the + * element. + */ + public DropOptions setPosition(double x, double y) { + return setPosition(new Position(x, y)); + } + /** + * A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the + * element. + */ + public DropOptions setPosition(Position position) { + this.position = position; + return this; + } + /** + * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default + * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout + * BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()} + * methods. + */ + public DropOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } class ElementHandleOptions { /** * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default @@ -943,6 +1000,13 @@ class GetByRoleOptions { *

    Learn more about {@code aria-checked}. */ public Boolean checked; + /** + * Option to match the accessible description. By + * default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior. + * + *

    Learn more about accessible description. + */ + public Object description; /** * An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * @@ -951,8 +1015,8 @@ class GetByRoleOptions { */ public Boolean disabled; /** - * Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} - * is a regular expression. Note that exact match still trims whitespace. + * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false. + * Ignored when the value is a regular expression. Note that exact match still trims whitespace. */ public Boolean exact; /** @@ -1004,6 +1068,26 @@ public GetByRoleOptions setChecked(boolean checked) { this.checked = checked; return this; } + /** + * Option to match the accessible description. By + * default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior. + * + *

    Learn more about accessible description. + */ + public GetByRoleOptions setDescription(String description) { + this.description = description; + return this; + } + /** + * Option to match the accessible description. By + * default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior. + * + *

    Learn more about accessible description. + */ + public GetByRoleOptions setDescription(Pattern description) { + this.description = description; + return this; + } /** * An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * @@ -1015,8 +1099,8 @@ public GetByRoleOptions setDisabled(boolean disabled) { return this; } /** - * Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} - * is a regular expression. Note that exact match still trims whitespace. + * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false. + * Ignored when the value is a regular expression. Note that exact match still trims whitespace. */ public GetByRoleOptions setExact(boolean exact) { this.exact = exact; @@ -1122,6 +1206,20 @@ public GetByTitleOptions setExact(boolean exact) { return this; } } + class HighlightOptions { + /** + * Additional inline CSS applied to the highlight overlay, e.g. {@code "outline: 2px dashed red"}. + */ + public String style; + + /** + * Additional inline CSS applied to the highlight overlay, e.g. {@code "outline: 2px dashed red"}. + */ + public HighlightOptions setStyle(String style) { + this.style = style; + return this; + } + } class HoverOptions { /** * Whether to bypass the actionability checks. Defaults to @@ -2900,6 +2998,54 @@ default void dragTo(Locator target) { * @since v1.18 */ void dragTo(Locator target, DragToOptions options); + /** + * Simulate an external drag-and-drop of files or clipboard-like data onto this locator. + * + *

    Details + * + *

    Dispatches the native {@code dragenter}, {@code dragover}, and {@code drop} events at the center of the target element + * with a synthetic [DataTransfer] carrying the provided files and/or data entries. Works cross-browser by constructing the + * [DataTransfer] in the page context. + * + *

    If the target element's {@code dragover} listener does not call {@code preventDefault()}, the target is considered to + * have rejected the drop: Playwright dispatches {@code dragleave} and this method throws. + * + *

    Usage + * + *

    Drop a file buffer onto an upload area: + * + *

    Drop plain text and a URL together: + * + * @param payload Data to drop onto the target. Provide {@code files} (file paths or in-memory buffers), {@code data} (a mime-type → + * string map for clipboard-like content such as {@code text/plain}, {@code text/html}, {@code text/uri-list}), or both. + * @since v1.60 + */ + default void drop(DropPayload payload) { + drop(payload, null); + } + /** + * Simulate an external drag-and-drop of files or clipboard-like data onto this locator. + * + *

    Details + * + *

    Dispatches the native {@code dragenter}, {@code dragover}, and {@code drop} events at the center of the target element + * with a synthetic [DataTransfer] carrying the provided files and/or data entries. Works cross-browser by constructing the + * [DataTransfer] in the page context. + * + *

    If the target element's {@code dragover} listener does not call {@code preventDefault()}, the target is considered to + * have rejected the drop: Playwright dispatches {@code dragleave} and this method throws. + * + *

    Usage + * + *

    Drop a file buffer onto an upload area: + * + *

    Drop plain text and a URL together: + * + * @param payload Data to drop onto the target. Provide {@code files} (file paths or in-memory buffers), {@code data} (a mime-type → + * string map for clipboard-like content such as {@code text/plain}, {@code text/html}, {@code text/uri-list}), or both. + * @since v1.60 + */ + void drop(DropPayload payload, DropOptions options); /** * Resolves given locator to the first matching DOM element. If there are no matching elements, waits for one. If multiple * elements match the locator, throws. @@ -3879,13 +4025,28 @@ default Locator getByTitle(Pattern text) { * @since v1.27 */ Locator getByTitle(Pattern text, GetByTitleOptions options); + /** + * Hides the element highlight previously added by {@link com.microsoft.playwright.Locator#highlight Locator.highlight()}. + * + * @since v1.60 + */ + void hideHighlight(); + /** + * Highlight the corresponding element(s) on the screen. Useful for debugging, don't commit the code that uses {@link + * com.microsoft.playwright.Locator#highlight Locator.highlight()}. + * + * @since v1.20 + */ + default AutoCloseable highlight() { + return highlight(null); + } /** * Highlight the corresponding element(s) on the screen. Useful for debugging, don't commit the code that uses {@link * com.microsoft.playwright.Locator#highlight Locator.highlight()}. * * @since v1.20 */ - void highlight(); + AutoCloseable highlight(HighlightOptions options); /** * Hover over the matching element. * diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java index 074ecdff0..da5f4e865 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Page.java +++ b/playwright/src/main/java/com/microsoft/playwright/Page.java @@ -1058,20 +1058,6 @@ public EvalOnSelectorOptions setStrict(boolean strict) { return this; } } - class ExposeBindingOptions { - /** - * @deprecated This option will be removed in the future. - */ - public Boolean handle; - - /** - * @deprecated This option will be removed in the future. - */ - public ExposeBindingOptions setHandle(boolean handle) { - this.handle = handle; - return this; - } - } class FillOptions { /** * Whether to bypass the actionability checks. Defaults to @@ -1250,6 +1236,13 @@ class GetByRoleOptions { *

    Learn more about {@code aria-checked}. */ public Boolean checked; + /** + * Option to match the accessible description. By + * default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior. + * + *

    Learn more about accessible description. + */ + public Object description; /** * An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * @@ -1258,8 +1251,8 @@ class GetByRoleOptions { */ public Boolean disabled; /** - * Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} - * is a regular expression. Note that exact match still trims whitespace. + * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false. + * Ignored when the value is a regular expression. Note that exact match still trims whitespace. */ public Boolean exact; /** @@ -1311,6 +1304,26 @@ public GetByRoleOptions setChecked(boolean checked) { this.checked = checked; return this; } + /** + * Option to match the accessible description. By + * default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior. + * + *

    Learn more about accessible description. + */ + public GetByRoleOptions setDescription(String description) { + this.description = description; + return this; + } + /** + * Option to match the accessible description. By + * default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior. + * + *

    Learn more about accessible description. + */ + public GetByRoleOptions setDescription(Pattern description) { + this.description = description; + return this; + } /** * An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * @@ -1322,8 +1335,8 @@ public GetByRoleOptions setDisabled(boolean disabled) { return this; } /** - * Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} - * is a regular expression. Note that exact match still trims whitespace. + * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false. + * Ignored when the value is a regular expression. Note that exact match still trims whitespace. */ public GetByRoleOptions setExact(boolean exact) { this.exact = exact; @@ -2981,6 +2994,13 @@ public SetInputFilesOptions setTimeout(double timeout) { } } class AriaSnapshotOptions { + /** + * When {@code true}, appends each element's bounding box as {@code [box=x,y,width,height]} to the snapshot. Coordinates + * are relative to the viewport, in CSS pixels, as returned by {@code + * Element.getBoundingClientRect()}. Defaults to {@code false}. + */ + public Boolean boxes; /** * When specified, limits the depth of the snapshot. */ @@ -2998,6 +3018,16 @@ class AriaSnapshotOptions { */ public Double timeout; + /** + * When {@code true}, appends each element's bounding box as {@code [box=x,y,width,height]} to the snapshot. Coordinates + * are relative to the viewport, in CSS pixels, as returned by {@code + * Element.getBoundingClientRect()}. Defaults to {@code false}. + */ + public AriaSnapshotOptions setBoxes(boolean boxes) { + this.boxes = boxes; + return this; + } /** * When specified, limits the depth of the snapshot. */ @@ -4676,57 +4706,7 @@ default JSHandle evaluateHandle(String expression) { * @param callback Callback function that will be called in the Playwright's context. * @since v1.8 */ - default AutoCloseable exposeBinding(String name, BindingCallback callback) { - return exposeBinding(name, callback, null); - } - /** - * The method adds a function called {@code name} on the {@code window} object of every frame in this page. When called, - * the function executes {@code callback} and returns a Promise which - * resolves to the return value of {@code callback}. If the {@code callback} returns a Promise, it will be - * awaited. - * - *

    The first argument of the {@code callback} function contains information about the caller: {@code { browserContext: - * BrowserContext, page: Page, frame: Frame }}. - * - *

    See {@link com.microsoft.playwright.BrowserContext#exposeBinding BrowserContext.exposeBinding()} for the context-wide - * version. - * - *

    NOTE: Functions installed via {@link com.microsoft.playwright.Page#exposeBinding Page.exposeBinding()} survive navigations. - * - *

    Usage - * - *

    An example of exposing page URL to all frames in a page: - *

    {@code
    -   * import com.microsoft.playwright.*;
    -   *
    -   * public class Example {
    -   *   public static void main(String[] args) {
    -   *     try (Playwright playwright = Playwright.create()) {
    -   *       BrowserType webkit = playwright.webkit();
    -   *       Browser browser = webkit.launch(new BrowserType.LaunchOptions().setHeadless(false));
    -   *       BrowserContext context = browser.newContext();
    -   *       Page page = context.newPage();
    -   *       page.exposeBinding("pageURL", (source, args) -> source.page().url());
    -   *       page.setContent("\n" +
    -   *         "\n" +
    -   *         "
    "); - * page.click("button"); - * } - * } - * } - * }
    - * - * @param name Name of the function on the window object. - * @param callback Callback function that will be called in the Playwright's context. - * @since v1.8 - */ - AutoCloseable exposeBinding(String name, BindingCallback callback, ExposeBindingOptions options); + AutoCloseable exposeBinding(String name, BindingCallback callback); /** * The method adds a function called {@code name} on the {@code window} object of every frame in the page. When called, the * function executes {@code callback} and returns a diff --git a/playwright/src/main/java/com/microsoft/playwright/Tracing.java b/playwright/src/main/java/com/microsoft/playwright/Tracing.java index d955a636a..d21cfe133 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Tracing.java +++ b/playwright/src/main/java/com/microsoft/playwright/Tracing.java @@ -18,6 +18,7 @@ import com.microsoft.playwright.options.*; import java.nio.file.Path; +import java.util.regex.Pattern; /** * API for collecting and saving Playwright traces. Playwright traces can be opened in Only one HAR recording can be active at a time per {@code BrowserContext}. + * + *

    Usage + *

    {@code
    +   * context.tracing().startHar(Paths.get("trace.har"));
    +   * Page page = context.newPage();
    +   * page.navigate("https://playwright.dev");
    +   * context.tracing().stopHar();
    +   * }
    + * + * @param path Path on the filesystem to write the HAR file to. If the file name ends with {@code .zip}, the HAR is saved as a zip + * archive with response bodies attached as separate files. + * @since v1.60 + */ + default AutoCloseable startHar(Path path) { + return startHar(path, null); + } + /** + * Start recording a HAR (HTTP Archive) of network activity in this context. The HAR file is written to disk when {@link + * com.microsoft.playwright.Tracing#stopHar Tracing.stopHar()} is called, or when the returned {@code Disposable} is + * disposed. + * + *

    Only one HAR recording can be active at a time per {@code BrowserContext}. + * + *

    Usage + *

    {@code
    +   * context.tracing().startHar(Paths.get("trace.har"));
    +   * Page page = context.newPage();
    +   * page.navigate("https://playwright.dev");
    +   * context.tracing().stopHar();
    +   * }
    + * + * @param path Path on the filesystem to write the HAR file to. If the file name ends with {@code .zip}, the HAR is saved as a zip + * archive with response bodies attached as separate files. + * @since v1.60 + */ + AutoCloseable startHar(Path path, StartHarOptions options); /** * NOTE: Use {@code test.step} instead when available. * @@ -408,5 +504,12 @@ default void stopChunk() { * @since v1.15 */ void stopChunk(StopChunkOptions options); + /** + * Stop HAR recording and save the HAR file to the path given to {@link com.microsoft.playwright.Tracing#startHar + * Tracing.startHar()}. + * + * @since v1.60 + */ + void stopHar(); } diff --git a/playwright/src/main/java/com/microsoft/playwright/WebError.java b/playwright/src/main/java/com/microsoft/playwright/WebError.java index 0eb5923a7..4576536ba 100644 --- a/playwright/src/main/java/com/microsoft/playwright/WebError.java +++ b/playwright/src/main/java/com/microsoft/playwright/WebError.java @@ -16,6 +16,7 @@ package com.microsoft.playwright; +import com.microsoft.playwright.options.*; /** * {@code WebError} class represents an unhandled exception thrown in the page. It is dispatched via the {@link @@ -43,5 +44,11 @@ public interface WebError { * @since v1.38 */ String error(); + /** + * + * + * @since v1.60 + */ + WebErrorLocation location(); } diff --git a/playwright/src/main/java/com/microsoft/playwright/WebSocketRoute.java b/playwright/src/main/java/com/microsoft/playwright/WebSocketRoute.java index d8303830b..29e5398bc 100644 --- a/playwright/src/main/java/com/microsoft/playwright/WebSocketRoute.java +++ b/playwright/src/main/java/com/microsoft/playwright/WebSocketRoute.java @@ -16,6 +16,7 @@ package com.microsoft.playwright; +import java.util.*; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -213,6 +214,27 @@ default void close() { * @since v1.48 */ void send(byte[] message); + /** + * The list of WebSocket subprotocols requested by the page, as passed via the second argument to the
    {@code WebSocket} constructor. + * Corresponds to the {@code Sec-WebSocket-Protocol} request header. + * + *

    Returns an empty array if no protocols were specified. + * + *

    Usage + *

    {@code
    +   * page.routeWebSocket("wss://example.com/ws", ws -> {
    +   *   if (ws.protocols().contains("chat.v2")) {
    +   *     ws.onMessage(frame -> ws.send("v2:" + frame.text()));
    +   *   } else {
    +   *     ws.close(1002, "Unsupported protocol");
    +   *   }
    +   * });
    +   * }
    + * + * @since v1.60 + */ + List protocols(); /** * URL of the WebSocket created in the page. * diff --git a/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java b/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java index 0cc98854b..780b87751 100644 --- a/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java +++ b/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java @@ -19,6 +19,7 @@ import java.util.*; import java.util.regex.Pattern; import com.microsoft.playwright.options.AriaRole; +import com.microsoft.playwright.options.PseudoElement; /** * The {@code LocatorAssertions} class provides assertion methods that can be used to make assertions about the {@code @@ -427,11 +428,22 @@ public HasCountOptions setTimeout(double timeout) { } } class HasCSSOptions { + /** + * Pseudo-element to read computed styles from. + */ + public PseudoElement pseudo; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ public Double timeout; + /** + * Pseudo-element to read computed styles from. + */ + public HasCSSOptions setPseudo(PseudoElement pseudo) { + this.pseudo = pseudo; + return this; + } /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ diff --git a/playwright/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java b/playwright/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java index d994e3209..88e529e38 100644 --- a/playwright/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java +++ b/playwright/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java @@ -37,6 +37,20 @@ * } */ public interface PageAssertions { + class MatchesAriaSnapshotOptions { + /** + * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. + */ + public Double timeout; + + /** + * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. + */ + public MatchesAriaSnapshotOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } class HasTitleOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -91,6 +105,40 @@ public HasURLOptions setTimeout(double timeout) { * @since v1.20 */ PageAssertions not(); + /** + * Asserts that the page body matches the given accessibility + * snapshot. + * + *

    Usage + *

    {@code
    +   * page.navigate("https://demo.playwright.dev/todomvc/");
    +   * assertThat(page).matchesAriaSnapshot("""
    +   *   - heading "todos"
    +   *   - textbox "What needs to be done?"
    +   * """);
    +   * }
    + * + * @since v1.60 + */ + default void matchesAriaSnapshot(String expected) { + matchesAriaSnapshot(expected, null); + } + /** + * Asserts that the page body matches the given accessibility + * snapshot. + * + *

    Usage + *

    {@code
    +   * page.navigate("https://demo.playwright.dev/todomvc/");
    +   * assertThat(page).matchesAriaSnapshot("""
    +   *   - heading "todos"
    +   *   - textbox "What needs to be done?"
    +   * """);
    +   * }
    + * + * @since v1.60 + */ + void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions options); /** * Ensures the page has the given title. * diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java index 21249c30f..e28cfa810 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java @@ -46,6 +46,11 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext { this.tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString()); } + @Override + public com.microsoft.playwright.Tracing tracing() { + return tracing; + } + @Override public APIResponse delete(String url, RequestOptions options) { return fetch(url, ensureOptions(options, "DELETE")); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/AssertionsBase.java b/playwright/src/main/java/com/microsoft/playwright/impl/AssertionsBase.java index 48c6c3f7e..5e9a6520b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/AssertionsBase.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/AssertionsBase.java @@ -57,7 +57,14 @@ void expectImpl(String expression, FrameExpectOptions expectOptions, Object expe } FrameExpectResult result = doExpect(expression, expectOptions, title); if (result.matches == isNot) { - Object actual = result.received == null ? null : Serialization.deserialize(result.received); + Object actual; + if (result.received == null) { + actual = null; + } else if (result.received.value != null) { + actual = Serialization.deserialize(result.received.value); + } else { + actual = result.received.ariaSnapshot; + } String log = (result.log == null) ? "" : String.join("\n", result.log); if (!log.isEmpty()) { log = "\nCall log:\n" + log; 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 0f503291e..daaed20bd 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -68,23 +68,18 @@ private static final Map eventSubscriptions() { } private final ListenerCollection listeners = new ListenerCollection<>(eventSubscriptions(), this); final TimeoutSettings timeoutSettings = new TimeoutSettings(); - final Map harRecorders = new HashMap<>(); - - static class HarRecorder { - final Path path; - final HarContentPolicy contentPolicy; - - HarRecorder(Path har, HarContentPolicy policy) { - path = har; - contentPolicy = policy; - } - } enum EventType { CLOSE, CONSOLE, DIALOG, + DOWNLOAD, + FRAMEATTACHED, + FRAMEDETACHED, + FRAMENAVIGATED, PAGE, + PAGECLOSE, + PAGELOAD, WEBERROR, REQUEST, REQUESTFAILED, @@ -139,6 +134,20 @@ public void onBackgroundPage(Consumer handler) { public void offBackgroundPage(Consumer handler) { } + @Override + public void onDownload(Consumer handler) { + listeners.add(EventType.DOWNLOAD, handler); + } + + @Override + public void offDownload(Consumer handler) { + listeners.remove(EventType.DOWNLOAD, handler); + } + + void notifyDownload(Download download) { + listeners.notify(EventType.DOWNLOAD, download); + } + @Override public void onClose(Consumer handler) { listeners.add(EventType.CLOSE, handler); @@ -179,6 +188,76 @@ public void offPage(Consumer handler) { listeners.remove(EventType.PAGE, handler); } + @Override + public void onFrameAttached(Consumer handler) { + listeners.add(EventType.FRAMEATTACHED, handler); + } + + @Override + public void offFrameAttached(Consumer handler) { + listeners.remove(EventType.FRAMEATTACHED, handler); + } + + void notifyFrameAttached(FrameImpl frame) { + listeners.notify(EventType.FRAMEATTACHED, frame); + } + + @Override + public void onFrameDetached(Consumer handler) { + listeners.add(EventType.FRAMEDETACHED, handler); + } + + @Override + public void offFrameDetached(Consumer handler) { + listeners.remove(EventType.FRAMEDETACHED, handler); + } + + void notifyFrameDetached(FrameImpl frame) { + listeners.notify(EventType.FRAMEDETACHED, frame); + } + + @Override + public void onFrameNavigated(Consumer handler) { + listeners.add(EventType.FRAMENAVIGATED, handler); + } + + @Override + public void offFrameNavigated(Consumer handler) { + listeners.remove(EventType.FRAMENAVIGATED, handler); + } + + void notifyFrameNavigated(FrameImpl frame) { + listeners.notify(EventType.FRAMENAVIGATED, frame); + } + + @Override + public void onPageClose(Consumer handler) { + listeners.add(EventType.PAGECLOSE, handler); + } + + @Override + public void offPageClose(Consumer handler) { + listeners.remove(EventType.PAGECLOSE, handler); + } + + void notifyPageClose(PageImpl page) { + listeners.notify(EventType.PAGECLOSE, page); + } + + @Override + public void onPageLoad(Consumer handler) { + listeners.add(EventType.PAGELOAD, handler); + } + + @Override + public void offPageLoad(Consumer handler) { + listeners.remove(EventType.PAGELOAD, handler); + } + + void notifyPageLoad(PageImpl page) { + listeners.notify(EventType.PAGELOAD, page); + } + @Override public void onWebError(Consumer handler) { listeners.add(EventType.WEBERROR, handler); @@ -284,27 +363,7 @@ public void close(CloseOptions options) { } closeReason = options.reason; request.dispose(convertType(options, APIRequestContext.DisposeOptions.class)); - for (Map.Entry entry : harRecorders.entrySet()) { - JsonObject params = new JsonObject(); - params.addProperty("harId", entry.getKey()); - JsonObject json = sendMessage("harExport", params, NO_TIMEOUT).getAsJsonObject(); - ArtifactImpl artifact = connection.getExistingObject(json.getAsJsonObject("artifact").get("guid").getAsString()); - // Server side will compress artifact if content is attach or if file is .zip. - HarRecorder harParams = entry.getValue(); - boolean isCompressed = harParams.contentPolicy == HarContentPolicy.ATTACH || harParams.path.toString().endsWith(".zip"); - boolean needCompressed = harParams.path.toString().endsWith(".zip"); - if (isCompressed && !needCompressed) { - String tmpPath = harParams.path + ".tmp"; - artifact.saveAs(Paths.get(tmpPath)); - JsonObject unzipParams = new JsonObject(); - unzipParams.addProperty("zipFile", tmpPath); - unzipParams.addProperty("harFile", harParams.path.toString()); - connection.localUtils.sendMessage("harUnzip", unzipParams, NO_TIMEOUT); - } else { - artifact.saveAs(harParams.path); - } - artifact.delete(); - } + tracing.exportAllHars(); JsonObject params = gson().toJsonTree(options).getAsJsonObject(); sendMessage("close", params, NO_TIMEOUT); } @@ -392,11 +451,11 @@ public List cookies(List urls) { } @Override - public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) { - return exposeBindingImpl(name, playwrightBinding, options); + public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding) { + return exposeBindingImpl(name, playwrightBinding); } - private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) { + private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding) { if (bindings.containsKey(name)) { throw new PlaywrightException("Function \"" + name + "\" has been already registered"); } @@ -409,16 +468,13 @@ private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightB JsonObject params = new JsonObject(); params.addProperty("name", name); - if (options != null && options.handle != null && options.handle) { - params.addProperty("needsHandle", true); - } JsonObject result = sendMessage("exposeBinding", params, NO_TIMEOUT).getAsJsonObject(); return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString()); } @Override public AutoCloseable exposeFunction(String name, FunctionCallback playwrightFunction) { - return exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args), null); + return exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args)); } @Override @@ -515,24 +571,7 @@ void recordIntoHar(PageImpl page, Path har, RouteFromHAROptions options, HarCont if (contentPolicy == null) { contentPolicy = Utils.convertType(options.updateContent, HarContentPolicy.class); } - if (contentPolicy == null) { - contentPolicy = HarContentPolicy.ATTACH; - } - - JsonObject params = new JsonObject(); - if (page != null) { - params.add("page", page.toProtocolRef()); - } - JsonObject recordHarArgs = new JsonObject(); - recordHarArgs.addProperty("zip", har.toString().endsWith(".zip")); - recordHarArgs.addProperty("content", contentPolicy.name().toLowerCase()); - recordHarArgs.addProperty("mode", (options.updateMode == null ? HarMode.MINIMAL : options.updateMode).name().toLowerCase()); - addHarUrlFilter(recordHarArgs, options.url); - - params.add("options", recordHarArgs); - JsonObject json = sendMessage("harStart", params, NO_TIMEOUT).getAsJsonObject(); - String harId = json.get("harId").getAsString(); - harRecorders.put(harId, new HarRecorder(har, contentPolicy)); + tracing.recordIntoHar(page, har, options.url, contentPolicy, options.updateMode, null); } @Override @@ -818,7 +857,11 @@ protected void handleEvent(String event, JsonObject params) { } catch (PlaywrightException e) { page = null; } - listeners.notify(BrowserContextImpl.EventType.WEBERROR, new WebErrorImpl(page, errorStr)); + WebErrorLocation location = null; + if (params.has("location")) { + location = gson().fromJson(params.getAsJsonObject("location"), WebErrorLocation.class); + } + listeners.notify(BrowserContextImpl.EventType.WEBERROR, new WebErrorImpl(page, errorStr, location)); if (page != null) { page.listeners.notify(PageImpl.EventType.PAGEERROR, errorStr); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java index 8aea0c196..f5236750c 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java @@ -43,6 +43,7 @@ class BrowserImpl extends ChannelOwner implements Browser { String closeReason; enum EventType { + CONTEXT, DISCONNECTED, } @@ -50,6 +51,16 @@ enum EventType { super(parent, type, guid, initializer); } + @Override + public void onContext(Consumer handler) { + listeners.add(EventType.CONTEXT, handler); + } + + @Override + public void offContext(Consumer handler) { + listeners.remove(EventType.CONTEXT, handler); + } + @Override public void onDisconnected(Consumer handler) { listeners.add(EventType.DISCONNECTED, handler); @@ -302,6 +313,7 @@ private void didCreateContext(BrowserContextImpl context) { context.tracing().setTracesDir(tracePath); browserType.playwright.selectors.contextsForSelectors.add(context); } + listeners.notify(EventType.CONTEXT, context); } private void didClose() { diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java index b4bd1e9c5..25f45bba9 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java @@ -1051,12 +1051,58 @@ int queryCount(String selector) { return result.get("value").getAsInt(); } - void highlightImpl(String selector) { + void dropImpl(String selector, DropPayload payload, com.microsoft.playwright.Locator.DropOptions options) { + if (options == null) { + options = new com.microsoft.playwright.Locator.DropOptions(); + } + JsonObject params = gson().toJsonTree(options).getAsJsonObject(); + params.addProperty("selector", selector); + params.addProperty("strict", true); + if (payload != null) { + if (payload.files != null) { + if (payload.files instanceof Path) { + addFilePathUploadParams(new Path[] { (Path) payload.files }, params, page.context()); + } else if (payload.files instanceof Path[]) { + addFilePathUploadParams((Path[]) payload.files, params, page.context()); + } else if (payload.files instanceof com.microsoft.playwright.options.FilePayload) { + checkFilePayloadSize(new com.microsoft.playwright.options.FilePayload[] { (com.microsoft.playwright.options.FilePayload) payload.files }); + params.add("payloads", toJsonArray(new com.microsoft.playwright.options.FilePayload[] { (com.microsoft.playwright.options.FilePayload) payload.files })); + } else if (payload.files instanceof com.microsoft.playwright.options.FilePayload[]) { + checkFilePayloadSize((com.microsoft.playwright.options.FilePayload[]) payload.files); + params.add("payloads", toJsonArray((com.microsoft.playwright.options.FilePayload[]) payload.files)); + } else { + throw new com.microsoft.playwright.PlaywrightException("Unsupported files type: " + payload.files.getClass()); + } + } + if (payload.data != null) { + com.google.gson.JsonArray dataArray = new com.google.gson.JsonArray(); + for (java.util.Map.Entry entry : payload.data.entrySet()) { + JsonObject e = new JsonObject(); + e.addProperty("mimeType", entry.getKey()); + e.addProperty("value", entry.getValue()); + dataArray.add(e); + } + params.add("data", dataArray); + } + } + sendMessage("drop", params, timeout(options.timeout)); + } + + void highlightImpl(String selector, String style) { JsonObject params = new JsonObject(); params.addProperty("selector", selector); + if (style != null) { + params.addProperty("style", style); + } sendMessage("highlight", params, NO_TIMEOUT); } + void hideHighlightImpl(String selector) { + JsonObject params = new JsonObject(); + params.addProperty("selector", selector); + sendMessage("hideHighlight", params, NO_TIMEOUT); + } + protected void handleEvent(String event, JsonObject params) { if ("loadstate".equals(event)) { JsonElement add = params.get("add"); @@ -1066,6 +1112,7 @@ protected void handleEvent(String event, JsonObject params) { if (parentFrame == null && page != null) { if (state == LOAD) { page.listeners.notify(PageImpl.EventType.LOAD, page); + page.browserContext.notifyPageLoad(page); } else if (state == DOMCONTENTLOADED) { page.listeners.notify(PageImpl.EventType.DOMCONTENTLOADED, page); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorImpl.java index 2bb9faabf..ba71aa5ef 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorImpl.java @@ -371,8 +371,20 @@ public Locator getByTitle(Pattern text, GetByTitleOptions options) { } @Override - public void highlight() { - frame.highlightImpl(selector); + public void drop(DropPayload payload, DropOptions options) { + frame.dropImpl(selector, payload, options); + } + + @Override + public AutoCloseable highlight(HighlightOptions options) { + String style = options == null ? null : options.style; + frame.highlightImpl(selector, style); + return new DisposableStub(this::hideHighlight); + } + + @Override + public void hideHighlight() { + frame.hideHighlightImpl(selector); } @Override diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java index cbfb2c7f7..1bbe68a5c 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java @@ -86,6 +86,10 @@ static String getByRoleSelector(AriaRole role, Locator.GetByRoleOptions options) String name = escapeForAttributeSelector(options.name, options.exact != null && options.exact); addAttr(result, "name", name); } + if (options.description != null) { + String description = escapeForAttributeSelector(options.description, options.exact != null && options.exact); + addAttr(result, "description", description); + } if (options.pressed != null) addAttr(result, "pressed", options.pressed.toString()); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PageAssertionsImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PageAssertionsImpl.java index 9a114f249..2cdd63d17 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageAssertionsImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageAssertionsImpl.java @@ -73,6 +73,16 @@ public void hasURL(Pattern pattern, HasURLOptions options) { expectImpl("to.have.url", expected, pattern, "Page URL expected to match regex", convertType(options, FrameExpectOptions.class), "Assert \"hasURL\""); } + @Override + public void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions snapshotOptions) { + if (snapshotOptions == null) { + snapshotOptions = new MatchesAriaSnapshotOptions(); + } + FrameExpectOptions options = convertType(snapshotOptions, FrameExpectOptions.class); + options.expectedValue = Serialization.serializeArgument(expected); + expectImpl("to.match.aria", options, expected, "Page expected to match Aria snapshot", "Assert \"matchesAriaSnapshot\""); + } + @Override public PageAssertions not() { return new PageAssertionsImpl(actualPage, !isNot); 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 d29df48b1..19d7b3839 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -41,7 +41,7 @@ public class PageImpl extends ChannelOwner implements Page { - private final BrowserContextImpl browserContext; + final BrowserContextImpl browserContext; private final FrameImpl mainFrame; private final KeyboardImpl keyboard; private final MouseImpl mouse; @@ -171,6 +171,7 @@ protected void handleEvent(String event, JsonObject params) { ArtifactImpl artifact = connection.getExistingObject(artifactGuid); DownloadImpl download = new DownloadImpl(this, artifact, params); listeners.notify(EventType.DOWNLOAD, download); + browserContext.notifyDownload(download); } else if ("fileChooser".equals(event)) { String guid = params.getAsJsonObject("element").get("guid").getAsString(); ElementHandleImpl elementHandle = connection.getExistingObject(guid); @@ -201,6 +202,7 @@ protected void handleEvent(String event, JsonObject params) { frame.parentFrame.childFrames.add(frame); } listeners.notify(EventType.FRAMEATTACHED, frame); + browserContext.notifyFrameAttached(frame); } else if ("frameDetached".equals(event)) { String guid = params.getAsJsonObject("frame").get("guid").getAsString(); FrameImpl frame = connection.getExistingObject(guid); @@ -210,6 +212,7 @@ protected void handleEvent(String event, JsonObject params) { frame.parentFrame.childFrames.remove(frame); } listeners.notify(EventType.FRAMEDETACHED, frame); + browserContext.notifyFrameDetached(frame); } else if ("locatorHandlerTriggered".equals(event)) { int uid = params.get("uid").getAsInt(); onLocatorHandlerTriggered(uid); @@ -245,6 +248,7 @@ void didClose() { isClosed = true; browserContext.pages.remove(this); listeners.notify(EventType.CLOSE, this); + browserContext.notifyPageClose(this); } private String effectiveCloseReason() { @@ -753,11 +757,11 @@ public JSHandle evaluateHandle(String pageFunction, Object arg) { } @Override - public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) { - return exposeBindingImpl(name, playwrightBinding, options); + public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding) { + return exposeBindingImpl(name, playwrightBinding); } - private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) { + private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding) { if (bindings.containsKey(name)) { throw new PlaywrightException("Function \"" + name + "\" has been already registered"); } @@ -768,16 +772,13 @@ private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightB JsonObject params = new JsonObject(); params.addProperty("name", name); - if (options != null && options.handle != null && options.handle) { - params.addProperty("needsHandle", true); - } JsonObject result = sendMessage("exposeBinding", params, NO_TIMEOUT).getAsJsonObject(); return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString()); } @Override public AutoCloseable exposeFunction(String name, FunctionCallback playwrightFunction) { - return exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args), null); + return exposeBindingImpl(name, (BindingCallback.Source source, Object... args) -> playwrightFunction.call(args)); } @Override @@ -1060,6 +1061,11 @@ public Frame mainFrame() { return mainFrame; } + @Override + public void hideHighlight() { + sendMessage("hideHighlight", new JsonObject(), NO_TIMEOUT); + } + @Override public Mouse mouse() { return mouse; @@ -1457,6 +1463,7 @@ private Response waitForNavigationImpl(Logger logger, Runnable code, WaitForNavi void frameNavigated(FrameImpl frame) { listeners.notify(EventType.FRAMENAVIGATED, frame); + browserContext.notifyFrameNavigated(frame); } private class WaitableFrameDetach extends WaitableEvent { diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java b/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java index e3363f20a..2983d1cfe 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java @@ -112,8 +112,12 @@ class FrameExpectOptions { } class FrameExpectResult { + static class Received { + SerializedValue value; + String ariaSnapshot; + } boolean matches; - SerializedValue received; + Received received; String errorMessage; List log; } 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 eec8554b7..7587b16f8 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java @@ -18,14 +18,21 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.Tracing; +import com.microsoft.playwright.options.HarContentPolicy; +import com.microsoft.playwright.options.HarMode; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter; import static com.microsoft.playwright.impl.Serialization.gson; class TracingImpl extends ChannelOwner implements Tracing { @@ -34,6 +41,17 @@ class TracingImpl extends ChannelOwner implements Tracing { private boolean isTracing; private String stacksId; private final Set additionalSources = new HashSet<>(); + final Map harRecorders = new HashMap<>(); + + static class HarRecorder { + final Path path; + final HarContentPolicy contentPolicy; + + HarRecorder(Path har, HarContentPolicy policy) { + this.path = har; + this.contentPolicy = policy; + } + } TracingImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { @@ -161,6 +179,110 @@ public void stopChunk(StopChunkOptions options) { stopChunkImpl(options == null ? null : options.path); } + private String currentHarId; + + @Override + public AutoCloseable startHar(Path path, StartHarOptions options) { + if (currentHarId != null) { + throw new PlaywrightException("HAR recording has already been started"); + } + if (options == null) { + options = new StartHarOptions(); + } + boolean isZip = path.toString().endsWith(".zip"); + HarContentPolicy contentPolicy = options.content != null + ? options.content + : (isZip ? HarContentPolicy.ATTACH : HarContentPolicy.EMBED); + HarMode mode = options.mode != null ? options.mode : HarMode.FULL; + currentHarId = recordIntoHar(null, path, options.urlFilter, contentPolicy, mode, null); + return new DisposableStub(this::stopHar); + } + + @Override + public void stopHar() { + if (currentHarId == null) { + throw new PlaywrightException("HAR recording has not been started"); + } + String harId = currentHarId; + currentHarId = null; + exportHar(harId); + } + + String recordIntoHar(PageImpl page, Path har, Object urlFilter, HarContentPolicy contentPolicy, HarMode mode, Path resourcesDir) { + if (contentPolicy == null) { + contentPolicy = HarContentPolicy.ATTACH; + } + if (mode == null) { + mode = HarMode.MINIMAL; + } + + JsonObject params = new JsonObject(); + if (page != null) { + params.add("page", page.toProtocolRef()); + } + JsonObject recordHarArgs = new JsonObject(); + recordHarArgs.addProperty("zip", har.toString().endsWith(".zip")); + recordHarArgs.addProperty("content", contentPolicy.name().toLowerCase()); + recordHarArgs.addProperty("mode", mode.name().toLowerCase()); + addHarUrlFilter(recordHarArgs, urlFilter); + if (resourcesDir != null) { + recordHarArgs.addProperty("resourcesDir", resourcesDir.toString()); + } + if (!har.toString().endsWith(".zip")) { + recordHarArgs.addProperty("harPath", har.toString()); + } + + params.add("options", recordHarArgs); + JsonObject json = sendMessage("harStart", params, NO_TIMEOUT).getAsJsonObject(); + String harId = json.get("harId").getAsString(); + harRecorders.put(harId, new HarRecorder(har, contentPolicy)); + return harId; + } + + void exportHar(String harId) { + HarRecorder harParams = harRecorders.remove(harId); + if (harParams == null) { + return; + } + boolean isLocal = !connection.isRemote; + boolean isZip = harParams.path.toString().endsWith(".zip"); + + JsonObject params = new JsonObject(); + params.addProperty("harId", harId); + if (isLocal) { + params.addProperty("mode", "entries"); + JsonObject json = sendMessage("harExport", params, NO_TIMEOUT).getAsJsonObject(); + if (!isZip) { + return; + } + JsonArray entries = json.getAsJsonArray("entries"); + connection.localUtils.zip(harParams.path, entries, null, false, false, java.util.Collections.emptyList()); + return; + } + + params.addProperty("mode", "archive"); + JsonObject json = sendMessage("harExport", params, NO_TIMEOUT).getAsJsonObject(); + ArtifactImpl artifact = connection.getExistingObject(json.getAsJsonObject("artifact").get("guid").getAsString()); + if (isZip) { + artifact.saveAs(harParams.path); + artifact.delete(); + return; + } + String tmpPath = harParams.path + ".tmp"; + artifact.saveAs(Paths.get(tmpPath)); + JsonObject unzipParams = new JsonObject(); + unzipParams.addProperty("zipFile", tmpPath); + unzipParams.addProperty("harFile", harParams.path.toString()); + connection.localUtils.sendMessage("harUnzip", unzipParams, NO_TIMEOUT); + artifact.delete(); + } + + void exportAllHars() { + for (String harId : new ArrayList<>(harRecorders.keySet())) { + exportHar(harId); + } + } + void setTracesDir(Path tracesDir) { this.tracesDir = tracesDir; } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/WebErrorImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/WebErrorImpl.java index d4e0a8e77..95530ac05 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/WebErrorImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/WebErrorImpl.java @@ -17,14 +17,17 @@ package com.microsoft.playwright.impl; import com.microsoft.playwright.WebError; +import com.microsoft.playwright.options.WebErrorLocation; public class WebErrorImpl implements WebError { private final PageImpl page; private final String error; + private final WebErrorLocation location; - WebErrorImpl(PageImpl page, String error) { + WebErrorImpl(PageImpl page, String error, WebErrorLocation location) { this.page = page; this.error = error; + this.location = location; } @Override @@ -36,4 +39,9 @@ public PageImpl page() { public String error() { return error; } + + @Override + public WebErrorLocation location() { + return location; + } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketRouteImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketRouteImpl.java index e39ba4a15..e8664c9d1 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketRouteImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/WebSocketRouteImpl.java @@ -1,11 +1,14 @@ package com.microsoft.playwright.impl; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.WebSocketFrame; import com.microsoft.playwright.WebSocketRoute; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -65,6 +68,11 @@ public void send(byte[] message) { public String url() { return initializer.get("url").getAsString(); } + + @Override + public List protocols() { + return readProtocols(); + } }; WebSocketRouteImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { @@ -123,6 +131,22 @@ public String url() { return initializer.get("url").getAsString(); } + @Override + public List protocols() { + return readProtocols(); + } + + private List readProtocols() { + List result = new ArrayList<>(); + if (!initializer.has("protocols")) { + return result; + } + for (JsonElement element : initializer.getAsJsonArray("protocols")) { + result.add(element.getAsString()); + } + return result; + } + void afterHandle() { if (this.connected) { return; diff --git a/playwright/src/main/java/com/microsoft/playwright/options/DropPayload.java b/playwright/src/main/java/com/microsoft/playwright/options/DropPayload.java new file mode 100644 index 000000000..7571471df --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/options/DropPayload.java @@ -0,0 +1,33 @@ +/* + * 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.options; + +import java.util.Map; + +public class DropPayload { + public Object files; + public Map data; + + public DropPayload setFiles(Object files) { + this.files = files; + return this; + } + public DropPayload setData(Map data) { + this.data = data; + return this; + } +} \ No newline at end of file diff --git a/playwright/src/main/java/com/microsoft/playwright/options/PseudoElement.java b/playwright/src/main/java/com/microsoft/playwright/options/PseudoElement.java new file mode 100644 index 000000000..a9bfe5d4a --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/options/PseudoElement.java @@ -0,0 +1,22 @@ +/* + * 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.options; + +public enum PseudoElement { + BEFORE, + AFTER +} \ No newline at end of file diff --git a/playwright/src/main/java/com/microsoft/playwright/options/WebErrorLocation.java b/playwright/src/main/java/com/microsoft/playwright/options/WebErrorLocation.java new file mode 100644 index 000000000..68ed5146b --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/options/WebErrorLocation.java @@ -0,0 +1,33 @@ +/* + * 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.options; + +public class WebErrorLocation { + /** + * URL of the resource. + */ + public String url; + /** + * 0-based line number in the resource. + */ + public int line; + /** + * 0-based column number in the resource. + */ + public int column; + +} \ No newline at end of file diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowser1.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowser1.java index 40c065404..938aebd35 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowser1.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowser1.java @@ -113,4 +113,13 @@ void shouldPropagateCloseReasonToPendingActions(Browser browser) { assertTrue(e.getMessage().contains("The reason."), e.getMessage()); } + @Test + void shouldFireContextEvent(Browser browser) { + BrowserContext[] contextEvent = { null }; + browser.onContext(c -> contextEvent[0] = c); + BrowserContext context = browser.newContext(); + assertEquals(context, contextEvent[0]); + context.close(); + } + } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextEvents.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextEvents.java index a1c95b87f..d228f8f3e 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextEvents.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextEvents.java @@ -186,4 +186,90 @@ void pageErrorEventShouldWork() { assertTrue(webError[0].error().contains("boom"), webError[0].error()); } + @Test + void weberrorEventShouldIncludeLocation() { + server.setRoute("/error.js", exchange -> { + exchange.getResponseHeaders().add("content-type", "application/javascript"); + exchange.sendResponseHeaders(200, 0); + try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write("\nfunction foo() {\n throw new Error('boom');\n}\nfoo();\n"); + } + }); + server.setRoute("/error.html", exchange -> { + exchange.getResponseHeaders().add("content-type", "text/html"); + exchange.sendResponseHeaders(200, 0); + try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write(""); + } + }); + WebError[] webError = { null }; + context.onWebError(e -> webError[0] = e); + page.navigate(server.PREFIX + "/error.html"); + waitForCondition(() -> webError[0] != null); + com.microsoft.playwright.options.WebErrorLocation location = webError[0].location(); + assertEquals(server.PREFIX + "/error.js", location.url); + assertEquals(2, location.line); + assertTrue(location.column > 0, "expected column > 0, got " + location.column); + } + + @Test + void pageLoadEventShouldWork() { + Page[] loaded = { null }; + context.onPageLoad(p -> loaded[0] = p); + page.navigate(server.EMPTY_PAGE); + waitForCondition(() -> loaded[0] != null); + assertEquals(page, loaded[0]); + } + + @Test + void frameNavigatedEventShouldWork() { + Frame[] navigated = { null }; + context.onFrameNavigated(f -> navigated[0] = f); + page.navigate(server.EMPTY_PAGE); + waitForCondition(() -> navigated[0] != null); + assertEquals(page.mainFrame(), navigated[0]); + assertEquals(server.EMPTY_PAGE, navigated[0].url()); + } + + @Test + void pageCloseEventShouldWork() { + Page newPage = context.newPage(); + Page[] closed = { null }; + context.onPageClose(p -> closed[0] = p); + newPage.close(); + waitForCondition(() -> closed[0] != null); + assertEquals(newPage, closed[0]); + } + + @Test + void frameAttachedEventShouldWork() { + page.navigate(server.EMPTY_PAGE); + Frame[] attached = { null }; + context.onFrameAttached(f -> attached[0] = f); + page.evaluate("() => {\n" + + " const iframe = document.createElement('iframe');\n" + + " iframe.src = 'about:blank';\n" + + " document.body.appendChild(iframe);\n" + + "}"); + waitForCondition(() -> attached[0] != null); + assertEquals(page.mainFrame(), attached[0].parentFrame()); + } + + @Test + void frameDetachedEventShouldWork() { + page.navigate(server.EMPTY_PAGE); + page.evaluate("() => {\n" + + " const iframe = document.createElement('iframe');\n" + + " iframe.id = 'x';\n" + + " iframe.src = 'about:blank';\n" + + " document.body.appendChild(iframe);\n" + + "}"); + page.waitForSelector("iframe"); + Frame[] detached = { null }; + context.onFrameDetached(f -> detached[0] = f); + page.evaluate("() => document.getElementById('x').remove()"); + waitForCondition(() -> detached[0] != null); + assertEquals(page.mainFrame(), detached[0].parentFrame()); + } + } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextExposeFunction.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextExposeFunction.java index 3d522bc0c..b8a7f7f76 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextExposeFunction.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextExposeFunction.java @@ -84,19 +84,4 @@ void shouldBeCallableFromInsideAddInitScript() { assertEquals(asList("context", "page"), actualArgs); } - @Test - void exposeBindingHandleShouldWork() { - JSHandle[] target = { null }; - context.exposeBinding("logme", (source, args) -> { - target[0] = (JSHandle) args[0]; - return 17; - }, new BrowserContext.ExposeBindingOptions().setHandle(true)); - Page page = context.newPage(); - Object result = page.evaluate("async function() {\n" + - " return window['logme']({ foo: 42 });\n" + - "}"); - assertNotNull(target[0]); - assertEquals(42, target[0].evaluate("x => x.foo")); - assertEquals(17, result); - } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestLocatorHighlight.java b/playwright/src/test/java/com/microsoft/playwright/TestLocatorHighlight.java index ecc04c4f2..c85226cc0 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestLocatorHighlight.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestLocatorHighlight.java @@ -36,4 +36,18 @@ void shouldHighlightLocator() { BoundingBox box2 = page.locator("x-pw-highlight").boundingBox(); assertEquals(new Gson().toJson(box2), new Gson().toJson(box1)); } + + @Test + void highlightAndHideHighlightShouldNotThrow() { + page.setContent(""); + AutoCloseable disposable = page.locator("input").highlight(new Locator.HighlightOptions().setStyle("outline: 2px dashed red")); + try { + disposable.close(); + } catch (Exception e) { + throw new RuntimeException(e); + } + page.locator("input").highlight(); + page.locator("input").hideHighlight(); + page.hideHighlight(); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshot.java b/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshot.java index 37902c853..442f2cd01 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshot.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshot.java @@ -341,4 +341,14 @@ void shouldSnapshotPlaceholderWhenDifferentFromName(Page page) { " - /placeholder: Placeholder"); } + @Test + void pageMatchesAriaSnapshot(Page page) { + page.setContent("

    hello

    "); + assertThat(page).matchesAriaSnapshot("- heading \"hello\" [level=1]"); + AssertionFailedError e = assertThrows(AssertionFailedError.class, + () -> assertThat(page).matchesAriaSnapshot("- heading \"world\"", + new com.microsoft.playwright.assertions.PageAssertions.MatchesAriaSnapshotOptions().setTimeout(1000))); + org.junit.jupiter.api.Assertions.assertTrue(e.getMessage().contains("Page expected to match Aria snapshot"), e.getMessage()); + } + } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageDrop.java b/playwright/src/test/java/com/microsoft/playwright/TestPageDrop.java new file mode 100644 index 000000000..bfa92700c --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageDrop.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright; + +import com.microsoft.playwright.options.FilePayload; +import com.microsoft.playwright.options.DropPayload; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.microsoft.playwright.Utils.mapOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestPageDrop extends TestBase { + private void setupDropzone() { + page.setContent("\n" + + "
    \n" + + ""); + } + + @SuppressWarnings("unchecked") + private Map waitForDropInfo() { + page.waitForCondition(() -> page.evaluate("window.__dropInfo") != null); + return (Map) page.evaluate("window.__dropInfo"); + } + + @Test + void shouldDropFilePayload() { + setupDropzone(); + page.locator("#dropzone").drop(new DropPayload().setFiles(new FilePayload("note.txt", "text/plain", "hello".getBytes(StandardCharsets.UTF_8)))); + Map info = waitForDropInfo(); + @SuppressWarnings("unchecked") + List> files = (List>) info.get("files"); + assertEquals(1, files.size()); + assertEquals("note.txt", files.get(0).get("name")); + assertEquals("text/plain", files.get(0).get("type")); + assertEquals("hello", files.get(0).get("text")); + } + + @Test + void shouldDropMultipleFilePayloads() { + setupDropzone(); + page.locator("#dropzone").drop(new DropPayload().setFiles(new FilePayload[] { + new FilePayload("a.txt", "text/plain", "AAA".getBytes(StandardCharsets.UTF_8)), + new FilePayload("b.txt", "text/plain", "BB".getBytes(StandardCharsets.UTF_8)), + })); + Map info = waitForDropInfo(); + @SuppressWarnings("unchecked") + List> files = (List>) info.get("files"); + assertEquals(2, files.size()); + assertEquals("a.txt", files.get(0).get("name")); + assertEquals("AAA", files.get(0).get("text")); + assertEquals("b.txt", files.get(1).get("name")); + assertEquals("BB", files.get(1).get("text")); + } + + @Test + void shouldDropClipboardLikeData() { + setupDropzone(); + Map data = new HashMap<>(); + data.put("text/plain", "hello world"); + data.put("text/uri-list", "https://example.com"); + page.locator("#dropzone").drop(new DropPayload().setData(data)); + Map info = waitForDropInfo(); + @SuppressWarnings("unchecked") + List files = (List) info.get("files"); + assertTrue(files.isEmpty(), "expected no files"); + @SuppressWarnings("unchecked") + Map droppedData = (Map) info.get("data"); + assertEquals("hello world", droppedData.get("text/plain")); + assertEquals("https://example.com", droppedData.get("text/uri-list")); + } + + @Test + void shouldDropFileByLocalPath(@org.junit.jupiter.api.io.TempDir Path dir) throws Exception { + setupDropzone(); + Path filePath = dir.resolve("hello.txt"); + Files.write(filePath, "path-content".getBytes(StandardCharsets.UTF_8)); + page.locator("#dropzone").drop(new DropPayload().setFiles(filePath)); + Map info = waitForDropInfo(); + @SuppressWarnings("unchecked") + List> files = (List>) info.get("files"); + assertEquals(1, files.size()); + assertEquals("hello.txt", files.get(0).get("name")); + assertEquals("path-content", files.get(0).get("text")); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageExposeFunction.java b/playwright/src/test/java/com/microsoft/playwright/TestPageExposeFunction.java index 45fc926e6..40bd7cc63 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageExposeFunction.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageExposeFunction.java @@ -164,35 +164,6 @@ void shouldWorkWithComplexObjects() { assertEquals( 7, ((Map) result).get("x")); } - @Test - void exposeBindingHandleShouldWork() { - JSHandle[] target = { null }; - page.exposeBinding("logme", (source, args) -> { - target[0] = (JSHandle) args[0]; - return 17; - }, new Page.ExposeBindingOptions().setHandle(true)); - Object result = page.evaluate("async function() {\n" + - " return window['logme']({ foo: 42 });\n" + - "}"); - assertEquals(42, target[0].evaluate("x => x.foo")); - assertEquals(17, result); - } - - @Test - void exposeBindingHandleShouldNotThrowDuringNavigation() { - page.exposeBinding("logme", (source, args) -> { - return 17; - }, new Page.ExposeBindingOptions().setHandle(true)); - page.navigate(server.EMPTY_PAGE); - - page.waitForNavigation(new Page.WaitForNavigationOptions().setWaitUntil(LOAD), () -> { - page.evaluate("async url => {\n" + - " window['logme']({ foo: 42 });\n" + - " window.location.href = url;\n" + - "}", server.PREFIX + "/one-style.html"); - }); - } - @Test void shouldThrowForDuplicateRegistrations() { page.exposeFunction("foo", args -> null); @@ -202,28 +173,6 @@ void shouldThrowForDuplicateRegistrations() { assertTrue(e.getMessage().contains("Function \"foo\" has been already registered")); } - @Test - void exposeBindingHandleShouldThrowForMultipleArguments() { - page.exposeBinding("logme", (source, args) -> { - return 17; - }, new Page.ExposeBindingOptions().setHandle(true)); - assertEquals(17, page.evaluate("async function() {\n" + - " return window['logme']({ foo: 42 });\n" + - "}")); - assertEquals(17, page.evaluate("async function() {\n" + - " return window['logme']({ foo: 42 }, undefined, undefined);\n" + - "}")); - assertEquals(17, page.evaluate("async function() {\n" + - " return window['logme'](undefined, undefined, undefined);\n" + - "}")); - PlaywrightException e = assertThrows(PlaywrightException.class, () -> { - page.evaluate("async function() {\n" + - " return window['logme'](1, 2);\n" + - "}"); - }); - assertTrue(e.getMessage().contains("exposeBindingHandle supports a single argument, 2 received")); - } - @Test void shouldSerializeCycles() { Object[] object = { null }; diff --git a/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java b/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java index 8811cdc64..9f3ff45b9 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java @@ -391,4 +391,29 @@ public void shouldWorkWithNoTrailingSlash(Page page) throws Exception { }); assertEquals(asList("response"), page.evaluate("window.log")); } + + @Test + public void shouldExposeProtocolsToTheRouteHandler(Page page, Server server) { + List routes = new ArrayList<>(); + page.routeWebSocket(Pattern.compile(".*"), ws -> routes.add(ws)); + + page.navigate(server.EMPTY_PAGE); + int port = webSocketServer.getPort(); + page.evaluate("({ port }) => {\n" + + " window.wsNone = new WebSocket('ws://localhost:' + port + '/ws-none');\n" + + " window.wsString = new WebSocket('ws://localhost:' + port + '/ws-string', 'chat.v1');\n" + + " window.wsArray = new WebSocket('ws://localhost:' + port + '/ws-array', ['chat.v2', 'chat.v1']);\n" + + "}", mapOf("port", port)); + + page.waitForCondition(() -> routes.size() == 3); + + java.util.Map byUrl = new java.util.HashMap<>(); + for (com.microsoft.playwright.WebSocketRoute r : routes) { + String path = java.net.URI.create(r.url()).getPath(); + byUrl.put(path, r); + } + assertEquals(asList(), byUrl.get("/ws-none").protocols()); + assertEquals(asList("chat.v1"), byUrl.get("/ws-string").protocols()); + assertEquals(asList("chat.v2", "chat.v1"), byUrl.get("/ws-array").protocols()); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestSelectorsRole.java b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsRole.java index 24f109948..827c06ae4 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestSelectorsRole.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsRole.java @@ -448,7 +448,7 @@ void errors() { assertTrue(e0.getMessage().contains("Role must not be empty"), e0.getMessage()); PlaywrightException e1 = assertThrows(PlaywrightException.class, () -> page.querySelector("role=foo[sElected]")); - assertTrue(e1.getMessage().contains("Unknown attribute \"sElected\", must be one of \"checked\", \"disabled\", \"expanded\", \"include-hidden\", \"level\", \"name\", \"pressed\", \"selected\""), e1.getMessage()); + assertTrue(e1.getMessage().contains("Unknown attribute \"sElected\", must be one of \"checked\", \"description\", \"disabled\", \"expanded\", \"include-hidden\", \"level\", \"name\", \"pressed\", \"selected\""), e1.getMessage()); PlaywrightException e2 = assertThrows(PlaywrightException.class, () -> page.querySelector("role=foo[bar . qux=true]")); assertTrue(e2.getMessage().contains("Unknown attribute \"bar.qux\""), e2.getMessage()); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java index 69008ab9e..0dd42048a 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java @@ -158,6 +158,7 @@ void shouldCollectSources(@TempDir Path tmpDir) throws Exception { Pattern.compile("Set content"), Pattern.compile("Click") }); + traceViewer.selectAction("Click"); traceViewer.showSourceTab(); assertThat(traceViewer.stackFrames()).containsText(new Pattern[] { Pattern.compile("myMethodInner"), @@ -379,4 +380,15 @@ public void shouldShowWaitForLoadState(@TempDir Path tempDir) throws Exception { }); }); } + + @Test + public void shouldRecordHarWithStartHarStopHar(@TempDir Path tempDir) throws Exception { + Path harPath = tempDir.resolve("tracing.har"); + context.tracing().startHar(harPath, new Tracing.StartHarOptions().setMode(com.microsoft.playwright.options.HarMode.MINIMAL)); + page.navigate(server.PREFIX + "/one-style.html"); + context.tracing().stopHar(); + String content = new String(Files.readAllBytes(harPath)); + assertTrue(content.contains("\"log\""), content); + assertTrue(content.contains("/one-style.html"), content); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TraceViewerPage.java b/playwright/src/test/java/com/microsoft/playwright/TraceViewerPage.java index b64079386..21a94d64f 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TraceViewerPage.java +++ b/playwright/src/test/java/com/microsoft/playwright/TraceViewerPage.java @@ -43,7 +43,7 @@ Locator actionTitles() { } Locator stackFrames() { - return this.page.getByRole(AriaRole.LIST, new Page.GetByRoleOptions().setName("stack trace")).getByRole(AriaRole.LISTITEM); + return this.page.getByRole(AriaRole.LISTBOX, new Page.GetByRoleOptions().setName("stack trace")).getByRole(AriaRole.OPTION); } void selectAction(String title, int ordinal) { diff --git a/scripts/DRIVER_VERSION b/scripts/DRIVER_VERSION index c81473397..bfba35b9c 100644 --- a/scripts/DRIVER_VERSION +++ b/scripts/DRIVER_VERSION @@ -1 +1 @@ -1.59.1-beta-1775762078000 +1.60.0-alpha-1778025033000 diff --git a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java index ba4b59f61..c71566c9f 100644 --- a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java +++ b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java @@ -976,7 +976,7 @@ void writeTo(List output, String offset) { if (methods.stream().anyMatch(m -> "create".equals(m.jsonName))) { output.add("import com.microsoft.playwright.impl." + jsonName + "Impl;"); } - if (asList("Page", "Request", "Response", "APIRequestContext", "APIRequest", "APIResponse", "FileChooser", "Frame", "FrameLocator", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard", "Tracing", "Video", "Debugger", "Screencast").contains(jsonName)) { + if (asList("Page", "Request", "Response", "APIRequestContext", "APIRequest", "APIResponse", "FileChooser", "Frame", "FrameLocator", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard", "Tracing", "Video", "Debugger", "Screencast", "WebError").contains(jsonName)) { output.add("import com.microsoft.playwright.options.*;"); } if ("Download".equals(jsonName)) { @@ -988,7 +988,7 @@ void writeTo(List output, String offset) { if ("Clock".equals(jsonName)) { output.add("import java.util.Date;"); } - if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright", "Debugger", "Screencast").contains(jsonName)) { + if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright", "Debugger", "Screencast", "WebSocketRoute").contains(jsonName)) { output.add("import java.util.*;"); } if (asList("WebSocketRoute").contains(jsonName)) { @@ -1004,7 +1004,7 @@ void writeTo(List output, String offset) { if (asList("Page", "Frame", "BrowserContext", "WebSocket", "Worker").contains(jsonName)) { output.add("import java.util.function.Predicate;"); } - if (asList("Page", "Frame", "FrameLocator", "Locator", "Browser", "BrowserType", "BrowserContext", "PageAssertions", "LocatorAssertions").contains(jsonName)) { + if (asList("Page", "Frame", "FrameLocator", "Locator", "Browser", "BrowserType", "BrowserContext", "PageAssertions", "LocatorAssertions", "Tracing").contains(jsonName)) { output.add("import java.util.regex.Pattern;"); } if ("CDPSession".equals(jsonName)) { @@ -1012,6 +1012,7 @@ void writeTo(List output, String offset) { } if ("LocatorAssertions".equals(jsonName)) { output.add("import com.microsoft.playwright.options.AriaRole;"); + output.add("import com.microsoft.playwright.options.PseudoElement;"); } if ("PlaywrightAssertions".equals(jsonName)) { output.add("import com.microsoft.playwright.APIResponse;"); @@ -1109,6 +1110,10 @@ void writeTo(List output, String offset) { output.add("import java.nio.file.Path;"); output.add(""); } + if (asList("DropPayload").contains(name)) { + output.add("import java.util.Map;"); + output.add(""); + } String access = (parent.typeScope() instanceof CustomClass) || topLevelTypes().containsKey(name) ? "public " : ""; output.add(offset + access + "class " + name + " {"); String bodyOffset = offset + " "; @@ -1185,12 +1190,30 @@ public class ApiGenerator { filterOtherLangs(api, new Stack<>()); File dir = new File(cwd, "playwright/src/main/java/com/microsoft/playwright"); + File optionsDir = new File(dir, "options"); System.out.println("Writing files to: " + dir.getCanonicalPath()); - generate(api, dir, "com.microsoft.playwright", isAssertion().negate()); + Map sharedTypes = new HashMap<>(); + generate(api, dir, "com.microsoft.playwright", isAssertion().negate(), sharedTypes); File assertionsDir = new File(cwd,"playwright/src/main/java/com/microsoft/playwright/assertions"); System.out.println("Writing assertion files to: " + dir.getCanonicalPath()); - generate(api, assertionsDir, "com.microsoft.playwright.assertions", isAssertion().and(isSoftAssertion().negate())); + generate(api, assertionsDir, "com.microsoft.playwright.assertions", isAssertion().and(isSoftAssertion().negate()), sharedTypes); + + writeTopLevelTypes(sharedTypes, optionsDir, "com.microsoft.playwright"); + } + + private void writeTopLevelTypes(Map topLevelTypes, File optionsDir, String packageName) throws IOException { + for (TypeDefinition e : topLevelTypes.values()) { + List lines = new ArrayList<>(); + lines.add(Interface.header); + lines.add("package " + packageName + ".options;"); + lines.add(""); + e.writeTo(lines, ""); + String text = String.join("\n", lines); + try (FileWriter writer = new FileWriter(new File(optionsDir, e.name() + ".java"))) { + writer.write(text); + } + } } private static Predicate isAssertion() { @@ -1206,8 +1229,7 @@ private static Predicate isSoftAssertion() { return className -> className.contains("SoftAssertions"); } - private void generate(JsonArray api, File dir, String packageName, Predicate classFilter) throws IOException { - Map topLevelTypes = new HashMap<>(); + private void generate(JsonArray api, File dir, String packageName, Predicate classFilter, Map topLevelTypes) throws IOException { for (JsonElement entry: api) { String name = entry.getAsJsonObject().get("name").getAsString(); // We write this one manually. @@ -1233,23 +1255,6 @@ private void generate(JsonArray api, File dir, String packageName, Predicate lines = new ArrayList<>(); - lines.add(Interface.header); - lines.add("package " + packageName + ".options;"); - lines.add(""); - e.writeTo(lines, ""); - String text = String.join("\n", lines); - try (FileWriter writer = new FileWriter(new File(dir, e.name() + ".java"))) { - writer.write(text); - } - } } private static void filterOtherLangs(JsonElement json, Stack path) { From a14b9be55e307eb7a82dc326a18b76c9be5cee5a Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 5 May 2026 18:53:07 -0700 Subject: [PATCH 2/2] chore(skill): document why each git-log path is watched Replace the stale reference to the upstream "client side changes" workflow with an inline explanation of what each watched path can affect on the Java side, so the rolling instructions stand on their own. --- .claude/skills/playwright-roll/SKILL.md | 44 ++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/.claude/skills/playwright-roll/SKILL.md b/.claude/skills/playwright-roll/SKILL.md index 0149d8acf..323a13926 100644 --- a/.claude/skills/playwright-roll/SKILL.md +++ b/.claude/skills/playwright-roll/SKILL.md @@ -7,10 +7,45 @@ Help the user roll to a new version of Playwright. ROLLING.md contains general instructions and scripts. Start with running ./scripts/roll_driver.sh to update the version and generate the API to see the state of things. -Afterwards, work through the list of changes that need to be backported. -You can find a list of pull requests that might need to be taking into account in the issue titled "Backport changes". -Work through them one-by-one and check off the items that you have handled. -Not all of them will be relevant, some might have partially been reverted, etc. - so feel free to check with the upstream release branch. +Afterwards, walk through the upstream changes that affect the Java client and port the relevant ones. + +## Determining what to port + +List the upstream commits that touched a client-relevant path since the last release. The paths cover everything that can change the public Java surface or the wire protocol: + +- `docs/src/api/` — the source of truth for `api.json`. Method/option additions, removals, and `langs:` filter changes flow from here. +- `packages/playwright-core/src/client/` — the JS client implementation that the Java client mirrors. +- `packages/isomorphic/` — selector engines, locator generation/parsing, and aria-snapshot logic shared between client and server. Changes here can affect client-side helpers like `getByRoleSelector`. +- `packages/playwright/src/matchers/matchers.ts` — assertion-method definitions. Changes here usually correspond to new options on `LocatorAssertions` / `PageAssertions`. +- `packages/protocol/src/protocol.yml` — the wire protocol schema. Method/event additions, parameter renames, and result-shape changes affect what the Java `*Impl` classes need to send/receive. + +```bash +cd ~/playwright +PREV_TAG=$(git tag | grep -E '^v1\.[0-9]+\.[0-9]+$' | sort -V | tail -1) # e.g. v1.59.1 +git log "$PREV_TAG"..HEAD --oneline -- \ + 'docs/src/api/' \ + 'packages/playwright-core/src/client/' \ + 'packages/isomorphic/' \ + 'packages/playwright/src/matchers/matchers.ts' \ + 'packages/protocol/src/protocol.yml' +``` + +Walk that list top-to-bottom (oldest-first is easier — newest is at top, so reverse). For each commit: +1. Read the commit (`git show `) to see what client/protocol/docs changed. +2. If it's JS-internal (bundling, dispatcher conventions, electron, mcp, dashboard, trace-viewer, test-runner) — skip. +3. If it touches `docs/src/api/` or types, check `langs:` annotations — features marked `langs: js`/`langs: js, python` don't apply to Java. +4. If it adds/changes a public API method or option that applies to Java, port it. The api.json regenerated by `roll_driver.sh` already contains the new types/options, so the generated Java interfaces usually pick them up automatically — what's typically missing is the `*Impl` wiring. +5. Watch for follow-up reverts — a "feat: X" commit might be undone by a later "Revert X". Check whether the change still exists in HEAD before porting. +6. Maintain a running notes file (e.g. `/tmp/roll-notes.md`) listing each upstream PR as ported / skipped / verified-already-supported, with a one-line reason. This file becomes the body of the eventual PR. + +## What to include in the rolling PR + +- Driver version bump +- Generated interface diffs from `roll_driver.sh` +- `*Impl` wiring for each ported feature +- Generator updates (import lists, special-cases) if new types appeared +- A small test per new public API surface — listener for new events, basic call for new methods, regression for changed return types +- PR description: list each upstream PR ported, each skipped (with reason), and each verified-already-supported Rolling includes: - updating client implementation to match changes in the upstream JS implementation (see ../playwright/packages/playwright-core/src/client) @@ -164,5 +199,4 @@ Branch naming for issue fixes: `fix-` ## Tips & Tricks - Project checkouts are in the parent directory (`../`). -- When updating checkboxes, store the issue content into /tmp and edit it there, then update the issue based on the file - use the "gh" cli to interact with GitHub