chore(driver): roll to 1.63.0-alpha-1788460676000 (#1972)
This commit is contained in:
@@ -10,9 +10,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
|
||||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->151.0.7922.34<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->26.5<!-- GEN:stop --> | ✅ | ✅ | ✅ |
|
||||
| Firefox <!-- GEN:firefox-version -->153.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Chromium <!-- GEN:chromium-version -->153.0.8010.12<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->26.6<!-- GEN:stop --> | ✅ | ✅ | ✅ |
|
||||
| Firefox <!-- GEN:firefox-version -->155.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -70,8 +70,11 @@ public interface APIRequest {
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public HttpCredentials httpCredentials;
|
||||
public Object httpCredentials;
|
||||
/**
|
||||
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
|
||||
*/
|
||||
@@ -166,6 +169,9 @@ public interface APIRequest {
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public NewContextOptions setHttpCredentials(String username, String password) {
|
||||
return setHttpCredentials(new HttpCredentials(username, password));
|
||||
@@ -173,11 +179,25 @@ public interface APIRequest {
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public NewContextOptions setHttpCredentials(HttpCredentials httpCredentials) {
|
||||
this.httpCredentials = httpCredentials;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public NewContextOptions setHttpCredentials(List<HttpCredentials> httpCredentials) {
|
||||
this.httpCredentials = httpCredentials;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
|
||||
*/
|
||||
|
||||
@@ -61,6 +61,10 @@ public interface APIRequestContext {
|
||||
* Set to {@code true} to include IndexedDB in the storage state snapshot.
|
||||
*/
|
||||
public Boolean indexedDB;
|
||||
/**
|
||||
* Set to {@code true} to include the origin private file system in the storage state snapshot.
|
||||
*/
|
||||
public Boolean opfs;
|
||||
/**
|
||||
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
|
||||
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
|
||||
@@ -74,6 +78,13 @@ public interface APIRequestContext {
|
||||
this.indexedDB = indexedDB;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Set to {@code true} to include the origin private file system in the storage state snapshot.
|
||||
*/
|
||||
public StorageStateOptions setOpfs(boolean opfs) {
|
||||
this.opfs = opfs;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
|
||||
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
|
||||
@@ -484,7 +495,7 @@ public interface APIRequestContext {
|
||||
*/
|
||||
String storageState(StorageStateOptions options);
|
||||
/**
|
||||
*
|
||||
* Tracing recorder for requests made through this API request context.
|
||||
*
|
||||
* @since v1.60
|
||||
*/
|
||||
|
||||
@@ -161,8 +161,11 @@ public interface Browser extends AutoCloseable {
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public HttpCredentials httpCredentials;
|
||||
public Object httpCredentials;
|
||||
/**
|
||||
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
|
||||
*/
|
||||
@@ -409,6 +412,9 @@ public interface Browser extends AutoCloseable {
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public NewContextOptions setHttpCredentials(String username, String password) {
|
||||
return setHttpCredentials(new HttpCredentials(username, password));
|
||||
@@ -416,11 +422,25 @@ public interface Browser extends AutoCloseable {
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public NewContextOptions setHttpCredentials(HttpCredentials httpCredentials) {
|
||||
this.httpCredentials = httpCredentials;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public NewContextOptions setHttpCredentials(List<HttpCredentials> httpCredentials) {
|
||||
this.httpCredentials = httpCredentials;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
|
||||
*/
|
||||
@@ -737,8 +757,11 @@ public interface Browser extends AutoCloseable {
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public HttpCredentials httpCredentials;
|
||||
public Object httpCredentials;
|
||||
/**
|
||||
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
|
||||
*/
|
||||
@@ -985,6 +1008,9 @@ public interface Browser extends AutoCloseable {
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public NewPageOptions setHttpCredentials(String username, String password) {
|
||||
return setHttpCredentials(new HttpCredentials(username, password));
|
||||
@@ -992,11 +1018,25 @@ public interface Browser extends AutoCloseable {
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public NewPageOptions setHttpCredentials(HttpCredentials httpCredentials) {
|
||||
this.httpCredentials = httpCredentials;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public NewPageOptions setHttpCredentials(List<HttpCredentials> httpCredentials) {
|
||||
this.httpCredentials = httpCredentials;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}.
|
||||
*/
|
||||
|
||||
@@ -114,6 +114,17 @@ public interface BrowserContext extends AutoCloseable {
|
||||
*/
|
||||
void offDialog(Consumer<Dialog> handler);
|
||||
|
||||
/**
|
||||
* Emitted when a JavaScript dialog in any page belonging to this context has been closed, either by {@link
|
||||
* com.microsoft.playwright.Dialog#accept Dialog.accept()}, by {@link com.microsoft.playwright.Dialog#dismiss
|
||||
* Dialog.dismiss()}, or manually by the user in the headed browser.
|
||||
*/
|
||||
void onDialogClosed(Consumer<Dialog> handler);
|
||||
/**
|
||||
* Removes handler that was previously added with {@link #onDialogClosed onDialogClosed(handler)}.
|
||||
*/
|
||||
void offDialogClosed(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
|
||||
@@ -465,6 +476,14 @@ public interface BrowserContext extends AutoCloseable {
|
||||
* Authentication, enable this.
|
||||
*/
|
||||
public Boolean indexedDB;
|
||||
/**
|
||||
* Set to {@code true} to include the <a
|
||||
* href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system">origin private file
|
||||
* system</a> in the storage state snapshot.
|
||||
*
|
||||
* <p> <strong>NOTE:</strong> OPFS is currently not supported in ephemeral WebKit contexts.
|
||||
*/
|
||||
public Boolean opfs;
|
||||
/**
|
||||
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
|
||||
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
|
||||
@@ -493,6 +512,17 @@ public interface BrowserContext extends AutoCloseable {
|
||||
this.indexedDB = indexedDB;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Set to {@code true} to include the <a
|
||||
* href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system">origin private file
|
||||
* system</a> in the storage state snapshot.
|
||||
*
|
||||
* <p> <strong>NOTE:</strong> OPFS is currently not supported in ephemeral WebKit contexts.
|
||||
*/
|
||||
public StorageStateOptions setOpfs(boolean opfs) {
|
||||
this.opfs = opfs;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current
|
||||
* working directory. If no path is provided, storage state is still returned, but won't be saved to the disk.
|
||||
@@ -1492,8 +1522,8 @@ public interface BrowserContext extends AutoCloseable {
|
||||
*/
|
||||
void setOffline(boolean offline);
|
||||
/**
|
||||
* Returns storage state for this browser context, contains current cookies, local storage snapshot, IndexedDB snapshot and
|
||||
* virtual WebAuthn credentials.
|
||||
* Returns storage state for this browser context, contains current cookies, local storage snapshot, IndexedDB snapshot,
|
||||
* origin private file system snapshot and virtual WebAuthn credentials.
|
||||
*
|
||||
* @since v1.8
|
||||
*/
|
||||
@@ -1501,17 +1531,17 @@ public interface BrowserContext extends AutoCloseable {
|
||||
return storageState(null);
|
||||
}
|
||||
/**
|
||||
* Returns storage state for this browser context, contains current cookies, local storage snapshot, IndexedDB snapshot and
|
||||
* virtual WebAuthn credentials.
|
||||
* Returns storage state for this browser context, contains current cookies, local storage snapshot, IndexedDB snapshot,
|
||||
* origin private file system snapshot and virtual WebAuthn credentials.
|
||||
*
|
||||
* @since v1.8
|
||||
*/
|
||||
String storageState(StorageStateOptions options);
|
||||
/**
|
||||
* Clears the existing cookies, local storage, IndexedDB entries and virtual WebAuthn credentials, and sets the new storage
|
||||
* state. When the storage state contains credentials, the virtual WebAuthn authenticator is installed (equivalent to
|
||||
* {@link com.microsoft.playwright.Credentials#install Credentials.install()}), preventing all real authenticators from
|
||||
* working in this context.
|
||||
* Clears the existing cookies, local storage, IndexedDB entries, origin private file system entries and virtual WebAuthn
|
||||
* credentials, and sets the new storage state. When the storage state contains credentials, the virtual WebAuthn
|
||||
* authenticator is installed (equivalent to {@link com.microsoft.playwright.Credentials#install Credentials.install()}),
|
||||
* preventing all real authenticators from working in this context.
|
||||
*
|
||||
* <p> <strong>Usage</strong>
|
||||
* <pre>{@code
|
||||
|
||||
@@ -632,8 +632,11 @@ public interface BrowserType {
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public HttpCredentials httpCredentials;
|
||||
public Object httpCredentials;
|
||||
/**
|
||||
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}.
|
||||
* Dangerous option; use with care. Defaults to {@code false}.
|
||||
@@ -1013,6 +1016,9 @@ public interface BrowserType {
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public LaunchPersistentContextOptions setHttpCredentials(String username, String password) {
|
||||
return setHttpCredentials(new HttpCredentials(username, password));
|
||||
@@ -1020,11 +1026,25 @@ public interface BrowserType {
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public LaunchPersistentContextOptions setHttpCredentials(HttpCredentials httpCredentials) {
|
||||
this.httpCredentials = httpCredentials;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Credentials for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
*
|
||||
* <p> Pass an array to use different credentials for different origins. The first entry that matches the request origin is
|
||||
* used, and entries with no origin match any request.
|
||||
*/
|
||||
public LaunchPersistentContextOptions setHttpCredentials(List<HttpCredentials> httpCredentials) {
|
||||
this.httpCredentials = httpCredentials;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* If {@code true}, Playwright does not pass its own configurations args and only uses the ones from {@code args}.
|
||||
* Dangerous option; use with care. Defaults to {@code false}.
|
||||
|
||||
@@ -3307,6 +3307,10 @@ public interface Frame {
|
||||
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
|
||||
* that iframe.
|
||||
*
|
||||
* <p> When called without {@code selector}, the search starts in this frame or in any of the iframes inside it, so that you
|
||||
* don't need to locate each iframe first. Note that the rest of the locator is resolved inside a single frame, just like
|
||||
* any other locator. If it matches elements inside multiple frames, an error is thrown.
|
||||
*
|
||||
* <p> <strong>Usage</strong>
|
||||
*
|
||||
* <p> Following snippet locates element with text "Submit" in the iframe with id {@code my-frame}, like {@code <iframe
|
||||
@@ -3316,7 +3320,42 @@ public interface Frame {
|
||||
* locator.click();
|
||||
* }</pre>
|
||||
*
|
||||
* @param selector A selector to use when resolving DOM element.
|
||||
* <p> Following snippet locates a button, either in the frame or in one of the iframes inside it:
|
||||
* <pre>{@code
|
||||
* Locator locator = frame.frameLocator().getByRole(AriaRole.BUTTON);
|
||||
* locator.click();
|
||||
* }</pre>
|
||||
*
|
||||
* @since v1.17
|
||||
*/
|
||||
default FrameLocator frameLocator() {
|
||||
return frameLocator(null);
|
||||
}
|
||||
/**
|
||||
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
|
||||
* that iframe.
|
||||
*
|
||||
* <p> When called without {@code selector}, the search starts in this frame or in any of the iframes inside it, so that you
|
||||
* don't need to locate each iframe first. Note that the rest of the locator is resolved inside a single frame, just like
|
||||
* any other locator. If it matches elements inside multiple frames, an error is thrown.
|
||||
*
|
||||
* <p> <strong>Usage</strong>
|
||||
*
|
||||
* <p> Following snippet locates element with text "Submit" in the iframe with id {@code my-frame}, like {@code <iframe
|
||||
* id="my-frame">}:
|
||||
* <pre>{@code
|
||||
* Locator locator = frame.frameLocator("#my-iframe").getByText("Submit");
|
||||
* locator.click();
|
||||
* }</pre>
|
||||
*
|
||||
* <p> Following snippet locates a button, either in the frame or in one of the iframes inside it:
|
||||
* <pre>{@code
|
||||
* Locator locator = frame.frameLocator().getByRole(AriaRole.BUTTON);
|
||||
* locator.click();
|
||||
* }</pre>
|
||||
*
|
||||
* @param selector A selector that matches the frame element. When not specified, locator is matched in this frame or in any of the iframes
|
||||
* inside it.
|
||||
* @since v1.17
|
||||
*/
|
||||
FrameLocator frameLocator(String selector);
|
||||
|
||||
@@ -41,6 +41,27 @@ import java.util.regex.Pattern;
|
||||
* page.locator(".result-frame").first().contentFrame().getByRole(AriaRole.BUTTON).click();
|
||||
* }</pre>
|
||||
*
|
||||
* <p> <strong>Any frame</strong>
|
||||
*
|
||||
* <p> Calling {@link com.microsoft.playwright.Page#frameLocator Page.frameLocator()} or {@link
|
||||
* com.microsoft.playwright.Frame#frameLocator Frame.frameLocator()} without a selector creates a frame locator that starts
|
||||
* the search in any frame of the subtree - so that you don't need to locate the iframe first.
|
||||
* <pre>{@code
|
||||
* // Finds the button in any frame on the page:
|
||||
* page.frameLocator().getByRole(AriaRole.BUTTON).click();
|
||||
*
|
||||
* // Finds the iframe with id "my-frame" anywhere on the page, and clicks the button inside it:
|
||||
* page.frameLocator().locator("#my-frame").contentFrame().getByRole(AriaRole.BUTTON).click();
|
||||
* }</pre>
|
||||
*
|
||||
* <p> Only the start of the search is affected - the rest of the locator is resolved inside a single frame, just like any
|
||||
* other locator. Following the strictness rules above, an error is thrown when elements are matched in multiple frames.
|
||||
*
|
||||
* <p> Such a frame locator does not point to a particular {@code iframe}, so {@link
|
||||
* com.microsoft.playwright.FrameLocator#owner FrameLocator.owner()}, {@link com.microsoft.playwright.FrameLocator#first
|
||||
* FrameLocator.first()}, {@link com.microsoft.playwright.FrameLocator#last FrameLocator.last()} and {@link
|
||||
* com.microsoft.playwright.FrameLocator#nth FrameLocator.nth()} are not supported on it.
|
||||
*
|
||||
* <p> <strong>Converting Locator to FrameLocator</strong>
|
||||
*
|
||||
* <p> If you have a {@code Locator} object pointing to an {@code iframe} it can be converted to {@code FrameLocator} using
|
||||
|
||||
@@ -900,7 +900,8 @@ public interface Locator {
|
||||
*/
|
||||
public Object hasText;
|
||||
/**
|
||||
* Only matches visible or invisible elements.
|
||||
* Only matches visible or invisible elements. Prefer the {@link com.microsoft.playwright.Locator#visible
|
||||
* Locator.visible()} shortcut when matching only visible elements.
|
||||
*/
|
||||
public Boolean visible;
|
||||
|
||||
@@ -966,7 +967,8 @@ public interface Locator {
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Only matches visible or invisible elements.
|
||||
* Only matches visible or invisible elements. Prefer the {@link com.microsoft.playwright.Locator#visible
|
||||
* Locator.visible()} shortcut when matching only visible elements.
|
||||
*/
|
||||
public FilterOptions setVisible(boolean visible) {
|
||||
this.visible = visible;
|
||||
@@ -5824,6 +5826,29 @@ public interface Locator {
|
||||
* @since v1.14
|
||||
*/
|
||||
void uncheck(UncheckOptions options);
|
||||
/**
|
||||
* Returns a locator that matches only <a href="https://playwright.dev/java/docs/actionability#visible">visible</a>
|
||||
* elements, ignoring the invisible ones. This is the recommended way to distinguish elements by visibility, as opposed to
|
||||
* the {@code :visible} CSS pseudo-class.
|
||||
*
|
||||
* <p> Note that visibility is checked every time the locator is used, and not at the moment of the {@link
|
||||
* com.microsoft.playwright.Locator#visible Locator.visible()} call.
|
||||
*
|
||||
* <p> <strong>Usage</strong>
|
||||
*
|
||||
* <p> Consider a page with two buttons, the first invisible and the second visible.
|
||||
*
|
||||
* <p> This will only find the second button, because it is visible, and then click it.
|
||||
* <pre>{@code
|
||||
* page.locator("button").visible().click();
|
||||
* }</pre>
|
||||
*
|
||||
* <p> To match invisible elements instead, use {@link com.microsoft.playwright.Locator#filter Locator.filter()} with the
|
||||
* {@code visible} option set to {@code false}.
|
||||
*
|
||||
* @since v1.63
|
||||
*/
|
||||
Locator visible();
|
||||
/**
|
||||
* Returns when element specified by locator satisfies the {@code state} option.
|
||||
*
|
||||
|
||||
@@ -145,6 +145,17 @@ public interface Page extends AutoCloseable {
|
||||
*/
|
||||
void offDialog(Consumer<Dialog> handler);
|
||||
|
||||
/**
|
||||
* Emitted when a JavaScript dialog has been closed, either by {@link com.microsoft.playwright.Dialog#accept
|
||||
* Dialog.accept()}, by {@link com.microsoft.playwright.Dialog#dismiss Dialog.dismiss()}, or manually by the user in the
|
||||
* headed browser.
|
||||
*/
|
||||
void onDialogClosed(Consumer<Dialog> handler);
|
||||
/**
|
||||
* Removes handler that was previously added with {@link #onDialogClosed onDialogClosed(handler)}.
|
||||
*/
|
||||
void offDialogClosed(Consumer<Dialog> handler);
|
||||
|
||||
/**
|
||||
* Emitted when the JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded">{@code
|
||||
* DOMContentLoaded}</a> event is dispatched.
|
||||
@@ -5005,6 +5016,10 @@ public interface Page extends AutoCloseable {
|
||||
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
|
||||
* that iframe.
|
||||
*
|
||||
* <p> When called without {@code selector}, the search starts in any frame on the page - the main frame or any of the iframes
|
||||
* - so that you don't need to locate each iframe first. Note that the rest of the locator is resolved inside a single
|
||||
* frame, just like any other locator. If it matches elements inside multiple frames, an error is thrown.
|
||||
*
|
||||
* <p> <strong>Usage</strong>
|
||||
*
|
||||
* <p> Following snippet locates element with text "Submit" in the iframe with id {@code my-frame}, like {@code <iframe
|
||||
@@ -5014,7 +5029,41 @@ public interface Page extends AutoCloseable {
|
||||
* locator.click();
|
||||
* }</pre>
|
||||
*
|
||||
* @param selector A selector to use when resolving DOM element.
|
||||
* <p> Following snippet locates a button, either in the main frame or in one of the iframes:
|
||||
* <pre>{@code
|
||||
* Locator locator = page.frameLocator().getByRole(AriaRole.BUTTON);
|
||||
* locator.click();
|
||||
* }</pre>
|
||||
*
|
||||
* @since v1.17
|
||||
*/
|
||||
default FrameLocator frameLocator() {
|
||||
return frameLocator(null);
|
||||
}
|
||||
/**
|
||||
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
|
||||
* that iframe.
|
||||
*
|
||||
* <p> When called without {@code selector}, the search starts in any frame on the page - the main frame or any of the iframes
|
||||
* - so that you don't need to locate each iframe first. Note that the rest of the locator is resolved inside a single
|
||||
* frame, just like any other locator. If it matches elements inside multiple frames, an error is thrown.
|
||||
*
|
||||
* <p> <strong>Usage</strong>
|
||||
*
|
||||
* <p> Following snippet locates element with text "Submit" in the iframe with id {@code my-frame}, like {@code <iframe
|
||||
* id="my-frame">}:
|
||||
* <pre>{@code
|
||||
* Locator locator = page.frameLocator("#my-iframe").getByText("Submit");
|
||||
* locator.click();
|
||||
* }</pre>
|
||||
*
|
||||
* <p> Following snippet locates a button, either in the main frame or in one of the iframes:
|
||||
* <pre>{@code
|
||||
* Locator locator = page.frameLocator().getByRole(AriaRole.BUTTON);
|
||||
* locator.click();
|
||||
* }</pre>
|
||||
*
|
||||
* @param selector A selector that matches the frame element. When not specified, locator is matched in any frame on the page.
|
||||
* @since v1.17
|
||||
*/
|
||||
FrameLocator frameLocator(String selector);
|
||||
@@ -5604,10 +5653,10 @@ public interface Page extends AutoCloseable {
|
||||
*
|
||||
* <p> Navigate to the previous page in history.
|
||||
*
|
||||
* <p> <strong>NOTE:</strong> **Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache
|
||||
* across all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events.
|
||||
* Because BFCache restores unfreeze the DOM without firing these events, using {@code page.goBack()} or {@code
|
||||
* page.goForward()} to trigger a BFCache restore will result in timeouts and a desynchronized {@code Page} state.
|
||||
* <p> <strong>NOTE:</strong> **Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache across
|
||||
* all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events. Because
|
||||
* BFCache restores unfreeze the DOM without firing these events, using {@code page.goBack()} or {@code page.goForward()}
|
||||
* to trigger a BFCache restore will result in timeouts and a desynchronized {@code Page} state.
|
||||
*
|
||||
* @since v1.8
|
||||
*/
|
||||
@@ -5620,10 +5669,10 @@ public interface Page extends AutoCloseable {
|
||||
*
|
||||
* <p> Navigate to the previous page in history.
|
||||
*
|
||||
* <p> <strong>NOTE:</strong> **Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache
|
||||
* across all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events.
|
||||
* Because BFCache restores unfreeze the DOM without firing these events, using {@code page.goBack()} or {@code
|
||||
* page.goForward()} to trigger a BFCache restore will result in timeouts and a desynchronized {@code Page} state.
|
||||
* <p> <strong>NOTE:</strong> **Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache across
|
||||
* all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events. Because
|
||||
* BFCache restores unfreeze the DOM without firing these events, using {@code page.goBack()} or {@code page.goForward()}
|
||||
* to trigger a BFCache restore will result in timeouts and a desynchronized {@code Page} state.
|
||||
*
|
||||
* @since v1.8
|
||||
*/
|
||||
@@ -5634,10 +5683,10 @@ public interface Page extends AutoCloseable {
|
||||
*
|
||||
* <p> Navigate to the next page in history.
|
||||
*
|
||||
* <p> <strong>NOTE:</strong> **Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache
|
||||
* across all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events.
|
||||
* Because BFCache restores unfreeze the DOM without firing these events, using {@code page.goBack()} or {@code
|
||||
* page.goForward()} to trigger a BFCache restore will result in timeouts and a desynchronized {@code Page} state.
|
||||
* <p> <strong>NOTE:</strong> **Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache across
|
||||
* all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events. Because
|
||||
* BFCache restores unfreeze the DOM without firing these events, using {@code page.goBack()} or {@code page.goForward()}
|
||||
* to trigger a BFCache restore will result in timeouts and a desynchronized {@code Page} state.
|
||||
*
|
||||
* @since v1.8
|
||||
*/
|
||||
@@ -5650,10 +5699,10 @@ public interface Page extends AutoCloseable {
|
||||
*
|
||||
* <p> Navigate to the next page in history.
|
||||
*
|
||||
* <p> <strong>NOTE:</strong> **Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache
|
||||
* across all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events.
|
||||
* Because BFCache restores unfreeze the DOM without firing these events, using {@code page.goBack()} or {@code
|
||||
* page.goForward()} to trigger a BFCache restore will result in timeouts and a desynchronized {@code Page} state.
|
||||
* <p> <strong>NOTE:</strong> **Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache across
|
||||
* all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events. Because
|
||||
* BFCache restores unfreeze the DOM without firing these events, using {@code page.goBack()} or {@code page.goForward()}
|
||||
* to trigger a BFCache restore will result in timeouts and a desynchronized {@code Page} state.
|
||||
*
|
||||
* @since v1.8
|
||||
*/
|
||||
|
||||
@@ -45,6 +45,10 @@ import java.util.regex.Pattern;
|
||||
*/
|
||||
public interface Tracing {
|
||||
class StartOptions {
|
||||
/**
|
||||
* Whether to capture aria snapshot of the page on every action.
|
||||
*/
|
||||
public Boolean ariaSnapshots;
|
||||
/**
|
||||
* When enabled, the trace is written to an unarchived file that is updated in real time as actions occur, instead of
|
||||
* caching changes and archiving them into a zip file at the end. This is useful for live trace viewing during test
|
||||
@@ -63,11 +67,11 @@ public interface Tracing {
|
||||
*/
|
||||
public Boolean screenshots;
|
||||
/**
|
||||
* If this option is true tracing will
|
||||
* <ul>
|
||||
* <li> capture DOM snapshot on every action</li>
|
||||
* <li> record network activity</li>
|
||||
* </ul>
|
||||
* Whether to capture a screenshot of the page on every action.
|
||||
*/
|
||||
public Boolean screenSnapshots;
|
||||
/**
|
||||
* Whether to capture DOM snapshot and record network activity on every action.
|
||||
*/
|
||||
public Boolean snapshots;
|
||||
/**
|
||||
@@ -81,6 +85,13 @@ public interface Tracing {
|
||||
*/
|
||||
public String title;
|
||||
|
||||
/**
|
||||
* Whether to capture aria snapshot of the page on every action.
|
||||
*/
|
||||
public StartOptions setAriaSnapshots(boolean ariaSnapshots) {
|
||||
this.ariaSnapshots = ariaSnapshots;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* When enabled, the trace is written to an unarchived file that is updated in real time as actions occur, instead of
|
||||
* caching changes and archiving them into a zip file at the end. This is useful for live trace viewing during test
|
||||
@@ -108,11 +119,14 @@ public interface Tracing {
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* If this option is true tracing will
|
||||
* <ul>
|
||||
* <li> capture DOM snapshot on every action</li>
|
||||
* <li> record network activity</li>
|
||||
* </ul>
|
||||
* Whether to capture a screenshot of the page on every action.
|
||||
*/
|
||||
public StartOptions setScreenSnapshots(boolean screenSnapshots) {
|
||||
this.screenSnapshots = screenSnapshots;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Whether to capture DOM snapshot and record network activity on every action.
|
||||
*/
|
||||
public StartOptions setSnapshots(boolean snapshots) {
|
||||
this.snapshots = snapshots;
|
||||
@@ -387,7 +401,7 @@ public interface Tracing {
|
||||
* 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> Only one HAR recording can be active at a time per {@code Tracing} instance.
|
||||
*
|
||||
* <p> <strong>Usage</strong>
|
||||
* <pre>{@code
|
||||
@@ -409,7 +423,7 @@ public interface Tracing {
|
||||
* 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> Only one HAR recording can be active at a time per {@code Tracing} instance.
|
||||
*
|
||||
* <p> <strong>Usage</strong>
|
||||
* <pre>{@code
|
||||
|
||||
@@ -47,7 +47,7 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.microsoft.playwright.Tracing tracing() {
|
||||
public TracingImpl tracing() {
|
||||
return tracing;
|
||||
}
|
||||
|
||||
@@ -220,9 +220,14 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext {
|
||||
|
||||
@Override
|
||||
public String storageState(StorageStateOptions options) {
|
||||
JsonElement json = sendMessage("storageState");
|
||||
if (options == null) {
|
||||
options = new StorageStateOptions();
|
||||
}
|
||||
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
|
||||
params.remove("path");
|
||||
JsonElement json = sendMessage("storageState", params, NO_TIMEOUT);
|
||||
String storageState = json.toString();
|
||||
if (options != null && options.path != null) {
|
||||
if (options.path != null) {
|
||||
Utils.writeToFile(storageState.getBytes(StandardCharsets.UTF_8), options.path);
|
||||
}
|
||||
return storageState;
|
||||
|
||||
@@ -69,6 +69,7 @@ class APIRequestImpl implements APIRequest {
|
||||
params.add("storageState", storageState);
|
||||
}
|
||||
addToProtocol(params, clientCertificateList);
|
||||
Utils.addHttpCredentialsToProtocol(params);
|
||||
JsonObject result = playwright.sendMessage("newRequest", params, NO_TIMEOUT).getAsJsonObject();
|
||||
APIRequestContextImpl context = playwright.connection.getExistingObject(result.getAsJsonObject("request").get("guid").getAsString());
|
||||
context.timeoutSettings.setDefaultTimeout(timeout);
|
||||
|
||||
@@ -61,6 +61,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
|
||||
Map<EventType, String> result = new HashMap<>();
|
||||
result.put(EventType.CONSOLE, "console");
|
||||
result.put(EventType.DIALOG, "dialog");
|
||||
result.put(EventType.DIALOGCLOSED, "dialogClosed");
|
||||
result.put(EventType.REQUEST, "request");
|
||||
result.put(EventType.RESPONSE, "response");
|
||||
result.put(EventType.REQUESTFINISHED, "requestFinished");
|
||||
@@ -74,6 +75,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
|
||||
CLOSE,
|
||||
CONSOLE,
|
||||
DIALOG,
|
||||
DIALOGCLOSED,
|
||||
DOWNLOAD,
|
||||
FRAMEATTACHED,
|
||||
FRAMEDETACHED,
|
||||
@@ -180,6 +182,16 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
|
||||
listeners.remove(EventType.DIALOG, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialogClosed(Consumer<Dialog> handler) {
|
||||
listeners.add(EventType.DIALOGCLOSED, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offDialogClosed(Consumer<Dialog> handler) {
|
||||
listeners.remove(EventType.DIALOGCLOSED, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPage(Consumer<Page> handler) {
|
||||
listeners.add(EventType.PAGE, handler);
|
||||
@@ -363,6 +375,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
|
||||
|
||||
@Override
|
||||
public void close(CloseOptions options) {
|
||||
RuntimeException harError = null;
|
||||
if (!closingOrClosed) {
|
||||
closingOrClosed = true;
|
||||
if (options == null) {
|
||||
@@ -370,11 +383,18 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
|
||||
}
|
||||
closeReason = options.reason;
|
||||
request.dispose(convertType(options, APIRequestContext.DisposeOptions.class));
|
||||
tracing.exportAllHars();
|
||||
try {
|
||||
tracing.exportAllHars();
|
||||
} catch (RuntimeException e) {
|
||||
harError = e;
|
||||
}
|
||||
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
|
||||
sendMessage("close", params, NO_TIMEOUT);
|
||||
}
|
||||
runUntil(() -> {}, closePromise);
|
||||
if (harError != null) {
|
||||
throw harError;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -626,7 +646,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
|
||||
try {
|
||||
String state = new String(readAllBytes(storageState), UTF_8);
|
||||
JsonObject params = new JsonObject();
|
||||
params.addProperty("storageState", state);
|
||||
params.add("storageState", gson().fromJson(state, JsonObject.class));
|
||||
sendMessage("setStorageState", params, NO_TIMEOUT);
|
||||
} catch (IOException e) {
|
||||
throw new PlaywrightException("Failed to read storage state from file", e);
|
||||
@@ -654,6 +674,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
|
||||
return debugger;
|
||||
}
|
||||
|
||||
void setTracesDir(Path tracesDir) {
|
||||
tracing.setTracesDir(tracesDir);
|
||||
request.tracing().setTracesDir(tracesDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TracingImpl tracing() {
|
||||
return tracing;
|
||||
@@ -776,6 +801,13 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
} else if ("dialogClosed".equals(event)) {
|
||||
DialogImpl dialog = connection.getExistingObject(params.getAsJsonObject("dialog").get("guid").getAsString());
|
||||
listeners.notify(EventType.DIALOGCLOSED, dialog);
|
||||
PageImpl page = dialog.page();
|
||||
if (page != null) {
|
||||
page.listeners.notify(PageImpl.EventType.DIALOGCLOSED, dialog);
|
||||
}
|
||||
} else if ("route".equals(event)) {
|
||||
RouteImpl route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
|
||||
route.browserContext = this;
|
||||
|
||||
@@ -177,6 +177,7 @@ class BrowserImpl extends ChannelOwner implements Browser {
|
||||
}
|
||||
}
|
||||
addToProtocol(params, options.clientCertificates);
|
||||
addHttpCredentialsToProtocol(params);
|
||||
params.remove("acceptDownloads");
|
||||
if (options.acceptDownloads != null) {
|
||||
params.addProperty("acceptDownloads", options.acceptDownloads ? "accept" : "deny");
|
||||
@@ -299,7 +300,7 @@ class BrowserImpl extends ChannelOwner implements Browser {
|
||||
this.tracePath = tracesDir;
|
||||
|
||||
for (BrowserContextImpl context : contexts) {
|
||||
context.tracing().setTracesDir(tracesDir);
|
||||
context.setTracesDir(tracesDir);
|
||||
browserType.playwright.selectors.contextsForSelectors.add(context);
|
||||
}
|
||||
}
|
||||
@@ -310,7 +311,7 @@ class BrowserImpl extends ChannelOwner implements Browser {
|
||||
// Note: when connecting to a browser, initial contexts arrive before `_browserType` is set,
|
||||
// and will be configured later in `ConnectToBrowserType`.
|
||||
if (browserType != null) {
|
||||
context.tracing().setTracesDir(tracePath);
|
||||
context.setTracesDir(tracePath);
|
||||
browserType.playwright.selectors.contextsForSelectors.add(context);
|
||||
}
|
||||
listeners.notify(EventType.CONTEXT, context);
|
||||
|
||||
@@ -29,6 +29,7 @@ import java.nio.file.Paths;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static com.microsoft.playwright.impl.Serialization.gson;
|
||||
import static com.microsoft.playwright.impl.Utils.addHttpCredentialsToProtocol;
|
||||
import static com.microsoft.playwright.impl.Utils.addToProtocol;
|
||||
import static com.microsoft.playwright.impl.Utils.convertType;
|
||||
|
||||
@@ -177,6 +178,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
|
||||
}
|
||||
}
|
||||
addToProtocol(params, options.clientCertificates);
|
||||
addHttpCredentialsToProtocol(params);
|
||||
params.remove("acceptDownloads");
|
||||
if (options.acceptDownloads != null) {
|
||||
params.addProperty("acceptDownloads", options.acceptDownloads ? "accept" : "deny");
|
||||
@@ -188,7 +190,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
|
||||
browser.connectToBrowserType(this, options.tracesDir);
|
||||
BrowserContextImpl context = connection.getExistingObject(json.getAsJsonObject("context").get("guid").getAsString());
|
||||
context.initializeHarFromOptions(harOptions);
|
||||
context.tracing().setTracesDir(options.tracesDir);
|
||||
context.setTracesDir(options.tracesDir);
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
@@ -328,7 +328,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
|
||||
|
||||
@Override
|
||||
public FrameLocator frameLocator(String selector) {
|
||||
return new FrameLocatorImpl(this, selector);
|
||||
return new FrameLocatorImpl(this, selector == null ? FrameLocatorImpl.ANY_FRAME_SELECTOR : selector);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,6 +27,9 @@ import static com.microsoft.playwright.impl.LocatorUtils.*;
|
||||
import static com.microsoft.playwright.impl.Utils.convertType;
|
||||
|
||||
class FrameLocatorImpl implements FrameLocator {
|
||||
// Created by page.frameLocator() / frame.frameLocator() without a selector: searches in any frame of the subtree.
|
||||
static final String ANY_FRAME_SELECTOR = "internal:control=any-frame";
|
||||
|
||||
private final FrameImpl frame;
|
||||
private final String frameSelector;
|
||||
|
||||
@@ -35,14 +38,28 @@ class FrameLocatorImpl implements FrameLocator {
|
||||
this.frameSelector = selector;
|
||||
}
|
||||
|
||||
private String childSelector(String selector) {
|
||||
if (ANY_FRAME_SELECTOR.equals(frameSelector)) {
|
||||
return frameSelector + " >> " + selector;
|
||||
}
|
||||
return frameSelector + " >> internal:control=enter-frame >> " + selector;
|
||||
}
|
||||
|
||||
private String nthSelector(int nth) {
|
||||
if (ANY_FRAME_SELECTOR.equals(frameSelector)) {
|
||||
throw new PlaywrightException("Selecting the nth frame is not allowed on frameLocator().");
|
||||
}
|
||||
return frameSelector + " >> nth=" + nth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FrameLocator first() {
|
||||
return new FrameLocatorImpl(frame, frameSelector + " >> nth=0");
|
||||
return new FrameLocatorImpl(frame, nthSelector(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public FrameLocatorImpl frameLocator(String selector) {
|
||||
return new FrameLocatorImpl(frame, frameSelector + " >> internal:control=enter-frame >> " + selector);
|
||||
return new FrameLocatorImpl(frame, childSelector(selector));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -112,12 +129,12 @@ class FrameLocatorImpl implements FrameLocator {
|
||||
|
||||
@Override
|
||||
public FrameLocator last() {
|
||||
return new FrameLocatorImpl(frame, frameSelector + " >> nth=-1");
|
||||
return new FrameLocatorImpl(frame, nthSelector(-1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Locator locator(String selector, LocatorOptions options) {
|
||||
return new LocatorImpl(frame, frameSelector + " >> internal:control=enter-frame >> " + selector, convertType(options, Locator.LocatorOptions.class));
|
||||
return new LocatorImpl(frame, childSelector(selector), convertType(options, Locator.LocatorOptions.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -131,7 +148,7 @@ class FrameLocatorImpl implements FrameLocator {
|
||||
|
||||
@Override
|
||||
public FrameLocator nth(int index) {
|
||||
return new FrameLocatorImpl(frame, frameSelector + " >> nth=" + index);
|
||||
return new FrameLocatorImpl(frame, nthSelector(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -285,6 +285,11 @@ class LocatorImpl implements Locator {
|
||||
return new LocatorImpl(frame, selector + " >> nth=0", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Locator visible() {
|
||||
return new LocatorImpl(frame, selector, null, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void focus(FocusOptions options) {
|
||||
if (options == null) {
|
||||
|
||||
@@ -88,6 +88,7 @@ public class PageImpl extends ChannelOwner implements Page {
|
||||
Map<EventType, String> result = new HashMap<>();
|
||||
result.put(EventType.CONSOLE, "console");
|
||||
result.put(EventType.DIALOG, "dialog");
|
||||
result.put(EventType.DIALOGCLOSED, "dialogClosed");
|
||||
result.put(EventType.REQUEST, "request");
|
||||
result.put(EventType.RESPONSE, "response");
|
||||
result.put(EventType.REQUESTFINISHED, "requestFinished");
|
||||
@@ -110,6 +111,7 @@ public class PageImpl extends ChannelOwner implements Page {
|
||||
CONSOLE,
|
||||
CRASH,
|
||||
DIALOG,
|
||||
DIALOGCLOSED,
|
||||
DOMCONTENTLOADED,
|
||||
DOWNLOAD,
|
||||
FILECHOOSER,
|
||||
@@ -303,6 +305,16 @@ public class PageImpl extends ChannelOwner implements Page {
|
||||
listeners.remove(EventType.DIALOG, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialogClosed(Consumer<Dialog> handler) {
|
||||
listeners.add(EventType.DIALOGCLOSED, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offDialogClosed(Consumer<Dialog> handler) {
|
||||
listeners.remove(EventType.DIALOGCLOSED, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDOMContentLoaded(Consumer<Page> handler) {
|
||||
listeners.add(EventType.DOMCONTENTLOADED, handler);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.microsoft.playwright.impl;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.microsoft.playwright.PlaywrightException;
|
||||
import com.microsoft.playwright.Tracing;
|
||||
@@ -63,11 +64,28 @@ class TracingImpl extends ChannelOwner implements Tracing {
|
||||
isTracing = false;
|
||||
connection.setIsTracing(false);
|
||||
}
|
||||
JsonObject params = new JsonObject();
|
||||
|
||||
List<String> capturedAdditionalSources = new ArrayList<>(additionalSources);
|
||||
additionalSources.clear();
|
||||
String stacksId = this.stacksId;
|
||||
this.stacksId = null;
|
||||
|
||||
try {
|
||||
saveChunk(path, stacksId, capturedAdditionalSources);
|
||||
} catch (RuntimeException e) {
|
||||
// Release the stack session even on failure, otherwise later traces keep appending to it.
|
||||
if (stacksId != null) {
|
||||
try {
|
||||
connection.localUtils().traceDiscarded(stacksId);
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveChunk(Path path, String stacksId, List<String> capturedAdditionalSources) {
|
||||
JsonObject params = new JsonObject();
|
||||
// Not interested in artifacts.
|
||||
if (path == null) {
|
||||
params.addProperty("mode", "discard");
|
||||
@@ -97,7 +115,16 @@ class TracingImpl extends ChannelOwner implements Tracing {
|
||||
return;
|
||||
}
|
||||
ArtifactImpl artifact = connection.getExistingObject(json.getAsJsonObject("artifact").get("guid").getAsString());
|
||||
artifact.saveAs(path);
|
||||
try {
|
||||
artifact.saveAs(path);
|
||||
} catch (RuntimeException e) {
|
||||
// Delete the artifact best-effort, the save error is the one to surface.
|
||||
try {
|
||||
artifact.delete();
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
artifact.delete();
|
||||
|
||||
connection.localUtils.zip(path, new JsonArray(), stacksId, true, includeSources, capturedAdditionalSources);
|
||||
@@ -159,19 +186,32 @@ class TracingImpl extends ChannelOwner implements Tracing {
|
||||
if (options == null) {
|
||||
options = new StartOptions();
|
||||
}
|
||||
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
|
||||
includeSources = options.sources != null && options.sources;
|
||||
if (includeSources) {
|
||||
params.addProperty("sources", true);
|
||||
}
|
||||
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
|
||||
params.remove("sources");
|
||||
params.remove("title");
|
||||
renameProperty(params, "snapshots", "snapshotDom");
|
||||
renameProperty(params, "ariaSnapshots", "snapshotAria");
|
||||
renameProperty(params, "screenSnapshots", "snapshotScreen");
|
||||
renameProperty(params, "screenshots", "screencast");
|
||||
sendMessage("tracingStart", params, NO_TIMEOUT);
|
||||
tracingStartChunk(options.name, options.title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(StopOptions options) {
|
||||
stopChunkImpl(options == null ? null : options.path);
|
||||
sendMessage("tracingStop");
|
||||
try {
|
||||
stopChunkImpl(options == null ? null : options.path);
|
||||
} finally {
|
||||
sendMessage("tracingStop");
|
||||
}
|
||||
}
|
||||
|
||||
private static void renameProperty(JsonObject params, String from, String to) {
|
||||
JsonElement value = params.remove(from);
|
||||
if (value != null) {
|
||||
params.add(to, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.microsoft.playwright.impl;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.microsoft.playwright.PlaywrightException;
|
||||
import com.microsoft.playwright.options.ClientCertificate;
|
||||
@@ -374,6 +375,23 @@ public class Utils {
|
||||
params.add("clientCertificates", clientCertificates);
|
||||
}
|
||||
|
||||
// Mirrors toHttpCredentialsProtocol() in the JS client: the protocol always takes a list.
|
||||
static void addHttpCredentialsToProtocol(JsonObject params) {
|
||||
JsonElement credentials = params.remove("httpCredentials");
|
||||
if (credentials == null) {
|
||||
return;
|
||||
}
|
||||
JsonArray list = new JsonArray();
|
||||
if (credentials.isJsonArray()) {
|
||||
list.addAll(credentials.getAsJsonArray());
|
||||
} else {
|
||||
list.add(credentials);
|
||||
}
|
||||
if (list.size() > 0) {
|
||||
params.add("httpCredentials", list);
|
||||
}
|
||||
}
|
||||
|
||||
private static String base64Buffer(byte[] bytes, Path path) throws IOException {
|
||||
if (path != null) {
|
||||
bytes = readAllBytes(path);
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.microsoft.playwright.options.HttpCredentials;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@@ -141,4 +142,60 @@ public class TestBrowserContextCredentials extends TestBase {
|
||||
assertEquals(401, response.status());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithASingleCredentialInAnArray() {
|
||||
server.setAuth("/empty.html", "user", "pass");
|
||||
try (BrowserContext context = browser.newContext(new Browser.NewContextOptions()
|
||||
.setHttpCredentials(asList(new HttpCredentials("user", "pass"))))) {
|
||||
Page page = context.newPage();
|
||||
Response response = page.navigate(server.EMPTY_PAGE);
|
||||
assertEquals(200, response.status());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithMultipleCredentialsForDifferentOrigins() {
|
||||
server.setAuth("/empty.html", "user1", "pass1");
|
||||
try (BrowserContext context = browser.newContext(new Browser.NewContextOptions()
|
||||
.setHttpCredentials(asList(
|
||||
new HttpCredentials("user1", "pass1").setOrigin(server.PREFIX),
|
||||
new HttpCredentials("user2", "pass2").setOrigin(server.CROSS_PROCESS_PREFIX))))) {
|
||||
Page page = context.newPage();
|
||||
Response response1 = page.navigate(server.EMPTY_PAGE);
|
||||
assertEquals(200, response1.status());
|
||||
// Wrong credentials are picked for the other origin.
|
||||
Response response2 = page.navigate(server.CROSS_PROCESS_PREFIX + "/empty.html");
|
||||
assertEquals(401, response2.status());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFallBackToCredentialsWithoutOrigin() {
|
||||
server.setAuth("/empty.html", "user", "pass");
|
||||
try (BrowserContext context = browser.newContext(new Browser.NewContextOptions()
|
||||
.setHttpCredentials(asList(
|
||||
new HttpCredentials("user2", "pass2").setOrigin(server.CROSS_PROCESS_PREFIX),
|
||||
new HttpCredentials("user", "pass"))))) {
|
||||
Page page = context.newPage();
|
||||
Response response1 = page.navigate(server.EMPTY_PAGE);
|
||||
assertEquals(200, response1.status());
|
||||
// First matching entry has wrong credentials for this origin.
|
||||
Response response2 = page.navigate(server.CROSS_PROCESS_PREFIX + "/empty.html");
|
||||
assertEquals(401, response2.status());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseTheFirstMatchingCredential() {
|
||||
server.setAuth("/empty.html", "user", "pass");
|
||||
try (BrowserContext context = browser.newContext(new Browser.NewContextOptions()
|
||||
.setHttpCredentials(asList(
|
||||
new HttpCredentials("wrong", "wrong"),
|
||||
new HttpCredentials("user", "pass").setOrigin(server.PREFIX))))) {
|
||||
Page page = context.newPage();
|
||||
Response response = page.navigate(server.EMPTY_PAGE);
|
||||
assertEquals(401, response.status());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,4 +272,21 @@ public class TestBrowserContextEvents extends TestBase {
|
||||
assertEquals(page.mainFrame(), detached[0].parentFrame());
|
||||
}
|
||||
|
||||
@Test
|
||||
void dialogClosedEventShouldWork() {
|
||||
Dialog[] dialog = {null};
|
||||
Dialog[] closedOnContext = {null};
|
||||
Dialog[] closedOnPage = {null};
|
||||
context.onDialogClosed(d -> closedOnContext[0] = d);
|
||||
page.onDialogClosed(d -> closedOnPage[0] = d);
|
||||
context.onDialog(d -> {
|
||||
dialog[0] = d;
|
||||
d.accept("hello");
|
||||
});
|
||||
Object result = page.evaluate("prompt('hey?')");
|
||||
assertEquals("hello", result);
|
||||
context.waitForCondition(() -> closedOnContext[0] != null && closedOnPage[0] != null);
|
||||
assertEquals(dialog[0], closedOnContext[0]);
|
||||
assertEquals(dialog[0], closedOnPage[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +246,6 @@ public class TestBrowserContextFetch extends TestBase {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldWorkWithHttpCredentials() throws ExecutionException, InterruptedException {
|
||||
server.setAuth("/empty.html", "user", "pass");
|
||||
@@ -926,4 +925,19 @@ public class TestBrowserContextFetch extends TestBase {
|
||||
assertEquals("Hello!", response.text());
|
||||
assertEquals(4, requestCount[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportMultipleHttpCredentials() {
|
||||
server.setAuth("/empty.html", "user1", "pass1");
|
||||
try (BrowserContext context = browser.newContext(new Browser.NewContextOptions()
|
||||
.setHttpCredentials(asList(
|
||||
new HttpCredentials("user1", "pass1").setOrigin(server.PREFIX),
|
||||
new HttpCredentials("user2", "pass2").setOrigin(server.CROSS_PROCESS_PREFIX))))) {
|
||||
APIResponse response1 = context.request().get(server.EMPTY_PAGE);
|
||||
assertEquals(200, response1.status());
|
||||
// Wrong credentials are picked for the other origin.
|
||||
APIResponse response2 = context.request().get(server.CROSS_PROCESS_PREFIX + "/empty.html");
|
||||
assertEquals(401, response2.status());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ package com.microsoft.playwright;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
@@ -25,6 +27,7 @@ import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static com.microsoft.playwright.Utils.assertJsonEquals;
|
||||
@@ -285,4 +288,78 @@ public class TestBrowserContextStorageState extends TestBase {
|
||||
PlaywrightException.class, () -> context.setStorageState(file));
|
||||
assertTrue(e.getMessage().contains("Failed to read storage state from file"), e.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRoundTripOPFS(@TempDir Path tempDir) throws IOException {
|
||||
Assumptions.assumeFalse(isWebKit(), "OPFS is unavailable in non-persistent WebKit contexts");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
page.evaluate("async () => {\n" +
|
||||
" const root = await navigator.storage.getDirectory();\n" +
|
||||
" const nested = await root.getDirectoryHandle('nested', { create: true });\n" +
|
||||
" await nested.getDirectoryHandle('empty', { create: true });\n" +
|
||||
" const binary = await nested.getFileHandle('data.bin', { create: true });\n" +
|
||||
" const binaryWritable = await binary.createWritable();\n" +
|
||||
" await binaryWritable.write(new Uint8Array([0, 1, 2, 255]));\n" +
|
||||
" await binaryWritable.close();\n" +
|
||||
" const text = await root.getFileHandle('hello.txt', { create: true });\n" +
|
||||
" const textWritable = await text.createWritable();\n" +
|
||||
" await textWritable.write('Hello, world!');\n" +
|
||||
" await textWritable.close();\n" +
|
||||
"}");
|
||||
|
||||
assertJsonEquals("{\"cookies\":[],\"origins\":[]}", JsonParser.parseString(context.storageState()));
|
||||
|
||||
Path path = tempDir.resolve("storage-state.json");
|
||||
String storageState = context.storageState(new BrowserContext.StorageStateOptions().setPath(path).setOpfs(true));
|
||||
String expected = "{\"cookies\":[],\"origins\":[{\n" +
|
||||
" \"origin\": \"" + server.PREFIX + "\",\n" +
|
||||
" \"localStorage\": [],\n" +
|
||||
" \"opfs\": [\n" +
|
||||
" { \"path\": \"hello.txt\", \"type\": \"file\", \"base64\": \"SGVsbG8sIHdvcmxkIQ==\" },\n" +
|
||||
" { \"path\": \"nested\", \"type\": \"directory\" },\n" +
|
||||
" { \"path\": \"nested/data.bin\", \"type\": \"file\", \"base64\": \"AAEC/w==\" },\n" +
|
||||
" { \"path\": \"nested/empty\", \"type\": \"directory\" }\n" +
|
||||
" ]\n" +
|
||||
"}]}";
|
||||
assertJsonEquals(expected, JsonParser.parseString(storageState));
|
||||
assertJsonEquals(expected, JsonParser.parseString(new String(Files.readAllBytes(path), StandardCharsets.UTF_8)));
|
||||
assertJsonEquals(expected, JsonParser.parseString(context.request().storageState(new APIRequestContext.StorageStateOptions().setOpfs(true))));
|
||||
|
||||
try (BrowserContext context2 = browser.newContext(new Browser.NewContextOptions().setStorageStatePath(path))) {
|
||||
checkOPFSContext(context2, expected);
|
||||
}
|
||||
|
||||
try (BrowserContext context3 = browser.newContext()) {
|
||||
Page page3 = context3.newPage();
|
||||
page3.navigate(server.EMPTY_PAGE);
|
||||
page3.evaluate("async () => {\n" +
|
||||
" const root = await navigator.storage.getDirectory();\n" +
|
||||
" await root.getFileHandle('stale.txt', { create: true });\n" +
|
||||
"}");
|
||||
context3.setStorageState(path);
|
||||
checkOPFSContext(context3, expected);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkOPFSContext(BrowserContext context, String expectedStorageState) {
|
||||
assertJsonEquals(expectedStorageState, JsonParser.parseString(context.storageState(new BrowserContext.StorageStateOptions().setOpfs(true))));
|
||||
Page checkPage = context.newPage();
|
||||
checkPage.navigate(server.EMPTY_PAGE);
|
||||
Object result = checkPage.evaluate("async () => {\n" +
|
||||
" const root = await navigator.storage.getDirectory();\n" +
|
||||
" const hello = await (await root.getFileHandle('hello.txt')).getFile();\n" +
|
||||
" const nested = await root.getDirectoryHandle('nested');\n" +
|
||||
" const data = await (await nested.getFileHandle('data.bin')).getFile();\n" +
|
||||
" const empty = await nested.getDirectoryHandle('empty');\n" +
|
||||
" const emptyEntries = [];\n" +
|
||||
" for await (const name of empty.keys())\n" +
|
||||
" emptyEntries.push(name);\n" +
|
||||
" return {\n" +
|
||||
" text: await hello.text(),\n" +
|
||||
" bytes: [...new Uint8Array(await data.arrayBuffer())],\n" +
|
||||
" empty: emptyEntries,\n" +
|
||||
" };\n" +
|
||||
"}");
|
||||
assertJsonEquals("{\"text\":\"Hello, world!\",\"bytes\":[0,1,2,255],\"empty\":[]}", result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +193,6 @@ public class TestGlobalFetch extends TestBase {
|
||||
assertTrue(e.getMessage().contains("Timeout 100ms exceeded"), e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldPropagateExtraHttpHeadersWithRedirects() throws ExecutionException, InterruptedException {
|
||||
server.setRedirect("/a/redirect1", "/b/c/redirect2");
|
||||
@@ -659,4 +658,42 @@ public class TestGlobalFetch extends TestBase {
|
||||
APIResponse response = request.get(server.EMPTY_PAGE);
|
||||
assertEquals(404, response.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportMultipleHttpCredentials() {
|
||||
server.setAuth("/empty.html", "user1", "pass1");
|
||||
APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions()
|
||||
.setHttpCredentials(asList(
|
||||
new HttpCredentials("user1", "pass1").setOrigin(server.PREFIX),
|
||||
new HttpCredentials("user2", "pass2").setOrigin(server.CROSS_PROCESS_PREFIX))));
|
||||
APIResponse response1 = request.get(server.EMPTY_PAGE);
|
||||
assertEquals(200, response1.status());
|
||||
// Wrong credentials are picked for the other origin.
|
||||
APIResponse response2 = request.get(server.CROSS_PROCESS_PREFIX + "/empty.html");
|
||||
assertEquals(401, response2.status());
|
||||
request.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportHTTPCredentialsSendWithMultipleHttpCredentials() throws InterruptedException, ExecutionException {
|
||||
APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions()
|
||||
.setHttpCredentials(asList(
|
||||
new HttpCredentials("user1", "pass1").setOrigin(server.PREFIX).setSend(HttpCredentialsSend.ALWAYS),
|
||||
new HttpCredentials("user2", "pass2").setOrigin(server.CROSS_PROCESS_PREFIX).setSend(HttpCredentialsSend.UNAUTHORIZED))));
|
||||
{
|
||||
Future<Server.Request> serverRequest = server.futureRequest("/empty.html");
|
||||
APIResponse response = request.get(server.EMPTY_PAGE);
|
||||
assertEquals("Basic " + Base64.getEncoder().encodeToString("user1:pass1".getBytes()),
|
||||
serverRequest.get().headers.getFirst("authorization"));
|
||||
assertEquals(200, response.status());
|
||||
}
|
||||
{
|
||||
Future<Server.Request> serverRequest = server.futureRequest("/empty.html");
|
||||
APIResponse response = request.get(server.CROSS_PROCESS_PREFIX + "/empty.html");
|
||||
// This origin has send: 'unauthorized', so credentials are not sent proactively.
|
||||
assertNull(serverRequest.get().headers.get("authorization"));
|
||||
assertEquals(200, response.status());
|
||||
}
|
||||
request.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ public class TestJavaSourceLocationInConstructor extends TestBase {
|
||||
context.tracing().stop(new Tracing.StopOptions().setPath(trace));
|
||||
|
||||
Map<String, byte[]> entries = Utils.parseZip(trace);
|
||||
Map<String, byte[]> sources = entries.entrySet().stream().filter(e -> e.getKey().endsWith(".txt")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
Map<String, byte[]> sources = entries.entrySet().stream().filter(e -> e.getKey().startsWith("src/")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
assertEquals(1, sources.size());
|
||||
|
||||
String path = getClass().getName().replace('.', File.separatorChar);
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* 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.assertions.LocatorAssertions;
|
||||
import com.microsoft.playwright.options.AriaRole;
|
||||
import com.microsoft.playwright.options.WaitForSelectorState;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opentest4j.AssertionFailedError;
|
||||
|
||||
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class TestLocatorAnyFrame extends TestBase {
|
||||
private static void routePage(Page page, String url, String body) {
|
||||
page.route("**/" + url, route -> route.fulfill(new Route.FulfillOptions().setBody(body).setContentType("text/html")));
|
||||
}
|
||||
|
||||
private static void waitForAllFrames(Page page, int frameCount, String selector) {
|
||||
// Wait for all child frames to load their content, so that the search
|
||||
// deterministically sees elements in all of them.
|
||||
page.waitForCondition(() -> page.frames().size() == frameCount);
|
||||
for (Frame frame : page.frames()) {
|
||||
if (frame != page.mainFrame()) {
|
||||
frame.waitForSelector(selector, new Frame.WaitForSelectorOptions().setState(WaitForSelectorState.ATTACHED));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldClickAButtonInsideAnIframe() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<button onclick=\"window.__clicked = true\">Click me</button>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
page.frameLocator().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Click me")).click();
|
||||
assertEquals(true, page.frames().get(1).evaluate("() => window.__clicked"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldClickAButtonInTheMainFrame() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe><button onclick=\"window.__clicked = true\">Click me</button>");
|
||||
routePage(page, "a.html", "<div>No buttons here</div>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
page.frameLocator().locator("button").click();
|
||||
assertEquals(true, page.evaluate("() => window.__clicked"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailClickWhenElementsMatchInMultipleFrames() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe><iframe src=\"b.html\"></iframe>");
|
||||
routePage(page, "a.html", "<button>one</button>");
|
||||
routePage(page, "b.html", "<button>two</button>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
waitForAllFrames(page, 3, "button");
|
||||
PlaywrightException e = assertThrows(PlaywrightException.class,
|
||||
() -> page.frameLocator().locator("button").click(new Locator.ClickOptions().setTimeout(3000)));
|
||||
assertTrue(e.getMessage().contains("frameLocator() matched elements in multiple frames"), e.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailClickUponStrictModeViolationInsideASingleFrame() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<button>one</button><button>two</button>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
waitForAllFrames(page, 2, "button");
|
||||
PlaywrightException e = assertThrows(PlaywrightException.class,
|
||||
() -> page.frameLocator().locator("button").click(new Locator.ClickOptions().setTimeout(3000)));
|
||||
assertTrue(e.getMessage().contains("strict mode violation"), e.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTimeOutOnClickWhenThereAreNoMatches() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<div>Nothing here</div>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
PlaywrightException e = assertThrows(PlaywrightException.class,
|
||||
() -> page.frameLocator().locator("button").click(new Locator.ClickOptions().setTimeout(1000)));
|
||||
assertTrue(e.getMessage().contains("Timeout 1000ms exceeded"), e.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCountElementsInASingleFrame() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<div>1</div><div>2</div><div>3</div>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
waitForAllFrames(page, 2, "div");
|
||||
assertEquals(3, page.frameLocator().locator("div").count());
|
||||
assertEquals(0, page.frameLocator().locator("button").count());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailCountWhenElementsMatchInMultipleFrames() {
|
||||
routePage(page, "empty.html", "<div>main</div><iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<div>child</div>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
waitForAllFrames(page, 2, "div");
|
||||
PlaywrightException e = assertThrows(PlaywrightException.class, () -> page.frameLocator().locator("div").count());
|
||||
assertTrue(e.getMessage().contains("frameLocator() matched elements in multiple frames"), e.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportHasCount() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<span>one</span><span>two</span>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
assertThat(page.frameLocator().locator("span")).hasCount(2);
|
||||
assertThat(page.frameLocator().locator("button")).hasCount(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWaitForAFrameToAppearWithHasCount() {
|
||||
routePage(page, "empty.html", "<div>No frames yet</div>");
|
||||
routePage(page, "a.html", "<span>one</span><span>two</span>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
page.evaluate("() => {\n" +
|
||||
" setTimeout(() => {\n" +
|
||||
" const iframe = document.createElement('iframe');\n" +
|
||||
" iframe.src = 'a.html';\n" +
|
||||
" document.body.appendChild(iframe);\n" +
|
||||
" }, 500);\n" +
|
||||
"}");
|
||||
assertThat(page.frameLocator().locator("span")).hasCount(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailHasCountWhenElementsMatchInMultipleFrames() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe><iframe src=\"b.html\"></iframe>");
|
||||
routePage(page, "a.html", "<span>one</span>");
|
||||
routePage(page, "b.html", "<span>two</span>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
waitForAllFrames(page, 3, "span");
|
||||
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> assertThat(page.frameLocator().locator("span"))
|
||||
.hasCount(2, new LocatorAssertions.HasCountOptions().setTimeout(3000)));
|
||||
assertTrue(e.getMessage().contains("frameLocator() matched elements in multiple frames"), e.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportHasText() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<div>Hello iframe</div>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
assertThat(page.frameLocator().locator("div")).hasText("Hello iframe");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportEvaluate() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<button>one</button>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
assertEquals("one", page.frameLocator().locator("button").evaluate("e => e.textContent"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportFirstLastNth() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<span>one</span><span>two</span><span>three</span>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
assertThat(page.frameLocator().locator("span").first()).hasText("one");
|
||||
assertThat(page.frameLocator().locator("span").last()).hasText("three");
|
||||
assertThat(page.frameLocator().locator("span").nth(1)).hasText("two");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotAllowNthFrameOnAnyFrameLocator() {
|
||||
PlaywrightException e = assertThrows(PlaywrightException.class, () -> page.frameLocator().first());
|
||||
assertTrue(e.getMessage().contains("Selecting the nth frame is not allowed on frameLocator()"), e.getMessage());
|
||||
assertThrows(PlaywrightException.class, () -> page.frameLocator().last());
|
||||
assertThrows(PlaywrightException.class, () -> page.frameLocator().nth(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportTwoFrameLocators() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<iframe id=\"x\" src=\"b.html\"></iframe>");
|
||||
routePage(page, "b.html", "<iframe id=\"y\" src=\"c.html\"></iframe><button>decoy</button>");
|
||||
routePage(page, "c.html", "<button>bottom</button>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
assertThat(page.frameLocator().frameLocator("#x").frameLocator("#y").locator("button")).hasText("bottom");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportLocatorBeforeFrameLocator() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<section><iframe src=\"b.html\"></iframe></section><iframe src=\"c.html\"></iframe>");
|
||||
routePage(page, "b.html", "<button>in-section</button>");
|
||||
routePage(page, "c.html", "<button>outside</button>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
assertThat(page.frameLocator().locator("section").frameLocator("iframe").locator("button")).hasText("in-section");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportOwnerOfAFrameLocator() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<iframe id=\"target\" src=\"b.html\"></iframe>");
|
||||
routePage(page, "b.html", "<button>inside</button>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
assertEquals("target", page.frameLocator().frameLocator("#target").owner().getAttribute("id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportContentFrameAfterAnyFrameLocator() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<iframe id=\"my-frame\" src=\"b.html\"></iframe>");
|
||||
routePage(page, "b.html", "<button>inside</button>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
assertThat(page.frameLocator().locator("#my-frame").contentFrame().getByRole(AriaRole.BUTTON)).hasText("inside");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkOnFrame() {
|
||||
routePage(page, "empty.html", "<iframe src=\"a.html\"></iframe>");
|
||||
routePage(page, "a.html", "<iframe src=\"b.html\"></iframe>");
|
||||
routePage(page, "b.html", "<button>deep</button>");
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
page.waitForCondition(() -> page.frames().size() == 3);
|
||||
assertThat(page.frames().get(1).frameLocator().locator("button")).hasText("deep");
|
||||
}
|
||||
}
|
||||
@@ -1070,7 +1070,7 @@ public class TestLocatorAssertions extends TestBase {
|
||||
Locator locator = page.locator("div");
|
||||
PlaywrightAssertions.setDefaultAssertionTimeout(1000);
|
||||
AssertionFailedError exception = assertThrows(AssertionFailedError.class, () -> assertThat(locator).hasText("foo"));
|
||||
assertTrue(exception.getMessage().contains("Assert \"hasText\" with timeout 1000ms"), exception.getMessage());
|
||||
assertTrue(exception.getMessage().contains("Assert \"hasText\" locator(\"div\") with timeout 1000ms"), exception.getMessage());
|
||||
// Restore default.
|
||||
PlaywrightAssertions.setDefaultAssertionTimeout(5_000);
|
||||
}
|
||||
@@ -1119,7 +1119,7 @@ public class TestLocatorAssertions extends TestBase {
|
||||
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> {
|
||||
assertThat(page.locator("div")).containsClass("does-not-exist", new ContainsClassOptions().setTimeout(1000));
|
||||
});
|
||||
assertTrue(e.getMessage().contains("Assert \"containsClass\" with timeout 1000ms"), e.getMessage());
|
||||
assertTrue(e.getMessage().contains("Assert \"containsClass\" locator(\"div\") with timeout 1000ms"), e.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1137,6 +1137,6 @@ public class TestLocatorAssertions extends TestBase {
|
||||
AssertionFailedError e = assertThrows(AssertionFailedError.class, () -> {
|
||||
assertThat(page.locator("div")).containsClass(asList("foo", "bar", "baz"), new ContainsClassOptions().setTimeout(1000));
|
||||
});
|
||||
assertTrue(e.getMessage().contains("Assert \"containsClass\" with timeout 1000ms"), e.getMessage());
|
||||
assertTrue(e.getMessage().contains("Assert \"containsClass\" locator(\"div\") with timeout 1000ms"), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ public class TestLocatorAssertions2 extends TestBase {
|
||||
AssertionFailedError e = assertThrows(AssertionFailedError.class, () ->
|
||||
assertThat(locator).isChecked(new LocatorAssertions.IsCheckedOptions().setIndeterminate(true).setTimeout(1000)));
|
||||
// TODO: should be "assertThat().isChecked() with timeout 1000ms"
|
||||
assertTrue(e.getMessage().contains("Assert \"isChecked\" with timeout 1000ms"), e.getMessage());
|
||||
assertTrue(e.getMessage().contains("Assert \"isChecked\" locator(\"input\") with timeout 1000ms"), e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -185,4 +185,20 @@ public class TestLocatorMisc extends TestBase{
|
||||
new Locator.WaitForFunctionOptions().setTimeout(500)));
|
||||
assertTrue(e.getMessage().contains("Timeout 500ms exceeded"), e.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSupportVisible() {
|
||||
page.setContent("<div>\n" +
|
||||
" <div class=\"item\" style=\"display: none\">Hidden data0</div>\n" +
|
||||
" <div class=\"item\">visible data1</div>\n" +
|
||||
" <div class=\"item\" style=\"display: none\">Hidden data1</div>\n" +
|
||||
" <div class=\"item\">visible data2</div>\n" +
|
||||
" <div class=\"item\" style=\"display: none\">Hidden data2</div>\n" +
|
||||
" <div class=\"item\">visible data3</div>\n" +
|
||||
"</div>");
|
||||
Locator locator = page.locator(".item").visible().nth(1);
|
||||
assertThat(locator).hasText("visible data2");
|
||||
assertThat(page.locator(".item").visible().getByText("data3")).hasText("visible data3");
|
||||
assertThat(page.locator(".item").visible()).hasCount(3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ package com.microsoft.playwright;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
@@ -124,4 +127,41 @@ public class TestPageDialog extends TestBase {
|
||||
assertEquals(true, page.evaluate("window._clicked"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFireDialogClosedWhenDialogIsAccepted() {
|
||||
List<Dialog> closed = new ArrayList<>();
|
||||
page.onDialogClosed(closed::add);
|
||||
Dialog[] opened = {null};
|
||||
page.onDialog(dialog -> {
|
||||
opened[0] = dialog;
|
||||
dialog.accept();
|
||||
});
|
||||
page.evaluate("alert('yo')");
|
||||
page.waitForCondition(() -> closed.size() == 1);
|
||||
assertEquals(opened[0], closed.get(0));
|
||||
// Perform some roundtrips to ensure the event does not fire twice.
|
||||
page.evaluate("1");
|
||||
page.evaluate("1");
|
||||
assertEquals(1, closed.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFireDialogClosedWhenDialogIsDismissed() {
|
||||
Dialog[] closed = {null};
|
||||
page.onDialogClosed(dialog -> closed[0] = dialog);
|
||||
page.onDialog(Dialog::dismiss);
|
||||
page.evaluate("confirm('boolean?')");
|
||||
page.waitForCondition(() -> closed[0] != null);
|
||||
assertEquals("confirm", closed[0].type());
|
||||
assertEquals("boolean?", closed[0].message());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFireDialogClosedForAutoDismissedDialogs() {
|
||||
Dialog[] closed = {null};
|
||||
page.onDialogClosed(dialog -> closed[0] = dialog);
|
||||
page.evaluate("alert('yo')");
|
||||
page.waitForCondition(() -> closed[0] != null);
|
||||
assertEquals("yo", closed[0].message());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package com.microsoft.playwright;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.microsoft.playwright.options.AriaRole;
|
||||
import com.microsoft.playwright.options.Location;
|
||||
import com.microsoft.playwright.options.MouseButton;
|
||||
@@ -26,11 +28,18 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class TestTracing extends TestBase {
|
||||
@@ -62,7 +71,7 @@ public class TestTracing extends TestBase {
|
||||
assertTrue(Files.exists(traceFile));
|
||||
TraceViewerPage.showTraceViewer(this.browserType, traceFile, traceViewer -> {
|
||||
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
|
||||
Pattern.compile("Navigate to \"/empty.html\""),
|
||||
Pattern.compile("Navigate.*/empty.html"),
|
||||
Pattern.compile("Set content"),
|
||||
Pattern.compile("Click"),
|
||||
Pattern.compile("Close")
|
||||
@@ -92,7 +101,7 @@ public class TestTracing extends TestBase {
|
||||
|
||||
TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
|
||||
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
|
||||
Pattern.compile("Navigate to \"/empty.html\""),
|
||||
Pattern.compile("Navigate.*/empty.html"),
|
||||
Pattern.compile("Set content"),
|
||||
Pattern.compile("Click")
|
||||
});
|
||||
@@ -154,7 +163,7 @@ public class TestTracing extends TestBase {
|
||||
|
||||
TraceViewerPage.showTraceViewer(this.browserType, trace, traceViewer -> {
|
||||
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
|
||||
Pattern.compile("Navigate to \"/empty.html\""),
|
||||
Pattern.compile("Navigate.*/empty.html"),
|
||||
Pattern.compile("Set content"),
|
||||
Pattern.compile("Click")
|
||||
});
|
||||
@@ -210,7 +219,7 @@ public class TestTracing extends TestBase {
|
||||
|
||||
TraceViewerPage.showTraceViewer(this.browserType, tempDir.resolve("trace1.zip"), traceViewer -> {
|
||||
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
|
||||
Pattern.compile("Navigate to \"/one-style.html\"")
|
||||
Pattern.compile("Navigate.*/one-style.html")
|
||||
});
|
||||
FrameLocator frame = traceViewer.snapshotFrame("Navigate", 0, false);
|
||||
assertThat(frame.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)");
|
||||
@@ -219,7 +228,7 @@ public class TestTracing extends TestBase {
|
||||
|
||||
TraceViewerPage.showTraceViewer(this.browserType, tempDir.resolve("trace2.zip"), traceViewer -> {
|
||||
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
|
||||
Pattern.compile("Navigate to \"/har.html\"")
|
||||
Pattern.compile("Navigate.*/har.html")
|
||||
});
|
||||
FrameLocator frame = traceViewer.snapshotFrame("Navigate", 0, false);
|
||||
assertThat(frame.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)");
|
||||
@@ -246,7 +255,7 @@ public class TestTracing extends TestBase {
|
||||
context.tracing().groupEnd();
|
||||
|
||||
TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
|
||||
assertThat(traceViewer.actionTitles()).containsText(new String[] {"actual", "Navigate to \"/empty.html\""});
|
||||
assertThat(traceViewer.actionTitles()).containsText(new Pattern[] {Pattern.compile("actual"), Pattern.compile("Navigate.*/empty.html")});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -270,7 +279,7 @@ public class TestTracing extends TestBase {
|
||||
traceViewer.expandAction("inner group 1");
|
||||
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
|
||||
Pattern.compile("outer group"),
|
||||
Pattern.compile("Navigate to \"data:"),
|
||||
Pattern.compile("Navigate.*data:"),
|
||||
Pattern.compile("inner group 1"),
|
||||
Pattern.compile("Click"),
|
||||
Pattern.compile("inner group 2"),
|
||||
@@ -356,7 +365,7 @@ public class TestTracing extends TestBase {
|
||||
|
||||
TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
|
||||
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
|
||||
Pattern.compile("Navigate to \"/empty.html\"")
|
||||
Pattern.compile("Navigate.*/empty.html")
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -375,7 +384,7 @@ public class TestTracing extends TestBase {
|
||||
|
||||
TraceViewerPage.showTraceViewer(this.browserType, traceFile1, traceViewer -> {
|
||||
assertThat(traceViewer.actionTitles()).hasText(new Pattern[] {
|
||||
Pattern.compile("Navigate to \"/empty.html\""),
|
||||
Pattern.compile("Navigate.*/empty.html"),
|
||||
Pattern.compile("Wait for load state \"load\""),
|
||||
});
|
||||
});
|
||||
@@ -391,4 +400,83 @@ public class TestTracing extends TestBase {
|
||||
assertTrue(content.contains("\"log\""), content);
|
||||
assertTrue(content.contains("/one-style.html"), content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRecoverTracingAfterAFailedStop(@TempDir Path tempDir) throws Exception {
|
||||
// https://github.com/microsoft/playwright/issues/42423
|
||||
context.tracing().start();
|
||||
// Saving fails: a parent of the destination is a file, not a directory.
|
||||
Path blocker = tempDir.resolve("blocker");
|
||||
Files.write(blocker, new byte[0]);
|
||||
assertThrows(PlaywrightException.class, () -> context.tracing().stop(
|
||||
new Tracing.StopOptions().setPath(blocker.resolve("trace1.zip"))));
|
||||
|
||||
// The failed stop must not wedge tracing for the rest of the context lifetime.
|
||||
context.tracing().start();
|
||||
page.navigate(server.PREFIX + "/input/button.html");
|
||||
page.click("button");
|
||||
Path trace = tempDir.resolve("trace2.zip");
|
||||
context.tracing().stop(new Tracing.StopOptions().setPath(trace));
|
||||
|
||||
List<JsonObject> events = traceEvents(Utils.parseZip(trace));
|
||||
assertEquals("context-options", events.get(0).get("type").getAsString());
|
||||
assertTrue(events.stream().anyMatch(e -> "before".equals(e.get("type").getAsString())
|
||||
&& "click".equals(e.get("method").getAsString())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCollectActionScreenshots(@TempDir Path tempDir) throws Exception {
|
||||
context.tracing().start(new Tracing.StartOptions().setScreenSnapshots(true));
|
||||
checkClickArtifacts(tempDir, "screenshot", "screenshots/", ".png",
|
||||
content -> assertTrue(content.length > 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCollectAriaSnapshots(@TempDir Path tempDir) throws Exception {
|
||||
context.tracing().start(new Tracing.StartOptions().setAriaSnapshots(true));
|
||||
checkClickArtifacts(tempDir, "aria-snapshot", "aria/", ".json",
|
||||
content -> assertTrue(new String(content, StandardCharsets.UTF_8).contains("Click target")));
|
||||
}
|
||||
|
||||
// Clicks a button while tracing and checks that before/action/after artifacts of the given type were recorded.
|
||||
private void checkClickArtifacts(Path tempDir, String eventType, String dir, String extension, Consumer<byte[]> checkContent) throws Exception {
|
||||
page.navigate(server.PREFIX + "/input/button.html");
|
||||
page.click("button");
|
||||
Path trace = tempDir.resolve("trace.zip");
|
||||
context.tracing().stop(new Tracing.StopOptions().setPath(trace));
|
||||
|
||||
Map<String, byte[]> resources = Utils.parseZip(trace);
|
||||
List<JsonObject> events = traceEvents(resources);
|
||||
String clickCallId = findCallId(events, "click");
|
||||
List<JsonObject> artifacts = events.stream().filter(e -> eventType.equals(e.get("type").getAsString())
|
||||
&& clickCallId.equals(e.get("callId").getAsString())).collect(Collectors.toList());
|
||||
assertEquals(asList("before", "action", "after"),
|
||||
artifacts.stream().map(e -> e.get("phase").getAsString()).collect(Collectors.toList()));
|
||||
for (JsonObject artifact : artifacts) {
|
||||
String file = artifact.get("file").getAsString();
|
||||
assertEquals(dir + clickCallId + "-" + artifact.get("phase").getAsString() + extension, file);
|
||||
assertTrue(resources.containsKey(file), file);
|
||||
checkContent.accept(resources.get(file));
|
||||
}
|
||||
}
|
||||
|
||||
private static List<JsonObject> traceEvents(Map<String, byte[]> resources) {
|
||||
List<JsonObject> events = new ArrayList<>();
|
||||
for (Map.Entry<String, byte[]> entry : resources.entrySet()) {
|
||||
if (!entry.getKey().endsWith(".trace")) {
|
||||
continue;
|
||||
}
|
||||
for (String line : new String(entry.getValue(), StandardCharsets.UTF_8).split("\n")) {
|
||||
if (!line.trim().isEmpty()) {
|
||||
events.add(JsonParser.parseString(line).getAsJsonObject());
|
||||
}
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
private static String findCallId(List<JsonObject> events, String method) {
|
||||
return events.stream().filter(e -> "before".equals(e.get("type").getAsString())
|
||||
&& method.equals(e.get("method").getAsString())).findFirst().get().get("callId").getAsString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.62.1
|
||||
1.63.0-alpha-1788460676000
|
||||
|
||||
+55
-20
@@ -406,6 +406,29 @@ class TypeRef extends Element {
|
||||
return supportedUnionTypes().size();
|
||||
}
|
||||
|
||||
// True for Object|Array<Object> unions where both refer to the same custom class.
|
||||
boolean isCustomClassOrListUnion() {
|
||||
if (customType == null || !isTypeUnion()) {
|
||||
return false;
|
||||
}
|
||||
List<JsonObject> types = supportedUnionTypes();
|
||||
if (types.size() != 2) {
|
||||
return false;
|
||||
}
|
||||
boolean hasObject = false;
|
||||
boolean hasList = false;
|
||||
for (JsonObject o : types) {
|
||||
String name = o.get("name").getAsString();
|
||||
if ("Object".equals(name)) {
|
||||
hasObject = true;
|
||||
} else if ("Array".equals(name) && o.has("templates") && o.getAsJsonArray("templates").size() == 1
|
||||
&& "Object".equals(o.getAsJsonArray("templates").get(0).getAsJsonObject().get("name").getAsString())) {
|
||||
hasList = true;
|
||||
}
|
||||
}
|
||||
return hasObject && hasList;
|
||||
}
|
||||
|
||||
String formatTypeFromUnion(int i) {
|
||||
JsonElement overloadedType = supportedUnionTypes().get(i);
|
||||
return convertBuiltinType(overloadedType.getAsJsonObject());
|
||||
@@ -898,32 +921,19 @@ class Field extends Element {
|
||||
}
|
||||
|
||||
void writeBuilderMethod(List<String> output, String offset, String parentClass) {
|
||||
if (type.customType == null && type.isTypeUnion()) {
|
||||
if (type.isTypeUnion() && (type.customType == null || type.isCustomClassOrListUnion())) {
|
||||
// Union of a custom class and its list, e.g. Object|Array<Object> for httpCredentials,
|
||||
// gets the convenience setter with the required fields in addition to the overloads.
|
||||
if (type.customType != null) {
|
||||
writeRequiredFieldsBuilderMethod(output, offset, parentClass);
|
||||
}
|
||||
for (int i = 0; i < type.unionSize(); i++) {
|
||||
writeGenericBuilderMethod(output, offset, parentClass, type.formatTypeFromUnion(i));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type.isCustomClass()) {
|
||||
TypeDefinition customType = topLevelTypes().get(type.customType);
|
||||
if (customType instanceof CustomClass) {
|
||||
CustomClass clazz = (CustomClass) customType;
|
||||
List<String> params = new ArrayList<>();
|
||||
List<String> args = new ArrayList<>();
|
||||
for (Field f : clazz.fields) {
|
||||
if (!f.isRequired()) {
|
||||
continue;
|
||||
}
|
||||
params.add(f.type.toJava() + " " + f.name);
|
||||
args.add(f.name);
|
||||
}
|
||||
if (!params.isEmpty()) {
|
||||
writeJavadoc(output, offset, comment());
|
||||
output.add(offset + "public " + parentClass + " set" + toTitle(name) + "(" + String.join(", ", params) + ") {");
|
||||
output.add(offset + " return set" + toTitle(name) + "(new " + type.toJava() + "(" + String.join(", ", args) + "));");
|
||||
output.add(offset + "}");
|
||||
}
|
||||
}
|
||||
writeRequiredFieldsBuilderMethod(output, offset, parentClass);
|
||||
}
|
||||
|
||||
if (isBrowserChannelOption()) {
|
||||
@@ -934,6 +944,31 @@ class Field extends Element {
|
||||
writeGenericBuilderMethod(output, offset, parentClass, type.toJava());
|
||||
}
|
||||
|
||||
// Convenience setter taking the required fields of the custom class, e.g. setHttpCredentials(username, password).
|
||||
private void writeRequiredFieldsBuilderMethod(List<String> output, String offset, String parentClass) {
|
||||
TypeDefinition customType = topLevelTypes().get(type.customType);
|
||||
if (!(customType instanceof CustomClass)) {
|
||||
return;
|
||||
}
|
||||
CustomClass clazz = (CustomClass) customType;
|
||||
List<String> params = new ArrayList<>();
|
||||
List<String> args = new ArrayList<>();
|
||||
for (Field f : clazz.fields) {
|
||||
if (!f.isRequired()) {
|
||||
continue;
|
||||
}
|
||||
params.add(f.type.toJava() + " " + f.name);
|
||||
args.add(f.name);
|
||||
}
|
||||
if (params.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
writeJavadoc(output, offset, comment());
|
||||
output.add(offset + "public " + parentClass + " set" + toTitle(name) + "(" + String.join(", ", params) + ") {");
|
||||
output.add(offset + " return set" + toTitle(name) + "(new " + type.customType + "(" + String.join(", ", args) + "));");
|
||||
output.add(offset + "}");
|
||||
}
|
||||
|
||||
private void writeGenericBuilderMethod(List<String> output, String offset, String parentClass, String paramType) {
|
||||
writeJavadoc(output, offset, comment());
|
||||
output.add(offset + "public " + parentClass + " set" + toTitle(name) + "(" + paramType + " " + name + ") {");
|
||||
|
||||
Reference in New Issue
Block a user