1
0
mirror of synced 2026-07-07 09:00:02 +00:00

chore: roll driver to 1.61.0-beta-1781285686000 (#1929)

This commit is contained in:
Yury Semikhatsky
2026-06-14 10:53:00 -07:00
committed by GitHub
parent e3f2f6fa5b
commit 423cbf4cc7
49 changed files with 1201 additions and 58 deletions
+3 -3
View File
@@ -10,9 +10,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->148.0.7778.96<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->150.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->149.0.7827.55<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.5<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->151.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
## Documentation
+1 -1
View File
@@ -10,7 +10,7 @@
<name>Playwright Client Examples</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<playwright.version>1.60.0</playwright.version>
<playwright.version>1.61.0</playwright.version>
</properties>
<dependencies>
<dependency>
@@ -55,6 +55,20 @@ public interface APIResponse {
* @since v1.16
*/
boolean ok();
/**
* Returns SSL and other security information. Resolves to {@code null} for non-HTTPS responses. For redirected requests,
* returns the information for the last request in the redirect chain.
*
* @since v1.61
*/
SecurityDetails securityDetails();
/**
* Returns the IP address and port of the server. Resolves to {@code null} if the server address is not available. For
* redirected requests, returns the information for the last request in the redirect chain.
*
* @since v1.61
*/
ServerAddr serverAddr();
/**
* Contains the status code of the response (e.g., 200 for a success).
*
@@ -563,6 +563,13 @@ public interface BrowserContext extends AutoCloseable {
* @since v1.45
*/
Clock clock();
/**
* Virtual WebAuthn authenticator for this context. Lets tests seed credentials and intercept {@code
* navigator.credentials.create()} / {@code navigator.credentials.get()} ceremonies.
*
* @since v1.61
*/
Credentials credentials();
/**
* Debugger allows to pause and resume the execution.
*
@@ -124,6 +124,10 @@ public interface BrowserType {
}
}
class ConnectOverCDPOptions {
/**
* If specified, browser artifacts (such as traces and downloads) are saved into this directory.
*/
public Path artifactsDir;
/**
* Additional HTTP headers to be sent with connect request. Optional.
*/
@@ -153,6 +157,13 @@ public interface BrowserType {
*/
public Double timeout;
/**
* If specified, browser artifacts (such as traces and downloads) are saved into this directory.
*/
public ConnectOverCDPOptions setArtifactsDir(Path artifactsDir) {
this.artifactsDir = artifactsDir;
return this;
}
/**
* Additional HTTP headers to be sent with connect request. Optional.
*/
@@ -0,0 +1,238 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* {@code Credentials} is a virtual WebAuthn authenticator scoped to a {@code BrowserContext}. It lets tests register
* passkeys and answer {@code navigator.credentials.create()} / {@code navigator.credentials.get()} ceremonies in the page,
* without a real authenticator or hardware security key.
*
* <p> There are two common ways to use it:
*
* <p> <strong>Usage: seed a known credential</strong>
* <pre>{@code
* BrowserContext context = browser.newContext();
*
* // A passkey your backend already provisioned for a test user.
* context.credentials().create("example.com", new Credentials.CreateOptions()
* .setId(knownCredentialId) // base64url
* .setUserHandle(knownUserHandle) // base64url
* .setPrivateKey(knownPrivateKey) // base64url PKCS#8 (DER)
* .setPublicKey(knownPublicKey)); // base64url SPKI (DER)
* context.credentials().install();
*
* Page page = context.newPage();
* page.navigate("https://example.com/login");
* // The page's navigator.credentials.get() is answered with the seeded passkey.
* }</pre>
*
* <p> <strong>Usage: capture a passkey, then reuse it</strong>
* <pre>{@code
* // setup test: let the app register a passkey, then save it.
* BrowserContext context = browser.newContext();
* context.credentials().install();
*
* Page page = context.newPage();
* page.navigate("https://example.com/register");
* page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Create a passkey")).click();
*
* // Read back the passkey the page registered — it includes the private key.
* VirtualCredential credential = context.credentials().get(
* new Credentials.GetOptions().setRpId("example.com")).get(0);
* Files.writeString(Paths.get("playwright/.auth/passkey.json"), new Gson().toJson(credential));
* }</pre>
* <pre>{@code
* // later test: seed the captured passkey so the app starts already enrolled.
* VirtualCredential credential = new Gson().fromJson(
* Files.readString(Paths.get("playwright/.auth/passkey.json")), VirtualCredential.class);
* BrowserContext context = browser.newContext();
* context.credentials().create(credential.rpId, new Credentials.CreateOptions()
* .setId(credential.id)
* .setUserHandle(credential.userHandle)
* .setPrivateKey(credential.privateKey)
* .setPublicKey(credential.publicKey));
* context.credentials().install();
*
* Page page = context.newPage();
* page.navigate("https://example.com/login");
* // navigator.credentials.get() resolves the captured passkey — already signed in.
* }</pre>
*
* <p> <strong>Defaults</strong>
*/
public interface Credentials {
class CreateOptions {
/**
* Base64url-encoded credential id. Auto-generated if omitted.
*/
public String id;
/**
* Base64url-encoded PKCS#8 (DER) private key. Auto-generated if omitted.
*/
public String privateKey;
/**
* Base64url-encoded SPKI (DER) public key. Auto-generated if omitted.
*/
public String publicKey;
/**
* Base64url-encoded user handle. Auto-generated if omitted.
*/
public String userHandle;
/**
* Base64url-encoded credential id. Auto-generated if omitted.
*/
public CreateOptions setId(String id) {
this.id = id;
return this;
}
/**
* Base64url-encoded PKCS#8 (DER) private key. Auto-generated if omitted.
*/
public CreateOptions setPrivateKey(String privateKey) {
this.privateKey = privateKey;
return this;
}
/**
* Base64url-encoded SPKI (DER) public key. Auto-generated if omitted.
*/
public CreateOptions setPublicKey(String publicKey) {
this.publicKey = publicKey;
return this;
}
/**
* Base64url-encoded user handle. Auto-generated if omitted.
*/
public CreateOptions setUserHandle(String userHandle) {
this.userHandle = userHandle;
return this;
}
}
class GetOptions {
/**
* Only return the credential with this base64url-encoded id.
*/
public String id;
/**
* Only return credentials for this relying party id.
*/
public String rpId;
/**
* Only return the credential with this base64url-encoded id.
*/
public GetOptions setId(String id) {
this.id = id;
return this;
}
/**
* Only return credentials for this relying party id.
*/
public GetOptions setRpId(String rpId) {
this.rpId = rpId;
return this;
}
}
/**
* Installs the virtual WebAuthn authenticator into the context, overriding {@code navigator.credentials.create()} and
* {@code navigator.credentials.get()} in all current and future pages. Call this before the page first touches {@code
* navigator.credentials}.
*
* <p> Required: until {@link com.microsoft.playwright.Credentials#install Credentials.install()} is called, no interception is
* in place and the page sees the platform's native (or absent) WebAuthn behaviour. Seeding credentials with {@link
* com.microsoft.playwright.Credentials#create Credentials.create()} without installing populates the authenticator, but
* the page will never see those credentials.
*
* @since v1.61
*/
void install();
/**
* Seeds a virtual WebAuthn credential and returns it.
*
* <p> With only {@code rpId}, generates a fresh **ECDSA P-256** keypair, credential id and user handle. The seeded credential
* is discoverable (resident), so the page can resolve it from both username-then-passkey and usernameless passkey flows.
* The returned object carries the private and public keys, so it can be persisted to disk and re-seeded in a later test.
*
* <p> To **import a known credential**, supply all four of {@code id}, {@code userHandle}, {@code privateKey} and {@code
* publicKey} together.
*
* <p> Call {@link com.microsoft.playwright.Credentials#install Credentials.install()} before navigating to a page that uses
* WebAuthn.
*
* @param rpId Relying party id (typically the site's effective domain).
* @since v1.61
*/
default VirtualCredential create(String rpId) {
return create(rpId, null);
}
/**
* Seeds a virtual WebAuthn credential and returns it.
*
* <p> With only {@code rpId}, generates a fresh **ECDSA P-256** keypair, credential id and user handle. The seeded credential
* is discoverable (resident), so the page can resolve it from both username-then-passkey and usernameless passkey flows.
* The returned object carries the private and public keys, so it can be persisted to disk and re-seeded in a later test.
*
* <p> To **import a known credential**, supply all four of {@code id}, {@code userHandle}, {@code privateKey} and {@code
* publicKey} together.
*
* <p> Call {@link com.microsoft.playwright.Credentials#install Credentials.install()} before navigating to a page that uses
* WebAuthn.
*
* @param rpId Relying party id (typically the site's effective domain).
* @since v1.61
*/
VirtualCredential create(String rpId, CreateOptions options);
/**
* Removes a credential from the authenticator by its id. Works for any credential currently held — both those seeded with
* {@link com.microsoft.playwright.Credentials#create Credentials.create()} and those the page registered itself by calling
* {@code navigator.credentials.create()}.
*
* @param id Base64url-encoded credential id.
* @since v1.61
*/
void delete(String id);
/**
* Returns every credential currently held by the authenticator, optionally filtered by {@code rpId} or {@code id}. This
* includes both credentials seeded with {@link com.microsoft.playwright.Credentials#create Credentials.create()} and
* credentials the page registered itself by calling {@code navigator.credentials.create()}.
*
* <p> Each returned credential includes its private and public keys, so a passkey the app just registered can be saved and
* re-seeded into a later test with {@link com.microsoft.playwright.Credentials#create Credentials.create()} — see the
* second example in the class overview.
*
* @since v1.61
*/
default List<VirtualCredential> get() {
return get(null);
}
/**
* Returns every credential currently held by the authenticator, optionally filtered by {@code rpId} or {@code id}. This
* includes both credentials seeded with {@link com.microsoft.playwright.Credentials#create Credentials.create()} and
* credentials the page registered itself by calling {@code navigator.credentials.create()}.
*
* <p> Each returned credential includes its private and public keys, so a passkey the app just registered can be saved and
* re-seeded into a later test with {@link com.microsoft.playwright.Credentials#create Credentials.create()} — see the
* second example in the class overview.
*
* @since v1.61
*/
List<VirtualCredential> get(GetOptions options);
}
@@ -2965,7 +2965,7 @@ public interface Frame {
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Frame#evaluate
* Frame.evaluate()}:
* <pre>{@code
* ElementHandle bodyHandle = frame.evaluate("document.body");
* ElementHandle bodyHandle = frame.evaluateHandle("document.body");
* String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -3005,7 +3005,7 @@ public interface Frame {
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Frame#evaluate
* Frame.evaluate()}:
* <pre>{@code
* ElementHandle bodyHandle = frame.evaluate("document.body");
* ElementHandle bodyHandle = frame.evaluateHandle("document.body");
* String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -4529,7 +4529,7 @@ public interface Page extends AutoCloseable {
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Page#evaluate
* Page.evaluate()}:
* <pre>{@code
* ElementHandle bodyHandle = page.evaluate("document.body");
* ElementHandle bodyHandle = page.evaluateHandle("document.body");
* String html = (String) page.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -4571,7 +4571,7 @@ public interface Page extends AutoCloseable {
* <p> {@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Page#evaluate
* Page.evaluate()}:
* <pre>{@code
* ElementHandle bodyHandle = page.evaluate("document.body");
* ElementHandle bodyHandle = page.evaluateHandle("document.body");
* String html = (String) page.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
* bodyHandle.dispose();
* }</pre>
@@ -5808,6 +5808,18 @@ public interface Page extends AutoCloseable {
* @since v1.59
*/
void clearPageErrors();
/**
* Provides access to the page's {@code localStorage} for the current origin. See {@code WebStorage}.
*
* @since v1.61
*/
WebStorage localStorage();
/**
* Provides access to the page's {@code sessionStorage} for the current origin. See {@code WebStorage}.
*
* @since v1.61
*/
WebStorage sessionStorage();
/**
* Returns up to (currently) 200 last console messages from this page. See {@link
* com.microsoft.playwright.Page#onConsoleMessage Page.onConsoleMessage()} for more details.
@@ -7552,8 +7564,8 @@ public interface Page extends AutoCloseable {
* <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code
* TimeoutError}. Passing zero timeout disables this.
*
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Page#tap Page.tap()} the method will throw if {@code hasTouch} option of the browser
* context is false.
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Page#tap Page.tap()} will throw if the {@code hasTouch} option of the browser context is
* false.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used.
* @since v1.8
@@ -7575,8 +7587,8 @@ public interface Page extends AutoCloseable {
* <p> When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code
* TimeoutError}. Passing zero timeout disables this.
*
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Page#tap Page.tap()} the method will throw if {@code hasTouch} option of the browser
* context is false.
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Page#tap Page.tap()} will throw if the {@code hasTouch} option of the browser context is
* false.
*
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used.
* @since v1.8
@@ -38,6 +38,13 @@ public interface Screencast {
* The quality of the image, between 0-100.
*/
public Integer quality;
/**
* Specifies the dimensions of screencast frames. The actual frame is scaled to preserve the page's aspect ratio and may be
* smaller than these bounds. If a screencast is already active (e.g. started by tracing or video recording), the existing
* configuration takes precedence and the frame size may exceed these bounds or this option may be ignored. If not
* specified the size will be equal to page viewport scaled down to fit into 800×800.
*/
public Size size;
/**
* Callback that receives JPEG-encoded frame data along with the page viewport size at the time of capture.
@@ -60,6 +67,25 @@ public interface Screencast {
this.quality = quality;
return this;
}
/**
* Specifies the dimensions of screencast frames. The actual frame is scaled to preserve the page's aspect ratio and may be
* smaller than these bounds. If a screencast is already active (e.g. started by tracing or video recording), the existing
* configuration takes precedence and the frame size may exceed these bounds or this option may be ignored. If not
* specified the size will be equal to page viewport scaled down to fit into 800×800.
*/
public StartOptions setSize(int width, int height) {
return setSize(new Size(width, height));
}
/**
* Specifies the dimensions of screencast frames. The actual frame is scaled to preserve the page's aspect ratio and may be
* smaller than these bounds. If a screencast is already active (e.g. started by tracing or video recording), the existing
* configuration takes precedence and the frame size may exceed these bounds or this option may be ignored. If not
* specified the size will be equal to page viewport scaled down to fit into 800×800.
*/
public StartOptions setSize(Size size) {
this.size = size;
return this;
}
}
class ShowOverlayOptions {
/**
@@ -103,6 +129,11 @@ public interface Screencast {
}
}
class ShowActionsOptions {
/**
* Cursor decoration shown for pointer actions. {@code "pointer"} (the default) renders a mouse pointer that animates from
* the previous action point to the next one. {@code "none"} disables the cursor decoration.
*/
public ScreencastCursor cursor;
/**
* How long each annotation is displayed in milliseconds. Defaults to {@code 500}.
*/
@@ -116,6 +147,14 @@ public interface Screencast {
*/
public AnnotatePosition position;
/**
* Cursor decoration shown for pointer actions. {@code "pointer"} (the default) renders a mouse pointer that animates from
* the previous action point to the next one. {@code "none"} disables the cursor decoration.
*/
public ShowActionsOptions setCursor(ScreencastCursor cursor) {
this.cursor = cursor;
return this;
}
/**
* How long each annotation is displayed in milliseconds. Defaults to {@code 500}.
*/
@@ -22,6 +22,11 @@ public interface ScreencastFrame {
*/
byte[] data();
/**
* The timestamp of when the frame was presented by the browser, in milliseconds since the Unix epoch.
*/
double timestamp();
/**
* Width of the page viewport at the time the frame was captured.
*/
@@ -201,7 +201,8 @@ public interface Selectors {
* Defines custom attribute name to be used in {@link com.microsoft.playwright.Page#getByTestId Page.getByTestId()}. {@code
* data-testid} is used by default.
*
* @param attributeName Test id attribute name.
* @param attributeName Test id attribute name. To match elements with any of several attributes, pass them as a comma-separated list, e.g.
* {@code "data-pw,data-ti"}.
* @since v1.27
*/
void setTestIdAttribute(String attributeName);
@@ -28,8 +28,8 @@ public interface Touchscreen {
/**
* Dispatches a {@code touchstart} and {@code touchend} event with a single touch at the position ({@code x},{@code y}).
*
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Page#tap Page.tap()} the method will throw if {@code hasTouch} option of the browser
* context is false.
* <p> <strong>NOTE:</strong> {@link com.microsoft.playwright.Touchscreen#tap Touchscreen.tap()} will throw if the {@code hasTouch} option of the
* browser context is false.
*
* @param x X coordinate relative to the main frame's viewport in CSS pixels.
* @param y Y coordinate relative to the main frame's viewport in CSS pixels.
@@ -0,0 +1,73 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.*;
import java.util.*;
/**
* WebStorage exposes the page's {@code localStorage} or {@code sessionStorage} for the current origin via an async, <a
* href="https://developer.mozilla.org/en-US/docs/Web/API/Storage">browser-consistent</a> API.
*
* <p> Instances are accessed through {@link com.microsoft.playwright.Page#localStorage Page.localStorage()} and {@link
* com.microsoft.playwright.Page#sessionStorage Page.sessionStorage()}.
* <pre>{@code
* page.navigate("https://example.com");
* page.localStorage().setItem("token", "abc");
* String token = page.localStorage().getItem("token");
* List<WebStorageItem> all = page.localStorage().items();
* page.localStorage().removeItem("token");
* page.localStorage().clear();
* }</pre>
*/
public interface WebStorage {
/**
* Returns all items in the storage as name/value pairs.
*
* @since v1.61
*/
List<WebStorageItem> items();
/**
* Returns the value for the given {@code name} if present.
*
* @param name Name of the item to retrieve.
* @since v1.61
*/
String getItem(String name);
/**
* Sets the value for the given {@code name}. Overwrites any existing value for that name.
*
* @param name Name of the item to set.
* @param value New value for the item.
* @since v1.61
*/
void setItem(String name, String value);
/**
* Removes the item with the given {@code name}. No-op if the item is absent.
*
* @param name Name of the item to remove.
* @since v1.61
*/
void removeItem(String name);
/**
* Removes all items from the storage.
*
* @since v1.61
*/
void clear();
}
@@ -22,6 +22,8 @@ import com.google.gson.reflect.TypeToken;
import com.microsoft.playwright.APIResponse;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.options.HttpHeader;
import com.microsoft.playwright.options.SecurityDetails;
import com.microsoft.playwright.options.ServerAddr;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@@ -85,6 +87,22 @@ class APIResponseImpl implements APIResponse {
return status == 0 || (status >= 200 && status <= 299);
}
@Override
public SecurityDetails securityDetails() {
if (!initializer.has("securityDetails")) {
return null;
}
return gson().fromJson(initializer.get("securityDetails"), SecurityDetails.class);
}
@Override
public ServerAddr serverAddr() {
if (!initializer.has("serverAddr")) {
return null;
}
return gson().fromJson(initializer.get("serverAddr"), ServerAddr.class);
}
@Override
public int status() {
return initializer.get("status").getAsInt();
@@ -46,6 +46,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
private final DebuggerImpl debugger;
private final APIRequestContextImpl request;
private final ClockImpl clock;
private final CredentialsImpl credentials;
final List<PageImpl> pages = new ArrayList<>();
final Router routes = new Router();
@@ -94,6 +95,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
request = connection.getExistingObject(initializer.getAsJsonObject("requestContext").get("guid").getAsString());
request.timeoutSettings = timeoutSettings;
clock = new ClockImpl(this);
credentials = new CredentialsImpl(this);
closePromise = new WaitableEvent<>(listeners, EventType.CLOSE);
}
@@ -313,6 +315,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
return clock;
}
@Override
public Credentials credentials() {
return credentials;
}
private <T> T waitForEventWithTimeout(EventType eventType, Runnable code, Predicate<T> predicate, Double timeout) {
List<Waitable<T>> waitables = new ArrayList<>();
waitables.add(new WaitableEvent<>(listeners, eventType, predicate));
@@ -112,8 +112,8 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
@Override
public Browser connectOverCDP(String endpointURL, ConnectOverCDPOptions options) {
if (!"chromium".equals(name())) {
throw new PlaywrightException("Connecting over CDP is only supported in Chromium.");
if (!"chromium".equals(name()) && !"webkit".equals(name())) {
throw new PlaywrightException("Connecting over CDP is only supported in Chromium and WebKit.");
}
if (options == null) {
options = new ConnectOverCDPOptions();
@@ -110,6 +110,14 @@ class ChannelOwner extends LoggingSupport {
return connection.sendMessageAsync(guid, method, params);
}
// Fire-and-forget: silently drop if the object was collected.
void sendMessageNoReply(String method, JsonObject params) {
if (wasCollected) {
return;
}
connection.sendMessageNoReply(guid, method, params);
}
JsonElement sendMessage(String method) {
return sendMessage(method, new JsonObject(), NO_TIMEOUT);
}
@@ -39,6 +39,7 @@ class Message {
JsonObject params;
JsonElement result;
SerializedError error;
JsonObject errorDetails;
JsonArray log;
@Override
@@ -132,13 +133,20 @@ public class Connection {
}
public WaitableResult<JsonElement> sendMessageAsync(String guid, String method, JsonObject params) {
return internalSendMessage(guid, method, params, true);
return internalSendMessage(guid, method, params, true, true);
}
private WaitableResult<JsonElement> internalSendMessage(String guid, String method, JsonObject params, boolean sendStack) {
// Fire-and-forget: the server never replies.
public void sendMessageNoReply(String guid, String method, JsonObject params) {
internalSendMessage(guid, method, params, false, false);
}
private WaitableResult<JsonElement> internalSendMessage(String guid, String method, JsonObject params, boolean sendStack, boolean expectsReply) {
int id = ++lastId;
WaitableResult<JsonElement> result = new WaitableResult<>();
callbacks.put(id, result);
if (expectsReply) {
callbacks.put(id, result);
}
JsonObject message = new JsonObject();
message.addProperty("id", id);
message.addProperty("guid", guid);
@@ -175,7 +183,7 @@ public class Connection {
callData.add("stack", stack);
JsonObject stackParams = new JsonObject();
stackParams.add("callData", callData);
internalSendMessage(localUtils.guid,"addStackToTracingNoReply", stackParams, false);
internalSendMessage(localUtils.guid,"addStackToTracingNoReply", stackParams, false, true);
}
return result;
}
@@ -251,16 +259,20 @@ public class Connection {
callback.complete(message.result);
} else {
String callLog = formatCallLog(message.log);
PlaywrightException exception;
if (message.error.error == null) {
callback.completeExceptionally(new PlaywrightException(message.error + callLog));
exception = new PlaywrightException(message.error + callLog);
} else if ("TimeoutError".equals(message.error.error.name)) {
callback.completeExceptionally(new TimeoutError(message.error.error + callLog));
exception = new TimeoutError(message.error.error + callLog);
} else if ("TargetClosedError".equals(message.error.error.name)) {
callback.completeExceptionally(new TargetClosedError(message.error.error + callLog));
exception = new TargetClosedError(message.error.error + callLog);
} else {
callback.completeExceptionally(new DriverException(message.error.error + callLog));
exception = new DriverException(message.error.error + callLog);
}
if (message.errorDetails != null) {
exception = new ServerErrorWithDetails(exception, message.errorDetails, message.log);
}
callback.completeExceptionally(exception);
}
return;
}
@@ -0,0 +1,62 @@
/*
* 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.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Credentials;
import com.microsoft.playwright.options.VirtualCredential;
import java.util.List;
import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT;
import static com.microsoft.playwright.impl.Serialization.gson;
import static java.util.Arrays.asList;
class CredentialsImpl implements Credentials {
private final BrowserContextImpl context;
CredentialsImpl(BrowserContextImpl context) {
this.context = context;
}
@Override
public void install() {
context.sendMessage("credentialsInstall", new JsonObject(), NO_TIMEOUT);
}
@Override
public VirtualCredential create(String rpId, CreateOptions options) {
JsonObject params = options == null ? new JsonObject() : gson().toJsonTree(options).getAsJsonObject();
params.addProperty("rpId", rpId);
JsonObject json = context.sendMessage("credentialsCreate", params, NO_TIMEOUT).getAsJsonObject();
return gson().fromJson(json.get("credential"), VirtualCredential.class);
}
@Override
public void delete(String id) {
JsonObject params = new JsonObject();
params.addProperty("id", id);
context.sendMessage("credentialsDelete", params, NO_TIMEOUT);
}
@Override
public List<VirtualCredential> get(GetOptions options) {
JsonObject params = options == null ? new JsonObject() : gson().toJsonTree(options).getAsJsonObject();
JsonObject json = context.sendMessage("credentialsGet", params, NO_TIMEOUT).getAsJsonObject();
return asList(gson().fromJson(json.getAsJsonArray("credentials"), VirtualCredential[].class));
}
}
@@ -1154,8 +1154,17 @@ public class FrameImpl extends ChannelOwner implements Frame {
FrameExpectResult expect(String expression, FrameExpectOptions options) {
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.addProperty("expression", expression);
JsonElement json = sendMessage("expect", params, options.timeout);
FrameExpectResult result = gson().fromJson(json, FrameExpectResult.class);
FrameExpectResult result = new FrameExpectResult();
try {
sendMessage("expect", params, options.timeout);
result.matches = !options.isNot;
} catch (ServerErrorWithDetails e) {
FrameExpectErrorDetails details = gson().fromJson(e.errorDetails(), FrameExpectErrorDetails.class);
result.matches = options.isNot;
result.received = details.received;
result.errorMessage = details.customErrorMessage == null ? null : "Error: " + details.customErrorMessage;
result.log = e.log();
}
return result;
}
}
@@ -43,8 +43,14 @@ public class LocatorUtils {
return "internal:describe=" + gson().toJson(description);
}
// Multiple test id attribute names can be joined with a comma. Attribute names cannot contain commas.
private static String encodeTestIdAttributeName(String testIdAttributeName) {
return testIdAttributeName.contains(",") ? gson().toJson(testIdAttributeName) : testIdAttributeName;
}
static String getByTestIdSelector(Object testId, PlaywrightImpl playwright) {
return getByAttributeTextSelector(playwright.selectors.testIdAttributeName, testId, true);
String attributeName = encodeTestIdAttributeName(playwright.selectors.testIdAttributeName);
return "internal:testid=[" + attributeName + "=" + escapeForAttributeSelector(testId, true) + "]";
}
static String getByAltTextSelector(Object text, Locator.GetByAltTextOptions options) {
@@ -47,6 +47,8 @@ public class PageImpl extends ChannelOwner implements Page {
private final MouseImpl mouse;
private final TouchscreenImpl touchscreen;
private final ScreencastImpl screencast;
private final WebStorageImpl localStorage;
private final WebStorageImpl sessionStorage;
final Waitable<?> waitableClosedOrCrashed;
private ViewportSize viewport;
private final Router routes = new Router();
@@ -137,6 +139,8 @@ public class PageImpl extends ChannelOwner implements Page {
mouse = new MouseImpl(this);
touchscreen = new TouchscreenImpl(this);
screencast = new ScreencastImpl(this);
localStorage = new WebStorageImpl(this, "local");
sessionStorage = new WebStorageImpl(this, "session");
frames.add(mainFrame);
timeoutSettings = new TimeoutSettings(browserContext.timeoutSettings);
waitableClosedOrCrashed = createWaitForCloseHelper();
@@ -555,8 +559,13 @@ public class PageImpl extends ChannelOwner implements Page {
try {
if (ownedContext != null) {
ownedContext.close();
} else if (options.runBeforeUnload != null && options.runBeforeUnload) {
sendMessage("runBeforeUnload", new JsonObject(), NO_TIMEOUT);
} else {
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
JsonObject params = new JsonObject();
if (options.reason != null) {
params.addProperty("reason", options.reason);
}
sendMessage("close", params, NO_TIMEOUT);
}
} catch (PlaywrightException exception) {
@@ -1362,6 +1371,16 @@ public class PageImpl extends ChannelOwner implements Page {
return screencast;
}
@Override
public WebStorage localStorage() {
return localStorage;
}
@Override
public WebStorage sessionStorage() {
return sessionStorage;
}
@Override
public void type(String selector, String text, TypeOptions options) {
mainFrame.type(selector, text, convertType(options, Frame.TypeOptions.class));
@@ -122,4 +122,10 @@ class FrameExpectResult {
List<String> log;
}
class FrameExpectErrorDetails {
FrameExpectResult.Received received;
Boolean timedOut;
String customErrorMessage;
}
@@ -20,11 +20,13 @@ import com.microsoft.playwright.ScreencastFrame;
class ScreencastFrameImpl implements ScreencastFrame {
private final byte[] data;
private final double timestamp;
private final int viewportWidth;
private final int viewportHeight;
ScreencastFrameImpl(byte[] data, int viewportWidth, int viewportHeight) {
ScreencastFrameImpl(byte[] data, double timestamp, int viewportWidth, int viewportHeight) {
this.data = data;
this.timestamp = timestamp;
this.viewportWidth = viewportWidth;
this.viewportHeight = viewportHeight;
}
@@ -34,6 +36,11 @@ class ScreencastFrameImpl implements ScreencastFrame {
return data;
}
@Override
public double timestamp() {
return timestamp;
}
@Override
public int viewportWidth() {
return viewportWidth;
@@ -44,9 +44,10 @@ class ScreencastImpl implements Screencast {
}
String dataBase64 = params.get("data").getAsString();
byte[] data = java.util.Base64.getDecoder().decode(dataBase64);
double timestamp = params.get("timestamp").getAsDouble();
int viewportWidth = params.get("viewportWidth").getAsInt();
int viewportHeight = params.get("viewportHeight").getAsInt();
onFrame.accept(new ScreencastFrameImpl(data, viewportWidth, viewportHeight));
onFrame.accept(new ScreencastFrameImpl(data, timestamp, viewportWidth, viewportHeight));
}
@Override
@@ -0,0 +1,57 @@
/*
* 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.impl;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import java.util.ArrayList;
import java.util.List;
class ServerErrorWithDetails extends PlaywrightException {
private final JsonObject errorDetails;
private final JsonArray log;
ServerErrorWithDetails(PlaywrightException cause, JsonObject errorDetails, JsonArray log) {
super(cause.getMessage(), cause);
this.errorDetails = errorDetails;
this.log = log;
}
// Rethrown with the calling thread's stack trace, see WaitableResult.get().
ServerErrorWithDetails(ServerErrorWithDetails cause) {
super(cause.getMessage(), cause);
this.errorDetails = cause.errorDetails;
this.log = cause.log;
}
JsonObject errorDetails() {
return errorDetails;
}
List<String> log() {
List<String> result = new ArrayList<>();
if (log != null) {
for (JsonElement e : log) {
result.add(e.getAsString());
}
}
return result;
}
}
@@ -41,7 +41,8 @@ public class WaitForEventLogger<T> implements Supplier<T>, Logger {
{
JsonObject info = new JsonObject();
info.addProperty("phase", "before");
sendWaitForEventInfo(info);
info.addProperty("event", "");
sendWaitInfo(info);
}
JsonObject info = new JsonObject();
info.addProperty("phase", "after");
@@ -51,7 +52,7 @@ public class WaitForEventLogger<T> implements Supplier<T>, Logger {
info.addProperty("error", e.getMessage());
throw e;
} finally {
sendWaitForEventInfo(info);
sendWaitInfo(info);
}
}
@@ -61,14 +62,15 @@ public class WaitForEventLogger<T> implements Supplier<T>, Logger {
JsonObject info = new JsonObject();
info.addProperty("phase", "log");
info.addProperty("message", message);
sendWaitForEventInfo(info);
sendWaitInfo(info);
}
private void sendWaitForEventInfo(JsonObject info) {
info.addProperty("event", "");
private void sendWaitInfo(JsonObject info) {
info.addProperty("waitId", waitId);
JsonObject params = new JsonObject();
params.add("info", info);
channel.sendMessageAsync("waitForEventInfo", params);
try {
channel.sendMessageNoReply("__waitInfo__", info);
} catch (RuntimeException e) {
// Fire-and-forget: never throw to the caller.
}
}
}
@@ -52,6 +52,8 @@ class WaitableResult<T> implements Waitable<T> {
throw new TimeoutError(exception.getMessage(), exception);
} if (exception instanceof TargetClosedError) {
throw new TargetClosedError(exception.getMessage(), exception);
} if (exception instanceof ServerErrorWithDetails) {
throw new ServerErrorWithDetails((ServerErrorWithDetails) exception);
}
throw new PlaywrightException(exception.getMessage(), exception);
}
@@ -0,0 +1,77 @@
/*
* 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.impl;
import com.google.gson.JsonObject;
import com.microsoft.playwright.WebStorage;
import com.microsoft.playwright.options.WebStorageItem;
import java.util.List;
import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT;
import static com.microsoft.playwright.impl.Serialization.gson;
import static java.util.Arrays.asList;
class WebStorageImpl implements WebStorage {
private final PageImpl page;
private final String kind;
WebStorageImpl(PageImpl page, String kind) {
this.page = page;
this.kind = kind;
}
private JsonObject createParams() {
JsonObject params = new JsonObject();
params.addProperty("kind", kind);
return params;
}
@Override
public List<WebStorageItem> items() {
JsonObject json = page.sendMessage("webStorageItems", createParams(), NO_TIMEOUT).getAsJsonObject();
return asList(gson().fromJson(json.getAsJsonArray("items"), WebStorageItem[].class));
}
@Override
public String getItem(String name) {
JsonObject params = createParams();
params.addProperty("name", name);
JsonObject json = page.sendMessage("webStorageGetItem", params, NO_TIMEOUT).getAsJsonObject();
return json.has("value") ? json.get("value").getAsString() : null;
}
@Override
public void setItem(String name, String value) {
JsonObject params = createParams();
params.addProperty("name", name);
params.addProperty("value", value);
page.sendMessage("webStorageSetItem", params, NO_TIMEOUT);
}
@Override
public void removeItem(String name) {
JsonObject params = createParams();
params.addProperty("name", name);
page.sendMessage("webStorageRemoveItem", params, NO_TIMEOUT);
}
@Override
public void clear() {
page.sendMessage("webStorageClear", createParams(), NO_TIMEOUT);
}
}
@@ -0,0 +1,22 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public enum ScreencastCursor {
NONE,
POINTER
}
@@ -18,11 +18,11 @@ package com.microsoft.playwright.options;
public class Size {
/**
* Video frame width.
* Max frame width in pixels.
*/
public int width;
/**
* Video frame height.
* Max frame height in pixels.
*/
public int height;
@@ -0,0 +1,41 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class VirtualCredential {
/**
* Base64url-encoded credential id.
*/
public String id;
/**
* Relying party id.
*/
public String rpId;
/**
* Base64url-encoded user handle.
*/
public String userHandle;
/**
* Base64url-encoded PKCS#8 (DER) private key.
*/
public String privateKey;
/**
* Base64url-encoded SPKI (DER) public key.
*/
public String publicKey;
}
@@ -0,0 +1,23 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.options;
public class WebStorageItem {
public String name;
public String value;
}
@@ -131,8 +131,9 @@ public class TestBrowserContextCDPSession extends TestBase {
CDPSession session = page.context().newCDPSession(page);
page.close();
PlaywrightException exception = assertThrows(PlaywrightException.class, session::detach);
assertTrue(exception.getMessage().contains("Target page, context or browser has been closed"), exception.getMessage());
// Like the upstream test, only check that detach fails the error depends on
// whether the session detached before or after the page closed.
assertThrows(PlaywrightException.class, session::detach);
context.close();
}
@@ -741,10 +741,12 @@ public class TestBrowserContextFetch extends TestBase {
});
page.evaluate("() => setTimeout(closeContext, 1000);");
PlaywrightException e = assertThrows(PlaywrightException.class, () -> context.request().get(server.EMPTY_PAGE));
assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
assertTrue(e.getMessage().contains("Request context disposed") ||
e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
e = assertThrows(PlaywrightException.class, () -> context.request().post(server.EMPTY_PAGE));
assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
assertTrue(e.getMessage().contains("Request context disposed") ||
e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
}
@Test
@@ -0,0 +1,179 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.VirtualCredential;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static com.microsoft.playwright.Utils.mapOf;
import static org.junit.jupiter.api.Assertions.*;
public class TestBrowserContextWebAuthn extends TestBase {
private static final String B64URL_TO_BYTES_JS =
" const b64UrlToBytes = s => {\n" +
" let str = s.replace(/-/g, '+').replace(/_/g, '/');\n" +
" while (str.length % 4)\n" +
" str += '=';\n" +
" const bin = atob(str);\n" +
" const u8 = new Uint8Array(bin.length);\n" +
" for (let i = 0; i < bin.length; i++)\n" +
" u8[i] = bin.charCodeAt(i);\n" +
" return u8;\n" +
" };\n";
@Test
void shouldNotInterceptNavigatorCredentialsWithoutInstall() {
// Seed a credential, but do not install the interceptor.
context.credentials().create("localhost");
page.navigate(server.EMPTY_PAGE);
Object intercepted = page.evaluate("() => globalThis.__pwWebAuthnInstalled === true");
assertEquals(false, intercepted);
}
@Test
void shouldSeedKnownCredentialAndAuthenticate() {
// This is the easiest way to create credentials. In practice, this
// probably comes from environment.
VirtualCredential known;
try (BrowserContext source = browser.newContext()) {
known = source.credentials().create("localhost");
}
// A fresh context imports the known credential and signs in with it.
context.credentials().create(known.rpId, new Credentials.CreateOptions()
.setId(known.id)
.setUserHandle(known.userHandle)
.setPrivateKey(known.privateKey)
.setPublicKey(known.publicKey));
context.credentials().install();
page.navigate(server.EMPTY_PAGE);
Map<String, Object> result = (Map<String, Object>) page.evaluate(
"async ({ rpId, credentialId }) => {\n" +
B64URL_TO_BYTES_JS +
" const challenge = crypto.getRandomValues(new Uint8Array(32));\n" +
" const cred = await navigator.credentials.get({\n" +
" publicKey: {\n" +
" challenge,\n" +
" rpId,\n" +
" allowCredentials: [{ type: 'public-key', id: b64UrlToBytes(credentialId) }],\n" +
" userVerification: 'preferred',\n" +
" },\n" +
" });\n" +
" const resp = cred.response;\n" +
" return {\n" +
" id: cred.id,\n" +
" type: cred.type,\n" +
" hasClientData: resp.clientDataJSON.byteLength > 0,\n" +
" hasAuthData: resp.authenticatorData.byteLength > 0,\n" +
" hasSignature: resp.signature.byteLength > 0,\n" +
" authDataFlags: new Uint8Array(resp.authenticatorData)[32],\n" +
" };\n" +
"}", mapOf("rpId", "localhost", "credentialId", known.id));
assertEquals(known.id, result.get("id"));
assertEquals("public-key", result.get("type"));
assertEquals(true, result.get("hasClientData"));
assertEquals(true, result.get("hasAuthData"));
assertEquals(true, result.get("hasSignature"));
// UP (0x01) | UV (0x04) = 0x05
assertEquals(0x05, ((Number) result.get("authDataFlags")).intValue() & 0x05);
// After the credential is deleted, the page can no longer authenticate with it.
context.credentials().delete(known.id);
assertEquals(0, context.credentials().get().size());
Object error = page.evaluate(
"async ({ rpId, credentialId }) => {\n" +
B64URL_TO_BYTES_JS +
" const challenge = crypto.getRandomValues(new Uint8Array(32));\n" +
" try {\n" +
" await navigator.credentials.get({\n" +
" publicKey: {\n" +
" challenge,\n" +
" rpId,\n" +
" allowCredentials: [{ type: 'public-key', id: b64UrlToBytes(credentialId) }],\n" +
" },\n" +
" });\n" +
" return 'no-error';\n" +
" } catch (e) {\n" +
" return e.name;\n" +
" }\n" +
"}", mapOf("rpId", "localhost", "credentialId", known.id));
assertEquals("NotAllowedError", error);
}
@Test
void shouldCapturePageCreatedCredentialAndReuseItInAnotherContext() {
// Setup context: the app registers a passkey via navigator.credentials.create().
String createdId;
VirtualCredential captured;
try (BrowserContext setupContext = browser.newContext()) {
setupContext.credentials().install();
Page setupPage = setupContext.newPage();
setupPage.navigate(server.EMPTY_PAGE);
createdId = (String) setupPage.evaluate(
"async ({ rpId }) => {\n" +
" const challenge = crypto.getRandomValues(new Uint8Array(32));\n" +
" const created = await navigator.credentials.create({\n" +
" publicKey: {\n" +
" challenge,\n" +
" rp: { id: rpId, name: 'Test RP' },\n" +
" user: { id: new Uint8Array([1, 2, 3, 4]), name: 'u', displayName: 'User' },\n" +
" pubKeyCredParams: [{ type: 'public-key', alg: -7 }],\n" +
" authenticatorSelection: { residentKey: 'required', userVerification: 'preferred' },\n" +
" },\n" +
" });\n" +
" return created.id;\n" +
"}", mapOf("rpId", "localhost"));
List<VirtualCredential> credentials = setupContext.credentials().get(
new Credentials.GetOptions().setRpId("localhost"));
assertEquals(1, credentials.size());
captured = credentials.get(0);
assertEquals(createdId, captured.id);
assertTrue(captured.privateKey.matches("^[A-Za-z0-9_-]+$"), captured.privateKey);
assertTrue(captured.publicKey.matches("^[A-Za-z0-9_-]+$"), captured.publicKey);
}
// Reuse the captured passkey in a fresh context and sign in with it.
context.credentials().create(captured.rpId, new Credentials.CreateOptions()
.setId(captured.id)
.setUserHandle(captured.userHandle)
.setPrivateKey(captured.privateKey)
.setPublicKey(captured.publicKey));
context.credentials().install();
page.navigate(server.EMPTY_PAGE);
Object gotId = page.evaluate(
"async ({ rpId }) => {\n" +
" const challenge = crypto.getRandomValues(new Uint8Array(32));\n" +
" // No allowCredentials — relies on the re-seeded credential being discoverable.\n" +
" const cred = await navigator.credentials.get({\n" +
" publicKey: { challenge, rpId, userVerification: 'preferred' },\n" +
" });\n" +
" return cred.id;\n" +
"}", mapOf("rpId", "localhost"));
assertEquals(createdId, gotId);
}
}
@@ -40,10 +40,14 @@ public class TestBrowserTypeBasic extends TestBase {
assertEquals(getBrowserNameFromEnv(), browserType.name());
}
static boolean isChromiumOrWebKit() {
return isChromium() || isWebKit();
}
@Test
@DisabledIf(value="com.microsoft.playwright.TestBase#isChromium", disabledReason="Non-chromium behavior")
@DisabledIf(value="isChromiumOrWebKit", disabledReason="Connecting over CDP is supported in Chromium and WebKit")
void shouldThrowWhenTryingToConnectWithNotChromium() {
PlaywrightException e = assertThrows(PlaywrightException.class, () -> browserType.connectOverCDP("foo"));
assertTrue(e.getMessage().contains("Connecting over CDP is only supported in Chromium."));
assertTrue(e.getMessage().contains("Connecting over CDP is only supported in Chromium and WebKit."));
}
}
@@ -259,7 +259,11 @@ public class TestBrowserTypeConnect extends TestBase {
}
assertFalse(browser.isConnected());
PlaywrightException e = assertThrows(PlaywrightException.class, () -> page.waitForNavigation(() -> {}));
assertTrue(e.getMessage().contains("Browser closed") || e.getMessage().contains("Page closed") || e.getMessage().contains("Browser has been closed"), e.getMessage());
// The surfaced message depends on which call hits the closed connection first.
assertTrue(e.getMessage().contains("Browser closed") ||
e.getMessage().contains("Page closed") ||
e.getMessage().contains("Browser has been closed") ||
e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
}
@Test
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
import org.junit.jupiter.api.io.CleanupMode;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
@@ -223,7 +224,9 @@ public class TestClientCertificates extends TestBase {
@Test
@DisabledIf(value="com.microsoft.playwright.TestClientCertificates#isWebKitMacOS", disabledReason="The network connection was lost.")
public void shouldWorkWithBrowserLaunchPersistentContext(@TempDir Path tmpDir) {
// No cleanup: on Windows Chromium may keep chrome_debug.log in the user data dir
// locked briefly after close, failing the deletion.
public void shouldWorkWithBrowserLaunchPersistentContext(@TempDir(cleanup = CleanupMode.NEVER) Path tmpDir) {
BrowserType.LaunchPersistentContextOptions options = new BrowserType.LaunchPersistentContextOptions()
.setIgnoreHTTPSErrors(true) // TODO: remove once we can pass a custom CA.
.setClientCertificates(asList(
@@ -22,6 +22,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
import org.junit.jupiter.api.io.CleanupMode;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
@@ -46,7 +47,9 @@ public class TestDefaultBrowserContext2 extends TestBase {
private BrowserContext persistentContext;
@TempDir Path tempDir;
// No cleanup: on Windows Chromium may keep chrome_debug.log in the user data dir
// locked briefly after close, failing the deletion.
@TempDir(cleanup = CleanupMode.NEVER) Path tempDir;
@AfterEach
void closePersistentContext() {
@@ -22,6 +22,7 @@ import com.microsoft.playwright.options.HttpCredentials;
import com.microsoft.playwright.options.HttpCredentialsSend;
import com.microsoft.playwright.options.HttpHeader;
import com.microsoft.playwright.options.RequestOptions;
import com.microsoft.playwright.options.ServerAddr;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -252,6 +253,25 @@ public class TestGlobalFetch extends TestBase {
assertEquals(200, response.status());
}
@Test
void shouldReturnServerAddressFromResponse() {
APIRequestContext request = playwright.request().newContext();
APIResponse response = request.get(server.EMPTY_PAGE);
ServerAddr address = response.serverAddr();
assertNotNull(address);
assertEquals(server.PORT, address.port);
assertTrue(asList("127.0.0.1", "::1").contains(address.ipAddress), address.ipAddress);
request.dispose();
}
@Test
void shouldReturnNullSecurityDetailsForHttpResponse() {
APIRequestContext request = playwright.request().newContext();
APIResponse response = request.get(server.EMPTY_PAGE);
assertNull(response.securityDetails());
request.dispose();
}
@Test
void shouldResolveUrlRelativeToGobalBaseURLOption() {
APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions().setBaseURL(server.PREFIX));
@@ -30,7 +30,7 @@ public class TestPageEventPageError extends TestBase {
page.evaluate("async () => {\n" +
" for (let i = 0; i < 301; i++)\n" +
" window.setTimeout(() => { throw new Error('error' + i); }, 0);\n" +
" await new Promise(f => window.setTimeout(f, 100));\n" +
" await new Promise(f => window.setTimeout(f, 2000));\n" +
" }");
List<String> errors = page.pageErrors();
@@ -122,12 +122,12 @@ public class TestScreencast extends TestBase {
}
@Test
void onFrameShouldReceiveViewportSize() {
void onFrameShouldReceiveViewportSizeAndTimestamp() {
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setViewportSize(1000, 400));
Page page = context.newPage();
try {
List<ScreencastFrame> frames = new ArrayList<>();
page.screencast().start(new Screencast.StartOptions().setOnFrame(frames::add));
page.screencast().start(new Screencast.StartOptions().setOnFrame(frames::add).setSize(500, 400));
page.navigate(server.EMPTY_PAGE);
page.evaluate("() => document.body.style.backgroundColor = 'red'");
page.waitForTimeout(500);
@@ -136,6 +136,7 @@ public class TestScreencast extends TestBase {
for (ScreencastFrame frame : frames) {
assertEquals(1000, frame.viewportWidth());
assertEquals(400, frame.viewportHeight());
assertTrue(frame.timestamp() > 0, "expected a positive timestamp, got " + frame.timestamp());
}
} finally {
context.close();
@@ -51,6 +51,20 @@ public class TestSelectorsGetBy extends TestBase {
assertThat(page.locator("div").getByTestId("Hello")).hasText("Hello world");
}
@Test
void getByTestIdWithCommaSeparatedTestIdAttributesShouldMatchAny() {
page.setContent("<section>\n" +
" <div data-pw='Hello'>first</div>\n" +
" <div data-ti='Hello'>second</div>\n" +
" <div data-testid='Hello'>third</div>\n" +
"</section>");
playwright.selectors().setTestIdAttribute("data-pw,data-ti");
assertThat(page.getByTestId("Hello")).hasCount(2);
assertThat(page.getByTestId("Hello")).hasText(new String[]{"first", "second"});
assertThat(page.mainFrame().getByTestId("Hello")).hasCount(2);
assertThat(page.locator("section").getByTestId("Hello")).hasCount(2);
}
@Test
void shouldUseDataTestidInStrictErrors() {
playwright.selectors().setTestIdAttribute("data-custom-id");
@@ -0,0 +1,132 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright;
import com.microsoft.playwright.options.WebStorageItem;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.microsoft.playwright.Utils.mapOf;
import static org.junit.jupiter.api.Assertions.*;
public class TestWebStorage extends TestBase {
private static Map<String, String> asMap(List<WebStorageItem> items) {
Map<String, String> map = new HashMap<>();
for (WebStorageItem item : items) {
map.put(item.name, item.value);
}
return map;
}
@Test
void localStorageItemsReturnsEmptyListOnFreshOrigin() {
page.navigate(server.EMPTY_PAGE);
assertEquals(0, page.localStorage().items().size());
}
@Test
void localStorageGetItemReturnsNullForMissingKey() {
page.navigate(server.EMPTY_PAGE);
assertNull(page.localStorage().getItem("absent"));
}
@Test
void localStorageSetItemPersistsAndSurfacesInItemsAndGetItem() {
page.navigate(server.EMPTY_PAGE);
page.localStorage().setItem("alpha", "1");
page.localStorage().setItem("beta", "2");
assertEquals(mapOf("alpha", "1", "beta", "2"), asMap(page.localStorage().items()));
assertEquals("1", page.localStorage().getItem("alpha"));
assertEquals("1", page.evaluate("() => localStorage.getItem('alpha')"));
}
@Test
void localStorageSetItemOverwritesExistingValue() {
page.navigate(server.EMPTY_PAGE);
page.localStorage().setItem("k", "first");
page.localStorage().setItem("k", "second");
assertEquals("second", page.localStorage().getItem("k"));
}
@Test
void localStorageRemoveItemRemovesSingleItem() {
page.navigate(server.EMPTY_PAGE);
page.localStorage().setItem("a", "1");
page.localStorage().setItem("b", "2");
page.localStorage().removeItem("a");
assertEquals(mapOf("b", "2"), asMap(page.localStorage().items()));
}
@Test
void localStorageClearEmptiesStorage() {
page.navigate(server.EMPTY_PAGE);
page.localStorage().setItem("a", "1");
page.localStorage().setItem("b", "2");
page.localStorage().clear();
assertEquals(0, page.localStorage().items().size());
}
@Test
void sessionStorageRoundTrip() {
page.navigate(server.EMPTY_PAGE);
assertEquals(0, page.sessionStorage().items().size());
page.sessionStorage().setItem("s1", "v1");
page.sessionStorage().setItem("s2", "v2");
assertEquals(mapOf("s1", "v1", "s2", "v2"), asMap(page.sessionStorage().items()));
assertEquals("v1", page.sessionStorage().getItem("s1"));
page.sessionStorage().removeItem("s1");
assertEquals(mapOf("s2", "v2"), asMap(page.sessionStorage().items()));
page.sessionStorage().clear();
assertEquals(0, page.sessionStorage().items().size());
}
@Test
void localStorageAndSessionStorageAreIndependent() {
page.navigate(server.EMPTY_PAGE);
page.localStorage().setItem("shared", "local");
page.sessionStorage().setItem("shared", "session");
assertEquals("local", page.localStorage().getItem("shared"));
assertEquals("session", page.sessionStorage().getItem("shared"));
page.localStorage().clear();
assertEquals(0, page.localStorage().items().size());
assertEquals("session", page.sessionStorage().getItem("shared"));
}
@Test
void storageMethodsAreScopedToTheCurrentOrigin() {
page.navigate(server.PREFIX + "/empty.html");
page.localStorage().setItem("k", "origin-1");
page.navigate(server.CROSS_PROCESS_PREFIX + "/empty.html");
assertEquals(0, page.localStorage().items().size());
page.localStorage().setItem("k", "origin-2");
page.navigate(server.PREFIX + "/empty.html");
assertEquals("origin-1", page.localStorage().getItem("k"));
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 39 KiB

+1 -1
View File
@@ -1 +1 @@
1.60.0
1.61.0-beta-1781285686000
@@ -997,10 +997,11 @@ class Interface extends TypeDefinition {
}
void writeTo(List<String> output, String offset) {
if (methods.stream().anyMatch(m -> "create".equals(m.jsonName))) {
// Interfaces with a static factory method, see Method.writeTo.
if (asList("Playwright", "FormData", "RequestOptions").contains(jsonName) && methods.stream().anyMatch(m -> "create".equals(m.jsonName))) {
output.add("import com.microsoft.playwright.impl." + jsonName + "Impl;");
}
if (asList("Page", "Request", "Response", "APIRequestContext", "APIRequest", "APIResponse", "FileChooser", "Frame", "FrameLocator", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard", "Tracing", "Video", "Debugger", "Screencast", "WebError").contains(jsonName)) {
if (asList("Page", "Request", "Response", "APIRequestContext", "APIRequest", "APIResponse", "FileChooser", "Frame", "FrameLocator", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard", "Tracing", "Video", "Debugger", "Screencast", "WebError", "Credentials", "WebStorage").contains(jsonName)) {
output.add("import com.microsoft.playwright.options.*;");
}
if ("Download".equals(jsonName)) {
@@ -1012,7 +1013,7 @@ class Interface extends TypeDefinition {
if ("Clock".equals(jsonName)) {
output.add("import java.util.Date;");
}
if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright", "Debugger", "Screencast", "WebSocketRoute").contains(jsonName)) {
if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright", "Debugger", "Screencast", "WebSocketRoute", "Credentials", "WebStorage").contains(jsonName)) {
output.add("import java.util.*;");
}
if (asList("WebSocketRoute").contains(jsonName)) {