1
0
mirror of synced 2026-05-22 18:53:15 +00:00

chore: roll to 1.60.0-alpha-2026-05-05 (#1916)

This commit is contained in:
Yury Semikhatsky
2026-05-06 08:30:19 -07:00
committed by GitHub
parent 5b33729849
commit f081667e44
45 changed files with 1409 additions and 331 deletions
+39 -5
View File
@@ -7,10 +7,45 @@ Help the user roll to a new version of Playwright.
ROLLING.md contains general instructions and scripts. 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. 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. Afterwards, walk through the upstream changes that affect the Java client and port the relevant ones.
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. ## Determining what to port
Not all of them will be relevant, some might have partially been reverted, etc. - so feel free to check with the upstream release branch.
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 <sha>`) 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: Rolling includes:
- updating client implementation to match changes in the upstream JS implementation (see ../playwright/packages/playwright-core/src/client) - 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-<issue-number>`
## Tips & Tricks ## Tips & Tricks
- Project checkouts are in the parent directory (`../`). - 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 - use the "gh" cli to interact with GitHub
+2 -2
View File
@@ -10,9 +10,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| | Linux | macOS | Windows | | | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: | | :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->147.0.7727.15<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Chromium <!-- GEN:chromium-version -->148.0.7778.96<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.4<!-- GEN:stop --> | ✅ | ✅ | ✅ | | WebKit <!-- GEN:webkit-version -->26.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->148.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox <!-- GEN:firefox-version -->150.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
## Documentation ## Documentation
+1 -1
View File
@@ -10,7 +10,7 @@
<name>Playwright Client Examples</name> <name>Playwright Client Examples</name>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<playwright.version>1.59.0</playwright.version> <playwright.version>1.60.0</playwright.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
@@ -23,24 +23,23 @@ import java.nio.file.Path;
* This API is used for the Web API testing. You can use it to trigger API endpoints, configure micro-services, prepare * 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. * environment or the service to your e2e test.
* *
* <p> Each Playwright browser context has associated with it {@code APIRequestContext} instance which shares cookie storage * <p> Each Playwright browser context has an associated {@code APIRequestContext}, accessible via {@link
* with the browser context and can be accessed via {@link com.microsoft.playwright.BrowserContext#request * com.microsoft.playwright.BrowserContext#request BrowserContext.request()} or {@link
* BrowserContext.request()} or {@link com.microsoft.playwright.Page#request Page.request()}. It is also possible to create * com.microsoft.playwright.Page#request Page.request()} (these return the
* a new APIRequestContext instance manually by calling {@link com.microsoft.playwright.APIRequest#newContext *
* APIRequest.newContext()}. * <p> **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()}.
* *
* <p> <strong>Cookie management</strong> * <p> <strong>Cookie management</strong>
* *
* <p> {@code APIRequestContext} returned by {@link com.microsoft.playwright.BrowserContext#request BrowserContext.request()} * <p> The {@code APIRequestContext} returned by {@link com.microsoft.playwright.BrowserContext#request
* and {@link com.microsoft.playwright.Page#request Page.request()} shares cookie storage with the corresponding {@code * BrowserContext.request()} and
* 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.
* *
* <p> If you want API requests to not interfere with the browser cookies you should create a new {@code APIRequestContext} by * <p> {@link com.microsoft.playwright.Page#request Page.request()} uses the same cookie jar as its {@code BrowserContext}:
* calling {@link com.microsoft.playwright.APIRequest#newContext APIRequest.newContext()}. Such {@code APIRequestContext} *
* object will have its own isolated cookie storage. * <p> 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 { public interface APIRequestContext {
class DisposeOptions { class DisposeOptions {
@@ -484,5 +483,11 @@ public interface APIRequestContext {
* @since v1.16 * @since v1.16
*/ */
String storageState(StorageStateOptions options); String storageState(StorageStateOptions options);
/**
*
*
* @since v1.60
*/
Tracing tracing();
} }
@@ -43,6 +43,15 @@ import java.util.regex.Pattern;
*/ */
public interface Browser extends AutoCloseable { public interface Browser extends AutoCloseable {
/**
* Emitted when a new browser context is created.
*/
void onContext(Consumer<BrowserContext> handler);
/**
* Removes handler that was previously added with {@link #onContext onContext(handler)}.
*/
void offContext(Consumer<BrowserContext> handler);
/** /**
* Emitted when Browser gets disconnected from the browser application. This might happen because of one of the following: * Emitted when Browser gets disconnected from the browser application. This might happen because of one of the following:
* <ul> * <ul>
@@ -114,6 +114,48 @@ public interface BrowserContext extends AutoCloseable {
*/ */
void offDialog(Consumer<Dialog> handler); void offDialog(Consumer<Dialog> 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<Download> handler);
/**
* Removes handler that was previously added with {@link #onDownload onDownload(handler)}.
*/
void offDownload(Consumer<Download> 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<Frame> handler);
/**
* Removes handler that was previously added with {@link #onFrameAttached onFrameAttached(handler)}.
*/
void offFrameAttached(Consumer<Frame> 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<Frame> handler);
/**
* Removes handler that was previously added with {@link #onFrameDetached onFrameDetached(handler)}.
*/
void offFrameDetached(Consumer<Frame> 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<Frame> handler);
/**
* Removes handler that was previously added with {@link #onFrameNavigated onFrameNavigated(handler)}.
*/
void offFrameNavigated(Consumer<Frame> handler);
/** /**
* The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event will * 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 * 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<Page> handler); void offPage(Consumer<Page> 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<Page> handler);
/**
* Removes handler that was previously added with {@link #onPageClose onPageClose(handler)}.
*/
void offPageClose(Consumer<Page> handler);
/**
* Emitted when the JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/Events/load">{@code load}</a> 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<Page> handler);
/**
* Removes handler that was previously added with {@link #onPageLoad onPageLoad(handler)}.
*/
void offPageLoad(Consumer<Page> handler);
/** /**
* Emitted when exception is unhandled in any of the pages in this context. To listen for errors from a particular page, * 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. * use {@link com.microsoft.playwright.Page#onPageError Page.onPageError()} instead.
@@ -271,20 +334,6 @@ public interface BrowserContext extends AutoCloseable {
return this; 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 { class GrantPermissionsOptions {
/** /**
* The [origin] to grant permissions to, e.g. "https://example.com". * The [origin] to grant permissions to, e.g. "https://example.com".
@@ -736,54 +785,7 @@ public interface BrowserContext extends AutoCloseable {
* @param callback Callback function that will be called in the Playwright's context. * @param callback Callback function that will be called in the Playwright's context.
* @since v1.8 * @since v1.8
*/ */
default AutoCloseable exposeBinding(String name, BindingCallback callback) { 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 <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a> which
* resolves to the return value of {@code callback}. If the {@code callback} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, it will be
* awaited.
*
* <p> The first argument of the {@code callback} function contains information about the caller: {@code { browserContext:
* BrowserContext, page: Page, frame: Frame }}.
*
* <p> See {@link com.microsoft.playwright.Page#exposeBinding Page.exposeBinding()} for page-only version.
*
* <p> <strong>Usage</strong>
*
* <p> An example of exposing page URL to all frames in all pages in the context:
* <pre>{@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("<script>\n" +
* " async function onClick() {\n" +
* " document.querySelector('div').textContent = await window.pageURL();\n" +
* " }\n" +
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>");
* page.getByRole(AriaRole.BUTTON).click();
* }
* }
* }
* }</pre>
*
* @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);
/** /**
* The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context. * 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 <a * When called, the function executes {@code callback} and returns a <a
@@ -133,6 +133,15 @@ public interface BrowserType {
* the file system being the same between Playwright and the Browser. * the file system being the same between Playwright and the Browser.
*/ */
public Boolean isLocal; public Boolean isLocal;
/**
* When true, Playwright will not apply its default overrides to the existing default browser context. Specifically, {@code
* acceptDownloads} is left at the browser's setting, focus emulation is not enabled, and media emulation options (such as
* {@code colorScheme}, {@code reducedMotion}, {@code forcedColors}, and {@code contrast}) are not applied. Useful when
* attaching to a user's daily-driver browser where these overrides would interfere with existing browser state. New
* contexts created via {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} are not affected. Defaults
* to {@code false}.
*/
public Boolean noDefaults;
/** /**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0. * Defaults to 0.
@@ -159,6 +168,18 @@ public interface BrowserType {
this.isLocal = isLocal; this.isLocal = isLocal;
return this; return this;
} }
/**
* When true, Playwright will not apply its default overrides to the existing default browser context. Specifically, {@code
* acceptDownloads} is left at the browser's setting, focus emulation is not enabled, and media emulation options (such as
* {@code colorScheme}, {@code reducedMotion}, {@code forcedColors}, and {@code contrast}) are not applied. Useful when
* attaching to a user's daily-driver browser where these overrides would interfere with existing browser state. New
* contexts created via {@link com.microsoft.playwright.Browser#newContext Browser.newContext()} are not affected. Defaults
* to {@code false}.
*/
public ConnectOverCDPOptions setNoDefaults(boolean noDefaults) {
this.noDefaults = noDefaults;
return this;
}
/** /**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0. * Defaults to 0.
@@ -867,6 +867,13 @@ public interface Frame {
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>. * <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>.
*/ */
public Boolean checked; public Boolean checked;
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public Object description;
/** /**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
* *
@@ -875,8 +882,8 @@ public interface Frame {
*/ */
public Boolean disabled; public Boolean disabled;
/** /**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* is a regular expression. Note that exact match still trims whitespace. * Ignored when the value is a regular expression. Note that exact match still trims whitespace.
*/ */
public Boolean exact; public Boolean exact;
/** /**
@@ -928,6 +935,26 @@ public interface Frame {
this.checked = checked; this.checked = checked;
return this; return this;
} }
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(String description) {
this.description = description;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(Pattern description) {
this.description = description;
return this;
}
/** /**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
* *
@@ -939,8 +966,8 @@ public interface Frame {
return this; return this;
} }
/** /**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* is a regular expression. Note that exact match still trims whitespace. * Ignored when the value is a regular expression. Note that exact match still trims whitespace.
*/ */
public GetByRoleOptions setExact(boolean exact) { public GetByRoleOptions setExact(boolean exact) {
this.exact = exact; this.exact = exact;
@@ -107,6 +107,13 @@ public interface FrameLocator {
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>. * <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>.
*/ */
public Boolean checked; public Boolean checked;
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public Object description;
/** /**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
* *
@@ -115,8 +122,8 @@ public interface FrameLocator {
*/ */
public Boolean disabled; public Boolean disabled;
/** /**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* is a regular expression. Note that exact match still trims whitespace. * Ignored when the value is a regular expression. Note that exact match still trims whitespace.
*/ */
public Boolean exact; public Boolean exact;
/** /**
@@ -168,6 +175,26 @@ public interface FrameLocator {
this.checked = checked; this.checked = checked;
return this; return this;
} }
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(String description) {
this.description = description;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(Pattern description) {
this.description = description;
return this;
}
/** /**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
* *
@@ -179,8 +206,8 @@ public interface FrameLocator {
return this; return this;
} }
/** /**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* is a regular expression. Note that exact match still trims whitespace. * Ignored when the value is a regular expression. Note that exact match still trims whitespace.
*/ */
public GetByRoleOptions setExact(boolean exact) { public GetByRoleOptions setExact(boolean exact) {
this.exact = exact; this.exact = exact;
@@ -30,6 +30,13 @@ import java.util.regex.Pattern;
*/ */
public interface Locator { public interface Locator {
class AriaSnapshotOptions { 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 <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">{@code
* Element.getBoundingClientRect()}</a>. Defaults to {@code false}.
*/
public Boolean boxes;
/** /**
* When specified, limits the depth of the snapshot. * When specified, limits the depth of the snapshot.
*/ */
@@ -47,6 +54,16 @@ public interface Locator {
*/ */
public Double timeout; 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 <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">{@code
* Element.getBoundingClientRect()}</a>. Defaults to {@code false}.
*/
public AriaSnapshotOptions setBoxes(boolean boxes) {
this.boxes = boxes;
return this;
}
/** /**
* When specified, limits the depth of the snapshot. * When specified, limits the depth of the snapshot.
*/ */
@@ -645,6 +662,46 @@ public interface Locator {
return this; 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 { class ElementHandleOptions {
/** /**
* Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default
@@ -943,6 +1000,13 @@ public interface Locator {
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>. * <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>.
*/ */
public Boolean checked; public Boolean checked;
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public Object description;
/** /**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
* *
@@ -951,8 +1015,8 @@ public interface Locator {
*/ */
public Boolean disabled; public Boolean disabled;
/** /**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* is a regular expression. Note that exact match still trims whitespace. * Ignored when the value is a regular expression. Note that exact match still trims whitespace.
*/ */
public Boolean exact; public Boolean exact;
/** /**
@@ -1004,6 +1068,26 @@ public interface Locator {
this.checked = checked; this.checked = checked;
return this; return this;
} }
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(String description) {
this.description = description;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(Pattern description) {
this.description = description;
return this;
}
/** /**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
* *
@@ -1015,8 +1099,8 @@ public interface Locator {
return this; return this;
} }
/** /**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* is a regular expression. Note that exact match still trims whitespace. * Ignored when the value is a regular expression. Note that exact match still trims whitespace.
*/ */
public GetByRoleOptions setExact(boolean exact) { public GetByRoleOptions setExact(boolean exact) {
this.exact = exact; this.exact = exact;
@@ -1122,6 +1206,20 @@ public interface Locator {
return this; 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 { class HoverOptions {
/** /**
* Whether to bypass the <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks. Defaults to * Whether to bypass the <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks. Defaults to
@@ -2900,6 +2998,54 @@ public interface Locator {
* @since v1.18 * @since v1.18
*/ */
void dragTo(Locator target, DragToOptions options); void dragTo(Locator target, DragToOptions options);
/**
* Simulate an external drag-and-drop of files or clipboard-like data onto this locator.
*
* <p> <strong>Details</strong>
*
* <p> 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.
*
* <p> 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.
*
* <p> <strong>Usage</strong>
*
* <p> Drop a file buffer onto an upload area:
*
* <p> 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.
*
* <p> <strong>Details</strong>
*
* <p> 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.
*
* <p> 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.
*
* <p> <strong>Usage</strong>
*
* <p> Drop a file buffer onto an upload area:
*
* <p> 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 * 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. * elements match the locator, throws.
@@ -3879,13 +4025,28 @@ public interface Locator {
* @since v1.27 * @since v1.27
*/ */
Locator getByTitle(Pattern text, GetByTitleOptions options); 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 * 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()}. * com.microsoft.playwright.Locator#highlight Locator.highlight()}.
* *
* @since v1.20 * @since v1.20
*/ */
void highlight(); 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
*/
AutoCloseable highlight(HighlightOptions options);
/** /**
* Hover over the matching element. * Hover over the matching element.
* *
@@ -1058,20 +1058,6 @@ public interface Page extends AutoCloseable {
return this; 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 { class FillOptions {
/** /**
* Whether to bypass the <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks. Defaults to * Whether to bypass the <a href="https://playwright.dev/java/docs/actionability">actionability</a> checks. Defaults to
@@ -1250,6 +1236,13 @@ public interface Page extends AutoCloseable {
* <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>. * <p> Learn more about <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-checked">{@code aria-checked}</a>.
*/ */
public Boolean checked; public Boolean checked;
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public Object description;
/** /**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
* *
@@ -1258,8 +1251,8 @@ public interface Page extends AutoCloseable {
*/ */
public Boolean disabled; public Boolean disabled;
/** /**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* is a regular expression. Note that exact match still trims whitespace. * Ignored when the value is a regular expression. Note that exact match still trims whitespace.
*/ */
public Boolean exact; public Boolean exact;
/** /**
@@ -1311,6 +1304,26 @@ public interface Page extends AutoCloseable {
this.checked = checked; this.checked = checked;
return this; return this;
} }
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(String description) {
this.description = description;
return this;
}
/**
* Option to match the <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>. By
* default, matching is case-insensitive and searches for a substring, use {@code exact} to control this behavior.
*
* <p> Learn more about <a href="https://w3c.github.io/accname/#dfn-accessible-description">accessible description</a>.
*/
public GetByRoleOptions setDescription(Pattern description) {
this.description = description;
return this;
}
/** /**
* An attribute that is usually set by {@code aria-disabled} or {@code disabled}. * An attribute that is usually set by {@code aria-disabled} or {@code disabled}.
* *
@@ -1322,8 +1335,8 @@ public interface Page extends AutoCloseable {
return this; return this;
} }
/** /**
* Whether {@code name} is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when {@code name} * Whether {@code name} and {@code description} are matched exactly: case-sensitive and whole-string. Defaults to false.
* is a regular expression. Note that exact match still trims whitespace. * Ignored when the value is a regular expression. Note that exact match still trims whitespace.
*/ */
public GetByRoleOptions setExact(boolean exact) { public GetByRoleOptions setExact(boolean exact) {
this.exact = exact; this.exact = exact;
@@ -2981,6 +2994,13 @@ public interface Page extends AutoCloseable {
} }
} }
class AriaSnapshotOptions { 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 <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">{@code
* Element.getBoundingClientRect()}</a>. Defaults to {@code false}.
*/
public Boolean boxes;
/** /**
* When specified, limits the depth of the snapshot. * When specified, limits the depth of the snapshot.
*/ */
@@ -2998,6 +3018,16 @@ public interface Page extends AutoCloseable {
*/ */
public Double timeout; 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 <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">{@code
* Element.getBoundingClientRect()}</a>. Defaults to {@code false}.
*/
public AriaSnapshotOptions setBoxes(boolean boxes) {
this.boxes = boxes;
return this;
}
/** /**
* When specified, limits the depth of the snapshot. * When specified, limits the depth of the snapshot.
*/ */
@@ -4676,57 +4706,7 @@ public interface Page extends AutoCloseable {
* @param callback Callback function that will be called in the Playwright's context. * @param callback Callback function that will be called in the Playwright's context.
* @since v1.8 * @since v1.8
*/ */
default AutoCloseable exposeBinding(String name, BindingCallback callback) { 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 <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a> which
* resolves to the return value of {@code callback}. If the {@code callback} returns a <a
* href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise'>Promise</a>, it will be
* awaited.
*
* <p> The first argument of the {@code callback} function contains information about the caller: {@code { browserContext:
* BrowserContext, page: Page, frame: Frame }}.
*
* <p> See {@link com.microsoft.playwright.BrowserContext#exposeBinding BrowserContext.exposeBinding()} for the context-wide
* version.
*
* <p> <strong>NOTE:</strong> Functions installed via {@link com.microsoft.playwright.Page#exposeBinding Page.exposeBinding()} survive navigations.
*
* <p> <strong>Usage</strong>
*
* <p> An example of exposing page URL to all frames in a page:
* <pre>{@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("<script>\n" +
* " async function onClick() {\n" +
* " document.querySelector('div').textContent = await window.pageURL();\n" +
* " }\n" +
* "</script>\n" +
* "<button onclick=\"onClick()\">Click me</button>\n" +
* "<div></div>");
* page.click("button");
* }
* }
* }
* }</pre>
*
* @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);
/** /**
* The method adds a function called {@code name} on the {@code window} object of every frame in the page. When called, the * 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 <a * function executes {@code callback} and returns a <a
@@ -5597,6 +5577,13 @@ public interface Page extends AutoCloseable {
* @since v1.8 * @since v1.8
*/ */
Response navigate(String url, NavigateOptions options); Response navigate(String url, NavigateOptions options);
/**
* Hide all locator highlight overlays previously added by {@link com.microsoft.playwright.Locator#highlight
* Locator.highlight()} on this page.
*
* @since v1.60
*/
void hideHighlight();
/** /**
* This method hovers over an element matching {@code selector} by performing the following steps: * This method hovers over an element matching {@code selector} by performing the following steps:
* <ol> * <ol>
@@ -18,6 +18,7 @@ package com.microsoft.playwright;
import com.microsoft.playwright.options.*; import com.microsoft.playwright.options.*;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.regex.Pattern;
/** /**
* API for collecting and saving Playwright traces. Playwright traces can be opened in <a * API for collecting and saving Playwright traces. Playwright traces can be opened in <a
@@ -165,6 +166,59 @@ public interface Tracing {
return this; return this;
} }
} }
class StartHarOptions {
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If
* {@code attach} is specified, resources are persisted as separate files or entries in the ZIP archive. If {@code embed}
* is specified, content is stored inline the HAR file as per HAR specification. Defaults to {@code attach} for {@code
* .zip} output files and to {@code embed} for all other file extensions.
*/
public HarContentPolicy content;
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page,
* cookies, security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code
* full}.
*/
public HarMode mode;
/**
* A glob or regex pattern to filter requests that are stored in the HAR. Defaults to none.
*/
public Object urlFilter;
/**
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If
* {@code attach} is specified, resources are persisted as separate files or entries in the ZIP archive. If {@code embed}
* is specified, content is stored inline the HAR file as per HAR specification. Defaults to {@code attach} for {@code
* .zip} output files and to {@code embed} for all other file extensions.
*/
public StartHarOptions setContent(HarContentPolicy content) {
this.content = content;
return this;
}
/**
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page,
* cookies, security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code
* full}.
*/
public StartHarOptions setMode(HarMode mode) {
this.mode = mode;
return this;
}
/**
* A glob or regex pattern to filter requests that are stored in the HAR. Defaults to none.
*/
public StartHarOptions setUrlFilter(String urlFilter) {
this.urlFilter = urlFilter;
return this;
}
/**
* A glob or regex pattern to filter requests that are stored in the HAR. Defaults to none.
*/
public StartHarOptions setUrlFilter(Pattern urlFilter) {
this.urlFilter = urlFilter;
return this;
}
}
class GroupOptions { class GroupOptions {
/** /**
* Specifies a custom location for the group to be shown in the trace viewer. Defaults to the location of the {@link * Specifies a custom location for the group to be shown in the trace viewer. Defaults to the location of the {@link
@@ -328,6 +382,48 @@ public interface Tracing {
* @since v1.15 * @since v1.15
*/ */
void startChunk(StartChunkOptions options); void startChunk(StartChunkOptions options);
/**
* 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.
*
* <p> Only one HAR recording can be active at a time per {@code BrowserContext}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* context.tracing().startHar(Paths.get("trace.har"));
* Page page = context.newPage();
* page.navigate("https://playwright.dev");
* context.tracing().stopHar();
* }</pre>
*
* @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.
*
* <p> Only one HAR recording can be active at a time per {@code BrowserContext}.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* context.tracing().startHar(Paths.get("trace.har"));
* Page page = context.newPage();
* page.navigate("https://playwright.dev");
* context.tracing().stopHar();
* }</pre>
*
* @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);
/** /**
* <strong>NOTE:</strong> Use {@code test.step} instead when available. * <strong>NOTE:</strong> Use {@code test.step} instead when available.
* *
@@ -408,5 +504,12 @@ public interface Tracing {
* @since v1.15 * @since v1.15
*/ */
void stopChunk(StopChunkOptions options); 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();
} }
@@ -16,6 +16,7 @@
package com.microsoft.playwright; 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 * {@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 * @since v1.38
*/ */
String error(); String error();
/**
*
*
* @since v1.60
*/
WebErrorLocation location();
} }
@@ -16,6 +16,7 @@
package com.microsoft.playwright; package com.microsoft.playwright;
import java.util.*;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -213,6 +214,27 @@ public interface WebSocketRoute {
* @since v1.48 * @since v1.48
*/ */
void send(byte[] message); void send(byte[] message);
/**
* The list of WebSocket subprotocols requested by the page, as passed via the second argument to the <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket">{@code WebSocket} constructor</a>.
* Corresponds to the {@code Sec-WebSocket-Protocol} request header.
*
* <p> Returns an empty array if no protocols were specified.
*
* <p> <strong>Usage</strong>
* <pre>{@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");
* }
* });
* }</pre>
*
* @since v1.60
*/
List<String> protocols();
/** /**
* URL of the WebSocket created in the page. * URL of the WebSocket created in the page.
* *
@@ -19,6 +19,7 @@ package com.microsoft.playwright.assertions;
import java.util.*; import java.util.*;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import com.microsoft.playwright.options.AriaRole; 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 * The {@code LocatorAssertions} class provides assertion methods that can be used to make assertions about the {@code
@@ -427,11 +428,22 @@ public interface LocatorAssertions {
} }
} }
class HasCSSOptions { class HasCSSOptions {
/**
* Pseudo-element to read computed styles from.
*/
public PseudoElement pseudo;
/** /**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/ */
public Double timeout; 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}. * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
*/ */
@@ -37,6 +37,20 @@ import java.util.regex.Pattern;
* }</pre> * }</pre>
*/ */
public interface PageAssertions { 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 { class HasTitleOptions {
/** /**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
@@ -91,6 +105,40 @@ public interface PageAssertions {
* @since v1.20 * @since v1.20
*/ */
PageAssertions not(); PageAssertions not();
/**
* Asserts that the page body matches the given <a href="https://playwright.dev/java/docs/aria-snapshots">accessibility
* snapshot</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.navigate("https://demo.playwright.dev/todomvc/");
* assertThat(page).matchesAriaSnapshot("""
* - heading "todos"
* - textbox "What needs to be done?"
* """);
* }</pre>
*
* @since v1.60
*/
default void matchesAriaSnapshot(String expected) {
matchesAriaSnapshot(expected, null);
}
/**
* Asserts that the page body matches the given <a href="https://playwright.dev/java/docs/aria-snapshots">accessibility
* snapshot</a>.
*
* <p> <strong>Usage</strong>
* <pre>{@code
* page.navigate("https://demo.playwright.dev/todomvc/");
* assertThat(page).matchesAriaSnapshot("""
* - heading "todos"
* - textbox "What needs to be done?"
* """);
* }</pre>
*
* @since v1.60
*/
void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions options);
/** /**
* Ensures the page has the given title. * Ensures the page has the given title.
* *
@@ -46,6 +46,11 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
this.tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString()); this.tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString());
} }
@Override
public com.microsoft.playwright.Tracing tracing() {
return tracing;
}
@Override @Override
public APIResponse delete(String url, RequestOptions options) { public APIResponse delete(String url, RequestOptions options) {
return fetch(url, ensureOptions(options, "DELETE")); return fetch(url, ensureOptions(options, "DELETE"));
@@ -57,7 +57,14 @@ abstract class AssertionsBase {
} }
FrameExpectResult result = doExpect(expression, expectOptions, title); FrameExpectResult result = doExpect(expression, expectOptions, title);
if (result.matches == isNot) { 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); String log = (result.log == null) ? "" : String.join("\n", result.log);
if (!log.isEmpty()) { if (!log.isEmpty()) {
log = "\nCall log:\n" + log; log = "\nCall log:\n" + log;
@@ -68,23 +68,18 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} }
private final ListenerCollection<EventType> listeners = new ListenerCollection<>(eventSubscriptions(), this); private final ListenerCollection<EventType> listeners = new ListenerCollection<>(eventSubscriptions(), this);
final TimeoutSettings timeoutSettings = new TimeoutSettings(); final TimeoutSettings timeoutSettings = new TimeoutSettings();
final Map<String, HarRecorder> harRecorders = new HashMap<>();
static class HarRecorder {
final Path path;
final HarContentPolicy contentPolicy;
HarRecorder(Path har, HarContentPolicy policy) {
path = har;
contentPolicy = policy;
}
}
enum EventType { enum EventType {
CLOSE, CLOSE,
CONSOLE, CONSOLE,
DIALOG, DIALOG,
DOWNLOAD,
FRAMEATTACHED,
FRAMEDETACHED,
FRAMENAVIGATED,
PAGE, PAGE,
PAGECLOSE,
PAGELOAD,
WEBERROR, WEBERROR,
REQUEST, REQUEST,
REQUESTFAILED, REQUESTFAILED,
@@ -139,6 +134,20 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
public void offBackgroundPage(Consumer<Page> handler) { public void offBackgroundPage(Consumer<Page> handler) {
} }
@Override
public void onDownload(Consumer<Download> handler) {
listeners.add(EventType.DOWNLOAD, handler);
}
@Override
public void offDownload(Consumer<Download> handler) {
listeners.remove(EventType.DOWNLOAD, handler);
}
void notifyDownload(Download download) {
listeners.notify(EventType.DOWNLOAD, download);
}
@Override @Override
public void onClose(Consumer<BrowserContext> handler) { public void onClose(Consumer<BrowserContext> handler) {
listeners.add(EventType.CLOSE, handler); listeners.add(EventType.CLOSE, handler);
@@ -179,6 +188,76 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
listeners.remove(EventType.PAGE, handler); listeners.remove(EventType.PAGE, handler);
} }
@Override
public void onFrameAttached(Consumer<Frame> handler) {
listeners.add(EventType.FRAMEATTACHED, handler);
}
@Override
public void offFrameAttached(Consumer<Frame> handler) {
listeners.remove(EventType.FRAMEATTACHED, handler);
}
void notifyFrameAttached(FrameImpl frame) {
listeners.notify(EventType.FRAMEATTACHED, frame);
}
@Override
public void onFrameDetached(Consumer<Frame> handler) {
listeners.add(EventType.FRAMEDETACHED, handler);
}
@Override
public void offFrameDetached(Consumer<Frame> handler) {
listeners.remove(EventType.FRAMEDETACHED, handler);
}
void notifyFrameDetached(FrameImpl frame) {
listeners.notify(EventType.FRAMEDETACHED, frame);
}
@Override
public void onFrameNavigated(Consumer<Frame> handler) {
listeners.add(EventType.FRAMENAVIGATED, handler);
}
@Override
public void offFrameNavigated(Consumer<Frame> handler) {
listeners.remove(EventType.FRAMENAVIGATED, handler);
}
void notifyFrameNavigated(FrameImpl frame) {
listeners.notify(EventType.FRAMENAVIGATED, frame);
}
@Override
public void onPageClose(Consumer<Page> handler) {
listeners.add(EventType.PAGECLOSE, handler);
}
@Override
public void offPageClose(Consumer<Page> handler) {
listeners.remove(EventType.PAGECLOSE, handler);
}
void notifyPageClose(PageImpl page) {
listeners.notify(EventType.PAGECLOSE, page);
}
@Override
public void onPageLoad(Consumer<Page> handler) {
listeners.add(EventType.PAGELOAD, handler);
}
@Override
public void offPageLoad(Consumer<Page> handler) {
listeners.remove(EventType.PAGELOAD, handler);
}
void notifyPageLoad(PageImpl page) {
listeners.notify(EventType.PAGELOAD, page);
}
@Override @Override
public void onWebError(Consumer<WebError> handler) { public void onWebError(Consumer<WebError> handler) {
listeners.add(EventType.WEBERROR, handler); listeners.add(EventType.WEBERROR, handler);
@@ -284,27 +363,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} }
closeReason = options.reason; closeReason = options.reason;
request.dispose(convertType(options, APIRequestContext.DisposeOptions.class)); request.dispose(convertType(options, APIRequestContext.DisposeOptions.class));
for (Map.Entry<String, HarRecorder> entry : harRecorders.entrySet()) { tracing.exportAllHars();
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();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonObject params = gson().toJsonTree(options).getAsJsonObject();
sendMessage("close", params, NO_TIMEOUT); sendMessage("close", params, NO_TIMEOUT);
} }
@@ -392,11 +451,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} }
@Override @Override
public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) { public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding) {
return exposeBindingImpl(name, playwrightBinding, options); return exposeBindingImpl(name, playwrightBinding);
} }
private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) { private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding) {
if (bindings.containsKey(name)) { if (bindings.containsKey(name)) {
throw new PlaywrightException("Function \"" + name + "\" has been already registered"); throw new PlaywrightException("Function \"" + name + "\" has been already registered");
} }
@@ -409,16 +468,13 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("name", name); params.addProperty("name", name);
if (options != null && options.handle != null && options.handle) {
params.addProperty("needsHandle", true);
}
JsonObject result = sendMessage("exposeBinding", params, NO_TIMEOUT).getAsJsonObject(); JsonObject result = sendMessage("exposeBinding", params, NO_TIMEOUT).getAsJsonObject();
return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString()); return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString());
} }
@Override @Override
public AutoCloseable exposeFunction(String name, FunctionCallback playwrightFunction) { 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 @Override
@@ -515,24 +571,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
if (contentPolicy == null) { if (contentPolicy == null) {
contentPolicy = Utils.convertType(options.updateContent, HarContentPolicy.class); contentPolicy = Utils.convertType(options.updateContent, HarContentPolicy.class);
} }
if (contentPolicy == null) { tracing.recordIntoHar(page, har, options.url, contentPolicy, options.updateMode, 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));
} }
@Override @Override
@@ -818,7 +857,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
} catch (PlaywrightException e) { } catch (PlaywrightException e) {
page = null; 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) { if (page != null) {
page.listeners.notify(PageImpl.EventType.PAGEERROR, errorStr); page.listeners.notify(PageImpl.EventType.PAGEERROR, errorStr);
} }
@@ -43,6 +43,7 @@ class BrowserImpl extends ChannelOwner implements Browser {
String closeReason; String closeReason;
enum EventType { enum EventType {
CONTEXT,
DISCONNECTED, DISCONNECTED,
} }
@@ -50,6 +51,16 @@ class BrowserImpl extends ChannelOwner implements Browser {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
} }
@Override
public void onContext(Consumer<BrowserContext> handler) {
listeners.add(EventType.CONTEXT, handler);
}
@Override
public void offContext(Consumer<BrowserContext> handler) {
listeners.remove(EventType.CONTEXT, handler);
}
@Override @Override
public void onDisconnected(Consumer<Browser> handler) { public void onDisconnected(Consumer<Browser> handler) {
listeners.add(EventType.DISCONNECTED, handler); listeners.add(EventType.DISCONNECTED, handler);
@@ -302,6 +313,7 @@ class BrowserImpl extends ChannelOwner implements Browser {
context.tracing().setTracesDir(tracePath); context.tracing().setTracesDir(tracePath);
browserType.playwright.selectors.contextsForSelectors.add(context); browserType.playwright.selectors.contextsForSelectors.add(context);
} }
listeners.notify(EventType.CONTEXT, context);
} }
private void didClose() { private void didClose() {
@@ -1051,12 +1051,58 @@ public class FrameImpl extends ChannelOwner implements Frame {
return result.get("value").getAsInt(); 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<String, String> 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(); JsonObject params = new JsonObject();
params.addProperty("selector", selector); params.addProperty("selector", selector);
if (style != null) {
params.addProperty("style", style);
}
sendMessage("highlight", params, NO_TIMEOUT); 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) { protected void handleEvent(String event, JsonObject params) {
if ("loadstate".equals(event)) { if ("loadstate".equals(event)) {
JsonElement add = params.get("add"); JsonElement add = params.get("add");
@@ -1066,6 +1112,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
if (parentFrame == null && page != null) { if (parentFrame == null && page != null) {
if (state == LOAD) { if (state == LOAD) {
page.listeners.notify(PageImpl.EventType.LOAD, page); page.listeners.notify(PageImpl.EventType.LOAD, page);
page.browserContext.notifyPageLoad(page);
} else if (state == DOMCONTENTLOADED) { } else if (state == DOMCONTENTLOADED) {
page.listeners.notify(PageImpl.EventType.DOMCONTENTLOADED, page); page.listeners.notify(PageImpl.EventType.DOMCONTENTLOADED, page);
} }
@@ -371,8 +371,20 @@ class LocatorImpl implements Locator {
} }
@Override @Override
public void highlight() { public void drop(DropPayload payload, DropOptions options) {
frame.highlightImpl(selector); 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 @Override
@@ -86,6 +86,10 @@ public class LocatorUtils {
String name = escapeForAttributeSelector(options.name, options.exact != null && options.exact); String name = escapeForAttributeSelector(options.name, options.exact != null && options.exact);
addAttr(result, "name", name); 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) if (options.pressed != null)
addAttr(result, "pressed", options.pressed.toString()); addAttr(result, "pressed", options.pressed.toString());
} }
@@ -73,6 +73,16 @@ public class PageAssertionsImpl extends AssertionsBase implements PageAssertions
expectImpl("to.have.url", expected, pattern, "Page URL expected to match regex", convertType(options, FrameExpectOptions.class), "Assert \"hasURL\""); 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 @Override
public PageAssertions not() { public PageAssertions not() {
return new PageAssertionsImpl(actualPage, !isNot); return new PageAssertionsImpl(actualPage, !isNot);
@@ -41,7 +41,7 @@ import static java.util.Arrays.asList;
public class PageImpl extends ChannelOwner implements Page { public class PageImpl extends ChannelOwner implements Page {
private final BrowserContextImpl browserContext; final BrowserContextImpl browserContext;
private final FrameImpl mainFrame; private final FrameImpl mainFrame;
private final KeyboardImpl keyboard; private final KeyboardImpl keyboard;
private final MouseImpl mouse; private final MouseImpl mouse;
@@ -171,6 +171,7 @@ public class PageImpl extends ChannelOwner implements Page {
ArtifactImpl artifact = connection.getExistingObject(artifactGuid); ArtifactImpl artifact = connection.getExistingObject(artifactGuid);
DownloadImpl download = new DownloadImpl(this, artifact, params); DownloadImpl download = new DownloadImpl(this, artifact, params);
listeners.notify(EventType.DOWNLOAD, download); listeners.notify(EventType.DOWNLOAD, download);
browserContext.notifyDownload(download);
} else if ("fileChooser".equals(event)) { } else if ("fileChooser".equals(event)) {
String guid = params.getAsJsonObject("element").get("guid").getAsString(); String guid = params.getAsJsonObject("element").get("guid").getAsString();
ElementHandleImpl elementHandle = connection.getExistingObject(guid); ElementHandleImpl elementHandle = connection.getExistingObject(guid);
@@ -201,6 +202,7 @@ public class PageImpl extends ChannelOwner implements Page {
frame.parentFrame.childFrames.add(frame); frame.parentFrame.childFrames.add(frame);
} }
listeners.notify(EventType.FRAMEATTACHED, frame); listeners.notify(EventType.FRAMEATTACHED, frame);
browserContext.notifyFrameAttached(frame);
} else if ("frameDetached".equals(event)) { } else if ("frameDetached".equals(event)) {
String guid = params.getAsJsonObject("frame").get("guid").getAsString(); String guid = params.getAsJsonObject("frame").get("guid").getAsString();
FrameImpl frame = connection.getExistingObject(guid); FrameImpl frame = connection.getExistingObject(guid);
@@ -210,6 +212,7 @@ public class PageImpl extends ChannelOwner implements Page {
frame.parentFrame.childFrames.remove(frame); frame.parentFrame.childFrames.remove(frame);
} }
listeners.notify(EventType.FRAMEDETACHED, frame); listeners.notify(EventType.FRAMEDETACHED, frame);
browserContext.notifyFrameDetached(frame);
} else if ("locatorHandlerTriggered".equals(event)) { } else if ("locatorHandlerTriggered".equals(event)) {
int uid = params.get("uid").getAsInt(); int uid = params.get("uid").getAsInt();
onLocatorHandlerTriggered(uid); onLocatorHandlerTriggered(uid);
@@ -245,6 +248,7 @@ public class PageImpl extends ChannelOwner implements Page {
isClosed = true; isClosed = true;
browserContext.pages.remove(this); browserContext.pages.remove(this);
listeners.notify(EventType.CLOSE, this); listeners.notify(EventType.CLOSE, this);
browserContext.notifyPageClose(this);
} }
private String effectiveCloseReason() { private String effectiveCloseReason() {
@@ -753,11 +757,11 @@ public class PageImpl extends ChannelOwner implements Page {
} }
@Override @Override
public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) { public AutoCloseable exposeBinding(String name, BindingCallback playwrightBinding) {
return exposeBindingImpl(name, playwrightBinding, options); return exposeBindingImpl(name, playwrightBinding);
} }
private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding, ExposeBindingOptions options) { private AutoCloseable exposeBindingImpl(String name, BindingCallback playwrightBinding) {
if (bindings.containsKey(name)) { if (bindings.containsKey(name)) {
throw new PlaywrightException("Function \"" + name + "\" has been already registered"); throw new PlaywrightException("Function \"" + name + "\" has been already registered");
} }
@@ -768,16 +772,13 @@ public class PageImpl extends ChannelOwner implements Page {
JsonObject params = new JsonObject(); JsonObject params = new JsonObject();
params.addProperty("name", name); params.addProperty("name", name);
if (options != null && options.handle != null && options.handle) {
params.addProperty("needsHandle", true);
}
JsonObject result = sendMessage("exposeBinding", params, NO_TIMEOUT).getAsJsonObject(); JsonObject result = sendMessage("exposeBinding", params, NO_TIMEOUT).getAsJsonObject();
return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString()); return connection.getExistingObject(result.getAsJsonObject("disposable").get("guid").getAsString());
} }
@Override @Override
public AutoCloseable exposeFunction(String name, FunctionCallback playwrightFunction) { 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 @Override
@@ -1060,6 +1061,11 @@ public class PageImpl extends ChannelOwner implements Page {
return mainFrame; return mainFrame;
} }
@Override
public void hideHighlight() {
sendMessage("hideHighlight", new JsonObject(), NO_TIMEOUT);
}
@Override @Override
public Mouse mouse() { public Mouse mouse() {
return mouse; return mouse;
@@ -1457,6 +1463,7 @@ public class PageImpl extends ChannelOwner implements Page {
void frameNavigated(FrameImpl frame) { void frameNavigated(FrameImpl frame) {
listeners.notify(EventType.FRAMENAVIGATED, frame); listeners.notify(EventType.FRAMENAVIGATED, frame);
browserContext.notifyFrameNavigated(frame);
} }
private class WaitableFrameDetach extends WaitableEvent<EventType, Frame> { private class WaitableFrameDetach extends WaitableEvent<EventType, Frame> {
@@ -112,8 +112,12 @@ class FrameExpectOptions {
} }
class FrameExpectResult { class FrameExpectResult {
static class Received {
SerializedValue value;
String ariaSnapshot;
}
boolean matches; boolean matches;
SerializedValue received; Received received;
String errorMessage; String errorMessage;
List<String> log; List<String> log;
} }
@@ -18,14 +18,21 @@ package com.microsoft.playwright.impl;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Tracing; 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.Path;
import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter;
import static com.microsoft.playwright.impl.Serialization.gson; import static com.microsoft.playwright.impl.Serialization.gson;
class TracingImpl extends ChannelOwner implements Tracing { class TracingImpl extends ChannelOwner implements Tracing {
@@ -34,6 +41,17 @@ class TracingImpl extends ChannelOwner implements Tracing {
private boolean isTracing; private boolean isTracing;
private String stacksId; private String stacksId;
private final Set<String> additionalSources = new HashSet<>(); private final Set<String> additionalSources = new HashSet<>();
final Map<String, HarRecorder> 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) { TracingImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -161,6 +179,110 @@ class TracingImpl extends ChannelOwner implements Tracing {
stopChunkImpl(options == null ? null : options.path); 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) { void setTracesDir(Path tracesDir) {
this.tracesDir = tracesDir; this.tracesDir = tracesDir;
} }
@@ -17,14 +17,17 @@
package com.microsoft.playwright.impl; package com.microsoft.playwright.impl;
import com.microsoft.playwright.WebError; import com.microsoft.playwright.WebError;
import com.microsoft.playwright.options.WebErrorLocation;
public class WebErrorImpl implements WebError { public class WebErrorImpl implements WebError {
private final PageImpl page; private final PageImpl page;
private final String error; private final String error;
private final WebErrorLocation location;
WebErrorImpl(PageImpl page, String error) { WebErrorImpl(PageImpl page, String error, WebErrorLocation location) {
this.page = page; this.page = page;
this.error = error; this.error = error;
this.location = location;
} }
@Override @Override
@@ -36,4 +39,9 @@ public class WebErrorImpl implements WebError {
public String error() { public String error() {
return error; return error;
} }
@Override
public WebErrorLocation location() {
return location;
}
} }
@@ -1,11 +1,14 @@
package com.microsoft.playwright.impl; package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.WebSocketFrame; import com.microsoft.playwright.WebSocketFrame;
import com.microsoft.playwright.WebSocketRoute; import com.microsoft.playwright.WebSocketRoute;
import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.List;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -65,6 +68,11 @@ class WebSocketRouteImpl extends ChannelOwner implements WebSocketRoute {
public String url() { public String url() {
return initializer.get("url").getAsString(); return initializer.get("url").getAsString();
} }
@Override
public List<String> protocols() {
return readProtocols();
}
}; };
WebSocketRouteImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { WebSocketRouteImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -123,6 +131,22 @@ class WebSocketRouteImpl extends ChannelOwner implements WebSocketRoute {
return initializer.get("url").getAsString(); return initializer.get("url").getAsString();
} }
@Override
public List<String> protocols() {
return readProtocols();
}
private List<String> readProtocols() {
List<String> result = new ArrayList<>();
if (!initializer.has("protocols")) {
return result;
}
for (JsonElement element : initializer.getAsJsonArray("protocols")) {
result.add(element.getAsString());
}
return result;
}
void afterHandle() { void afterHandle() {
if (this.connected) { if (this.connected) {
return; return;
@@ -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<String, String> data;
public DropPayload setFiles(Object files) {
this.files = files;
return this;
}
public DropPayload setData(Map<String, String> data) {
this.data = data;
return this;
}
}
@@ -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
}
@@ -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;
}
@@ -113,4 +113,13 @@ public class TestBrowser1 {
assertTrue(e.getMessage().contains("The reason."), e.getMessage()); 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();
}
} }
@@ -186,4 +186,90 @@ public class TestBrowserContextEvents extends TestBase {
assertTrue(webError[0].error().contains("boom"), webError[0].error()); 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("<script src=\"/error.js\"></script>");
}
});
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());
}
} }
@@ -84,19 +84,4 @@ public class TestBrowserContextExposeFunction extends TestBase {
assertEquals(asList("context", "page"), actualArgs); 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);
}
} }
@@ -36,4 +36,18 @@ public class TestLocatorHighlight extends TestBase {
BoundingBox box2 = page.locator("x-pw-highlight").boundingBox(); BoundingBox box2 = page.locator("x-pw-highlight").boundingBox();
assertEquals(new Gson().toJson(box2), new Gson().toJson(box1)); assertEquals(new Gson().toJson(box2), new Gson().toJson(box1));
} }
@Test
void highlightAndHideHighlightShouldNotThrow() {
page.setContent("<input type='text' />");
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();
}
} }
@@ -341,4 +341,14 @@ public class TestPageAriaSnapshot {
" - /placeholder: Placeholder"); " - /placeholder: Placeholder");
} }
@Test
void pageMatchesAriaSnapshot(Page page) {
page.setContent("<h1>hello</h1>");
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());
}
} }
@@ -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("<style>#dropzone { width: 300px; height: 200px; border: 2px dashed #888; }</style>\n" +
"<div id=\"dropzone\"></div>\n" +
"<script>\n" +
" window.__dropInfo = null;\n" +
" const zone = document.getElementById('dropzone');\n" +
" zone.addEventListener('dragenter', e => e.preventDefault());\n" +
" zone.addEventListener('dragover', e => e.preventDefault());\n" +
" zone.addEventListener('drop', async e => {\n" +
" e.preventDefault();\n" +
" const files = [];\n" +
" for (const file of e.dataTransfer.files)\n" +
" files.push({ name: file.name, type: file.type, size: file.size, text: await file.text() });\n" +
" const data = {};\n" +
" for (const t of e.dataTransfer.types) {\n" +
" if (t !== 'Files')\n" +
" data[t] = e.dataTransfer.getData(t);\n" +
" }\n" +
" window.__dropInfo = { files, data };\n" +
" });\n" +
"</script>");
}
@SuppressWarnings("unchecked")
private Map<String, Object> waitForDropInfo() {
page.waitForCondition(() -> page.evaluate("window.__dropInfo") != null);
return (Map<String, Object>) 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<String, Object> info = waitForDropInfo();
@SuppressWarnings("unchecked")
List<Map<String, Object>> files = (List<Map<String, Object>>) 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<String, Object> info = waitForDropInfo();
@SuppressWarnings("unchecked")
List<Map<String, Object>> files = (List<Map<String, Object>>) 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<String, String> 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<String, Object> info = waitForDropInfo();
@SuppressWarnings("unchecked")
List<?> files = (List<?>) info.get("files");
assertTrue(files.isEmpty(), "expected no files");
@SuppressWarnings("unchecked")
Map<String, String> droppedData = (Map<String, String>) 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<String, Object> info = waitForDropInfo();
@SuppressWarnings("unchecked")
List<Map<String, Object>> files = (List<Map<String, Object>>) info.get("files");
assertEquals(1, files.size());
assertEquals("hello.txt", files.get(0).get("name"));
assertEquals("path-content", files.get(0).get("text"));
}
}
@@ -164,35 +164,6 @@ public class TestPageExposeFunction extends TestBase {
assertEquals( 7, ((Map) result).get("x")); 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 @Test
void shouldThrowForDuplicateRegistrations() { void shouldThrowForDuplicateRegistrations() {
page.exposeFunction("foo", args -> null); page.exposeFunction("foo", args -> null);
@@ -202,28 +173,6 @@ public class TestPageExposeFunction extends TestBase {
assertTrue(e.getMessage().contains("Function \"foo\" has been already registered")); 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 @Test
void shouldSerializeCycles() { void shouldSerializeCycles() {
Object[] object = { null }; Object[] object = { null };
@@ -391,4 +391,29 @@ public class TestRouteWebSocket {
}); });
assertEquals(asList("response"), page.evaluate("window.log")); assertEquals(asList("response"), page.evaluate("window.log"));
} }
@Test
public void shouldExposeProtocolsToTheRouteHandler(Page page, Server server) {
List<com.microsoft.playwright.WebSocketRoute> 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<String, com.microsoft.playwright.WebSocketRoute> 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());
}
} }
@@ -448,7 +448,7 @@ public class TestSelectorsRole extends TestBase {
assertTrue(e0.getMessage().contains("Role must not be empty"), e0.getMessage()); assertTrue(e0.getMessage().contains("Role must not be empty"), e0.getMessage());
PlaywrightException e1 = assertThrows(PlaywrightException.class, () -> page.querySelector("role=foo[sElected]")); 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]")); PlaywrightException e2 = assertThrows(PlaywrightException.class, () -> page.querySelector("role=foo[bar . qux=true]"));
assertTrue(e2.getMessage().contains("Unknown attribute \"bar.qux\""), e2.getMessage()); assertTrue(e2.getMessage().contains("Unknown attribute \"bar.qux\""), e2.getMessage());
@@ -158,6 +158,7 @@ public class TestTracing extends TestBase {
Pattern.compile("Set content"), Pattern.compile("Set content"),
Pattern.compile("Click") Pattern.compile("Click")
}); });
traceViewer.selectAction("Click");
traceViewer.showSourceTab(); traceViewer.showSourceTab();
assertThat(traceViewer.stackFrames()).containsText(new Pattern[] { assertThat(traceViewer.stackFrames()).containsText(new Pattern[] {
Pattern.compile("myMethodInner"), Pattern.compile("myMethodInner"),
@@ -379,4 +380,15 @@ public class TestTracing extends TestBase {
}); });
}); });
} }
@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);
}
} }
@@ -43,7 +43,7 @@ class TraceViewerPage {
} }
Locator stackFrames() { 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) { void selectAction(String title, int ordinal) {
+1 -1
View File
@@ -1 +1 @@
1.59.1-beta-1775762078000 1.60.0-alpha-1778025033000
@@ -976,7 +976,7 @@ class Interface extends TypeDefinition {
if (methods.stream().anyMatch(m -> "create".equals(m.jsonName))) { if (methods.stream().anyMatch(m -> "create".equals(m.jsonName))) {
output.add("import com.microsoft.playwright.impl." + jsonName + "Impl;"); 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.*;"); output.add("import com.microsoft.playwright.options.*;");
} }
if ("Download".equals(jsonName)) { if ("Download".equals(jsonName)) {
@@ -988,7 +988,7 @@ class Interface extends TypeDefinition {
if ("Clock".equals(jsonName)) { if ("Clock".equals(jsonName)) {
output.add("import java.util.Date;"); 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.*;"); output.add("import java.util.*;");
} }
if (asList("WebSocketRoute").contains(jsonName)) { if (asList("WebSocketRoute").contains(jsonName)) {
@@ -1004,7 +1004,7 @@ class Interface extends TypeDefinition {
if (asList("Page", "Frame", "BrowserContext", "WebSocket", "Worker").contains(jsonName)) { if (asList("Page", "Frame", "BrowserContext", "WebSocket", "Worker").contains(jsonName)) {
output.add("import java.util.function.Predicate;"); 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;"); output.add("import java.util.regex.Pattern;");
} }
if ("CDPSession".equals(jsonName)) { if ("CDPSession".equals(jsonName)) {
@@ -1012,6 +1012,7 @@ class Interface extends TypeDefinition {
} }
if ("LocatorAssertions".equals(jsonName)) { if ("LocatorAssertions".equals(jsonName)) {
output.add("import com.microsoft.playwright.options.AriaRole;"); output.add("import com.microsoft.playwright.options.AriaRole;");
output.add("import com.microsoft.playwright.options.PseudoElement;");
} }
if ("PlaywrightAssertions".equals(jsonName)) { if ("PlaywrightAssertions".equals(jsonName)) {
output.add("import com.microsoft.playwright.APIResponse;"); output.add("import com.microsoft.playwright.APIResponse;");
@@ -1109,6 +1110,10 @@ class CustomClass extends TypeDefinition {
output.add("import java.nio.file.Path;"); output.add("import java.nio.file.Path;");
output.add(""); 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 " : ""; String access = (parent.typeScope() instanceof CustomClass) || topLevelTypes().containsKey(name) ? "public " : "";
output.add(offset + access + "class " + name + " {"); output.add(offset + access + "class " + name + " {");
String bodyOffset = offset + " "; String bodyOffset = offset + " ";
@@ -1185,12 +1190,30 @@ public class ApiGenerator {
filterOtherLangs(api, new Stack<>()); filterOtherLangs(api, new Stack<>());
File dir = new File(cwd, "playwright/src/main/java/com/microsoft/playwright"); 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()); System.out.println("Writing files to: " + dir.getCanonicalPath());
generate(api, dir, "com.microsoft.playwright", isAssertion().negate()); Map<String, TypeDefinition> 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"); File assertionsDir = new File(cwd,"playwright/src/main/java/com/microsoft/playwright/assertions");
System.out.println("Writing assertion files to: " + dir.getCanonicalPath()); 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<String, TypeDefinition> topLevelTypes, File optionsDir, String packageName) throws IOException {
for (TypeDefinition e : topLevelTypes.values()) {
List<String> 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<String> isAssertion() { private static Predicate<String> isAssertion() {
@@ -1206,8 +1229,7 @@ public class ApiGenerator {
return className -> className.contains("SoftAssertions"); return className -> className.contains("SoftAssertions");
} }
private void generate(JsonArray api, File dir, String packageName, Predicate<String> classFilter) throws IOException { private void generate(JsonArray api, File dir, String packageName, Predicate<String> classFilter, Map<String, TypeDefinition> topLevelTypes) throws IOException {
Map<String, TypeDefinition> topLevelTypes = new HashMap<>();
for (JsonElement entry: api) { for (JsonElement entry: api) {
String name = entry.getAsJsonObject().get("name").getAsString(); String name = entry.getAsJsonObject().get("name").getAsString();
// We write this one manually. // We write this one manually.
@@ -1233,23 +1255,6 @@ public class ApiGenerator {
} }
} }
// No options under assertions.
if (packageName.contains(".assertions")) {
return;
}
dir = new File(dir, "options");
for (TypeDefinition e : topLevelTypes.values()) {
List<String> 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<String> path) { private static void filterOtherLangs(JsonElement json, Stack<String> path) {