diff --git a/README.md b/README.md index a6d4a38d7..e3e411c3b 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 148.0.7778.96 | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| WebKit 26.4 | ✅ | ✅ | ✅ | -| Firefox 150.0.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 149.0.7827.55 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| WebKit 26.5 | ✅ | ✅ | ✅ | +| Firefox 151.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | ## Documentation diff --git a/examples/pom.xml b/examples/pom.xml index dd612f2c6..3c2db93f1 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -10,7 +10,7 @@ Playwright Client Examples UTF-8 - 1.60.0 + 1.61.0 diff --git a/playwright/src/main/java/com/microsoft/playwright/APIResponse.java b/playwright/src/main/java/com/microsoft/playwright/APIResponse.java index f9409166b..13bd81064 100644 --- a/playwright/src/main/java/com/microsoft/playwright/APIResponse.java +++ b/playwright/src/main/java/com/microsoft/playwright/APIResponse.java @@ -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). * diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java index 02f76f61b..b254cf5c2 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java @@ -563,6 +563,13 @@ public WaitForPageOptions setTimeout(double timeout) { * @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. * diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java index c57fd7cd2..2fa39586d 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java @@ -124,6 +124,10 @@ public ConnectOptions setTimeout(double timeout) { } } 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 @@ class ConnectOverCDPOptions { */ 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. */ diff --git a/playwright/src/main/java/com/microsoft/playwright/Credentials.java b/playwright/src/main/java/com/microsoft/playwright/Credentials.java new file mode 100644 index 000000000..a3a180095 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/Credentials.java @@ -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. + * + *

There are two common ways to use it: + * + *

Usage: seed a known credential + *

{@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.
+ * }
+ * + *

Usage: capture a passkey, then reuse it + *

{@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));
+ * }
+ *
{@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.
+ * }
+ * + *

Defaults + */ +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}. + * + *

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. + * + *

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. + * + *

To **import a known credential**, supply all four of {@code id}, {@code userHandle}, {@code privateKey} and {@code + * publicKey} together. + * + *

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. + * + *

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. + * + *

To **import a known credential**, supply all four of {@code id}, {@code userHandle}, {@code privateKey} and {@code + * publicKey} together. + * + *

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()}. + * + *

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 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()}. + * + *

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 get(GetOptions options); +} + diff --git a/playwright/src/main/java/com/microsoft/playwright/Frame.java b/playwright/src/main/java/com/microsoft/playwright/Frame.java index 343a4a5ef..b00258a6b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Frame.java +++ b/playwright/src/main/java/com/microsoft/playwright/Frame.java @@ -2965,7 +2965,7 @@ default Object evalOnSelectorAll(String selector, String expression) { *

{@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Frame#evaluate * Frame.evaluate()}: *

{@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();
    * }
@@ -3005,7 +3005,7 @@ default Object evaluate(String expression) { *

{@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Frame#evaluate * Frame.evaluate()}: *

{@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();
    * }
diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java index da5f4e865..db240171f 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Page.java +++ b/playwright/src/main/java/com/microsoft/playwright/Page.java @@ -4529,7 +4529,7 @@ default Object evalOnSelectorAll(String selector, String expression) { *

{@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Page#evaluate * Page.evaluate()}: *

{@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();
    * }
@@ -4571,7 +4571,7 @@ default Object evaluate(String expression) { *

{@code ElementHandle} instances can be passed as an argument to the {@link com.microsoft.playwright.Page#evaluate * Page.evaluate()}: *

{@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();
    * }
@@ -5808,6 +5808,18 @@ default boolean isVisible(String selector) { * @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 @@ default String ariaSnapshot() { *

When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code * TimeoutError}. Passing zero timeout disables this. * - *

NOTE: {@link com.microsoft.playwright.Page#tap Page.tap()} the method will throw if {@code hasTouch} option of the browser - * context is false. + *

NOTE: {@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 @@ default void tap(String selector) { *

When all steps combined have not finished during the specified {@code timeout}, this method throws a {@code * TimeoutError}. Passing zero timeout disables this. * - *

NOTE: {@link com.microsoft.playwright.Page#tap Page.tap()} the method will throw if {@code hasTouch} option of the browser - * context is false. + *

NOTE: {@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 diff --git a/playwright/src/main/java/com/microsoft/playwright/Screencast.java b/playwright/src/main/java/com/microsoft/playwright/Screencast.java index 0c0122ccd..ea545e1a9 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Screencast.java +++ b/playwright/src/main/java/com/microsoft/playwright/Screencast.java @@ -38,6 +38,13 @@ class StartOptions { * 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 StartOptions setQuality(int quality) { 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 ShowChapterOptions setDuration(double duration) { } } 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 @@ class ShowActionsOptions { */ 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}. */ diff --git a/playwright/src/main/java/com/microsoft/playwright/ScreencastFrame.java b/playwright/src/main/java/com/microsoft/playwright/ScreencastFrame.java index ee4056ca5..8a690792b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/ScreencastFrame.java +++ b/playwright/src/main/java/com/microsoft/playwright/ScreencastFrame.java @@ -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. */ diff --git a/playwright/src/main/java/com/microsoft/playwright/Selectors.java b/playwright/src/main/java/com/microsoft/playwright/Selectors.java index 1bc0fe47a..465de6f37 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Selectors.java +++ b/playwright/src/main/java/com/microsoft/playwright/Selectors.java @@ -201,7 +201,8 @@ default void register(String name, Path script) { * 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); diff --git a/playwright/src/main/java/com/microsoft/playwright/Touchscreen.java b/playwright/src/main/java/com/microsoft/playwright/Touchscreen.java index 2024016c1..c90dbb435 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Touchscreen.java +++ b/playwright/src/main/java/com/microsoft/playwright/Touchscreen.java @@ -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}). * - *

NOTE: {@link com.microsoft.playwright.Page#tap Page.tap()} the method will throw if {@code hasTouch} option of the browser - * context is false. + *

NOTE: {@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. diff --git a/playwright/src/main/java/com/microsoft/playwright/WebStorage.java b/playwright/src/main/java/com/microsoft/playwright/WebStorage.java new file mode 100644 index 000000000..d141c09c0 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/WebStorage.java @@ -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, browser-consistent API. + * + *

Instances are accessed through {@link com.microsoft.playwright.Page#localStorage Page.localStorage()} and {@link + * com.microsoft.playwright.Page#sessionStorage Page.sessionStorage()}. + *

{@code
+ * page.navigate("https://example.com");
+ * page.localStorage().setItem("token", "abc");
+ * String token = page.localStorage().getItem("token");
+ * List all = page.localStorage().items();
+ * page.localStorage().removeItem("token");
+ * page.localStorage().clear();
+ * }
+ */ +public interface WebStorage { + /** + * Returns all items in the storage as name/value pairs. + * + * @since v1.61 + */ + List 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(); +} + diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/APIResponseImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/APIResponseImpl.java index 771fb3658..385aca2c8 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/APIResponseImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/APIResponseImpl.java @@ -22,6 +22,8 @@ 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 @@ public boolean ok() { 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(); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java index daaed20bd..78c63f897 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -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 pages = new ArrayList<>(); final Router routes = new Router(); @@ -94,6 +95,7 @@ enum EventType { 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 @@ public ClockImpl clock() { return clock; } + @Override + public Credentials credentials() { + return credentials; + } + private T waitForEventWithTimeout(EventType eventType, Runnable code, Predicate predicate, Double timeout) { List> waitables = new ArrayList<>(); waitables.add(new WaitableEvent<>(listeners, eventType, predicate)); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java index d68d80cac..03efeab7c 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java @@ -112,8 +112,8 @@ public Browser connect(String wsEndpoint, ConnectOptions options) { @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(); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ChannelOwner.java b/playwright/src/main/java/com/microsoft/playwright/impl/ChannelOwner.java index ce68ad523..729c300f3 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/ChannelOwner.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ChannelOwner.java @@ -110,6 +110,14 @@ WaitableResult sendMessageAsync(String method, JsonObject params) { 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); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java index 0a7cbe540..46354abf2 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java @@ -39,6 +39,7 @@ class Message { JsonObject params; JsonElement result; SerializedError error; + JsonObject errorDetails; JsonArray log; @Override @@ -132,13 +133,20 @@ public JsonElement sendMessage(String guid, String method, JsonObject params) { } public WaitableResult sendMessageAsync(String guid, String method, JsonObject params) { - return internalSendMessage(guid, method, params, true); + return internalSendMessage(guid, method, params, true, true); } - private WaitableResult 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 internalSendMessage(String guid, String method, JsonObject params, boolean sendStack, boolean expectsReply) { int id = ++lastId; WaitableResult 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 @@ private WaitableResult internalSendMessage(String guid, String meth 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 @@ private void dispatch(Message message) { 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; } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/CredentialsImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/CredentialsImpl.java new file mode 100644 index 000000000..aa68f4a7d --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/CredentialsImpl.java @@ -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 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)); + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java index 25f45bba9..9a94fdf86 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java @@ -1154,8 +1154,17 @@ FrameExpectResult expect(String expression, FrameExpectOptions options, String t 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; } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java index 1bbe68a5c..8d9c025cd 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorUtils.java @@ -43,8 +43,14 @@ static String describeSelector(String description) { 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) { diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java index 19d7b3839..4956ea623 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -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 @@ enum EventType { 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 void close(CloseOptions options) { 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 Screencast screencast() { 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)); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java b/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java index 2983d1cfe..b7e02eed2 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java @@ -122,4 +122,10 @@ static class Received { List log; } +class FrameExpectErrorDetails { + FrameExpectResult.Received received; + Boolean timedOut; + String customErrorMessage; +} + diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ScreencastFrameImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/ScreencastFrameImpl.java index d12513dbf..e70b5d51a 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/ScreencastFrameImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ScreencastFrameImpl.java @@ -20,11 +20,13 @@ 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 @@ public byte[] data() { return data; } + @Override + public double timestamp() { + return timestamp; + } + @Override public int viewportWidth() { return viewportWidth; diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ScreencastImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/ScreencastImpl.java index 2e3a9cb56..a9d80aab0 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/ScreencastImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ScreencastImpl.java @@ -44,9 +44,10 @@ void handleScreencastFrame(JsonObject params) { } 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 diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ServerErrorWithDetails.java b/playwright/src/main/java/com/microsoft/playwright/impl/ServerErrorWithDetails.java new file mode 100644 index 000000000..64bc19179 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ServerErrorWithDetails.java @@ -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 log() { + List result = new ArrayList<>(); + if (log != null) { + for (JsonElement e : log) { + result.add(e.getAsString()); + } + } + return result; + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/WaitForEventLogger.java b/playwright/src/main/java/com/microsoft/playwright/impl/WaitForEventLogger.java index 26216d5fa..125af1a51 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/WaitForEventLogger.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/WaitForEventLogger.java @@ -41,7 +41,8 @@ public T get() { { 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 T get() { info.addProperty("error", e.getMessage()); throw e; } finally { - sendWaitForEventInfo(info); + sendWaitInfo(info); } } @@ -61,14 +62,15 @@ public void log(String message) { 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. + } } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/WaitableResult.java b/playwright/src/main/java/com/microsoft/playwright/impl/WaitableResult.java index 831874dbf..76cf50034 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/WaitableResult.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/WaitableResult.java @@ -52,6 +52,8 @@ public T get() { 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); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/WebStorageImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/WebStorageImpl.java new file mode 100644 index 000000000..96d507d6e --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/WebStorageImpl.java @@ -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 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); + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/options/ScreencastCursor.java b/playwright/src/main/java/com/microsoft/playwright/options/ScreencastCursor.java new file mode 100644 index 000000000..0e09c1ef0 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/options/ScreencastCursor.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright.options; + +public enum ScreencastCursor { + NONE, + POINTER +} \ No newline at end of file diff --git a/playwright/src/main/java/com/microsoft/playwright/options/Size.java b/playwright/src/main/java/com/microsoft/playwright/options/Size.java index 6955e1686..fb9ac7d8a 100644 --- a/playwright/src/main/java/com/microsoft/playwright/options/Size.java +++ b/playwright/src/main/java/com/microsoft/playwright/options/Size.java @@ -18,11 +18,11 @@ 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; diff --git a/playwright/src/main/java/com/microsoft/playwright/options/VirtualCredential.java b/playwright/src/main/java/com/microsoft/playwright/options/VirtualCredential.java new file mode 100644 index 000000000..06a7f74ca --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/options/VirtualCredential.java @@ -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; + +} \ No newline at end of file diff --git a/playwright/src/main/java/com/microsoft/playwright/options/WebStorageItem.java b/playwright/src/main/java/com/microsoft/playwright/options/WebStorageItem.java new file mode 100644 index 000000000..0b95002fc --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/options/WebStorageItem.java @@ -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; + +} \ No newline at end of file diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java index 14e586917..bf0ef7d5a 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java @@ -131,8 +131,9 @@ void shouldDetachWhenPageCloses() { 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(); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java index d7352c748..9f487a2f3 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java @@ -741,10 +741,12 @@ void shouldAbortRequestsWhenBrowserContextCloses() { }); 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 diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextWebAuthn.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextWebAuthn.java new file mode 100644 index 000000000..20b0a39e3 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextWebAuthn.java @@ -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 result = (Map) 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 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); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeBasic.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeBasic.java index 08522ffee..1d81dd6b1 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeBasic.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeBasic.java @@ -40,10 +40,14 @@ void browserTypeNameShouldWork() { 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.")); } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java index 618a36604..191d98da7 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java @@ -259,7 +259,11 @@ void shouldThrowWhenCallingWaitForNavigationAfterDisconnect() throws Interrupted } 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 diff --git a/playwright/src/test/java/com/microsoft/playwright/TestClientCertificates.java b/playwright/src/test/java/com/microsoft/playwright/TestClientCertificates.java index f6028c6ee..90c6336ca 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestClientCertificates.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestClientCertificates.java @@ -5,6 +5,7 @@ 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 void shouldWorkWithBrowserNewPageWhenPassingAsContent() throws IOExceptio @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( diff --git a/playwright/src/test/java/com/microsoft/playwright/TestDefaultBrowserContext2.java b/playwright/src/test/java/com/microsoft/playwright/TestDefaultBrowserContext2.java index 527e63e35..458738b37 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestDefaultBrowserContext2.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestDefaultBrowserContext2.java @@ -22,6 +22,7 @@ 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() { diff --git a/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java b/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java index ce9e21f48..37fc6e59f 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java @@ -22,6 +22,7 @@ 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 @@ void shouldPropagateIgnoreHTTPSErrorsOnRedirects() { 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)); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageEventPageError.java b/playwright/src/test/java/com/microsoft/playwright/TestPageEventPageError.java index f70e2cbd5..f90f895f7 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageEventPageError.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageEventPageError.java @@ -30,7 +30,7 @@ void pageErrorsShouldWork() { 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 errors = page.pageErrors(); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java b/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java index ecf0cce7c..2d81b3d47 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java @@ -122,12 +122,12 @@ void screencastStartShouldDeliverFramesViaOnFrame() throws Exception { } @Test - void onFrameShouldReceiveViewportSize() { + void onFrameShouldReceiveViewportSizeAndTimestamp() { BrowserContext context = browser.newContext(new Browser.NewContextOptions().setViewportSize(1000, 400)); Page page = context.newPage(); try { List 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 @@ void onFrameShouldReceiveViewportSize() { 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(); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestSelectorsGetBy.java b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsGetBy.java index a04ace027..da1f729cf 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestSelectorsGetBy.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsGetBy.java @@ -51,6 +51,20 @@ void getByTestIdWithCustomTestIdShouldWork() { assertThat(page.locator("div").getByTestId("Hello")).hasText("Hello world"); } + @Test + void getByTestIdWithCommaSeparatedTestIdAttributesShouldMatchAny() { + page.setContent("
\n" + + "
first
\n" + + "
second
\n" + + "
third
\n" + + "
"); + 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"); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestWebStorage.java b/playwright/src/test/java/com/microsoft/playwright/TestWebStorage.java new file mode 100644 index 000000000..98d1c6e17 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestWebStorage.java @@ -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 asMap(List items) { + Map 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")); + } +} diff --git a/playwright/src/test/resources/expectations/hide-should-work-firefox.png b/playwright/src/test/resources/expectations/hide-should-work-firefox.png index 7af4f1af7..ac1e22e22 100644 Binary files a/playwright/src/test/resources/expectations/hide-should-work-firefox.png and b/playwright/src/test/resources/expectations/hide-should-work-firefox.png differ diff --git a/playwright/src/test/resources/expectations/remove-should-work-firefox.png b/playwright/src/test/resources/expectations/remove-should-work-firefox.png index cbae7f34d..e4ee20d9e 100644 Binary files a/playwright/src/test/resources/expectations/remove-should-work-firefox.png and b/playwright/src/test/resources/expectations/remove-should-work-firefox.png differ diff --git a/scripts/DRIVER_VERSION b/scripts/DRIVER_VERSION index 4d5fde5bd..96023ba38 100644 --- a/scripts/DRIVER_VERSION +++ b/scripts/DRIVER_VERSION @@ -1 +1 @@ -1.60.0 +1.61.0-beta-1781285686000 diff --git a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java index a640897d4..a41df3f87 100644 --- a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java +++ b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java @@ -997,10 +997,11 @@ Map topLevelTypes() { } void writeTo(List 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 @@ void writeTo(List output, String offset) { if ("Clock".equals(jsonName)) { output.add("import java.util.Date;"); } - if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright", "Debugger", "Screencast", "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)) {