From 65140d776d918c7752eafb6315ba2f2205f7562e Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 9 Apr 2026 10:57:34 -0700 Subject: [PATCH] chore: roll to 1.59.1-beta-1775752988000 - Replace Overlay with Screencast (overlay/chapter/actions APIs moved to page.screencast()). - Add Browser.bind/unbind backed by startServer/stopServer protocol calls. - Drop Video.start/stop (moved to Screencast.start) and remove obsolete TestOverlay/TestVideo cases. - Teach ApiGenerator to handle function callbacks whose return type is Promise and add a Screencast.start.options.onFrame mapping to Consumer. - Port upstream test fix from microsoft/playwright#39840: navigate to server.EMPTY_PAGE instead of about:blank in TestRouteWebSocket so the arraybuffer parameterized variant no longer hangs. --- .claude/skills/playwright-roll/SKILL.md | 26 ++ README.md | 2 +- .../com/microsoft/playwright/Browser.java | 60 +++++ .../com/microsoft/playwright/Locator.java | 4 +- .../com/microsoft/playwright/Overlay.java | 109 -------- .../java/com/microsoft/playwright/Page.java | 19 +- .../com/microsoft/playwright/Screencast.java | 237 ++++++++++++++++++ .../java/com/microsoft/playwright/Video.java | 90 ------- .../assertions/LocatorAssertions.java | 8 +- .../playwright/impl/BrowserImpl.java | 27 ++ .../playwright/impl/OverlayImpl.java | 68 ----- .../microsoft/playwright/impl/PageImpl.java | 10 +- .../playwright/impl/ScreencastImpl.java | 158 ++++++++++++ .../microsoft/playwright/impl/VideoImpl.java | 33 +-- .../microsoft/playwright/options/Bind.java | 22 ++ .../playwright/options/ScreencastFrame.java | 32 +++ .../microsoft/playwright/TestBrowserBind.java | 49 ++++ .../com/microsoft/playwright/TestOverlay.java | 99 -------- .../playwright/TestRouteWebSocket.java | 46 ++-- .../microsoft/playwright/TestScreencast.java | 129 ++++++++++ .../com/microsoft/playwright/TestVideo.java | 47 ---- scripts/DRIVER_VERSION | 2 +- .../playwright/tools/ApiGenerator.java | 18 +- 23 files changed, 800 insertions(+), 495 deletions(-) delete mode 100644 playwright/src/main/java/com/microsoft/playwright/Overlay.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/Screencast.java delete mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/OverlayImpl.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/ScreencastImpl.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/options/Bind.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/options/ScreencastFrame.java create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestBrowserBind.java delete mode 100644 playwright/src/test/java/com/microsoft/playwright/TestOverlay.java diff --git a/.claude/skills/playwright-roll/SKILL.md b/.claude/skills/playwright-roll/SKILL.md index 0b3e5fa7c..0149d8acf 100644 --- a/.claude/skills/playwright-roll/SKILL.md +++ b/.claude/skills/playwright-roll/SKILL.md @@ -94,6 +94,17 @@ Common patterns: **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. +## Rebuilding the driver-bundle after a roll + +`./scripts/roll_driver.sh` does the whole roll pipeline end-to-end: bumps `DRIVER_VERSION`, downloads new driver files into `driver-bundle/src/main/resources/driver//`, regenerates `api.json` and the Java interfaces, and updates the README. When all of that succeeds, the next `mvn` invocation that touches `driver-bundle` will pick up the new files and you don't need to think about it. + +But if any step in the pipeline fails (the very common case is the API generator throwing on a new type — see *Fixing generator and compilation errors*), the run aborts before `driver-bundle/target/classes/` has been refreshed. From that point on, until you manually rebuild `driver-bundle`, the test JVM will load the **old** driver from the cached `target/classes`/installed jar even though the source resources have already been swapped to the new version. + +Fix — rebuild `driver-bundle` once before re-running tests: +``` +mvn -f driver-bundle/pom.xml install -DskipTests +``` + ## 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. @@ -104,6 +115,21 @@ Common patterns: **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. +## Diagnosing hanging tests + +When `mvn test` hangs and surefire eventually times the JVM out, it writes thread dumps to `playwright/target/surefire-reports/-jvmRun*.dump`. To find the stuck test: + +``` +grep "com.microsoft.playwright.Test" playwright/target/surefire-reports/*-jvmRun1.dump | sort -u +``` + +Each line is a stack frame inside a test method — typically you'll see one or two test methods blocked on a `Future.get()`, `waitForCondition`, or similar. That's the hanging test. + +When you've identified a hanging test: +1. Run it in isolation: `mvn -f playwright/pom.xml test -Dtest='TestClass#testMethod'`. If it passes alone, it's a parallel-load flake — note it but move on. +2. If it still hangs in isolation, look for a recent fix in the upstream repo for the *same* test name. Use `git log --oneline tests/library/.spec.ts` in `~/playwright`. Upstream fixes for client-side hangs are often small and portable (e.g. `about:blank` → `server.EMPTY_PAGE` from microsoft/playwright#39840 fixed `route-web-socket.spec.ts` arraybuffer hangs — apparently some browser changed the WebSocket origin policy on `about:blank`). +3. When porting an upstream fix, mirror the helper signature change rather than hard-coding workarounds. E.g. if upstream added a `server` parameter to `setupWS`, do the same in Java by injecting `Server server` via the JUnit fixture (`@FixtureTest` already wires up `ServerLifecycle`, so adding `Server server` to the test method signature is enough — no class-level boilerplate). Watch for local-variable shadowing when you add a `Server server` parameter to a method that already has a `WebSocketRoute server` local; rename the local. + ## Commit Convention Semantic commit messages: `label(scope): description` diff --git a/README.md b/README.md index c93f7ed29..82bf24d25 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | | Chromium 147.0.7727.15 | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| WebKit 26.0 | ✅ | ✅ | ✅ | +| WebKit 26.4 | ✅ | ✅ | ✅ | | Firefox 148.0.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | ## Documentation diff --git a/playwright/src/main/java/com/microsoft/playwright/Browser.java b/playwright/src/main/java/com/microsoft/playwright/Browser.java index 793ea1dc5..4684e2cb3 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Browser.java +++ b/playwright/src/main/java/com/microsoft/playwright/Browser.java @@ -1222,6 +1222,44 @@ public NewPageOptions setViewportSize(ViewportSize viewportSize) { return this; } } + class BindOptions { + /** + * Host to bind the web socket server to. When specified, a web socket server is created instead of a named pipe. + */ + public String host; + /** + * Port to bind the web socket server to. When specified, a web socket server is created instead of a named pipe. Use + * {@code 0} to let the OS pick an available port. + */ + public Integer port; + /** + * Working directory associated with this browser server. + */ + public String workspaceDir; + + /** + * Host to bind the web socket server to. When specified, a web socket server is created instead of a named pipe. + */ + public BindOptions setHost(String host) { + this.host = host; + return this; + } + /** + * Port to bind the web socket server to. When specified, a web socket server is created instead of a named pipe. Use + * {@code 0} to let the OS pick an available port. + */ + public BindOptions setPort(int port) { + this.port = port; + return this; + } + /** + * Working directory associated with this browser server. + */ + public BindOptions setWorkspaceDir(String workspaceDir) { + this.workspaceDir = workspaceDir; + return this; + } + } class StartTracingOptions { /** * specify custom categories to use instead of default. @@ -1404,6 +1442,22 @@ default Page newPage() { * @since v1.8 */ Page newPage(NewPageOptions options); + /** + * Binds the browser to a named pipe or web socket, making it available for other clients to connect to. + * + * @param title Title of the browser server, used for identification. + * @since v1.59 + */ + default Bind bind(String title) { + return bind(title, null); + } + /** + * Binds the browser to a named pipe or web socket, making it available for other clients to connect to. + * + * @param title Title of the browser server, used for identification. + * @since v1.59 + */ + Bind bind(String title, BindOptions options); /** * NOTE: This API controls Chromium Tracing * which is a low-level chromium-specific debugging tool. API to control Usage *
{@code
-   * String[] texts = page.getByRole(AriaRole.LINK).allInnerTexts();
+   * List texts = page.getByRole(AriaRole.LINK).allInnerTexts();
    * }
* * @since v1.14 @@ -2276,7 +2276,7 @@ public WaitForOptions setTimeout(double timeout) { * *

Usage *

{@code
-   * String[] texts = page.getByRole(AriaRole.LINK).allTextContents();
+   * List texts = page.getByRole(AriaRole.LINK).allTextContents();
    * }
* * @since v1.14 diff --git a/playwright/src/main/java/com/microsoft/playwright/Overlay.java b/playwright/src/main/java/com/microsoft/playwright/Overlay.java deleted file mode 100644 index 16adb13f0..000000000 --- a/playwright/src/main/java/com/microsoft/playwright/Overlay.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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; - - -/** - * Interface for managing page overlays that display persistent visual indicators on top of the page. - */ -public interface Overlay { - class ShowOptions { - /** - * Duration in milliseconds after which the overlay is automatically removed. Overlay stays until dismissed if not - * provided. - */ - public Double duration; - - /** - * Duration in milliseconds after which the overlay is automatically removed. Overlay stays until dismissed if not - * provided. - */ - public ShowOptions setDuration(double duration) { - this.duration = duration; - return this; - } - } - class ChapterOptions { - /** - * Optional description text displayed below the title. - */ - public String description; - /** - * Duration in milliseconds after which the overlay is automatically removed. Defaults to {@code 2000}. - */ - public Double duration; - - /** - * Optional description text displayed below the title. - */ - public ChapterOptions setDescription(String description) { - this.description = description; - return this; - } - /** - * Duration in milliseconds after which the overlay is automatically removed. Defaults to {@code 2000}. - */ - public ChapterOptions setDuration(double duration) { - this.duration = duration; - return this; - } - } - /** - * Adds an overlay with the given HTML content. The overlay is displayed on top of the page until removed. Returns a - * disposable that removes the overlay when disposed. - * - * @param html HTML content for the overlay. - * @since v1.59 - */ - default AutoCloseable show(String html) { - return show(html, null); - } - /** - * Adds an overlay with the given HTML content. The overlay is displayed on top of the page until removed. Returns a - * disposable that removes the overlay when disposed. - * - * @param html HTML content for the overlay. - * @since v1.59 - */ - AutoCloseable show(String html, ShowOptions options); - /** - * Shows a chapter overlay with a title and optional description, centered on the page with a blurred backdrop. Useful for - * narrating video recordings. The overlay is removed after the specified duration, or 2000ms. - * - * @param title Title text displayed prominently in the overlay. - * @since v1.59 - */ - default void chapter(String title) { - chapter(title, null); - } - /** - * Shows a chapter overlay with a title and optional description, centered on the page with a blurred backdrop. Useful for - * narrating video recordings. The overlay is removed after the specified duration, or 2000ms. - * - * @param title Title text displayed prominently in the overlay. - * @since v1.59 - */ - void chapter(String title, ChapterOptions options); - /** - * Sets visibility of all overlays without removing them. - * - * @param visible Whether overlays should be visible. - * @since v1.59 - */ - void setVisible(boolean visible); -} - diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java index 45652b87d..074ecdff0 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Page.java +++ b/playwright/src/main/java/com/microsoft/playwright/Page.java @@ -5880,12 +5880,6 @@ default Locator locator(String selector) { * @since v1.8 */ Mouse mouse(); - /** - * - * - * @since v1.59 - */ - Overlay overlay(); /** * Adds one-off {@code Dialog} handler. The handler will be removed immediately after next {@code Dialog} is created. *
{@code
@@ -6879,6 +6873,14 @@ default void routeFromHAR(Path har) {
    * @since v1.48
    */
   void routeWebSocket(Predicate url, Consumer handler);
+  /**
+   * {@code Screencast} object associated with this page.
+   *
+   * 

Usage + * + * @since v1.59 + */ + Screencast screencast(); /** * Returns the buffer with the captured screenshot. * @@ -7756,9 +7758,8 @@ default void unroute(Predicate url) { */ String url(); /** - * Video object associated with this page. Can be used to control video recording with {@link - * com.microsoft.playwright.Video#start Video.start()} and {@link com.microsoft.playwright.Video#stop Video.stop()}, or to - * access the video file when using the {@code recordVideo} context option. + * Video object associated with this page. Can be used to access the video file when using the {@code recordVideo} context + * option. * * @since v1.8 */ diff --git a/playwright/src/main/java/com/microsoft/playwright/Screencast.java b/playwright/src/main/java/com/microsoft/playwright/Screencast.java new file mode 100644 index 000000000..15307ad6b --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/Screencast.java @@ -0,0 +1,237 @@ +/* + * 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.nio.file.Path; +import java.util.*; +import java.util.function.Consumer; + +/** + * Interface for capturing screencast frames from a page. + */ +public interface Screencast { + class StartOptions { + /** + * Callback that receives JPEG-encoded frame data. + */ + public Consumer onFrame; + /** + * Path where the video should be saved when the screencast is stopped. When provided, video recording is started. + */ + public Path path; + /** + * The quality of the image, between 0-100. + */ + public Integer quality; + + /** + * Callback that receives JPEG-encoded frame data. + */ + public StartOptions setOnFrame(Consumer onFrame) { + this.onFrame = onFrame; + return this; + } + /** + * Path where the video should be saved when the screencast is stopped. When provided, video recording is started. + */ + public StartOptions setPath(Path path) { + this.path = path; + return this; + } + /** + * The quality of the image, between 0-100. + */ + public StartOptions setQuality(int quality) { + this.quality = quality; + return this; + } + } + class ShowOverlayOptions { + /** + * Duration in milliseconds after which the overlay is automatically removed. Overlay stays until dismissed if not + * provided. + */ + public Double duration; + + /** + * Duration in milliseconds after which the overlay is automatically removed. Overlay stays until dismissed if not + * provided. + */ + public ShowOverlayOptions setDuration(double duration) { + this.duration = duration; + return this; + } + } + class ShowChapterOptions { + /** + * Optional description text displayed below the title. + */ + public String description; + /** + * Duration in milliseconds after which the overlay is automatically removed. Defaults to {@code 2000}. + */ + public Double duration; + + /** + * Optional description text displayed below the title. + */ + public ShowChapterOptions setDescription(String description) { + this.description = description; + return this; + } + /** + * Duration in milliseconds after which the overlay is automatically removed. Defaults to {@code 2000}. + */ + public ShowChapterOptions setDuration(double duration) { + this.duration = duration; + return this; + } + } + class ShowActionsOptions { + /** + * How long each annotation is displayed in milliseconds. Defaults to {@code 500}. + */ + public Double duration; + /** + * Font size of the action title in pixels. Defaults to {@code 24}. + */ + public Integer fontSize; + /** + * Position of the action title overlay. Defaults to {@code "top-right"}. + */ + public AnnotatePosition position; + + /** + * How long each annotation is displayed in milliseconds. Defaults to {@code 500}. + */ + public ShowActionsOptions setDuration(double duration) { + this.duration = duration; + return this; + } + /** + * Font size of the action title in pixels. Defaults to {@code 24}. + */ + public ShowActionsOptions setFontSize(int fontSize) { + this.fontSize = fontSize; + return this; + } + /** + * Position of the action title overlay. Defaults to {@code "top-right"}. + */ + public ShowActionsOptions setPosition(AnnotatePosition position) { + this.position = position; + return this; + } + } + /** + * Starts the screencast. When {@code path} is provided, it saves video recording to the specified file. When {@code + * onFrame} is provided, delivers JPEG-encoded frames to the callback. Both can be used together. + * + *

Usage + * + * @since v1.59 + */ + default AutoCloseable start() { + return start(null); + } + /** + * Starts the screencast. When {@code path} is provided, it saves video recording to the specified file. When {@code + * onFrame} is provided, delivers JPEG-encoded frames to the callback. Both can be used together. + * + *

Usage + * + * @since v1.59 + */ + AutoCloseable start(StartOptions options); + /** + * Stops the screencast and video recording if active. If a video was being recorded, saves it to the path specified in + * {@link com.microsoft.playwright.Screencast#start Screencast.start()}. + * + * @since v1.59 + */ + void stop(); + /** + * Adds an overlay with the given HTML content. The overlay is displayed on top of the page until removed. Returns a + * disposable that removes the overlay when disposed. + * + * @param html HTML content for the overlay. + * @since v1.59 + */ + default AutoCloseable showOverlay(String html) { + return showOverlay(html, null); + } + /** + * Adds an overlay with the given HTML content. The overlay is displayed on top of the page until removed. Returns a + * disposable that removes the overlay when disposed. + * + * @param html HTML content for the overlay. + * @since v1.59 + */ + AutoCloseable showOverlay(String html, ShowOverlayOptions options); + /** + * Shows a chapter overlay with a title and optional description, centered on the page with a blurred backdrop. Useful for + * narrating video recordings. The overlay is removed after the specified duration, or 2000ms. + * + * @param title Title text displayed prominently in the overlay. + * @since v1.59 + */ + default void showChapter(String title) { + showChapter(title, null); + } + /** + * Shows a chapter overlay with a title and optional description, centered on the page with a blurred backdrop. Useful for + * narrating video recordings. The overlay is removed after the specified duration, or 2000ms. + * + * @param title Title text displayed prominently in the overlay. + * @since v1.59 + */ + void showChapter(String title, ShowChapterOptions options); + /** + * Enables visual annotations on interacted elements. Returns a disposable that stops showing actions when disposed. + * + * @since v1.59 + */ + default AutoCloseable showActions() { + return showActions(null); + } + /** + * Enables visual annotations on interacted elements. Returns a disposable that stops showing actions when disposed. + * + * @since v1.59 + */ + AutoCloseable showActions(ShowActionsOptions options); + /** + * Shows overlays. + * + * @since v1.59 + */ + void showOverlays(); + /** + * Removes action decorations. + * + * @since v1.59 + */ + void hideActions(); + /** + * Hides overlays without removing them. + * + * @since v1.59 + */ + void hideOverlays(); +} + diff --git a/playwright/src/main/java/com/microsoft/playwright/Video.java b/playwright/src/main/java/com/microsoft/playwright/Video.java index 6273dbdde..351ba87fe 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Video.java +++ b/playwright/src/main/java/com/microsoft/playwright/Video.java @@ -24,64 +24,8 @@ *

{@code
  * System.out.println(page.video().path());
  * }
- * - *

Alternatively, you can use {@link com.microsoft.playwright.Video#start Video.start()} and {@link - * com.microsoft.playwright.Video#stop Video.stop()} to record video manually. This approach is mutually exclusive with the - * {@code recordVideo} option. - *

{@code
- * page.video().start(new Video.StartOptions().setPath(Paths.get("video.webm")));
- * // ... perform actions ...
- * page.video().stop();
- * }
*/ public interface Video { - class StartOptions { - /** - * If specified, enables visual annotations on interacted elements during video recording. Interacted elements are - * highlighted with a semi-transparent blue box and click points are shown as red circles. - */ - public Annotate annotate; - /** - * Path where the video should be saved when the recording is stopped. - */ - public Path path; - /** - * Optional dimensions of the recorded video. If not specified the size will be equal to page viewport scaled down to fit - * into 800x800. Actual picture of the page will be scaled down if necessary to fit the specified size. - */ - public Size size; - - /** - * If specified, enables visual annotations on interacted elements during video recording. Interacted elements are - * highlighted with a semi-transparent blue box and click points are shown as red circles. - */ - public StartOptions setAnnotate(Annotate annotate) { - this.annotate = annotate; - return this; - } - /** - * Path where the video should be saved when the recording is stopped. - */ - public StartOptions setPath(Path path) { - this.path = path; - return this; - } - /** - * Optional dimensions of the recorded video. If not specified the size will be equal to page viewport scaled down to fit - * into 800x800. Actual picture of the page will be scaled down if necessary to fit the specified size. - */ - public StartOptions setSize(int width, int height) { - return setSize(new Size(width, height)); - } - /** - * Optional dimensions of the recorded video. If not specified the size will be equal to page viewport scaled down to fit - * into 800x800. Actual picture of the page will be scaled down if necessary to fit the specified size. - */ - public StartOptions setSize(Size size) { - this.size = size; - return this; - } - } /** * Deletes the video file. Will wait for the video to finish if necessary. * @@ -103,39 +47,5 @@ public StartOptions setSize(Size size) { * @since v1.11 */ void saveAs(Path path); - /** - * Starts video recording. This method is mutually exclusive with the {@code recordVideo} context option. - * - *

Usage - *

{@code
-   * page.video().start(new Video.StartOptions().setPath(Paths.get("video.webm")));
-   * // ... perform actions ...
-   * page.video().stop();
-   * }
- * - * @since v1.59 - */ - default AutoCloseable start() { - return start(null); - } - /** - * Starts video recording. This method is mutually exclusive with the {@code recordVideo} context option. - * - *

Usage - *

{@code
-   * page.video().start(new Video.StartOptions().setPath(Paths.get("video.webm")));
-   * // ... perform actions ...
-   * page.video().stop();
-   * }
- * - * @since v1.59 - */ - AutoCloseable start(StartOptions options); - /** - * Stops video recording started with {@link com.microsoft.playwright.Video#start Video.start()}. - * - * @since v1.59 - */ - void stop(); } diff --git a/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java b/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java index 23c0c5a73..0cc98854b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java +++ b/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java @@ -888,7 +888,7 @@ default void isVisible() { *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected * class lists. Each element's class attribute is matched against the corresponding class in the array: *

{@code
-   * assertThat(page.locator(".list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
+   * assertThat(page.locator(".list > .component")).containsClass(Arrays.asList("inactive", "active", "inactive"));
    * }
* * @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements. @@ -912,7 +912,7 @@ default void containsClass(String expected) { *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected * class lists. Each element's class attribute is matched against the corresponding class in the array: *

{@code
-   * assertThat(page.locator(".list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
+   * assertThat(page.locator(".list > .component")).containsClass(Arrays.asList("inactive", "active", "inactive"));
    * }
* * @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements. @@ -934,7 +934,7 @@ default void containsClass(String expected) { *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected * class lists. Each element's class attribute is matched against the corresponding class in the array: *

{@code
-   * assertThat(page.locator(".list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
+   * assertThat(page.locator(".list > .component")).containsClass(Arrays.asList("inactive", "active", "inactive"));
    * }
* * @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements. @@ -958,7 +958,7 @@ default void containsClass(List expected) { *

When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected * class lists. Each element's class attribute is matched against the corresponding class in the array: *

{@code
-   * assertThat(page.locator(".list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
+   * assertThat(page.locator(".list > .component")).containsClass(Arrays.asList("inactive", "active", "inactive"));
    * }
* * @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements. diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java index 795ccb2cb..1023721e1 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java @@ -20,6 +20,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.microsoft.playwright.*; +import com.microsoft.playwright.options.Bind; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -195,6 +196,32 @@ public void startTracing(Page page, StartTracingOptions options) { sendMessage("startTracing", params, NO_TIMEOUT); } + @Override + public Bind bind(String title, BindOptions options) { + JsonObject params = new JsonObject(); + params.addProperty("title", title); + if (options != null) { + if (options.host != null) { + params.addProperty("host", options.host); + } + if (options.port != null) { + params.addProperty("port", options.port); + } + if (options.workspaceDir != null) { + params.addProperty("workspaceDir", options.workspaceDir); + } + } + JsonObject result = sendMessage("startServer", params, NO_TIMEOUT).getAsJsonObject(); + Bind bind = new Bind(); + bind.endpoint = result.get("endpoint").getAsString(); + return bind; + } + + @Override + public void unbind() { + sendMessage("stopServer", new JsonObject(), NO_TIMEOUT); + } + @Override public byte[] stopTracing() { JsonObject json = sendMessage("stopTracing").getAsJsonObject(); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/OverlayImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/OverlayImpl.java deleted file mode 100644 index 4f5a62559..000000000 --- a/playwright/src/main/java/com/microsoft/playwright/impl/OverlayImpl.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.microsoft.playwright.impl; - -import com.google.gson.JsonObject; -import com.microsoft.playwright.Overlay; - -import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT; - -class OverlayImpl implements Overlay { - private final ChannelOwner page; - - OverlayImpl(ChannelOwner page) { - this.page = page; - } - - @Override - public AutoCloseable show(String html, ShowOptions options) { - JsonObject params = new JsonObject(); - params.addProperty("html", html); - if (options != null && options.duration != null) { - params.addProperty("duration", options.duration); - } - JsonObject result = (JsonObject) page.sendMessage("overlayShow", params, NO_TIMEOUT); - String id = result.get("id").getAsString(); - return () -> { - JsonObject removeParams = new JsonObject(); - removeParams.addProperty("id", id); - page.sendMessage("overlayRemove", removeParams, NO_TIMEOUT); - }; - } - - @Override - public void chapter(String title, ChapterOptions options) { - JsonObject params = new JsonObject(); - params.addProperty("title", title); - if (options != null) { - if (options.description != null) { - params.addProperty("description", options.description); - } - if (options.duration != null) { - params.addProperty("duration", options.duration); - } - } - page.sendMessage("overlayChapter", params, NO_TIMEOUT); - } - - @Override - public void setVisible(boolean visible) { - JsonObject params = new JsonObject(); - params.addProperty("visible", visible); - page.sendMessage("overlaySetVisible", params, NO_TIMEOUT); - } -} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java index 5405fcb46..d29df48b1 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -46,7 +46,7 @@ public class PageImpl extends ChannelOwner implements Page { private final KeyboardImpl keyboard; private final MouseImpl mouse; private final TouchscreenImpl touchscreen; - private final OverlayImpl overlay; + private final ScreencastImpl screencast; final Waitable waitableClosedOrCrashed; private ViewportSize viewport; private final Router routes = new Router(); @@ -136,7 +136,7 @@ enum EventType { keyboard = new KeyboardImpl(this); mouse = new MouseImpl(this); touchscreen = new TouchscreenImpl(this); - overlay = new OverlayImpl(this); + screencast = new ScreencastImpl(this); frames.add(mainFrame); timeoutSettings = new TimeoutSettings(browserContext.timeoutSettings); waitableClosedOrCrashed = createWaitForCloseHelper(); @@ -232,6 +232,8 @@ protected void handleEvent(String event, JsonObject params) { listeners.notify(EventType.CRASH, this); } else if ("close".equals(event)) { didClose(); + } else if ("screencastFrame".equals(event)) { + screencast.handleScreencastFrame(params); } } @@ -1350,8 +1352,8 @@ public Touchscreen touchscreen() { } @Override - public Overlay overlay() { - return overlay; + public Screencast screencast() { + return screencast; } @Override diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ScreencastImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/ScreencastImpl.java new file mode 100644 index 000000000..f4c8f4c8f --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ScreencastImpl.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright.impl; + +import com.google.gson.JsonObject; +import com.microsoft.playwright.PlaywrightException; +import com.microsoft.playwright.Screencast; +import com.microsoft.playwright.options.ScreencastFrame; + +import java.nio.file.Path; +import java.util.function.Consumer; + +import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT; +import static com.microsoft.playwright.impl.Serialization.gson; + +class ScreencastImpl implements Screencast { + private final PageImpl page; + private boolean started; + private Path savePath; + private Consumer onFrame; + private ArtifactImpl artifact; + + ScreencastImpl(PageImpl page) { + this.page = page; + } + + void handleScreencastFrame(JsonObject params) { + if (onFrame == null) { + return; + } + String dataBase64 = params.get("data").getAsString(); + byte[] data = java.util.Base64.getDecoder().decode(dataBase64); + onFrame.accept(new ScreencastFrame(data)); + } + + @Override + public AutoCloseable start(StartOptions options) { + if (started) { + throw new PlaywrightException("Screencast is already started"); + } + started = true; + JsonObject params = new JsonObject(); + if (options != null) { + if (options.onFrame != null) { + onFrame = options.onFrame; + } + if (options.quality != null) { + params.addProperty("quality", options.quality); + } + params.addProperty("sendFrames", options.onFrame != null); + params.addProperty("record", options.path != null); + savePath = options.path; + } else { + params.addProperty("sendFrames", false); + params.addProperty("record", false); + } + JsonObject result = page.sendMessage("screencastStart", params, NO_TIMEOUT).getAsJsonObject(); + if (result.has("artifact")) { + String artifactGuid = result.getAsJsonObject("artifact").get("guid").getAsString(); + artifact = page.connection.getExistingObject(artifactGuid); + } + return new DisposableStub(this::stop); + } + + @Override + public void stop() { + started = false; + onFrame = null; + page.sendMessage("screencastStop", new JsonObject(), NO_TIMEOUT); + if (savePath != null && artifact != null) { + artifact.saveAs(savePath); + } + artifact = null; + savePath = null; + } + + @Override + public AutoCloseable showOverlay(String html, ShowOverlayOptions options) { + JsonObject params = new JsonObject(); + params.addProperty("html", html); + if (options != null && options.duration != null) { + params.addProperty("duration", options.duration); + } + JsonObject result = (JsonObject) page.sendMessage("screencastShowOverlay", params, NO_TIMEOUT); + String id = result.get("id").getAsString(); + return () -> { + JsonObject removeParams = new JsonObject(); + removeParams.addProperty("id", id); + page.sendMessage("screencastRemoveOverlay", removeParams, NO_TIMEOUT); + }; + } + + @Override + public void showChapter(String title, ShowChapterOptions options) { + JsonObject params = new JsonObject(); + params.addProperty("title", title); + if (options != null) { + if (options.description != null) { + params.addProperty("description", options.description); + } + if (options.duration != null) { + params.addProperty("duration", options.duration); + } + } + page.sendMessage("screencastChapter", params, NO_TIMEOUT); + } + + @Override + public AutoCloseable showActions(ShowActionsOptions options) { + JsonObject params = new JsonObject(); + if (options != null) { + if (options.duration != null) { + params.addProperty("duration", options.duration); + } + if (options.fontSize != null) { + params.addProperty("fontSize", options.fontSize); + } + if (options.position != null) { + params.add("position", gson().toJsonTree(options.position)); + } + } + page.sendMessage("screencastShowActions", params, NO_TIMEOUT); + return new DisposableStub(this::hideActions); + } + + @Override + public void showOverlays() { + JsonObject params = new JsonObject(); + params.addProperty("visible", true); + page.sendMessage("screencastSetOverlayVisible", params, NO_TIMEOUT); + } + + @Override + public void hideActions() { + page.sendMessage("screencastHideActions", new JsonObject(), NO_TIMEOUT); + } + + @Override + public void hideOverlays() { + JsonObject params = new JsonObject(); + params.addProperty("visible", false); + page.sendMessage("screencastSetOverlayVisible", params, NO_TIMEOUT); + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/VideoImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/VideoImpl.java index 517b45a8a..a4bb66d65 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/VideoImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/VideoImpl.java @@ -16,19 +16,15 @@ package com.microsoft.playwright.impl; -import com.google.gson.JsonObject; import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.Video; -import static com.microsoft.playwright.impl.Serialization.gson; - import java.nio.file.Path; import java.nio.file.Paths; class VideoImpl implements Video { private final PageImpl page; - private ArtifactImpl artifact; - private Path savePath; + ArtifactImpl artifact; VideoImpl(PageImpl page) { this.page = page; @@ -55,33 +51,6 @@ public Path path() { return Paths.get(artifact.initializer.get("absolutePath").getAsString()); } - @Override - public AutoCloseable start(StartOptions options) { - JsonObject params = new JsonObject(); - if (options != null) { - if (options.size != null) { - params.add("size", gson().toJsonTree(options.size)); - } - if (options.annotate != null) { - params.add("annotate", gson().toJsonTree(options.annotate)); - } - savePath = options.path; - } - JsonObject result = page.sendMessage("videoStart", params, ChannelOwner.NO_TIMEOUT).getAsJsonObject(); - String artifactGuid = result.getAsJsonObject("artifact").get("guid").getAsString(); - artifact = page.connection.getExistingObject(artifactGuid); - return new DisposableStub(this::stop); - } - - @Override - public void stop() { - page.sendMessage("videoStop", new JsonObject(), ChannelOwner.NO_TIMEOUT); - if (savePath != null) { - saveAs(savePath); - savePath = null; - } - } - @Override public void saveAs(Path path) { if (artifact == null) diff --git a/playwright/src/main/java/com/microsoft/playwright/options/Bind.java b/playwright/src/main/java/com/microsoft/playwright/options/Bind.java new file mode 100644 index 000000000..51a781810 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/options/Bind.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright.options; + +public class Bind { + public String endpoint; + +} \ No newline at end of file diff --git a/playwright/src/main/java/com/microsoft/playwright/options/ScreencastFrame.java b/playwright/src/main/java/com/microsoft/playwright/options/ScreencastFrame.java new file mode 100644 index 000000000..1b4d3e25f --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/options/ScreencastFrame.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright.options; + +/** + * A single screencast frame delivered to {@link com.microsoft.playwright.Screencast#start Screencast.start()}'s + * {@code onFrame} callback. + */ +public class ScreencastFrame { + /** + * JPEG-encoded frame data. + */ + public byte[] data; + + public ScreencastFrame(byte[] data) { + this.data = data; + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserBind.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserBind.java new file mode 100644 index 000000000..b34b69aa9 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserBind.java @@ -0,0 +1,49 @@ +/* + * 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.Bind; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestBrowserBind extends TestBase { + @Test + void shouldBindAndUnbindBrowser() { + Bind serverInfo = browser.bind("default"); + try { + assertNotNull(serverInfo); + assertNotNull(serverInfo.endpoint); + assertFalse(serverInfo.endpoint.isEmpty()); + } finally { + browser.unbind(); + } + } + + @Test + void shouldBindWithCustomTitleAndOptions() { + Bind serverInfo = browser.bind("my-title", + new Browser.BindOptions().setHost("127.0.0.1").setPort(0)); + try { + assertNotNull(serverInfo); + assertNotNull(serverInfo.endpoint); + assertFalse(serverInfo.endpoint.isEmpty()); + } finally { + browser.unbind(); + } + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestOverlay.java b/playwright/src/test/java/com/microsoft/playwright/TestOverlay.java deleted file mode 100644 index 28fb2d83d..000000000 --- a/playwright/src/test/java/com/microsoft/playwright/TestOverlay.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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.Overlay; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -// Note: The overlay elements are rendered inside a closed shadow root in driver mode, -// so locator-based DOM assertions are not possible in Java. These tests verify that -// the protocol calls succeed without errors. -public class TestOverlay extends TestBase { - @Test - void shouldAddAndRemoveOverlay() throws Exception { - page.navigate(server.EMPTY_PAGE); - AutoCloseable disposable = page.overlay().show("
Hello Overlay
"); - assertNotNull(disposable); - disposable.close(); - } - - @Test - void shouldAddMultipleOverlays() throws Exception { - page.navigate(server.EMPTY_PAGE); - AutoCloseable d1 = page.overlay().show("
First
"); - AutoCloseable d2 = page.overlay().show("
Second
"); - d1.close(); - d2.close(); - } - - @Test - void shouldHideAndShowOverlays() throws Exception { - page.navigate(server.EMPTY_PAGE); - page.overlay().show("
Visible
"); - page.overlay().setVisible(false); - page.overlay().setVisible(true); - } - - @Test - void shouldSurviveNavigation() { - page.navigate(server.EMPTY_PAGE); - page.overlay().show("
Survives Reload
"); - page.navigate(server.EMPTY_PAGE); - page.reload(); - } - - @Test - void shouldRemoveOverlayAndNotRestoreAfterNavigation() throws Exception { - page.navigate(server.EMPTY_PAGE); - AutoCloseable disposable = page.overlay().show("
Temporary
"); - disposable.close(); - page.reload(); - } - - @Test - void shouldSanitizeScriptsFromOverlayHtml() { - page.navigate(server.EMPTY_PAGE); - page.overlay().show("
Safe
"); - } - - @Test - void shouldStripEventHandlersFromOverlayHtml() { - page.navigate(server.EMPTY_PAGE); - page.overlay().show("
Click me
"); - } - - @Test - void shouldAutoRemoveOverlayAfterTimeout() { - page.navigate(server.EMPTY_PAGE); - page.overlay().show("
Temporary
", new Overlay.ShowOptions().setDuration(1)); - } - - @Test - void shouldAllowStylesInOverlayHtml() { - page.navigate(server.EMPTY_PAGE); - page.overlay().show("
Styled
"); - } - - @Test - void shouldShowChapter() { - page.navigate(server.EMPTY_PAGE); - page.overlay().chapter("Chapter Title"); - page.overlay().chapter("With Description", new Overlay.ChapterOptions().setDescription("Some details").setDuration(100)); - } -} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java b/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java index 9be72a8e6..08af287d1 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java @@ -40,11 +40,11 @@ void resetWebSocketServer() { webSocketServer.reset(); } - private void setupWS(Page target, int port, String binaryType) { - setupWS(target.mainFrame(), port, binaryType); + private void setupWS(Page target, Server server, int port, String binaryType) { + setupWS(target.mainFrame(), server, port, binaryType); } - private void setupWS(Frame target, int port, String binaryType) { - target.navigate("about:blank"); + private void setupWS(Frame target, Server server, int port, String binaryType) { + target.navigate(server.EMPTY_PAGE); target.evaluate("({ port, binaryType }) => {\n" + " window.log = [];\n" + " window.ws = new WebSocket('ws://localhost:' + port + '/ws');\n" + @@ -92,10 +92,10 @@ private void setupRoute(Page page, String mock) { @ParameterizedTest @ValueSource(strings = {"no-mock", "no-match", "pass-through"}) - public void shouldWorkWithTextMessage(String mock, Page page) throws Exception { + public void shouldWorkWithTextMessage(String mock, Page page, Server server) throws Exception { setupRoute(page, mock); Future wsPromise = webSocketServer.waitForWebSocket(); - setupWS(page, webSocketServer.getPort(), "blob"); + setupWS(page, server, webSocketServer.getPort(), "blob"); page.waitForCondition(() -> { Boolean result = (Boolean) page.evaluate("() => window.log.length >= 1"); @@ -134,10 +134,10 @@ public void shouldWorkWithTextMessage(String mock, Page page) throws Exception { @ParameterizedTest @ValueSource(strings = {"no-mock", "no-match", "pass-through"}) - public void shouldWorkWithBinaryTypeBlob(String mock, Page page) throws Exception { + public void shouldWorkWithBinaryTypeBlob(String mock, Page page, Server server) throws Exception { setupRoute(page, mock); Future wsPromise = webSocketServer.waitForWebSocket(); - setupWS(page, webSocketServer.getPort(), "blob"); + setupWS(page, server, webSocketServer.getPort(), "blob"); org.java_websocket.WebSocket ws = wsPromise.get(); ws.send("hi".getBytes(StandardCharsets.UTF_8)); page.waitForCondition(() -> { @@ -157,10 +157,10 @@ public void shouldWorkWithBinaryTypeBlob(String mock, Page page) throws Exceptio @ParameterizedTest @ValueSource(strings = {"no-mock", "no-match", "pass-through"}) - public void shouldWorkWithBinaryTypeArrayBuffer(String mock, Page page) throws Exception { + public void shouldWorkWithBinaryTypeArrayBuffer(String mock, Page page, Server server) throws Exception { setupRoute(page, mock); Future wsPromise = webSocketServer.waitForWebSocket(); - setupWS(page, webSocketServer.getPort(), "arraybuffer"); + setupWS(page, server, webSocketServer.getPort(), "arraybuffer"); org.java_websocket.WebSocket ws = wsPromise.get(); ws.send("hi".getBytes(StandardCharsets.UTF_8)); page.waitForCondition(() -> { @@ -179,10 +179,10 @@ public void shouldWorkWithBinaryTypeArrayBuffer(String mock, Page page) throws E } @Test - public void shouldWorkWithServer(Page page) throws ExecutionException, InterruptedException { + public void shouldWorkWithServer(Page page, Server server) throws ExecutionException, InterruptedException { WebSocketRoute[] wsRoute = new WebSocketRoute[]{null}; page.routeWebSocket(Pattern.compile("/.*/"), ws -> { - WebSocketRoute server = ws.connectToServer(); + WebSocketRoute serverRoute = ws.connectToServer(); ws.onMessage(frame -> { String message = frame.text(); switch (message) { @@ -192,13 +192,13 @@ public void shouldWorkWithServer(Page page) throws ExecutionException, Interrupt case "to-block": break; case "to-modify": - server.send("modified"); + serverRoute.send("modified"); break; default: - server.send(message); + serverRoute.send(message); } }); - server.onMessage(frame -> { + serverRoute.onMessage(frame -> { String message = frame.text(); switch (message) { case "to-block": @@ -210,12 +210,12 @@ public void shouldWorkWithServer(Page page) throws ExecutionException, Interrupt ws.send(message); } }); - server.send("fake"); + serverRoute.send("fake"); wsRoute[0] = ws; }); Future ws = webSocketServer.waitForWebSocket(); - setupWS(page, webSocketServer.getPort(), "blob"); + setupWS(page, server, webSocketServer.getPort(), "blob"); page.waitForCondition(() -> webSocketServer.logCopy().size() >= 1); assertEquals( asList("message: fake"), @@ -277,7 +277,7 @@ public void shouldWorkWithServer(Page page) throws ExecutionException, Interrupt } @Test - public void shouldWorkWithoutServer(Page page) { + public void shouldWorkWithoutServer(Page page, Server server) { WebSocketRoute[] wsRoute = new WebSocketRoute[]{ null }; page.routeWebSocket(Pattern.compile("/.*/"), ws -> { ws.onMessage(frame -> { @@ -288,7 +288,7 @@ public void shouldWorkWithoutServer(Page page) { }); wsRoute[0] = ws; }); - setupWS(page, webSocketServer.getPort(), "blob"); + setupWS(page, server, webSocketServer.getPort(), "blob"); page.evaluate("async () => {\n" + " await window.wsOpened;\n" + @@ -321,7 +321,7 @@ public void shouldWorkWithoutServer(Page page) { } @Test - public void shouldWorkWithBaseURL(Browser browser) throws Exception { + public void shouldWorkWithBaseURL(Browser browser, Server server) throws Exception { BrowserContext context = browser.newContext(new Browser.NewContextOptions().setBaseURL("http://localhost:" + webSocketServer.getPort())); Page newPage = context.newPage(); @@ -335,7 +335,7 @@ public void shouldWorkWithBaseURL(Browser browser) throws Exception { }); }); - setupWS(newPage, webSocketServer.getPort(), "blob"); + setupWS(newPage, server, webSocketServer.getPort(), "blob"); newPage.evaluate("async () => {\n" + " await window.wsOpened;\n" + @@ -353,7 +353,7 @@ public void shouldWorkWithBaseURL(Browser browser) throws Exception { } @Test - public void shouldWorkWithNoTrailingSlash(Page page) throws Exception { + public void shouldWorkWithNoTrailingSlash(Page page) throws Exception { List log = new ArrayList<>(); // No trailing slash in the route pattern @@ -384,7 +384,7 @@ public void shouldWorkWithNoTrailingSlash(Page page) throws Exception { page.waitForCondition(() -> log.size() >= 1); assertEquals(asList("query"), log); - // Wait and verify client received response + // Wait and verify client received response page.waitForCondition(() -> { Boolean result = (Boolean) page.evaluate("() => window.log.length >= 1"); return result; diff --git a/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java b/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java index 53c1724b1..c95c86a48 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java @@ -16,12 +16,14 @@ package com.microsoft.playwright; +import com.microsoft.playwright.options.ScreencastFrame; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -97,4 +99,131 @@ void shouldWaitForVideoFinishWhenPageIsClosed(@TempDir Path videosDir) throws IO assertTrue(Files.size(files.get(0)) > 0); } + @Test + void screencastStartShouldDeliverFramesViaOnFrame() throws Exception { + BrowserContext context = browser.newContext(new Browser.NewContextOptions().setViewportSize(500, 400)); + Page page = context.newPage(); + try { + List frames = new ArrayList<>(); + page.screencast().start(new Screencast.StartOptions().setOnFrame(frames::add)); + page.navigate(server.EMPTY_PAGE); + page.evaluate("() => document.body.style.backgroundColor = 'red'"); + page.waitForTimeout(500); + page.screencast().stop(); + assertFalse(frames.isEmpty(), "expected at least one frame"); + // JPEG-encoded frames start with FF D8. + for (ScreencastFrame frame : frames) { + assertNotNull(frame.data); + assertEquals((byte) 0xFF, frame.data[0]); + assertEquals((byte) 0xD8, frame.data[1]); + } + } finally { + context.close(); + } + } + + @Test + void screencastStartShouldThrowIfAlreadyStarted() { + BrowserContext context = browser.newContext(); + Page page = context.newPage(); + try { + page.screencast().start(new Screencast.StartOptions().setOnFrame(data -> {})); + PlaywrightException e = assertThrows(PlaywrightException.class, + () -> page.screencast().start(new Screencast.StartOptions().setOnFrame(data -> {}))); + assertTrue(e.getMessage().contains("Screencast is already started"), e.getMessage()); + page.screencast().stop(); + } finally { + context.close(); + } + } + + @Test + void screencastStartShouldRecordVideoToPath(@TempDir Path tmpDir) throws Exception { + Path videoPath = tmpDir.resolve("video.webm"); + BrowserContext context = browser.newContext(new Browser.NewContextOptions().setViewportSize(800, 600)); + Page page = context.newPage(); + try { + page.screencast().start(new Screencast.StartOptions().setPath(videoPath)); + page.navigate(server.EMPTY_PAGE); + page.evaluate("() => document.body.style.backgroundColor = 'red'"); + page.waitForTimeout(500); + page.screencast().stop(); + assertTrue(Files.exists(videoPath), "video file should exist: " + videoPath); + assertTrue(Files.size(videoPath) > 0); + } finally { + context.close(); + } + } + + @Test + void screencastStartReturnsDisposable() throws Exception { + BrowserContext context = browser.newContext(); + Page page = context.newPage(); + try { + AutoCloseable disposable = page.screencast().start(new Screencast.StartOptions().setOnFrame(data -> {})); + disposable.close(); + // After dispose, starting again should succeed. + page.screencast().start(new Screencast.StartOptions().setOnFrame(data -> {})); + page.screencast().stop(); + } finally { + context.close(); + } + } + + @Test + void screencastShowOverlay() throws Exception { + BrowserContext context = browser.newContext(); + Page page = context.newPage(); + try { + page.navigate(server.EMPTY_PAGE); + AutoCloseable disposable = page.screencast().showOverlay("
Hello Overlay
"); + assertNotNull(disposable); + disposable.close(); + } finally { + context.close(); + } + } + + @Test + void screencastShowChapter() { + BrowserContext context = browser.newContext(); + Page page = context.newPage(); + try { + page.navigate(server.EMPTY_PAGE); + page.screencast().showChapter("Chapter Title"); + page.screencast().showChapter("With Description", + new Screencast.ShowChapterOptions().setDescription("Some details").setDuration(100)); + } finally { + context.close(); + } + } + + @Test + void screencastHideShowOverlays() { + BrowserContext context = browser.newContext(); + Page page = context.newPage(); + try { + page.navigate(server.EMPTY_PAGE); + page.screencast().showOverlay("
visible
"); + page.screencast().hideOverlays(); + page.screencast().showOverlays(); + } finally { + context.close(); + } + } + + @Test + void screencastShowAndHideActions() throws Exception { + BrowserContext context = browser.newContext(); + Page page = context.newPage(); + try { + page.navigate(server.EMPTY_PAGE); + AutoCloseable disposable = page.screencast().showActions(); + assertNotNull(disposable); + disposable.close(); + page.screencast().hideActions(); + } finally { + context.close(); + } + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestVideo.java b/playwright/src/test/java/com/microsoft/playwright/TestVideo.java index 24b80fae7..c2b6ee564 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestVideo.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestVideo.java @@ -16,7 +16,6 @@ package com.microsoft.playwright; -import com.microsoft.playwright.options.Size; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -38,50 +37,4 @@ 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 dec777b08..038e96b81 100644 --- a/scripts/DRIVER_VERSION +++ b/scripts/DRIVER_VERSION @@ -1 +1 @@ -1.59.0-alpha-1774622285000 +1.59.1-beta-1775752988000 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 48a185bad..1b8fdfddf 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 @@ -547,15 +547,21 @@ private String convertBuiltinType(JsonObject jsonType) { if ("WebSocketRoute.onClose.handler".equals(jsonPath)) { return "BiConsumer"; } + if ("Screencast.start.options.onFrame".equals(jsonPath)) { + return "Consumer"; + } if (jsonType.getAsJsonArray("args").size() == 1) { String paramType = convertBuiltinType(jsonType.getAsJsonArray("args").get(0).getAsJsonObject()); if (!jsonType.has("returnType") || jsonType.get("returnType").isJsonNull()) { return "Consumer<" + paramType + ">"; } - if (jsonType.has("returnType") - && "boolean".equals(jsonType.getAsJsonObject("returnType").get("name").getAsString())) { + String returnTypeName = jsonType.getAsJsonObject("returnType").get("name").getAsString(); + if ("boolean".equals(returnTypeName)) { return "Predicate<" + paramType + ">"; } + if ("Promise".equals(returnTypeName) || "void".equals(returnTypeName)) { + return "Consumer<" + paramType + ">"; + } throw new RuntimeException("Missing mapping for " + jsonType); } } @@ -989,25 +995,25 @@ 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", "Video", "Debugger").contains(jsonName)) { + if (asList("Page", "Request", "Response", "APIRequestContext", "APIRequest", "APIResponse", "FileChooser", "Frame", "FrameLocator", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard", "Tracing", "Video", "Debugger", "Screencast").contains(jsonName)) { output.add("import com.microsoft.playwright.options.*;"); } if ("Download".equals(jsonName)) { output.add("import java.io.InputStream;"); } - if (asList("Page", "Frame", "ElementHandle", "Locator", "FormData", "APIRequest", "APIRequestContext", "FileChooser", "Browser", "BrowserContext", "BrowserType", "Download", "Route", "Selectors", "Tracing", "Video").contains(jsonName)) { + if (asList("Page", "Frame", "ElementHandle", "Locator", "FormData", "APIRequest", "APIRequestContext", "FileChooser", "Browser", "BrowserContext", "BrowserType", "Download", "Route", "Selectors", "Tracing", "Video", "Screencast").contains(jsonName)) { output.add("import java.nio.file.Path;"); } if ("Clock".equals(jsonName)) { output.add("import java.util.Date;"); } - if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright", "Debugger").contains(jsonName)) { + if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright", "Debugger", "Screencast").contains(jsonName)) { output.add("import java.util.*;"); } if (asList("WebSocketRoute").contains(jsonName)) { output.add("import java.util.function.BiConsumer;"); } - if (asList("Page", "Browser", "BrowserContext", "WebSocket", "Worker", "CDPSession", "WebSocketRoute").contains(jsonName)) { + if (asList("Page", "Browser", "BrowserContext", "WebSocket", "Worker", "CDPSession", "WebSocketRoute", "Screencast").contains(jsonName)) { output.add("import java.util.function.Consumer;"); } if (asList("Page", "BrowserContext").contains(jsonName)) {