diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java index d595cf53..8820f8ba 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java @@ -491,6 +491,8 @@ public interface BrowserContext extends AutoCloseable { *

Page routes (set up with {@link Page#route Page.route()}) take precedence over browser context routes when request * matches both handlers. * + *

To remove a route with its handler you can use {@link BrowserContext#unroute BrowserContext.unroute()}. + * *

NOTE: Enabling routing disables http cache. * * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. @@ -522,6 +524,8 @@ public interface BrowserContext extends AutoCloseable { *

Page routes (set up with {@link Page#route Page.route()}) take precedence over browser context routes when request * matches both handlers. * + *

To remove a route with its handler you can use {@link BrowserContext#unroute BrowserContext.unroute()}. + * *

NOTE: Enabling routing disables http cache. * * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. @@ -553,6 +557,8 @@ public interface BrowserContext extends AutoCloseable { *

Page routes (set up with {@link Page#route Page.route()}) take precedence over browser context routes when request * matches both handlers. * + *

To remove a route with its handler you can use {@link BrowserContext#unroute BrowserContext.unroute()}. + * *

NOTE: Enabling routing disables http cache. * * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. diff --git a/playwright/src/main/java/com/microsoft/playwright/Download.java b/playwright/src/main/java/com/microsoft/playwright/Download.java index 02f4385f..8d173a79 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Download.java +++ b/playwright/src/main/java/com/microsoft/playwright/Download.java @@ -29,7 +29,7 @@ import java.util.*; *

Download event is emitted once the download starts. Download path becomes available once download completes: *

{@code
  * // wait for download to start
- * Download download  = page.waitForDownload(() -> page.click("a")); 
+ * Download download  = page.waitForDownload(() -> page.click("a"));
  * // wait for download to complete
  * Path path = download.path();
  * }
@@ -61,7 +61,7 @@ public interface Download { String failure(); /** * Returns path to the downloaded file in case of successful download. The method will wait for the download to finish if - * necessary. + * necessary. The method throws when connected remotely via {@link BrowserType#connect BrowserType.connect()}. */ Path path(); /** diff --git a/playwright/src/main/java/com/microsoft/playwright/Frame.java b/playwright/src/main/java/com/microsoft/playwright/Frame.java index 34a2f373..3f1a64a0 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Frame.java +++ b/playwright/src/main/java/com/microsoft/playwright/Frame.java @@ -888,6 +888,33 @@ public interface Frame { return this; } } + class WaitForURLOptions { + /** + * Maximum operation time in milliseconds, defaults to 30 seconds, pass {@code 0} to disable timeout. The default value can be + * changed by using the {@link BrowserContext#setDefaultNavigationTimeout BrowserContext.setDefaultNavigationTimeout()}, + * {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}, {@link Page#setDefaultNavigationTimeout + * Page.setDefaultNavigationTimeout()} or {@link Page#setDefaultTimeout Page.setDefaultTimeout()} methods. + */ + public Double timeout; + /** + * When to consider operation succeeded, defaults to {@code load}. Events can be either: + * + */ + public WaitUntilState waitUntil; + + public WaitForURLOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + public WaitForURLOptions setWaitUntil(WaitUntilState waitUntil) { + this.waitUntil = waitUntil; + return this; + } + } /** * Returns the added tag when the script's onload fires or when the script content was injected into frame. * @@ -2614,5 +2641,71 @@ public interface Frame { * @param timeout A timeout to wait for */ void waitForTimeout(double timeout); + /** + * Waits for the frame to navigate to the given URL. + *
{@code
+   * frame.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
+   * frame.waitForURL("**\/target.html");
+   * }
+ * + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + */ + default void waitForURL(String url) { + waitForURL(url, null); + } + /** + * Waits for the frame to navigate to the given URL. + *
{@code
+   * frame.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
+   * frame.waitForURL("**\/target.html");
+   * }
+ * + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + */ + void waitForURL(String url, WaitForURLOptions options); + /** + * Waits for the frame to navigate to the given URL. + *
{@code
+   * frame.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
+   * frame.waitForURL("**\/target.html");
+   * }
+ * + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + */ + default void waitForURL(Pattern url) { + waitForURL(url, null); + } + /** + * Waits for the frame to navigate to the given URL. + *
{@code
+   * frame.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
+   * frame.waitForURL("**\/target.html");
+   * }
+ * + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + */ + void waitForURL(Pattern url, WaitForURLOptions options); + /** + * Waits for the frame to navigate to the given URL. + *
{@code
+   * frame.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
+   * frame.waitForURL("**\/target.html");
+   * }
+ * + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + */ + default void waitForURL(Predicate url) { + waitForURL(url, null); + } + /** + * Waits for the frame to navigate to the given URL. + *
{@code
+   * frame.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
+   * frame.waitForURL("**\/target.html");
+   * }
+ * + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + */ + void waitForURL(Predicate url, WaitForURLOptions options); } diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java index a955c36b..c80e27f1 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Page.java +++ b/playwright/src/main/java/com/microsoft/playwright/Page.java @@ -1561,6 +1561,33 @@ public interface Page extends AutoCloseable { return this; } } + class WaitForURLOptions { + /** + * Maximum operation time in milliseconds, defaults to 30 seconds, pass {@code 0} to disable timeout. The default value can be + * changed by using the {@link BrowserContext#setDefaultNavigationTimeout BrowserContext.setDefaultNavigationTimeout()}, + * {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}, {@link Page#setDefaultNavigationTimeout + * Page.setDefaultNavigationTimeout()} or {@link Page#setDefaultTimeout Page.setDefaultTimeout()} methods. + */ + public Double timeout; + /** + * When to consider operation succeeded, defaults to {@code load}. Events can be either: + * + */ + public WaitUntilState waitUntil; + + public WaitForURLOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + public WaitForURLOptions setWaitUntil(WaitUntilState waitUntil) { + this.waitUntil = waitUntil; + return this; + } + } class WaitForWebSocketOptions { /** * Receives the {@code WebSocket} object and resolves to truthy value when the waiting should resolve. @@ -3079,6 +3106,8 @@ public interface Page extends AutoCloseable { *

Page routes take precedence over browser context routes (set up with {@link BrowserContext#route * BrowserContext.route()}) when request matches both handlers. * + *

To remove a route with its handler you can use {@link Page#unroute Page.unroute()}. + * *

NOTE: Enabling routing disables http cache. * * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. @@ -3111,6 +3140,8 @@ public interface Page extends AutoCloseable { *

Page routes take precedence over browser context routes (set up with {@link BrowserContext#route * BrowserContext.route()}) when request matches both handlers. * + *

To remove a route with its handler you can use {@link Page#unroute Page.unroute()}. + * *

NOTE: Enabling routing disables http cache. * * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. @@ -3143,6 +3174,8 @@ public interface Page extends AutoCloseable { *

Page routes take precedence over browser context routes (set up with {@link BrowserContext#route * BrowserContext.route()}) when request matches both handlers. * + *

To remove a route with its handler you can use {@link Page#unroute Page.unroute()}. + * *

NOTE: Enabling routing disables http cache. * * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. @@ -3494,6 +3527,7 @@ public interface Page extends AutoCloseable { *

  • {@link Page#reload Page.reload()}
  • *
  • {@link Page#setContent Page.setContent()}
  • *
  • {@link Page#waitForNavigation Page.waitForNavigation()}
  • + *
  • {@link Page#waitForURL Page.waitForURL()}
  • * * *

    NOTE: {@link Page#setDefaultNavigationTimeout Page.setDefaultNavigationTimeout()} takes priority over {@link @@ -4410,6 +4444,84 @@ public interface Page extends AutoCloseable { * @param timeout A timeout to wait for */ void waitForTimeout(double timeout); + /** + * Waits for the main frame to navigate to the given URL. + *

    {@code
    +   * page.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
    +   * page.waitForURL("**\/target.html");
    +   * }
    + * + *

    Shortcut for main frame's {@link Frame#waitForURL Frame.waitForURL()}. + * + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + */ + default void waitForURL(String url) { + waitForURL(url, null); + } + /** + * Waits for the main frame to navigate to the given URL. + *

    {@code
    +   * page.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
    +   * page.waitForURL("**\/target.html");
    +   * }
    + * + *

    Shortcut for main frame's {@link Frame#waitForURL Frame.waitForURL()}. + * + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + */ + void waitForURL(String url, WaitForURLOptions options); + /** + * Waits for the main frame to navigate to the given URL. + *

    {@code
    +   * page.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
    +   * page.waitForURL("**\/target.html");
    +   * }
    + * + *

    Shortcut for main frame's {@link Frame#waitForURL Frame.waitForURL()}. + * + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + */ + default void waitForURL(Pattern url) { + waitForURL(url, null); + } + /** + * Waits for the main frame to navigate to the given URL. + *

    {@code
    +   * page.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
    +   * page.waitForURL("**\/target.html");
    +   * }
    + * + *

    Shortcut for main frame's {@link Frame#waitForURL Frame.waitForURL()}. + * + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + */ + void waitForURL(Pattern url, WaitForURLOptions options); + /** + * Waits for the main frame to navigate to the given URL. + *

    {@code
    +   * page.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
    +   * page.waitForURL("**\/target.html");
    +   * }
    + * + *

    Shortcut for main frame's {@link Frame#waitForURL Frame.waitForURL()}. + * + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + */ + default void waitForURL(Predicate url) { + waitForURL(url, null); + } + /** + * Waits for the main frame to navigate to the given URL. + *

    {@code
    +   * page.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
    +   * page.waitForURL("**\/target.html");
    +   * }
    + * + *

    Shortcut for main frame's {@link Frame#waitForURL Frame.waitForURL()}. + * + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation. + */ + void waitForURL(Predicate url, WaitForURLOptions options); /** * Performs action and waits for a new {@code WebSocket}. If predicate is provided, it passes {@code WebSocket} value into the * {@code predicate} function and waits for {@code predicate(webSocket)} to return a truthy value. Will throw an error if the page is diff --git a/playwright/src/main/java/com/microsoft/playwright/Video.java b/playwright/src/main/java/com/microsoft/playwright/Video.java index 1ba5f9b0..5589adf4 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Video.java +++ b/playwright/src/main/java/com/microsoft/playwright/Video.java @@ -20,16 +20,28 @@ import java.nio.file.Path; import java.util.*; /** - * When browser context is created with the {@code videosPath} option, each page has a video object associated with it. + * When browser context is created with the {@code recordVideo} option, each page has a video object associated with it. *

    {@code
      * System.out.println(page.video().path());
      * }
    */ public interface Video { + /** + * Deletes the video file. Will wait for the video to finish if necessary. + */ + void delete(); /** * Returns the file system path this video will be recorded to. The video is guaranteed to be written to the filesystem - * upon closing the browser context. + * upon closing the browser context. This method throws when connected remotely via {@link BrowserType#connect + * BrowserType.connect()}. */ Path path(); + /** + * Saves the video to a user-specified path. It is safe to call this method while the video is still in progress, or after + * the page has closed. This method waits until the page is closed and the video is fully saved. + * + * @param path Path where the video should be saved. + */ + void saveAs(Path path); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ArtifactImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/ArtifactImpl.java new file mode 100644 index 00000000..2f5a0c5c --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ArtifactImpl.java @@ -0,0 +1,75 @@ +/* + * 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 java.io.InputStream; +import java.nio.file.FileSystems; +import java.nio.file.Path; + +import static com.microsoft.playwright.impl.Utils.writeToFile; + +class ArtifactImpl extends ChannelOwner { + boolean isRemote; + public ArtifactImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { + super(parent, type, guid, initializer); + } + + public InputStream createReadStream() { + JsonObject result = sendMessage("stream").getAsJsonObject(); + if (!result.has("stream")) { + return null; + } + Stream stream = connection.getExistingObject(result.getAsJsonObject("stream").get("guid").getAsString()); + return stream.stream(); + } + + public void delete() { + sendMessage("delete"); + } + + public String failure() { + JsonObject result = sendMessage("failure").getAsJsonObject(); + if (result.has("error")) { + return result.get("error").getAsString(); + } + return null; + } + + public Path pathAfterFinished() { + if (isRemote) { + throw new PlaywrightException("Path is not available when using browserType.connect(). Use download.saveAs() to save a local copy."); + } + JsonObject json = sendMessage("pathAfterFinished").getAsJsonObject(); + return FileSystems.getDefault().getPath(json.get("value").getAsString()); + } + + public void saveAs(Path path) { + if (isRemote) { + JsonObject jsonObject = sendMessage("saveAsStream").getAsJsonObject(); + Stream stream = connection.getExistingObject(jsonObject.getAsJsonObject("stream").get("guid").getAsString()); + writeToFile(stream.stream(), path); + return; + } + + JsonObject params = new JsonObject(); + params.addProperty("path", path.toString()); + sendMessage("saveAs", params); + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java index a1d1175b..105b1a14 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java @@ -255,6 +255,9 @@ public class Connection { case "AndroidDevice": // result = new AndroidDevice(parent, type, guid, initializer); break; + case "Artifact": + result = new ArtifactImpl(parent, type, guid, initializer); + break; case "BindingCall": result = new BindingCall(parent, type, guid, initializer); break; @@ -273,9 +276,6 @@ public class Connection { case "Dialog": result = new DialogImpl(parent, type, guid, initializer); break; - case "Download": - result = new DownloadImpl(parent, type, guid, initializer); - break; case "Electron": // result = new Playwright(parent, type, guid, initializer); break; diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/DownloadImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/DownloadImpl.java index abe524c8..e2bc1e31 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/DownloadImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/DownloadImpl.java @@ -16,27 +16,19 @@ package com.microsoft.playwright.impl; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.microsoft.playwright.BrowserContext; import com.microsoft.playwright.Download; -import com.microsoft.playwright.PlaywrightException; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; import java.io.InputStream; -import java.nio.file.FileSystems; import java.nio.file.Path; -import static com.microsoft.playwright.impl.Utils.writeToFile; +class DownloadImpl extends LoggingSupport implements Download { + private final ArtifactImpl artifact; + private final JsonObject initializer; -public class DownloadImpl extends ChannelOwner implements Download { - private final BrowserImpl browser; - - public DownloadImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { - super(parent, type, guid, initializer); - browser = ((BrowserContextImpl) parent).browser(); + DownloadImpl(ArtifactImpl artifact, JsonObject initializer) { + this.artifact = artifact; + this.initializer = initializer; } @Override @@ -51,58 +43,26 @@ public class DownloadImpl extends ChannelOwner implements Download { @Override public InputStream createReadStream() { - return withLogging("Download.createReadStream", () -> { - JsonObject result = sendMessage("stream").getAsJsonObject(); - if (!result.has("stream")) { - return null; - } - Stream stream = connection.getExistingObject(result.getAsJsonObject("stream").get("guid").getAsString()); - return stream.stream(); - }); + return withLogging("Download.createReadStream", () -> artifact.createReadStream()); } @Override public void delete() { - withLogging("Download.delete", () -> { - sendMessage("delete"); - }); + withLogging("Download.delete", () -> artifact.delete()); } @Override public String failure() { - return withLogging("Download.failure", () -> { - JsonObject result = sendMessage("failure").getAsJsonObject(); - if (result.has("error")) { - return result.get("error").getAsString(); - } - return null; - }); + return withLogging("Download.failure", () -> artifact.failure()); } @Override public Path path() { - return withLogging("Download.path", () -> { - if (browser != null && browser.isRemote) { - throw new PlaywrightException("Path is not available when using browserType.connect(). Use download.saveAs() to save a local copy."); - } - JsonObject json = sendMessage("path").getAsJsonObject(); - return FileSystems.getDefault().getPath(json.get("value").getAsString()); - }); + return withLogging("Download.path", () -> artifact.pathAfterFinished()); } @Override public void saveAs(Path path) { - withLogging("Download.saveAs", () -> { - if (browser != null && browser.isRemote) { - JsonObject jsonObject = sendMessage("saveAsStream").getAsJsonObject(); - Stream stream = connection.getExistingObject(jsonObject.getAsJsonObject("stream").get("guid").getAsString()); - writeToFile(stream.stream(), path); - return; - } - - JsonObject params = new JsonObject(); - params.addProperty("path", path.toString()); - sendMessage("saveAs", params); - }); + withLogging("Download.saveAs", () -> artifact.saveAs(path)); } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java index b153f16c..b3ee4a9a 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java @@ -28,6 +28,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.regex.Pattern; import static com.microsoft.playwright.options.LoadState.*; import static com.microsoft.playwright.impl.Serialization.*; @@ -856,10 +858,14 @@ public class FrameImpl extends ChannelOwner implements Frame { @Override public Response waitForNavigation(WaitForNavigationOptions options, Runnable code) { - return withLogging("Frame.waitForNavigation", () -> waitForNavigationImpl(code, options)); + return withLogging("Frame.waitForNavigation", () -> waitForNavigationImpl(code, options, null)); } Response waitForNavigationImpl(Runnable code, WaitForNavigationOptions options) { + return waitForNavigationImpl(code, options, null); + } + + private Response waitForNavigationImpl(Runnable code, WaitForNavigationOptions options, UrlMatcher matcher) { if (options == null) { options = new WaitForNavigationOptions(); } @@ -868,7 +874,9 @@ public class FrameImpl extends ChannelOwner implements Frame { } List> waitables = new ArrayList<>(); - UrlMatcher matcher = UrlMatcher.forOneOf(options.url); + if (matcher == null) { + matcher = UrlMatcher.forOneOf(options.url); + } waitables.add(new WaitForNavigationHelper(matcher, convertViaJson(options.waitUntil, LoadState.class))); waitables.add(page.createWaitForCloseHelper()); waitables.add(page.createWaitableFrameDetach(this)); @@ -910,6 +918,37 @@ public class FrameImpl extends ChannelOwner implements Frame { }); } + @Override + public void waitForURL(String url, WaitForURLOptions options) { + waitForURL(new UrlMatcher(url), options); + } + + @Override + public void waitForURL(Pattern url, WaitForURLOptions options) { + waitForURL(new UrlMatcher(url), options); + } + + @Override + public void waitForURL(Predicate url, WaitForURLOptions options) { + waitForURL(new UrlMatcher(url), options); + } + + private void waitForURL(UrlMatcher matcher, WaitForURLOptions options) { + withLogging("Frame.waitForURL", () -> waitForURLImpl(matcher, options)); + } + + void waitForURLImpl(UrlMatcher matcher, WaitForURLOptions options) { + if (options == null) { + options = new WaitForURLOptions(); + } + if (matcher.test(url())) { + waitForLoadStateImpl(convertViaJson(options.waitUntil, LoadState.class), + convertViaJson(options, WaitForLoadStateOptions.class)); + return; + } + waitForNavigationImpl(() -> {}, convertViaJson(options, WaitForNavigationOptions.class), matcher); + } + protected void handleEvent(String event, JsonObject params) { if ("loadstate".equals(event)) { JsonElement add = params.get("add"); 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 02d1bed8..774d2cef 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -44,6 +44,7 @@ public class PageImpl extends ChannelOwner implements Page { private final KeyboardImpl keyboard; private final MouseImpl mouse; private final TouchscreenImpl touchscreen; + final Waitable waitableClosedOrCrashed; private ViewportSize viewport; private final Router routes = new Router(); private final Set frames = new LinkedHashSet<>(); @@ -107,6 +108,7 @@ public class PageImpl extends ChannelOwner implements Page { touchscreen = new TouchscreenImpl(this); frames.add(mainFrame); timeoutSettings = new TimeoutSettings(browserContext.timeoutSettings); + waitableClosedOrCrashed = createWaitForCloseHelper(); } @Override @@ -138,8 +140,10 @@ public class PageImpl extends ChannelOwner implements Page { ConsoleMessageImpl message = connection.getExistingObject(guid); listeners.notify(EventType.CONSOLE, message); } else if ("download".equals(event)) { - String guid = params.getAsJsonObject("download").get("guid").getAsString(); - DownloadImpl download = connection.getExistingObject(guid); + String artifactGuid = params.getAsJsonObject("artifact").get("guid").getAsString(); + ArtifactImpl artifact = connection.getExistingObject(artifactGuid); + artifact.isRemote = browserContext.browser() != null && browserContext.browser().isRemote; + DownloadImpl download = new DownloadImpl(artifact, params); listeners.notify(EventType.DOWNLOAD, download); } else if ("fileChooser".equals(event)) { String guid = params.getAsJsonObject("element").get("guid").getAsString(); @@ -211,7 +215,9 @@ public class PageImpl extends ChannelOwner implements Page { route.resume(); } } else if ("video".equals(event)) { - video().setRelativePath(params.get("relativePath").getAsString()); + String artifactGuid = params.getAsJsonObject("artifact").get("guid").getAsString(); + ArtifactImpl artifact = connection.getExistingObject(artifactGuid); + forceVideo().setArtifact(artifact); } else if ("pageError".equals(event)) { SerializedError error = gson().fromJson(params.getAsJsonObject("error"), SerializedError.class); String errorStr = ""; @@ -1144,20 +1150,23 @@ public class PageImpl extends ChannelOwner implements Page { return mainFrame.url(); } + + private VideoImpl forceVideo() { + if (video == null) { + video = new VideoImpl(this); + } + return video; + } + @Override public VideoImpl video() { - if (video != null) { - return video; - } + // Note: we are creating Video object lazily, because we do not know + // BrowserContextOptions when constructing the page - it is assigned + // too late during launchPersistentContext. if (browserContext.videosDir == null) { return null; } - video = new VideoImpl(this); - // In case of persistent profile, we already have it. - if (initializer.has("videoRelativePath")) { - video.setRelativePath(initializer.get("videoRelativePath").getAsString()); - } - return video; + return forceVideo(); } @Override @@ -1320,6 +1329,25 @@ public class PageImpl extends ChannelOwner implements Page { withLogging("Page.waitForTimeout", () -> mainFrame.waitForTimeoutImpl(timeout)); } + @Override + public void waitForURL(String url, WaitForURLOptions options) { + waitForURL(new UrlMatcher(url), options); + } + + @Override + public void waitForURL(Pattern url, WaitForURLOptions options) { + waitForURL(new UrlMatcher(url), options); + } + + @Override + public void waitForURL(Predicate url, WaitForURLOptions options) { + waitForURL(new UrlMatcher(url), options); + } + + private void waitForURL(UrlMatcher matcher, WaitForURLOptions options) { + withLogging("Page.waitForURL", () -> mainFrame.waitForURLImpl(matcher, convertViaJson(options, Frame.WaitForURLOptions.class))); + } + @Override public List workers() { return new ArrayList<>(workers); 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 69ed8c6e..bf51900a 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/VideoImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/VideoImpl.java @@ -16,27 +16,67 @@ package com.microsoft.playwright.impl; +import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.Video; import java.nio.file.Path; +import java.nio.file.Paths; -class VideoImpl implements Video { +import static java.util.Arrays.asList; + +class VideoImpl extends LoggingSupport implements Video { private final PageImpl page; - private Path fullPath; + private final WaitableResult waitableArtifact = new WaitableResult<>(); + private final boolean isRemote; VideoImpl(PageImpl page) { this.page = page; + BrowserImpl browser = page.context().browser(); + isRemote = browser != null && browser.isRemote; } - void setRelativePath(String path) { - fullPath = page.context().videosDir.resolve(path); + void setArtifact(ArtifactImpl artifact) { + artifact.isRemote = isRemote; + waitableArtifact.complete(artifact); + } + + private ArtifactImpl waitForArtifact() { + Waitable waitable = new WaitableRace<>(asList(waitableArtifact, (Waitable) page.waitableClosedOrCrashed)); + return page.runUntil(() -> {}, waitable); + } + + @Override + public void delete() { + withLogging("Video.delete", () -> { + try { + waitForArtifact().delete(); + } catch (PlaywrightException e) { + } + }); } @Override public Path path() { - while (fullPath == null) { - page.connection.processOneMessage(); - } - return fullPath; + return withLogging("Video.path", () -> { + if (isRemote) { + throw new PlaywrightException("Path is not available when using browserType.connect(). Use saveAs() to save a local copy."); + } + try { + return Paths.get(waitForArtifact().initializer.get("absolutePath").getAsString()); + } catch (PlaywrightException e) { + throw new PlaywrightException("Page did not produce any video frames", e); + } + }); + } + + @Override + public void saveAs(Path path) { + withLogging("Video.saveAs", () -> { + try { + waitForArtifact().saveAs(path); + } catch (PlaywrightException e) { + throw new PlaywrightException("Page did not produce any video frames", e); + } + }); } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestDownload.java b/playwright/src/test/java/com/microsoft/playwright/TestDownload.java index 1965f7b8..0d79232f 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestDownload.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestDownload.java @@ -213,7 +213,7 @@ public class TestDownload extends TestBase { download.saveAs(userPath); fail("did not throw"); } catch (PlaywrightException e) { - assertTrue(e.getMessage().contains("Download already deleted. Save before deleting.")); + assertTrue(e.getMessage().contains("File already deleted. Save before deleting."), e.getMessage()); } page.close(); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java b/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java index fc1b53fc..9aed7b02 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java @@ -523,6 +523,7 @@ public class TestPageRoute extends TestBase { } @Test + @DisabledIf(value="com.microsoft.playwright.TestBase#isChromium", disabledReason="https://github.com/microsoft/playwright/issues/6016") void shouldSupportCorsWithPOST() { page.navigate(server.EMPTY_PAGE); page.route("**/cars", route -> { @@ -545,6 +546,7 @@ public class TestPageRoute extends TestBase { } @Test + @DisabledIf(value="com.microsoft.playwright.TestBase#isChromium", disabledReason="https://github.com/microsoft/playwright/issues/6016") void shouldSupportCorsWithCredentials() { page.navigate(server.EMPTY_PAGE); page.route("**/cars", route -> { @@ -597,6 +599,7 @@ public class TestPageRoute extends TestBase { } @Test + @DisabledIf(value="com.microsoft.playwright.TestBase#isChromium", disabledReason="https://github.com/microsoft/playwright/issues/6016") void shouldSupportCorsForDifferentMethods() { page.navigate(server.EMPTY_PAGE); page.route("**/cars", route -> { diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageWaitForUrl.java b/playwright/src/test/java/com/microsoft/playwright/TestPageWaitForUrl.java new file mode 100644 index 00000000..f2a2e236 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageWaitForUrl.java @@ -0,0 +1,111 @@ +/* + * 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.WaitUntilState; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestPageWaitForUrl extends TestBase { + @Test + void shouldWork() { + page.navigate(server.EMPTY_PAGE); + page.evaluate("url => window.location.href = url", server.PREFIX + "/grid.html"); + page.waitForURL("**/grid.html"); + } + + @Test + void shouldRespectTimeout() { + page.navigate(server.EMPTY_PAGE); + try { + page.waitForURL("**/frame.html", new Page.WaitForURLOptions().setTimeout(2500)); + fail("did not throw"); + } catch (TimeoutError e) { + assertTrue(e.getMessage().contains("Timeout 2500ms exceeded")); + } + } + + @Test + void shouldWorkWithBothDomcontentloadedAndLoad() { + page.navigate(server.PREFIX + "/one-style.html"); + page.waitForURL("**/one-style.html", new Page.WaitForURLOptions().setWaitUntil(WaitUntilState.DOMCONTENTLOADED)); + page.waitForURL("**/one-style.html", new Page.WaitForURLOptions().setWaitUntil(WaitUntilState.LOAD)); + } + + @Test + void shouldWorkWithClickingOnAnchorLinks() { + page.navigate(server.EMPTY_PAGE); + page.setContent("foobar"); + page.click("a"); + page.waitForURL("**/*#foobar"); + } + + @Test + void shouldWorkWithHistoryPushState() { + page.navigate(server.EMPTY_PAGE); + page.setContent("SPA\n" + + ""); + page.click("a"); + page.waitForURL("**/wow.html"); + assertEquals(server.PREFIX + "/wow.html", page.url()); + } + + @Test + void shouldWorkWithHistoryReplaceState() { + page.navigate(server.EMPTY_PAGE); + page.setContent(" SPA\n" + + ""); + page.click("a"); + page.waitForURL("**/replaced.html"); + assertEquals(server.PREFIX + "/replaced.html", page.url()); + } + + @Test + void shouldWorkWithDOMHistoryBackHistoryForward() { + page.navigate(server.EMPTY_PAGE); + page.setContent("back\n" + + "forward\n" + + ""); + assertEquals(server.PREFIX + "/second.html", page.url()); + + page.click("a#back"); + page.waitForURL("**/first.html"); + assertEquals(server.PREFIX + "/first.html", page.url()); + + page.click("a#forward"); + page.waitForURL("**/second.html"); + assertEquals(server.PREFIX + "/second.html", page.url()); + } + + @Test + void shouldWorkOnFrame() { + page.navigate(server.PREFIX + "/frames/one-frame.html"); + Frame frame = page.frames().get(1); + frame.evaluate("url => window.location.href = url", server.PREFIX + "/grid.html"); + frame.waitForURL("**/grid.html"); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java b/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java index 21e3142a..d7361675 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java @@ -22,7 +22,7 @@ import org.junit.jupiter.api.io.TempDir; import java.nio.file.Files; import java.nio.file.Path; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class TestScreencast extends TestBase { @Test @@ -36,5 +36,69 @@ public class TestScreencast extends TestBase { assertTrue(path.startsWith(videosDir)); context.close(); assertTrue(Files.exists(path)); - }; + } + + @Test + void shouldSaveAsVideo(@TempDir Path videosDir) { + BrowserContext context = browser.newContext( + new Browser.NewContextOptions() + .setRecordVideoDir(videosDir) + .setRecordVideoSize(320, 240) + .setViewportSize(320, 240)); + Page page = context.newPage(); + page.evaluate("() => document.body.style.backgroundColor = 'red'"); + page.waitForTimeout(1000); + context.close(); + + Path saveAsPath = videosDir.resolve("my-video.webm"); + page.video().saveAs(saveAsPath); + assertTrue(Files.exists(saveAsPath)); + } + + @Test + void saveAsShouldThrowWhenNoVideoFrames(@TempDir Path videosDir) { + 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"); + try { + popup.video().saveAs(saveAsPath); + } catch (PlaywrightException e) { + // WebKit pauses renderer before win.close() and actually writes something. + if (isWebKit()) { + assertTrue(Files.exists(saveAsPath)); + } else { + assertTrue(e.getMessage().contains("Page did not produce any video frames"), e.getMessage()); + } + } + } + + @Test + void shouldDeleteVideo(@TempDir Path videosDir) { + BrowserContext context = browser.newContext( + new Browser.NewContextOptions() + .setRecordVideoDir(videosDir) + .setRecordVideoSize(320, 240) + .setViewportSize(320, 240)); + Page page = context.newPage(); + page.evaluate("() => document.body.style.backgroundColor = 'red'"); + page.waitForTimeout(1000); + context.close(); + + page.video().delete(); + Path videoPath = page.video().path(); + assertFalse(Files.exists(videoPath)); + } } diff --git a/scripts/CLI_VERSION b/scripts/CLI_VERSION index e3872e0a..b3b3163a 100644 --- a/scripts/CLI_VERSION +++ b/scripts/CLI_VERSION @@ -1 +1 @@ -1.11.0-next-1617087307000 +1.11.0-next-1617218236000