diff --git a/.claude/skills/playwright-roll/SKILL.md b/.claude/skills/playwright-roll/SKILL.md index c33ecdcbb..0b3e5fa7c 100644 --- a/.claude/skills/playwright-roll/SKILL.md +++ b/.claude/skills/playwright-roll/SKILL.md @@ -6,7 +6,7 @@ description: Roll Playwright Java to a new version Help the user roll to a new version of Playwright. ROLLING.md contains general instructions and scripts. -Start with updating the version and generating the API to see the state of things. +Start with running ./scripts/roll_driver.sh to update the version and generate the API to see the state of things. Afterwards, work through the list of changes that need to be backported. You can find a list of pull requests that might need to be taking into account in the issue titled "Backport changes". Work through them one-by-one and check off the items that you have handled. @@ -16,6 +16,126 @@ Rolling includes: - updating client implementation to match changes in the upstream JS implementation (see ../playwright/packages/playwright-core/src/client) - adding a couple of new tests to verify new/changed functionality +## Mimicking the JavaScript implementation + +The Java client is a port of the JS client in `../playwright/packages/playwright-core/src/client/`. When implementing a new or changed method, always read the corresponding JS file first and mirror its logic: + +``` +../playwright/packages/playwright-core/src/client/browserContext.ts +../playwright/packages/playwright-core/src/client/page.ts +../playwright/packages/playwright-core/src/client/tracing.ts +../playwright/packages/playwright-core/src/client/video.ts +../playwright/packages/playwright-core/src/client/locator.ts +../playwright/packages/playwright-core/src/client/network.ts +... +``` + +Key translation rules: + +**Protocol calls** — `await this._channel.methodName(params)` → `sendMessage("methodName", params, NO_TIMEOUT)` + +**Extracting a returned channel object from a result** — JS uses `SomeClass.from(result.foo)` which resolves the JS-side object for a channel reference. In Java, the object was already created when the server sent `__create__`, so extract it from the connection: `connection.getExistingObject(result.getAsJsonObject("foo").get("guid").getAsString())` + +**Async/await** — all `await` calls become synchronous `sendMessage(...)` calls since the Java client is synchronous. + +**`undefined` / optional params** — JS `options?.foo` checks translate to `if (options != null && options.foo != null)` null checks before adding to the params `JsonObject`. + +**`_channel` fields** — the JS `this._channel.foo` maps to calling `sendMessage("foo", ...)` on `this` in the Impl class. + +**Channel object references in params** — when a JS call passes a channel object as a param (e.g. `{ frame: frame._channel }`), in Java pass the guid: `params.addProperty("frame", ((FrameImpl) frame).guid)`. + +## Fixing generator and compilation errors + +After running `./scripts/roll_driver.sh`, the build often fails because the generated Java interfaces reference new types or methods that the generator doesn't know how to handle yet, and the `*Impl` classes don't implement new interface methods. + +### ApiGenerator.java fixes (tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java) + +The generator has hardcoded lists that control which imports are added to each generated file. When new classes appear in the API, add them to the relevant lists in `Interface.writeTo`: +- `options.*` import list — add new classes that use types from the options package +- `java.util.*` import list — add new classes that use `List`, `Map`, etc. +- `java.util.function.Consumer` list — add new classes with `Consumer`-typed event handlers + +Type mapping: when JS-only types (like `Disposable`) are used as return types in Java-compatible methods, add a mapping in `convertBuiltinType`. For example, `Disposable` → `AutoCloseable`. + +Event handler generation: events with `void` type generate invalid `Consumer`. Handle this case in `Event.writeListenerMethods` by emitting `Runnable` instead. + +After editing the generator, recompile and re-run it: +``` +mvn -f tools/api-generator/pom.xml compile -q +mvn -f tools/api-generator/pom.xml exec:java -Dexec.mainClass=com.microsoft.playwright.tools.ApiGenerator +``` + +### Impl class fixes (playwright/src/main/java/com/microsoft/playwright/impl/) + +After regenerating, compile `playwright/` to find what's missing: +``` +mvn -f playwright/pom.xml compile 2>&1 | grep "ERROR" +``` + +Common patterns: + +**Return type changed (e.g. `void` → `AutoCloseable`):** Update the method signature in the Impl class and return an appropriate `AutoCloseable`. Check the JS client to see what kind of disposable is used: +- If JS returns `DisposableObject.from(result.disposable)` — the server created a disposable channel object. Extract its guid from the protocol result and return `connection.getExistingObject(guid)` (a `DisposableObject`). +- If JS returns `new DisposableStub(() => this.someCleanup())` — it's a local callback. Return `new DisposableStub(this::someCleanup)` in Java. +- Examples: `addInitScript`/`exposeBinding`/`exposeFunction` → `DisposableObject`; `route(...)` → `DisposableStub(() -> unroute(...))`; `Tracing.group` → `DisposableStub(this::groupEnd)`; `Video.start` → `DisposableStub(this::stop)`. + +**New method missing:** Add a stub implementation. Common patterns: +- Simple protocol message: `sendMessage("methodName", params, NO_TIMEOUT)` +- New property accessor (e.g. from initializer): `return initializer.get("fieldName").getAsString()` +- Delegation to mainFrame (for Page methods): `return mainFrame.locator(":root").method(...)` + +**New interface entirely (e.g. `Debugger`):** Create a new `*Impl` class extending `ChannelOwner`, implement the interface, and register the type in `Connection.java`'s switch statement. Initialize the field from the parent's initializer in the parent's constructor (e.g. `connection.getExistingObject(initializer.getAsJsonObject("debugger").get("guid").getAsString())`). + +**Field visibility:** If a field needs to be accessed from a sibling Impl class (e.g. setting `existingResponse` on `RequestImpl` from `BrowserContextImpl`), change it from `private` to package-private. + +**`ListenerCollection` only supports `Consumer`, not `Runnable`.** For void events that use `Runnable` handlers, maintain a plain `List` instead. + +**Protocol changes that remove events** — when a method's response now returns an object directly instead of via a subsequent event, update the Impl to capture it from the `sendMessage` result and remove the old event handler. Example: `videoStart` used to fire a `"video"` page event to deliver the artifact; it now returns the artifact directly in the response. Check git history of the upstream JS client when tests hang unexpectedly. + +**Protocol parameter renames** — protocol parameter names can change between versions (e.g. `wsEndpoint` → `endpoint` in `BrowserType.connect`). When a test fails with `expected string, got undefined` or similar validation errors from the driver, check `packages/protocol/src/protocol.yml` for the current parameter names and update the corresponding `params.addProperty(...)` call in the Impl class. Also check the JS client (`src/client/`) to see how it builds the params object. + +## Porting and verifying tests + +**Before porting an upstream test file, check the API exists in Java.** The upstream repo may have test files for brand-new APIs that haven't been added to the Java interface yet (e.g., `screencast.spec.ts` tests `page.screencast` which may not be in the generated `Page.java`). Check `git diff main --name-only` to see what interfaces were added this roll, and verify the method exists in the generated Java interface before porting. + +**Java test file names don't always match upstream spec names.** `TestScreencast.java` tests `recordVideo` video-file recording (which corresponds to `video.spec.ts`), not the newer `page.screencast` streaming API (`screencast.spec.ts`). When comparing coverage, check test *content*, not just file names. + +**Remove tests for behavior that was removed upstream.** When the JS client drops a client-side error check (e.g., "Page is not yet closed before saveAs", "Page did not produce any video frames"), delete the corresponding Java tests rather than trying to keep them passing. Check the upstream `tests/library/` spec to confirm the behavior is gone. + +**Run the full suite to catch regressions, re-run flaky failures in isolation.** Some tests (e.g., `TestClientCertificates#shouldKeepSupportingHttp`) time out only under heavy parallel load. Run the failing test alone to confirm it's flaky before investigating further. + +## Commit Convention + +Semantic commit messages: `label(scope): description` + +Labels: `fix`, `feat`, `chore`, `docs`, `test`, `devops` + +```bash +git checkout -b fix-39562 +# ... make changes ... +git add +git commit -m "$(cat <<'EOF' +fix(proxy): handle SOCKS proxy authentication + +Fixes: https://github.com/microsoft/playwright-java/issues/39562 +EOF +)" +git push origin fix-39562 +gh pr create --repo microsoft/playwright-java --head username:fix-39562 \ + --title "fix(proxy): handle SOCKS proxy authentication" \ + --body "$(cat <<'EOF' +## Summary +- + +Fixes https://github.com/microsoft/playwright-java/issues/39562 +EOF +)" +``` + +Never add Co-Authored-By agents in commit message. +Never add "Generated with" in commit message. +Branch naming for issue fixes: `fix-` + ## Tips & Tricks - Project checkouts are in the parent directory (`../`). - When updating checkboxes, store the issue content into /tmp and edit it there, then update the issue based on the file diff --git a/README.md b/README.md index e5b87f976..07a7206b0 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 145.0.7632.6 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 146.0.7680.31 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 26.0 | ✅ | ✅ | ✅ | -| Firefox 146.0.1 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 148.0.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | ## Documentation diff --git a/ROLLING.md b/ROLLING.md index c66d49bdc..bb57d2577 100644 --- a/ROLLING.md +++ b/ROLLING.md @@ -2,18 +2,6 @@ * make sure to have at least Java 8 and Maven 3.6.3 * clone playwright for java: http://github.com/microsoft/playwright-java -* `./scripts/roll_driver.sh 1.47.0-beta-1726138322000` +* roll the driver and update generated sources: `./scripts/roll_driver.sh next` +* fix any errors * commit & send PR with the roll - -## Finding driver version - -For development versions of Playwright, you can find the latest version by looking at [publish_canary](https://github.com/microsoft/playwright/actions/workflows/publish_canary.yml) workflow -> `publish canary NPM & Publish canary Docker` -> `build & publish driver` step -> `PACKAGE_VERSION` -image - - -# Updating Version - -```bash -./scripts/set_maven_version.sh 1.15.0 -``` - diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java index 89929d891..7b3c4a3c3 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java @@ -514,6 +514,12 @@ public WaitForPageOptions setTimeout(double timeout) { * @since v1.45 */ Clock clock(); + /** + * Debugger allows to pause and resume the execution. + * + * @since v1.59 + */ + Debugger debugger(); /** * Adds cookies into this browser context. All pages within this context will have these cookies installed. Cookies can be * obtained via {@link com.microsoft.playwright.BrowserContext#cookies BrowserContext.cookies()}. @@ -552,7 +558,7 @@ public WaitForPageOptions setTimeout(double timeout) { * @param script Script to be evaluated in all pages in the browser context. * @since v1.8 */ - void addInitScript(String script); + AutoCloseable addInitScript(String script); /** * Adds a script which would be evaluated in one of the following scenarios: *
    @@ -579,7 +585,7 @@ public WaitForPageOptions setTimeout(double timeout) { * @param script Script to be evaluated in all pages in the browser context. * @since v1.8 */ - void addInitScript(Path script); + AutoCloseable addInitScript(Path script); /** * @deprecated Background pages have been removed from Chromium together with Manifest V2 extensions. * @@ -730,8 +736,8 @@ default List cookies() { * @param callback Callback function that will be called in the Playwright's context. * @since v1.8 */ - default void exposeBinding(String name, BindingCallback callback) { - exposeBinding(name, callback, null); + default AutoCloseable exposeBinding(String name, BindingCallback callback) { + return exposeBinding(name, callback, null); } /** * The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context. @@ -777,7 +783,7 @@ default void exposeBinding(String name, BindingCallback callback) { * @param callback Callback function that will be called in the Playwright's context. * @since v1.8 */ - void exposeBinding(String name, BindingCallback callback, ExposeBindingOptions options); + AutoCloseable exposeBinding(String name, BindingCallback callback, ExposeBindingOptions options); /** * The method adds a function called {@code name} on the {@code window} object of every frame in every page in the context. * When called, the function executes {@code callback} and returns a {@code "notifications"} *
  • {@code "payment-handler"}
  • *
  • {@code "storage-access"}
  • + *
  • {@code "screen-wake-lock"}
  • *
* @since v1.8 */ @@ -899,10 +906,17 @@ default void grantPermissions(List permissions) { *
  • {@code "notifications"}
  • *
  • {@code "payment-handler"}
  • *
  • {@code "storage-access"}
  • + *
  • {@code "screen-wake-lock"}
  • * * @since v1.8 */ void grantPermissions(List permissions, GrantPermissionsOptions options); + /** + * Indicates that the browser context is in the process of closing or has already been closed. + * + * @since v1.59 + */ + boolean isClosed(); /** * NOTE: CDP sessions are only supported on Chromium-based browsers. * @@ -994,8 +1008,8 @@ default void grantPermissions(List permissions) { * @param handler handler function to route the request. * @since v1.8 */ - default void route(String url, Consumer handler) { - route(url, handler, null); + default AutoCloseable route(String url, Consumer handler) { + return route(url, handler, null); } /** * Routing provides the capability to modify network requests that are made by any page in the browser context. Once route @@ -1050,7 +1064,7 @@ default void route(String url, Consumer handler) { * @param handler handler function to route the request. * @since v1.8 */ - void route(String url, Consumer handler, RouteOptions options); + AutoCloseable route(String url, Consumer handler, RouteOptions options); /** * Routing provides the capability to modify network requests that are made by any page in the browser context. Once route * is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. @@ -1104,8 +1118,8 @@ default void route(String url, Consumer handler) { * @param handler handler function to route the request. * @since v1.8 */ - default void route(Pattern url, Consumer handler) { - route(url, handler, null); + default AutoCloseable route(Pattern url, Consumer handler) { + return route(url, handler, null); } /** * Routing provides the capability to modify network requests that are made by any page in the browser context. Once route @@ -1160,7 +1174,7 @@ default void route(Pattern url, Consumer handler) { * @param handler handler function to route the request. * @since v1.8 */ - void route(Pattern url, Consumer handler, RouteOptions options); + AutoCloseable route(Pattern url, Consumer handler, RouteOptions options); /** * Routing provides the capability to modify network requests that are made by any page in the browser context. Once route * is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. @@ -1214,8 +1228,8 @@ default void route(Pattern url, Consumer handler) { * @param handler handler function to route the request. * @since v1.8 */ - default void route(Predicate url, Consumer handler) { - route(url, handler, null); + default AutoCloseable route(Predicate url, Consumer handler) { + return route(url, handler, null); } /** * Routing provides the capability to modify network requests that are made by any page in the browser context. Once route @@ -1270,7 +1284,7 @@ default void route(Predicate url, Consumer handler) { * @param handler handler function to route the request. * @since v1.8 */ - void route(Predicate url, Consumer handler, RouteOptions options); + AutoCloseable route(Predicate url, Consumer handler, RouteOptions options); /** * If specified the network requests that are made in the context will be served from the HAR file. Read more about
    Replaying from HAR. @@ -1459,6 +1473,21 @@ default String storageState() { * @since v1.8 */ String storageState(StorageStateOptions options); + /** + * Clears the existing cookies, local storage and IndexedDB entries for all origins and sets the new storage state. + * + *

    Usage + *

    {@code
    +   * // Load storage state from a file and apply it to the context.
    +   * context.setStorageState(Paths.get("state.json"));
    +   * }
    + * + * @param storageState Populates context with given storage state. This option can be used to initialize context with logged-in information + * obtained via {@link com.microsoft.playwright.BrowserContext#storageState BrowserContext.storageState()}. Path to the + * file with saved storage state. + * @since v1.59 + */ + void setStorageState(Path storageState); /** * * @@ -1476,7 +1505,7 @@ default String storageState() { * Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code * handler} is not specified, removes all routes for the {@code url}. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link + * @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link * com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. * @since v1.8 */ @@ -1487,7 +1516,7 @@ default void unroute(String url) { * Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code * handler} is not specified, removes all routes for the {@code url}. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link + * @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link * com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. * @param handler Optional handler function used to register a routing with {@link com.microsoft.playwright.BrowserContext#route * BrowserContext.route()}. @@ -1498,7 +1527,7 @@ default void unroute(String url) { * Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code * handler} is not specified, removes all routes for the {@code url}. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link + * @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link * com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. * @since v1.8 */ @@ -1509,7 +1538,7 @@ default void unroute(Pattern url) { * Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code * handler} is not specified, removes all routes for the {@code url}. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link + * @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link * com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. * @param handler Optional handler function used to register a routing with {@link com.microsoft.playwright.BrowserContext#route * BrowserContext.route()}. @@ -1520,7 +1549,7 @@ default void unroute(Pattern url) { * Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code * handler} is not specified, removes all routes for the {@code url}. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link + * @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link * com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. * @since v1.8 */ @@ -1531,7 +1560,7 @@ default void unroute(Predicate url) { * Removes a route created with {@link com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. When {@code * handler} is not specified, removes all routes for the {@code url}. * - * @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with {@link + * @param url A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with {@link * com.microsoft.playwright.BrowserContext#route BrowserContext.route()}. * @param handler Optional handler function used to register a routing with {@link com.microsoft.playwright.BrowserContext#route * BrowserContext.route()}. diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java index a0a7383b6..9c2b000f5 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java @@ -184,6 +184,12 @@ class LaunchOptions { * href="https://peter.sh/experiments/chromium-command-line-switches/">here. */ public List args; + /** + * If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory is not + * cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the browser + * closes. + */ + public Path artifactsDir; /** * Browser distribution channel. * @@ -279,6 +285,15 @@ public LaunchOptions setArgs(List args) { this.args = args; return this; } + /** + * If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory is not + * cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the browser + * closes. + */ + public LaunchOptions setArtifactsDir(Path artifactsDir) { + this.artifactsDir = artifactsDir; + return this; + } @Deprecated /** * Browser distribution channel. @@ -445,6 +460,12 @@ class LaunchPersistentContextOptions { * href="https://peter.sh/experiments/chromium-command-line-switches/">here. */ public List args; + /** + * If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory is not + * cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the browser + * closes. + */ + public Path artifactsDir; /** * When using {@link com.microsoft.playwright.Page#navigate Page.navigate()}, {@link com.microsoft.playwright.Page#route * Page.route()}, {@link com.microsoft.playwright.Page#waitForURL Page.waitForURL()}, {@link @@ -739,6 +760,15 @@ public LaunchPersistentContextOptions setArgs(List args) { this.args = args; return this; } + /** + * If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory is not + * cleaned up when the browser closes. If not specified, a temporary directory is used and cleaned up when the browser + * closes. + */ + public LaunchPersistentContextOptions setArtifactsDir(Path artifactsDir) { + this.artifactsDir = artifactsDir; + return this; + } /** * When using {@link com.microsoft.playwright.Page#navigate Page.navigate()}, {@link com.microsoft.playwright.Page#route * Page.route()}, {@link com.microsoft.playwright.Page#waitForURL Page.waitForURL()}, {@link @@ -1224,11 +1254,11 @@ public LaunchPersistentContextOptions setViewportSize(ViewportSize viewportSize) *

    NOTE: The major and minor version of the Playwright instance that connects needs to match the version of Playwright that * launches the browser (1.2.3 → is compatible with 1.2.x). * - * @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}. + * @param endpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}. * @since v1.8 */ - default Browser connect(String wsEndpoint) { - return connect(wsEndpoint, null); + default Browser connect(String endpoint) { + return connect(endpoint, null); } /** * This method attaches Playwright to an existing browser instance created via {@code BrowserType.launchServer} in Node.js. @@ -1236,10 +1266,10 @@ default Browser connect(String wsEndpoint) { *

    NOTE: The major and minor version of the Playwright instance that connects needs to match the version of Playwright that * launches the browser (1.2.3 → is compatible with 1.2.x). * - * @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}. + * @param endpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}. * @since v1.8 */ - Browser connect(String wsEndpoint, ConnectOptions options); + Browser connect(String endpoint, ConnectOptions options); /** * This method attaches Playwright to an existing browser instance using the Chrome DevTools Protocol. * diff --git a/playwright/src/main/java/com/microsoft/playwright/CDPSession.java b/playwright/src/main/java/com/microsoft/playwright/CDPSession.java index eb14c7e8b..1c137a4a2 100644 --- a/playwright/src/main/java/com/microsoft/playwright/CDPSession.java +++ b/playwright/src/main/java/com/microsoft/playwright/CDPSession.java @@ -48,6 +48,16 @@ * } */ public interface CDPSession { + + /** + * Emitted when the session is closed, either because the target was closed or {@code session.detach()} was called. + */ + void onClose(Consumer handler); + /** + * Removes handler that was previously added with {@link #onClose onClose(handler)}. + */ + void offClose(Consumer handler); + /** * Detaches the CDPSession from the target. Once detached, the CDPSession object won't emit any events and can't be used to * send messages. diff --git a/playwright/src/main/java/com/microsoft/playwright/ConsoleMessage.java b/playwright/src/main/java/com/microsoft/playwright/ConsoleMessage.java index 012f140c0..db548f8be 100644 --- a/playwright/src/main/java/com/microsoft/playwright/ConsoleMessage.java +++ b/playwright/src/main/java/com/microsoft/playwright/ConsoleMessage.java @@ -69,6 +69,12 @@ public interface ConsoleMessage { * @since v1.8 */ String text(); + /** + * The timestamp of the console message in milliseconds since the Unix epoch. + * + * @since v1.59 + */ + double timestamp(); /** * One of the following values: {@code "log"}, {@code "debug"}, {@code "info"}, {@code "error"}, {@code "warning"}, {@code * "dir"}, {@code "dirxml"}, {@code "table"}, {@code "trace"}, {@code "clear"}, {@code "startGroup"}, {@code diff --git a/playwright/src/main/java/com/microsoft/playwright/Debugger.java b/playwright/src/main/java/com/microsoft/playwright/Debugger.java new file mode 100644 index 000000000..4b50975de --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/Debugger.java @@ -0,0 +1,78 @@ +/* + * 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.*; + +/** + * API for controlling the Playwright debugger. The debugger allows pausing script execution and inspecting the page. + * Obtain the debugger instance via {@link com.microsoft.playwright.BrowserContext#debugger BrowserContext.debugger()}. + */ +public interface Debugger { + + /** + * Emitted when the debugger pauses or resumes. + */ + void onPausedStateChanged(Runnable handler); + /** + * Removes handler that was previously added with {@link #onPausedStateChanged onPausedStateChanged(handler)}. + */ + void offPausedStateChanged(Runnable handler); + + /** + * Returns details about the currently paused calls. Returns an empty array if the debugger is not paused. + * + * @since v1.59 + */ + List pausedDetails(); + /** + * Configures the debugger to pause before the next action is executed. + * + *

    Throws if the debugger is already paused. Use {@link com.microsoft.playwright.Debugger#next Debugger.next()} or {@link + * com.microsoft.playwright.Debugger#runTo Debugger.runTo()} to step while paused. + * + *

    Note that {@link com.microsoft.playwright.Page#pause Page.pause()} is equivalent to a "debugger" statement — it pauses + * execution at the call site immediately. On the contrary, {@link com.microsoft.playwright.Debugger#pause + * Debugger.pause()} is equivalent to "pause on next statement" — it configures the debugger to pause before the next + * action is executed. + * + * @since v1.59 + */ + void pause(); + /** + * Resumes script execution. Throws if the debugger is not paused. + * + * @since v1.59 + */ + void resume(); + /** + * Resumes script execution and pauses again before the next action. Throws if the debugger is not paused. + * + * @since v1.59 + */ + void next(); + /** + * Resumes script execution and pauses when an action originates from the given source location. Throws if the debugger is + * not paused. + * + * @param location The source location to pause at. + * @since v1.59 + */ + void runTo(Location location); +} + diff --git a/playwright/src/main/java/com/microsoft/playwright/Frame.java b/playwright/src/main/java/com/microsoft/playwright/Frame.java index ace8d2e63..74120f1ce 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Frame.java +++ b/playwright/src/main/java/com/microsoft/playwright/Frame.java @@ -2272,7 +2272,7 @@ class WaitForNavigationOptions { */ public Double timeout; /** - * A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. */ @@ -2302,7 +2302,7 @@ public WaitForNavigationOptions setTimeout(double timeout) { return this; } /** - * A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. */ @@ -2311,7 +2311,7 @@ public WaitForNavigationOptions setUrl(String url) { return this; } /** - * A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. */ @@ -2320,7 +2320,7 @@ public WaitForNavigationOptions setUrl(Pattern url) { return this; } /** - * A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. */ @@ -3380,7 +3380,7 @@ default Locator getByPlaceholder(Pattern text) { * *

    Consider the following DOM structure. * - *

    You can locate each element by it's implicit role: + *

    You can locate each element by its implicit role: *

    {@code
        * assertThat(page
        *     .getByRole(AriaRole.HEADING,
    @@ -3423,7 +3423,7 @@ default Locator getByRole(AriaRole role) {
        *
        * 

    Consider the following DOM structure. * - *

    You can locate each element by it's implicit role: + *

    You can locate each element by its implicit role: *

    {@code
        * assertThat(page
        *     .getByRole(AriaRole.HEADING,
    @@ -3462,7 +3462,7 @@ default Locator getByRole(AriaRole role) {
        *
        * 

    Consider the following DOM structure. * - *

    You can locate the element by it's test id: + *

    You can locate the element by its test id: *

    {@code
        * page.getByTestId("directions").click();
        * }
    @@ -3484,7 +3484,7 @@ default Locator getByRole(AriaRole role) { * *

    Consider the following DOM structure. * - *

    You can locate the element by it's test id: + *

    You can locate the element by its test id: *

    {@code
        * page.getByTestId("directions").click();
        * }
    @@ -5139,7 +5139,7 @@ default ElementHandle waitForSelector(String selector) { * frame.waitForURL("**\/target.html"); * }
    * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. * @since v1.11 @@ -5156,7 +5156,7 @@ default void waitForURL(String url) { * frame.waitForURL("**\/target.html"); * }
    * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. * @since v1.11 @@ -5171,7 +5171,7 @@ default void waitForURL(String url) { * frame.waitForURL("**\/target.html"); * } * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. * @since v1.11 @@ -5188,7 +5188,7 @@ default void waitForURL(Pattern url) { * frame.waitForURL("**\/target.html"); * } * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. * @since v1.11 @@ -5203,7 +5203,7 @@ default void waitForURL(Pattern url) { * frame.waitForURL("**\/target.html"); * } * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. * @since v1.11 @@ -5220,7 +5220,7 @@ default void waitForURL(Predicate url) { * frame.waitForURL("**\/target.html"); * } * - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * @param url A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. * @since v1.11 diff --git a/playwright/src/main/java/com/microsoft/playwright/FrameLocator.java b/playwright/src/main/java/com/microsoft/playwright/FrameLocator.java index 007460217..dc386fbd2 100644 --- a/playwright/src/main/java/com/microsoft/playwright/FrameLocator.java +++ b/playwright/src/main/java/com/microsoft/playwright/FrameLocator.java @@ -602,7 +602,7 @@ default Locator getByPlaceholder(Pattern text) { * *

    Consider the following DOM structure. * - *

    You can locate each element by it's implicit role: + *

    You can locate each element by its implicit role: *

    {@code
        * assertThat(page
        *     .getByRole(AriaRole.HEADING,
    @@ -645,7 +645,7 @@ default Locator getByRole(AriaRole role) {
        *
        * 

    Consider the following DOM structure. * - *

    You can locate each element by it's implicit role: + *

    You can locate each element by its implicit role: *

    {@code
        * assertThat(page
        *     .getByRole(AriaRole.HEADING,
    @@ -684,7 +684,7 @@ default Locator getByRole(AriaRole role) {
        *
        * 

    Consider the following DOM structure. * - *

    You can locate the element by it's test id: + *

    You can locate the element by its test id: *

    {@code
        * page.getByTestId("directions").click();
        * }
    @@ -706,7 +706,7 @@ default Locator getByRole(AriaRole role) { * *

    Consider the following DOM structure. * - *

    You can locate the element by it's test id: + *

    You can locate the element by its test id: *

    {@code
        * page.getByTestId("directions").click();
        * }
    diff --git a/playwright/src/main/java/com/microsoft/playwright/Locator.java b/playwright/src/main/java/com/microsoft/playwright/Locator.java index 8e4317f4f..e05d86c9f 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Locator.java +++ b/playwright/src/main/java/com/microsoft/playwright/Locator.java @@ -30,6 +30,15 @@ */ public interface Locator { class AriaSnapshotOptions { + /** + * When specified, limits the depth of the snapshot. + */ + public Integer depth; + /** + * When set to {@code "ai"}, returns a snapshot optimized for AI consumption with element references. Defaults to {@code + * "default"}. + */ + public AriaSnapshotMode mode; /** * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout @@ -38,6 +47,21 @@ class AriaSnapshotOptions { */ public Double timeout; + /** + * When specified, limits the depth of the snapshot. + */ + public AriaSnapshotOptions setDepth(int depth) { + this.depth = depth; + return this; + } + /** + * When set to {@code "ai"}, returns a snapshot optimized for AI consumption with element references. Defaults to {@code + * "default"}. + */ + public AriaSnapshotOptions setMode(AriaSnapshotMode mode) { + this.mode = mode; + return this; + } /** * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout @@ -3492,7 +3516,7 @@ default Locator getByPlaceholder(Pattern text) { * *

    Consider the following DOM structure. * - *

    You can locate each element by it's implicit role: + *

    You can locate each element by its implicit role: *

    {@code
        * assertThat(page
        *     .getByRole(AriaRole.HEADING,
    @@ -3535,7 +3559,7 @@ default Locator getByRole(AriaRole role) {
        *
        * 

    Consider the following DOM structure. * - *

    You can locate each element by it's implicit role: + *

    You can locate each element by its implicit role: *

    {@code
        * assertThat(page
        *     .getByRole(AriaRole.HEADING,
    @@ -3574,7 +3598,7 @@ default Locator getByRole(AriaRole role) {
        *
        * 

    Consider the following DOM structure. * - *

    You can locate the element by it's test id: + *

    You can locate the element by its test id: *

    {@code
        * page.getByTestId("directions").click();
        * }
    @@ -3596,7 +3620,7 @@ default Locator getByRole(AriaRole role) { * *

    Consider the following DOM structure. * - *

    You can locate the element by it's test id: + *

    You can locate the element by its test id: *

    {@code
        * page.getByTestId("directions").click();
        * }
    @@ -4245,6 +4269,14 @@ default Locator locator(Locator selectorOrLocator) { * @since v1.14 */ Locator locator(Locator selectorOrLocator, LocatorOptions options); + /** + * Returns a new locator that uses best practices for referencing the matched element, prioritizing test ids, aria roles, + * and other user-facing attributes over CSS selectors. This is useful for converting implementation-detail selectors into + * more resilient, human-readable locators. + * + * @since v1.59 + */ + Locator normalize(); /** * Returns locator to the n-th matching element. It's zero based, {@code nth(0)} selects the first element. * diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java index 5e01ee569..98f9d7e26 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Page.java +++ b/playwright/src/main/java/com/microsoft/playwright/Page.java @@ -1987,6 +1987,20 @@ public IsVisibleOptions setTimeout(double timeout) { return this; } } + class ConsoleMessagesOptions { + /** + * Controls which messages are returned: + */ + public ConsoleMessagesFilter filter; + + /** + * Controls which messages are returned: + */ + public ConsoleMessagesOptions setFilter(ConsoleMessagesFilter filter) { + this.filter = filter; + return this; + } + } class LocatorOptions { /** * Narrows down the results of the method to those which contain elements matching this relative locator. For example, @@ -2966,6 +2980,50 @@ public SetInputFilesOptions setTimeout(double timeout) { return this; } } + class AriaSnapshotOptions { + /** + * When specified, limits the depth of the snapshot. + */ + public Integer depth; + /** + * When set to {@code "ai"}, returns a snapshot optimized for AI consumption with element references. Defaults to {@code + * "default"}. + */ + public AriaSnapshotMode mode; + /** + * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default + * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout + * BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()} + * methods. + */ + public Double timeout; + + /** + * When specified, limits the depth of the snapshot. + */ + public AriaSnapshotOptions setDepth(int depth) { + this.depth = depth; + return this; + } + /** + * When set to {@code "ai"}, returns a snapshot optimized for AI consumption with element references. Defaults to {@code + * "default"}. + */ + public AriaSnapshotOptions setMode(AriaSnapshotMode mode) { + this.mode = mode; + return this; + } + /** + * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default + * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout + * BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()} + * methods. + */ + public AriaSnapshotOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } class TapOptions { /** * Whether to bypass the actionability checks. Defaults to @@ -3428,7 +3486,7 @@ class WaitForNavigationOptions { */ public Double timeout; /** - * A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. */ @@ -3458,7 +3516,7 @@ public WaitForNavigationOptions setTimeout(double timeout) { return this; } /** - * A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. */ @@ -3467,7 +3525,7 @@ public WaitForNavigationOptions setUrl(String url) { return this; } /** - * A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. */ @@ -3476,7 +3534,7 @@ public WaitForNavigationOptions setUrl(Pattern url) { return this; } /** - * A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. Note that if the + * A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if the * parameter is a string without wildcard characters, the method will wait for navigation to URL that is exactly equal to * the string. */ @@ -3812,7 +3870,7 @@ public WaitForWorkerOptions setTimeout(double timeout) { * @param script Script to be evaluated in all pages in the browser context. * @since v1.8 */ - void addInitScript(String script); + AutoCloseable addInitScript(String script); /** * Adds a script which would be evaluated in one of the following scenarios: *
      @@ -3839,7 +3897,7 @@ public WaitForWorkerOptions setTimeout(double timeout) { * @param script Script to be evaluated in all pages in the browser context. * @since v1.8 */ - void addInitScript(Path script); + AutoCloseable addInitScript(Path script); /** * Adds a {@code "); + checkAndMatchSnapshot(page.locator("body"), "- button \"foo\""); + + // Text "foo" is assigned to the slot, should be used instead of slot content. + page.setContent( + "
      foo
      " + + ""); + checkAndMatchSnapshot(page.locator("body"), "- button \"foo\""); + + // Nothing is assigned to the slot, should use slot content. + page.setContent( + "
      " + + ""); + checkAndMatchSnapshot(page.locator("body"), "- button \"pre\""); + } + + @Test + void shouldSnapshotInnerText(Page page) { + page.setContent( + "
      a.test.ts
      " + + "
      " + + "
      snapshot
      " + + "
      30ms
      " + + "
      "); + checkAndMatchSnapshot(page.locator("body"), + " - listitem:\n" + + " - text: a.test.ts\n" + + " - button \"Run\"\n" + + " - button \"Show source\"\n" + + " - button \"Watch\"\n" + + " - listitem:\n" + + " - text: snapshot 30ms\n" + + " - button \"Run\"\n" + + " - button \"Show source\"\n" + + " - button \"Watch\""); + } + + @Test + void checkAriaHiddenText(Page page) { + page.setContent("

      helloworld

      "); + checkAndMatchSnapshot(page.locator("body"), "- paragraph: hello"); + } + + @Test + void shouldIgnorePresentationAndNoneRoles(Page page) { + page.setContent("
      • hello
      • world
      "); + checkAndMatchSnapshot(page.locator("body"), "- list: hello world"); + } + + @Test + void shouldNotUseOnAsCheckboxValue(Page page) { + page.setContent(""); + checkAndMatchSnapshot(page.locator("body"), "- checkbox\n- radio"); + } + + @Test + void shouldNotReportTextareaTextContent(Page page) { + page.setContent(""); + checkAndMatchSnapshot(page.locator("body"), "- textbox: Before"); + page.evaluate("document.querySelector('textarea').value = 'After'"); + checkAndMatchSnapshot(page.locator("body"), "- textbox: After"); + } + + @Test + void shouldNotShowVisibleChildrenOfHiddenElements(Page page) { + page.setContent( + "
      " + + "
      " + + "
      "); + assertEquals("", page.locator("body").ariaSnapshot()); + } + + @Test + void shouldNotShowUnhiddenChildrenOfAriaHiddenElements(Page page) { + page.setContent( + "
      " + + "
      " + + "
      "); + assertEquals("", page.locator("body").ariaSnapshot()); + } + + @Test + void shouldSnapshotPlaceholderWhenDifferentFromName(Page page) { + page.setContent(""); + assertThat(page.locator("body")).matchesAriaSnapshot("- textbox \"Placeholder\""); + + page.setContent(""); + assertThat(page.locator("body")).matchesAriaSnapshot( + "- textbox \"Label\":\n" + + " - /placeholder: Placeholder"); + } + } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshotAI.java b/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshotAI.java new file mode 100644 index 000000000..b29813211 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshotAI.java @@ -0,0 +1,191 @@ +package com.microsoft.playwright; + +import com.microsoft.playwright.junit.FixtureTest; +import com.microsoft.playwright.junit.UsePlaywright; +import com.microsoft.playwright.options.AriaSnapshotMode; +import org.junit.jupiter.api.Test; + +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@FixtureTest +@UsePlaywright +public class TestPageAriaSnapshotAI { + private static String aiSnapshot(Page page) { + return page.ariaSnapshot(new Page.AriaSnapshotOptions().setMode(AriaSnapshotMode.AI)); + } + + @Test + void shouldGenerateRefs(Page page) { + page.setContent(""); + + String snapshot1 = aiSnapshot(page); + assertTrue(snapshot1.contains("button \"One\" [ref=e2]"), snapshot1); + assertTrue(snapshot1.contains("button \"Two\" [ref=e3]"), snapshot1); + assertTrue(snapshot1.contains("button \"Three\" [ref=e4]"), snapshot1); + assertThat(page.locator("aria-ref=e2")).hasText("One"); + assertThat(page.locator("aria-ref=e3")).hasText("Two"); + assertThat(page.locator("aria-ref=e4")).hasText("Three"); + + page.locator("aria-ref=e3").evaluate("e => e.textContent = 'Not Two'"); + + String snapshot2 = aiSnapshot(page); + assertTrue(snapshot2.contains("button \"One\" [ref=e2]"), snapshot2); + assertTrue(snapshot2.contains("button \"Not Two\" [ref=e5]"), snapshot2); + assertTrue(snapshot2.contains("button \"Three\" [ref=e4]"), snapshot2); + } + + @Test + void shouldListIframes(Page page) { + page.setContent( + "

      Hello

      " + + ""); + + Locator list = page.frames().get(1).locator("ul"); + String snapshot = list.ariaSnapshot(new Locator.AriaSnapshotOptions().setMode(AriaSnapshotMode.AI)); + assertTrue(snapshot.contains("list [ref=f1e1]"), snapshot); + assertTrue(snapshot.contains("listitem [ref=f1e2]: Item 1"), snapshot); + assertTrue(snapshot.contains("listitem [ref=f1e3]: Item 2"), snapshot); + } + + @Test + void shouldCollapseGenericNodes(Page page) { + page.setContent("
      "); + String snapshot = aiSnapshot(page); + assertTrue(snapshot.contains("button \"Button\" [ref=e5]"), snapshot); + } + + @Test + void shouldIncludeCursorPointerHint(Page page) { + page.setContent(""); + String snapshot = aiSnapshot(page); + assertTrue(snapshot.contains("button \"Button\" [ref=e2] [cursor=pointer]"), snapshot); + } + + @Test + void shouldNotNestCursorPointerHints(Page page) { + page.setContent( + "" + + "Link with a button " + + ""); + String snapshot = aiSnapshot(page); + assertTrue(snapshot.contains("link \"Link with a button Button\" [ref=e2] [cursor=pointer]"), snapshot); + // The button inside a cursor-pointer link should not get a redundant [cursor=pointer] + assertTrue(snapshot.contains("button \"Button\" [ref=e3]"), snapshot); + assertFalse(snapshot.contains("button \"Button\" [ref=e3] [cursor=pointer]"), snapshot); + } + + @Test + void shouldShowVisibleChildrenOfHiddenElements(Page page) { + page.setContent( + "
      " + + "
      " + + "
      " + + "
      " + + "
      " + + " " + + "
      " + + "
      "); + String snapshot = aiSnapshot(page); + assertEquals( + "- generic [active] [ref=e1]:\n" + + " - button \"Visible\" [ref=e3]\n" + + " - button \"Visible\" [ref=e4]", + snapshot); + } + + @Test + void shouldIncludeActiveElementInformation(Page page) { + page.setContent( + "" + + "" + + "
      Not focusable
      "); + page.waitForFunction("document.activeElement?.id === 'btn2'"); + + String snapshot = aiSnapshot(page); + assertTrue(snapshot.contains("button \"Button 2\" [active] [ref=e3]"), snapshot); + assertFalse(snapshot.contains("button \"Button 1\" [active]"), snapshot); + } + + @Test + void shouldUpdateActiveElementOnFocus(Page page) { + page.setContent( + "" + + ""); + + String initialSnapshot = aiSnapshot(page); + assertTrue(initialSnapshot.contains("textbox \"First input\" [ref=e2]"), initialSnapshot); + assertTrue(initialSnapshot.contains("textbox \"Second input\" [ref=e3]"), initialSnapshot); + assertFalse(initialSnapshot.contains("textbox \"First input\" [active]"), initialSnapshot); + assertFalse(initialSnapshot.contains("textbox \"Second input\" [active]"), initialSnapshot); + + page.locator("#input2").focus(); + + String afterFocusSnapshot = aiSnapshot(page); + assertTrue(afterFocusSnapshot.contains("textbox \"Second input\" [active] [ref=e3]"), afterFocusSnapshot); + assertFalse(afterFocusSnapshot.contains("textbox \"First input\" [active]"), afterFocusSnapshot); + } + + @Test + void shouldCollapseInlineGenericNodes(Page page) { + page.setContent( + "
        " + + "
      • 3 bds
      • " + + "
      • 2 ba
      • " + + "
      • 1,200 sqft
      • " + + "
      "); + String snapshot = aiSnapshot(page); + assertTrue(snapshot.contains("listitem [ref=e3]: 3 bds"), snapshot); + assertTrue(snapshot.contains("listitem [ref=e4]: 2 ba"), snapshot); + assertTrue(snapshot.contains("listitem [ref=e5]: 1,200 sqft"), snapshot); + } + + @Test + void shouldNotRemoveGenericNodesWithTitle(Page page) { + page.setContent("
      Element content
      "); + String snapshot = aiSnapshot(page); + assertTrue(snapshot.contains("generic \"Element title\" [ref=e2]"), snapshot); + } + + @Test + void shouldLimitDepth(Page page) { + page.setContent( + "
        " + + "
      • item1
      • " + + "link" + + "
        • item2
          • item3
      • " + + "
      "); + + String snapshot1 = page.ariaSnapshot(new Page.AriaSnapshotOptions().setMode(AriaSnapshotMode.AI).setDepth(1)); + assertTrue(snapshot1.contains("listitem [ref=e3]: item1"), snapshot1); + assertFalse(snapshot1.contains("item2"), snapshot1); + assertFalse(snapshot1.contains("item3"), snapshot1); + + String snapshot2 = page.ariaSnapshot(new Page.AriaSnapshotOptions().setMode(AriaSnapshotMode.AI).setDepth(3)); + assertTrue(snapshot2.contains("item1"), snapshot2); + assertTrue(snapshot2.contains("item2"), snapshot2); + assertFalse(snapshot2.contains("item3"), snapshot2); + + String snapshot3 = page.ariaSnapshot(new Page.AriaSnapshotOptions().setMode(AriaSnapshotMode.AI).setDepth(100)); + assertTrue(snapshot3.contains("item1"), snapshot3); + assertTrue(snapshot3.contains("item2"), snapshot3); + assertTrue(snapshot3.contains("item3"), snapshot3); + + String snapshot4 = page.locator("#target").ariaSnapshot(new Locator.AriaSnapshotOptions().setMode(AriaSnapshotMode.AI).setDepth(1)); + assertTrue(snapshot4.contains("listitem [ref=e7]: item2"), snapshot4); + assertFalse(snapshot4.contains("item3"), snapshot4); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageEventConsole.java b/playwright/src/test/java/com/microsoft/playwright/TestPageEventConsole.java index f3c5b6766..8b4b0b2fc 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageEventConsole.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageEventConsole.java @@ -16,6 +16,7 @@ package com.microsoft.playwright; +import com.microsoft.playwright.options.ConsoleMessagesFilter; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; @@ -26,8 +27,7 @@ import static com.microsoft.playwright.Utils.mapOf; import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class TestPageEventConsole extends TestBase { @Test @@ -149,4 +149,60 @@ void consoleMessagesShouldWork() { assertEquals(page, message.page()); } } + + @Test + void shouldHaveTimestamp() { + double before = (double) System.currentTimeMillis() - 1; + ConsoleMessage message = page.waitForConsoleMessage( + () -> page.evaluate("() => console.log('timestamp test')")); + double after = (double) System.currentTimeMillis() + 1; + assertTrue(message.timestamp() >= before, + "timestamp " + message.timestamp() + " should be >= " + before); + assertTrue(message.timestamp() <= after, + "timestamp " + message.timestamp() + " should be <= " + after); + } + + @Test + void shouldHaveIncreasingTimestamps() { + List messages = new ArrayList<>(); + page.onConsoleMessage(messages::add); + page.evaluate("() => { console.log('first'); console.log('second'); console.log('third'); }"); + assertEquals(3, messages.size()); + for (int i = 1; i < messages.size(); i++) + assertTrue(messages.get(i).timestamp() >= messages.get(i - 1).timestamp()); + } + + @Test + void clearConsoleMessagesShouldWork() { + page.evaluate("() => { console.log('message1'); console.log('message2'); }"); + List messages = page.consoleMessages(); + assertTrue(messages.stream().anyMatch(m -> "message1".equals(m.text()))); + assertTrue(messages.stream().anyMatch(m -> "message2".equals(m.text()))); + + page.clearConsoleMessages(); + messages = page.consoleMessages(); + assertEquals(0, messages.size()); + + page.waitForConsoleMessage(() -> page.evaluate("() => console.log('message3')")); + messages = page.consoleMessages(); + assertEquals(1, messages.size()); + assertEquals("message3", messages.get(0).text()); + } + + @Test + void consoleMessagesSinceNavigationFilterShouldWork() { + page.evaluate("() => console.log('before navigation')"); + page.navigate(server.EMPTY_PAGE); + page.evaluate("() => console.log('after navigation')"); + + List all = page.consoleMessages( + new Page.ConsoleMessagesOptions().setFilter(ConsoleMessagesFilter.ALL)); + assertTrue(all.stream().anyMatch(m -> "before navigation".equals(m.text()))); + assertTrue(all.stream().anyMatch(m -> "after navigation".equals(m.text()))); + + // sinceNavigation is the default + List sinceNav = page.consoleMessages(); + assertFalse(sinceNav.stream().anyMatch(m -> "before navigation".equals(m.text()))); + assertTrue(sinceNav.stream().anyMatch(m -> "after navigation".equals(m.text()))); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageEventPageError.java b/playwright/src/test/java/com/microsoft/playwright/TestPageEventPageError.java index a994102ef..f70e2cbd5 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageEventPageError.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageEventPageError.java @@ -18,7 +18,6 @@ import org.junit.jupiter.api.Test; -import java.util.ArrayList; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -44,4 +43,28 @@ void pageErrorsShouldWork() { assertTrue(error.startsWith("Error: error" + (201 + i)), error); } } + + @Test + void clearPageErrorsShouldWork() { + page.navigate(server.EMPTY_PAGE); + page.evaluate("async () => {\n" + + " window.setTimeout(() => { throw new Error('error1'); }, 0);\n" + + " await new Promise(f => window.setTimeout(f, 100));\n" + + "}"); + + List errors = page.pageErrors(); + assertTrue(errors.stream().anyMatch(e -> e.contains("error1"))); + + page.clearPageErrors(); + errors = page.pageErrors(); + assertEquals(0, errors.size()); + + page.evaluate("async () => {\n" + + " window.setTimeout(() => { throw new Error('error2'); }, 0);\n" + + " await new Promise(f => window.setTimeout(f, 100));\n" + + "}"); + errors = page.pageErrors(); + assertEquals(1, errors.size()); + assertTrue(errors.get(0).contains("error2")); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPopup.java b/playwright/src/test/java/com/microsoft/playwright/TestPopup.java index c8bd17d00..5e0f36715 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPopup.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPopup.java @@ -17,6 +17,7 @@ package com.microsoft.playwright; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assumptions; import java.util.ArrayList; import java.util.Arrays; @@ -107,6 +108,8 @@ void shouldInheritHttpCredentialsFromBrowserContext() { @Test void shouldInheritTouchSupportFromBrowserContext() { + // https://bugzilla.mozilla.org/show_bug.cgi?id=2014330 + Assumptions.assumeFalse(isFirefox() && Integer.parseInt(browser.version().split("\\.")[0]) >= 148); BrowserContext context = browser.newContext(new Browser.NewContextOptions() .setViewportSize(400, 500) .setHasTouch(true)); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java b/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java index 94847f337..53c1724b1 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java @@ -58,32 +58,6 @@ void shouldSaveAsVideo(@TempDir Path videosDir) { assertTrue(Files.exists(saveAsPath)); } - @Test - void saveAsShouldThrowWhenNoVideoFrames(@TempDir Path videosDir) { - try (BrowserContext context = browser.newContext( - new Browser.NewContextOptions() - .setRecordVideoDir(videosDir) - .setRecordVideoSize(320, 240) - .setViewportSize(320, 240))) { - - Page page = context.newPage(); - Page popup = context.waitForPage(() -> { - page.evaluate("() => {\n" + - " const win = window.open('about:blank');\n" + - " win.close();\n" + - "}"); - }); - page.close(); - - Path saveAsPath = videosDir.resolve("my-video.webm"); - if (!popup.isClosed()) { - popup.waitForClose(() -> {}); - } - PlaywrightException e = assertThrows(PlaywrightException.class, () -> popup.video().saveAs(saveAsPath)); - assertTrue(e.getMessage().contains("Page did not produce any video frames"), e.getMessage()); - } - } - @Test void shouldDeleteVideo(@TempDir Path videosDir) { try (BrowserContext context = browser.newContext( @@ -123,16 +97,4 @@ void shouldWaitForVideoFinishWhenPageIsClosed(@TempDir Path videosDir) throws IO assertTrue(Files.size(files.get(0)) > 0); } - @Test - void shouldErrorIfPageNotClosedBeforeSaveAs(@TempDir Path tmpDir) { - try (Page page = browser.newPage(new Browser.NewPageOptions().setRecordVideoDir(tmpDir))) { - page.navigate(server.PREFIX + "/grid.html"); - Path outPath = tmpDir.resolve("some-video.webm"); - Video video = page.video(); - PlaywrightException exception = assertThrows(PlaywrightException.class, () -> video.saveAs(outPath)); - assertTrue( - exception.getMessage().contains("Page is not yet closed. Close the page prior to calling saveAs"), - exception.getMessage()); - } - } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestVideo.java b/playwright/src/test/java/com/microsoft/playwright/TestVideo.java index 6d6606590..24b80fae7 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestVideo.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestVideo.java @@ -16,6 +16,7 @@ package com.microsoft.playwright; +import com.microsoft.playwright.options.Size; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -23,7 +24,7 @@ import java.nio.file.Path; import static com.microsoft.playwright.Utils.relativePathOrSkipTest; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class TestVideo extends TestBase { @Test @@ -37,4 +38,50 @@ void shouldWorkWithRelativePathForRecordVideoDir(@TempDir Path tmpDir) { assertTrue(videoPath.isAbsolute(), "videosPath = " + videoPath); assertTrue(Files.exists(videoPath), "videosPath = " + videoPath); } + + @Test + void videoStartShouldFailWhenRecordVideoIsSet(@TempDir Path tmpDir) { + BrowserContext ctx = browser.newContext(new Browser.NewContextOptions() + .setRecordVideoSize(320, 240).setRecordVideoDir(tmpDir)); + Page pg = ctx.newPage(); + try { + PlaywrightException e = assertThrows(PlaywrightException.class, + () -> pg.video().start()); + assertTrue(e.getMessage().contains("Video is already being recorded"), e.getMessage()); + // stop should still work + pg.video().stop(); + } finally { + ctx.close(); + } + } + + @Test + void videoStopShouldFailWhenNoRecordingIsInProgress() { + BrowserContext ctx = browser.newContext(); + Page pg = ctx.newPage(); + try { + PlaywrightException e = assertThrows(PlaywrightException.class, + () -> pg.video().stop()); + assertTrue(e.getMessage().contains("Video is not being recorded"), e.getMessage()); + } finally { + ctx.close(); + } + } + + @Test + void videoStartAndStopShouldProduceVideoFile(@TempDir Path tmpDir) throws Exception { + BrowserContext ctx = browser.newContext(new Browser.NewContextOptions() + .setViewportSize(800, 800)); + Page pg = ctx.newPage(); + try { + Size size = new Size(800, 800); + pg.video().start(new Video.StartOptions().setSize(size)); + pg.video().stop(); + Path videoPath = pg.video().path(); + assertNotNull(videoPath); + assertTrue(Files.exists(videoPath), "video file should exist: " + videoPath); + } finally { + ctx.close(); + } + } } diff --git a/scripts/DRIVER_VERSION b/scripts/DRIVER_VERSION index 79f82f6b8..47f0c6e34 100644 --- a/scripts/DRIVER_VERSION +++ b/scripts/DRIVER_VERSION @@ -1 +1 @@ -1.58.0 +1.59.0-alpha-1774287265000 diff --git a/scripts/roll_driver.sh b/scripts/roll_driver.sh index 243bdebd4..f8535e57f 100755 --- a/scripts/roll_driver.sh +++ b/scripts/roll_driver.sh @@ -6,15 +6,23 @@ set +x trap "cd $(pwd -P)" EXIT cd "$(dirname $0)" -if [ "$#" -ne 1 ]; then +if [ "$#" -gt 1 ]; then echo "" - echo "Usage: scripts/roll_driver.sh [new version]" + echo "Usage: scripts/roll_driver.sh [next|beta|]" echo "" exit 1 fi -NEW_VERSION=$1 +ARG=${1:-next} +if [[ "$ARG" == "next" ]]; then + NEW_VERSION=$(npm view playwright@next version) +elif [[ "$ARG" == "beta" ]]; then + NEW_VERSION=$(npm view playwright@beta version) +else + NEW_VERSION=$ARG +fi CURRENT_VERSION=$(head -1 ./DRIVER_VERSION) +echo "Rolling driver from $CURRENT_VERSION to $NEW_VERSION" if [[ "$CURRENT_VERSION" == "$NEW_VERSION" ]]; then echo "Current version is up to date. Skipping driver download."; 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 9efde32a6..48a185bad 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 @@ -500,6 +500,9 @@ private String convertBuiltinType(JsonObject jsonType) { if ("Buffer".equals(name)) { return "byte[]"; } + if ("Disposable".equals(name)) { + return "AutoCloseable"; + } if ("URL".equals(name)) { return "String"; } @@ -639,7 +642,7 @@ void writeListenerMethods(List output, String offset) { writeJavadoc(output, offset, comment()); String name = toTitle(jsonName); String paramType = type.toJava(); - String listenerType = "Consumer<" + paramType + ">"; + String listenerType = "void".equals(paramType) ? "Runnable" : "Consumer<" + paramType + ">"; output.add(offset + "void on" + name + "(" + listenerType + " handler);"); writeJavadoc(output, offset, "Removes handler that was previously added with {@link #on" + name + " on" + name + "(handler)}."); output.add(offset + "void off" + name + "(" + listenerType + " handler);"); @@ -986,7 +989,7 @@ void writeTo(List output, String offset) { if (methods.stream().anyMatch(m -> "create".equals(m.jsonName))) { output.add("import com.microsoft.playwright.impl." + jsonName + "Impl;"); } - if (asList("Page", "Request", "Response", "APIRequestContext", "APIRequest", "APIResponse", "FileChooser", "Frame", "FrameLocator", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard", "Tracing").contains(jsonName)) { + if (asList("Page", "Request", "Response", "APIRequestContext", "APIRequest", "APIResponse", "FileChooser", "Frame", "FrameLocator", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard", "Tracing", "Video", "Debugger").contains(jsonName)) { output.add("import com.microsoft.playwright.options.*;"); } if ("Download".equals(jsonName)) { @@ -998,7 +1001,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").contains(jsonName)) { + if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright", "Debugger").contains(jsonName)) { output.add("import java.util.*;"); } if (asList("WebSocketRoute").contains(jsonName)) {