diff --git a/.claude/skills/playwright-roll/SKILL.md b/.claude/skills/playwright-roll/SKILL.md index c33ecdcb..0b3e5fa7 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 e5b87f97..07a7206b 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 c66d49bd..bb57d257 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 89929d89..7b3c4a3c 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 interface BrowserContext extends AutoCloseable { * @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 interface BrowserContext extends AutoCloseable { * @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 interface BrowserContext extends AutoCloseable { * @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 @@ public interface BrowserContext extends AutoCloseable { * @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 @@ public interface BrowserContext extends AutoCloseable { * @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 @@ public interface BrowserContext extends AutoCloseable { *
  • {@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 @@ public interface BrowserContext extends AutoCloseable { * @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 @@ public interface BrowserContext extends AutoCloseable { * @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 @@ public interface BrowserContext extends AutoCloseable { * @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 @@ public interface BrowserContext extends AutoCloseable { * @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 @@ public interface BrowserContext extends AutoCloseable { * @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 @@ public interface BrowserContext extends AutoCloseable { * @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 @@ public interface BrowserContext extends AutoCloseable { * @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 @@ public interface BrowserContext extends AutoCloseable { * 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 @@ public interface BrowserContext extends AutoCloseable { * 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 @@ public interface BrowserContext extends AutoCloseable { * 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 @@ public interface BrowserContext extends AutoCloseable { * 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 @@ public interface BrowserContext extends AutoCloseable { * 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 @@ public interface BrowserContext extends AutoCloseable { * 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 a0a7383b..9c2b000f 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 @@ public interface BrowserType { * 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 interface BrowserType { 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 @@ public interface BrowserType { * 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 interface BrowserType { 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 interface BrowserType { *

    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 @@ public interface BrowserType { *

    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 eb14c7e8..1c137a4a 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 @@ import com.google.gson.JsonObject; * } */ 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 012f140c..db548f8b 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 00000000..4b50975d --- /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 ace8d2e6..74120f1c 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 @@ public interface Frame { */ 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 interface Frame { 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 interface Frame { 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 interface Frame { 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 @@ public interface Frame { * *

    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 @@ public interface Frame {
        *
        * 

    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 @@ public interface Frame {
        *
        * 

    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 @@ public interface Frame { * *

    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 @@ public interface Frame { * 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 @@ public interface Frame { * 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 @@ public interface Frame { * 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 @@ public interface Frame { * 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 @@ public interface Frame { * 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 @@ public interface Frame { * 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 00746021..dc386fbd 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 @@ public interface FrameLocator { * *

    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 @@ public interface FrameLocator {
        *
        * 

    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 @@ public interface FrameLocator {
        *
        * 

    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 @@ public interface FrameLocator { * *

    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 8e4317f4..e05d86c9 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 @@ import java.util.regex.Pattern; */ 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 @@ public interface Locator { */ 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 @@ public interface Locator { * *

    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 @@ public interface Locator {
        *
        * 

    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 @@ public interface Locator {
        *
        * 

    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 @@ public interface Locator { * *

    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 @@ public interface Locator { * @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 5e01ee56..98f9d7e2 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 interface Page extends AutoCloseable { 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 interface Page extends AutoCloseable { 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 @@ public interface Page extends AutoCloseable { */ 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 interface Page extends AutoCloseable { 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 interface Page extends AutoCloseable { 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 interface Page extends AutoCloseable { 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 interface Page extends AutoCloseable { * @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 interface Page extends AutoCloseable { * @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 00000000..b2981321 --- /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 f3c5b676..8b4b0b2f 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.getOS; 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 @@ public class TestPageEventConsole extends TestBase { 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 a994102e..f70e2cbd 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 @@ package com.microsoft.playwright; import org.junit.jupiter.api.Test; -import java.util.ArrayList; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -44,4 +43,28 @@ public class TestPageEventPageError extends TestBase { 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 c8bd17d0..5e0f3671 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 @@ public class TestPopup extends TestBase { @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 94847f33..53c1724b 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 @@ public class TestScreencast extends TestBase { 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 @@ public class TestScreencast extends TestBase { 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 6d660659..24b80fae 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.Files; 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 @@ public class TestVideo extends TestBase { 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 79f82f6b..47f0c6e3 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 243bdebd..f8535e57 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 9efde32a..48a185ba 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 @@ class TypeRef extends Element { if ("Buffer".equals(name)) { return "byte[]"; } + if ("Disposable".equals(name)) { + return "AutoCloseable"; + } if ("URL".equals(name)) { return "String"; } @@ -639,7 +642,7 @@ class Event extends Element { 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 @@ class Interface extends TypeDefinition { 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 @@ class Interface extends TypeDefinition { if ("Clock".equals(jsonName)) { output.add("import java.util.Date;"); } - if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright").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)) {