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 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) {