1
0
mirror of synced 2026-07-07 09:00:02 +00:00

feat: roll driver, implement waitForUrl and new Video APIs (#382)

This commit is contained in:
Yury Semikhatsky
2021-04-02 08:53:26 -07:00
committed by GitHub
parent f332a536b1
commit 1ff5671b12
16 changed files with 627 additions and 84 deletions
@@ -491,6 +491,8 @@ public interface BrowserContext extends AutoCloseable {
* <p> Page routes (set up with {@link Page#route Page.route()}) take precedence over browser context routes when request
* matches both handlers.
*
* <p> To remove a route with its handler you can use {@link BrowserContext#unroute BrowserContext.unroute()}.
*
* <p> <strong>NOTE:</strong> 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 {
* <p> Page routes (set up with {@link Page#route Page.route()}) take precedence over browser context routes when request
* matches both handlers.
*
* <p> To remove a route with its handler you can use {@link BrowserContext#unroute BrowserContext.unroute()}.
*
* <p> <strong>NOTE:</strong> 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 {
* <p> Page routes (set up with {@link Page#route Page.route()}) take precedence over browser context routes when request
* matches both handlers.
*
* <p> To remove a route with its handler you can use {@link BrowserContext#unroute BrowserContext.unroute()}.
*
* <p> <strong>NOTE:</strong> Enabling routing disables http cache.
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing.
@@ -29,7 +29,7 @@ import java.util.*;
* <p> Download event is emitted once the download starts. Download path becomes available once download completes:
* <pre>{@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();
* }</pre>
@@ -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();
/**
@@ -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:
* <ul>
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* </ul>
*/
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.
* <pre>{@code
* frame.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @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.
* <pre>{@code
* frame.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @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.
* <pre>{@code
* frame.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @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.
* <pre>{@code
* frame.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @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.
* <pre>{@code
* frame.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
*/
default void waitForURL(Predicate<String> url) {
waitForURL(url, null);
}
/**
* Waits for the frame to navigate to the given URL.
* <pre>{@code
* frame.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
* frame.waitForURL("**\/target.html");
* }</pre>
*
* @param url A glob pattern, regex pattern or predicate receiving [URL] to match while waiting for the navigation.
*/
void waitForURL(Predicate<String> url, WaitForURLOptions options);
}
@@ -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:
* <ul>
* <li> {@code "domcontentloaded"} - consider operation to be finished when the {@code DOMContentLoaded} event is fired.</li>
* <li> {@code "load"} - consider operation to be finished when the {@code load} event is fired.</li>
* <li> {@code "networkidle"} - consider operation to be finished when there are no network connections for at least {@code 500} ms.</li>
* </ul>
*/
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 {
* <p> Page routes take precedence over browser context routes (set up with {@link BrowserContext#route
* BrowserContext.route()}) when request matches both handlers.
*
* <p> To remove a route with its handler you can use {@link Page#unroute Page.unroute()}.
*
* <p> <strong>NOTE:</strong> 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 {
* <p> Page routes take precedence over browser context routes (set up with {@link BrowserContext#route
* BrowserContext.route()}) when request matches both handlers.
*
* <p> To remove a route with its handler you can use {@link Page#unroute Page.unroute()}.
*
* <p> <strong>NOTE:</strong> 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 {
* <p> Page routes take precedence over browser context routes (set up with {@link BrowserContext#route
* BrowserContext.route()}) when request matches both handlers.
*
* <p> To remove a route with its handler you can use {@link Page#unroute Page.unroute()}.
*
* <p> <strong>NOTE:</strong> 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 {
* <li> {@link Page#reload Page.reload()}</li>
* <li> {@link Page#setContent Page.setContent()}</li>
* <li> {@link Page#waitForNavigation Page.waitForNavigation()}</li>
* <li> {@link Page#waitForURL Page.waitForURL()}</li>
* </ul>
*
* <p> <strong>NOTE:</strong> {@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.
* <pre>{@code
* page.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
* page.waitForURL("**\/target.html");
* }</pre>
*
* <p> 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.
* <pre>{@code
* page.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
* page.waitForURL("**\/target.html");
* }</pre>
*
* <p> 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.
* <pre>{@code
* page.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
* page.waitForURL("**\/target.html");
* }</pre>
*
* <p> 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.
* <pre>{@code
* page.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
* page.waitForURL("**\/target.html");
* }</pre>
*
* <p> 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.
* <pre>{@code
* page.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
* page.waitForURL("**\/target.html");
* }</pre>
*
* <p> 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<String> url) {
waitForURL(url, null);
}
/**
* Waits for the main frame to navigate to the given URL.
* <pre>{@code
* page.click("a.delayed-navigation"); // Clicking the link will indirectly cause a navigation
* page.waitForURL("**\/target.html");
* }</pre>
*
* <p> 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<String> 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
@@ -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.
* <pre>{@code
* System.out.println(page.video().path());
* }</pre>
*/
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);
}
@@ -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);
}
}
@@ -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;
@@ -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));
}
}
@@ -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<Waitable<Response>> 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<String> 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");
@@ -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<FrameImpl> 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<String> 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<Worker> workers() {
return new ArrayList<>(workers);
@@ -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<ArtifactImpl> 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<ArtifactImpl> waitable = new WaitableRace<>(asList(waitableArtifact, (Waitable<ArtifactImpl>) 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);
}
});
}
}
@@ -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();
}
@@ -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 -> {
@@ -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("<a href='#foobar'>foobar</a>");
page.click("a");
page.waitForURL("**/*#foobar");
}
@Test
void shouldWorkWithHistoryPushState() {
page.navigate(server.EMPTY_PAGE);
page.setContent("<a onclick='javascript:pushState()'>SPA</a>\n" +
"<script>\n" +
" function pushState() { history.pushState({}, '', 'wow.html') }\n" +
"</script>");
page.click("a");
page.waitForURL("**/wow.html");
assertEquals(server.PREFIX + "/wow.html", page.url());
}
@Test
void shouldWorkWithHistoryReplaceState() {
page.navigate(server.EMPTY_PAGE);
page.setContent(" <a onclick='javascript:replaceState()'>SPA</a>\n" +
"<script>\n" +
" function replaceState() { history.replaceState({}, '', '/replaced.html') }\n" +
"</script>");
page.click("a");
page.waitForURL("**/replaced.html");
assertEquals(server.PREFIX + "/replaced.html", page.url());
}
@Test
void shouldWorkWithDOMHistoryBackHistoryForward() {
page.navigate(server.EMPTY_PAGE);
page.setContent("<a id=back onclick='javascript:goBack()'>back</a>\n" +
"<a id=forward onclick='javascript:goForward()'>forward</a>\n" +
"<script>\n" +
" function goBack() { history.back(); }\n" +
" function goForward() { history.forward(); }\n" +
" history.pushState({}, '', '/first.html');\n" +
" history.pushState({}, '', '/second.html');\n" +
"</script>");
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");
}
}
@@ -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));
}
}
+1 -1
View File
@@ -1 +1 @@
1.11.0-next-1617087307000
1.11.0-next-1617218236000