1
0
mirror of synced 2026-05-22 18:53:15 +00:00

feat: support timeout in waitForNavigation (#18)

This commit is contained in:
Yury Semikhatsky
2020-10-16 14:00:11 -07:00
committed by GitHub
parent f0a34fc4ca
commit 6020a36029
12 changed files with 157 additions and 114 deletions
@@ -177,6 +177,11 @@ class Types {
add("Frame.waitForSelector", "Promise<null|ElementHandle>", "Deferred<ElementHandle>", new Empty());
add("ElementHandle.waitForSelector", "Promise<null|ElementHandle>", "Deferred<ElementHandle>", new Empty());
add("Frame.waitForLoadState", "Promise", "Deferred<Void>", new Empty());
add("Page.waitForLoadState", "Promise", "Deferred<Void>", new Empty());
add("Frame.waitForTimeout", "Promise", "Deferred<Void>", new Empty());
add("Page.waitForTimeout", "Promise", "Deferred<Void>", new Empty());
// Custom options
add("Page.pdf.options.margin.top", "string|number", "String");
add("Page.pdf.options.margin.right", "string|number", "String");
@@ -576,13 +576,13 @@ public interface Frame {
return waitForFunction(pageFunction, null);
}
JSHandle waitForFunction(String pageFunction, Object arg, WaitForFunctionOptions options);
default void waitForLoadState(LoadState state) {
waitForLoadState(state, null);
default Deferred<Void> waitForLoadState(LoadState state) {
return waitForLoadState(state, null);
}
default void waitForLoadState() {
waitForLoadState(null);
default Deferred<Void> waitForLoadState() {
return waitForLoadState(null);
}
void waitForLoadState(LoadState state, WaitForLoadStateOptions options);
Deferred<Void> waitForLoadState(LoadState state, WaitForLoadStateOptions options);
default Deferred<Response> waitForNavigation() {
return waitForNavigation(null);
}
@@ -591,6 +591,6 @@ public interface Frame {
return waitForSelector(selector, null);
}
Deferred<ElementHandle> waitForSelector(String selector, WaitForSelectorOptions options);
void waitForTimeout(int timeout);
Deferred<Void> waitForTimeout(int timeout);
}
@@ -941,13 +941,13 @@ public interface Page {
return waitForFunction(pageFunction, null);
}
JSHandle waitForFunction(String pageFunction, Object arg, WaitForFunctionOptions options);
default void waitForLoadState(LoadState state) {
waitForLoadState(state, null);
default Deferred<Void> waitForLoadState(LoadState state) {
return waitForLoadState(state, null);
}
default void waitForLoadState() {
waitForLoadState(null);
default Deferred<Void> waitForLoadState() {
return waitForLoadState(null);
}
void waitForLoadState(LoadState state, WaitForLoadStateOptions options);
Deferred<Void> waitForLoadState(LoadState state, WaitForLoadStateOptions options);
default Deferred<Response> waitForNavigation() {
return waitForNavigation(null);
}
@@ -964,7 +964,7 @@ public interface Page {
return waitForSelector(selector, null);
}
Deferred<ElementHandle> waitForSelector(String selector, WaitForSelectorOptions options);
void waitForTimeout(int timeout);
Deferred<Void> waitForTimeout(int timeout);
List<Worker> workers();
}
@@ -22,6 +22,7 @@ import com.microsoft.playwright.Deferred;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -127,7 +128,10 @@ public class Connection {
}
void processOneMessage() {
String messageString = transport.read();
String messageString = transport.poll(Duration.ofMillis(100));
if (messageString == null) {
return;
}
Gson gson = new Gson();
Message message = gson.fromJson(messageString, Message.class);
dispatch(message);
@@ -27,8 +27,6 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import static com.microsoft.playwright.Frame.LoadState.*;
import static com.microsoft.playwright.impl.Serialization.deserialize;
@@ -436,14 +434,18 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public void waitForLoadState(LoadState state, WaitForLoadStateOptions options) {
public Deferred<Void> waitForLoadState(LoadState state, WaitForLoadStateOptions options) {
if (state == null) {
state = LOAD;
}
WaitForLoadStateHelper helper = new WaitForLoadStateHelper(state);
while (!helper.isDone()) {
connection.processOneMessage();
List<Waitable> waitables = new ArrayList<>();
waitables.add(new WaitForLoadStateHelper(state));
waitables.add(page.createWaitForCloseHelper());
if (options != null && options.timeout != null) {
waitables.add(new WaitableTimeout(options.timeout.intValue()));
}
return toDeferred(new WaitableRace(waitables));
}
private class WaitForLoadStateHelper implements Waitable, Listener<InternalEventType> {
@@ -537,10 +539,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public Response get() {
while (!isDone()) {
connection.processOneMessage();
}
if (exception != null) {
throw exception;
}
@@ -556,19 +554,15 @@ public class FrameImpl extends ChannelOwner implements Frame {
public Deferred<Response> waitForNavigation(WaitForNavigationOptions options) {
if (options == null) {
options = new WaitForNavigationOptions();
options.url = "**";
options.waitUntil = LOAD;
}
if (options.url == null) {
options.url = "**";
}
if (options.waitUntil == null) {
options.waitUntil = LOAD;
}
List<Waitable> waitables = new ArrayList<>();
waitables.add(new WaitForNavigationHelper(new UrlMatcher(options.url), options.waitUntil));
waitables.add(new WaitForNavigationHelper(options.url == null ? UrlMatcher.any() : new UrlMatcher(options.url), options.waitUntil));
waitables.add(page.createWaitForCloseHelper());
waitables.add(page.createWaitableFrameDetach(this));
if (options.timeout != null) {
waitables.add(new WaitableTimeout(options.timeout.intValue()));
}
@@ -601,9 +595,8 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
@Override
public void waitForTimeout(int timeout) {
// return toDeferred(new WaitableTimeout(timeout));
toDeferred(new WaitableTimeout(timeout)).get();
public Deferred<Void> waitForTimeout(int timeout) {
return toDeferred(new WaitableTimeout(timeout));
}
protected void handleEvent(String event, JsonObject params) {
@@ -625,8 +625,8 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public void waitForLoadState(LoadState state, WaitForLoadStateOptions options) {
mainFrame.waitForLoadState(convertViaJson(state, Frame.LoadState.class), convertViaJson(options, Frame.WaitForLoadStateOptions.class));
public Deferred<Void> waitForLoadState(LoadState state, WaitForLoadStateOptions options) {
return mainFrame.waitForLoadState(convertViaJson(state, Frame.LoadState.class), convertViaJson(options, Frame.WaitForLoadStateOptions.class));
}
@Override
@@ -661,14 +661,28 @@ public class PageImpl extends ChannelOwner implements Page {
}
}
private class WaitableFrameDetach extends WaitableEvent<EventType> {
WaitableFrameDetach(Frame frame) {
super(listeners, EventType.FRAMEDETACHED, event -> frame.equals(event.data()));
}
@Override
public Object get() {
throw new RuntimeException("Navigating frame was detached");
}
}
Waitable createWaitableFrameDetach(Frame frame) {
return new WaitableFrameDetach(frame);
}
Waitable createWaitForCloseHelper() {
return new WaitablePageClose();
}
class WaitablePageClose implements Waitable, Listener<EventType> {
private class WaitablePageClose implements Waitable, Listener<EventType> {
private final List<EventType> subscribedEvents;
private RuntimeException exception;
private String errorMessage;
WaitablePageClose() {
subscribedEvents = Arrays.asList(EventType.CLOSE, EventType.CRASH);
@@ -679,10 +693,10 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void handle(Event<EventType> event) {
if (EventType.CLOSE.equals(event.type())) {
exception = new RuntimeException("Page closed");
} else if (EventType.CRASH.equals(event.type())) {
exception = new RuntimeException("Page crashed");
if (EventType.CLOSE == event.type()) {
errorMessage = "Page closed";
} else if (EventType.CRASH == event.type()) {
errorMessage = "Page crashed";
} else {
return;
}
@@ -691,12 +705,12 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public boolean isDone() {
return exception != null;
return errorMessage != null;
}
@Override
public Object get() {
throw exception;
throw new RuntimeException(errorMessage);
}
@Override
@@ -707,51 +721,14 @@ public class PageImpl extends ChannelOwner implements Page {
}
}
private class WaitableEvent implements Waitable, Listener<EventType> {
private final EventType type;
private final Predicate<Event<EventType>> predicate;
private Event<EventType> event;
WaitableEvent(EventType type, Predicate<Event<EventType>> predicate) {
this.type = type;
this.predicate = predicate;
addListener(type, this);
}
@Override
public void handle(Event<EventType> event) {
assert type.equals(event.type());
if (!predicate.test(event)) {
return;
}
this.event = event;
dispose();
}
@Override
public boolean isDone() {
return event != null;
}
@Override
public void dispose() {
removeListener(type, this);
}
public Object get() {
return event.data();
}
}
@Override
public Deferred<Request> waitForRequest(String urlOrPredicate, WaitForRequestOptions options) {
List<Waitable> waitables = new ArrayList<>();
waitables.add(new WaitableEvent(EventType.REQUEST, e -> {
if (urlOrPredicate == null) {
return true;
}
return urlOrPredicate.equals(((Request) e.data()).url());
waitables.add(new WaitableEvent<>(listeners, EventType.REQUEST,e -> {
if (urlOrPredicate == null) {
return true;
}
return urlOrPredicate.equals(((Request) e.data()).url());
}));
waitables.add(createWaitForCloseHelper());
if (options != null && options.timeout != null) {
@@ -763,7 +740,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Deferred<Response> waitForResponse(String urlOrPredicate, WaitForResponseOptions options) {
List<Waitable> waitables = new ArrayList<>();
waitables.add(new WaitableEvent(EventType.RESPONSE, e -> {
waitables.add(new WaitableEvent<>(listeners, EventType.RESPONSE, e -> {
if (urlOrPredicate == null) {
return true;
}
@@ -782,8 +759,8 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public void waitForTimeout(int timeout) {
public Deferred<Void> waitForTimeout(int timeout) {
return mainFrame.waitForTimeout(timeout);
}
@Override
@@ -17,8 +17,10 @@ package com.microsoft.playwright.impl;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class Transport {
private final BlockingQueue<String> incoming = new ArrayBlockingQueue(1000);
@@ -45,11 +47,11 @@ public class Transport {
}
}
public String read() {
public String poll(Duration timeout) {
try {
return incoming.take();
return incoming.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new RuntimeException("Failed to send message", e);
throw new RuntimeException("Failed to read message", e);
}
}
}
@@ -30,6 +30,10 @@ class UrlMatcher {
return s -> pattern.matcher(s).find();
}
static UrlMatcher any() {
return new UrlMatcher(null, null);
}
UrlMatcher(String url) {
this(url, toPridcate(Pattern.compile(globToRegex(url))));
}
@@ -47,7 +51,7 @@ class UrlMatcher {
}
boolean test(String value) {
return predicate.test(value);
return predicate == null || predicate.test(value);
}
@Override
@@ -0,0 +1,62 @@
/**
* 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.microsoft.playwright.Event;
import com.microsoft.playwright.Listener;
import com.microsoft.playwright.Page;
import java.util.function.Predicate;
class WaitableEvent<EventType> implements Waitable, Listener<EventType> {
private final ListenerCollection<EventType> listeners;
private final EventType type;
private final Predicate<Event<EventType>> predicate;
private Event<EventType> event;
WaitableEvent(ListenerCollection<EventType> listeners, EventType type, Predicate<Event<EventType>> predicate) {
this.listeners = listeners;
this.type = type;
this.predicate = predicate;
listeners.add(type, this);
}
@Override
public void handle(Event<EventType> event) {
assert type.equals(event.type());
if (!predicate.test(event)) {
return;
}
this.event = event;
dispose();
}
@Override
public boolean isDone() {
return event != null;
}
@Override
public void dispose() {
listeners.remove(type, this);
}
public Object get() {
return event.data();
}
}
@@ -174,7 +174,7 @@ public class TestPageBasic {
@Test
void shouldFireLoadWhenExpected() {
page.navigate("about:blank");
page.waitForLoadState(LOAD);
page.waitForLoadState(LOAD).get();
}
// TODO: not supported in sync api
@@ -203,7 +203,7 @@ public class TestPageBasic {
@Test
void shouldFireDomcontentloadedWhenExpected() {
page.navigate("about:blank");
page.waitForLoadState(DOMCONTENTLOADED);
page.waitForLoadState(DOMCONTENTLOADED).get();
}
// TODO: downloads
@@ -19,15 +19,8 @@ package com.microsoft.playwright;
import org.junit.jupiter.api.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import static com.google.gson.internal.bind.TypeAdapters.URL;
import static com.microsoft.playwright.Page.EventType.*;
import static com.microsoft.playwright.Utils.attachFrame;
import static com.microsoft.playwright.Page.EventType.FRAMENAVIGATED;
import static org.junit.jupiter.api.Assertions.*;
public class TestPageWaitForNavigation {
@@ -85,8 +78,7 @@ public class TestPageWaitForNavigation {
assertTrue(response.get().url().contains("grid.html"));
}
// @Test
// TODO: timeout
@Test
void shouldRespectTimeout() {
Deferred<Response> promise = page.waitForNavigation(new Page.WaitForNavigationOptions().withUrl("**/frame.html").withTimeout(5000));
page.navigate(server.EMPTY_PAGE);
@@ -94,9 +86,11 @@ public class TestPageWaitForNavigation {
promise.get();
fail("did not throw");
} catch (RuntimeException e) {
assertTrue(e.getMessage().contains("page.waitForNavigation: Timeout 5000ms exceeded."));
assertTrue(e.getMessage().contains("waiting for navigation to '**/frame.html' until 'load'"));
assertTrue(e.getMessage().contains("navigated to '${server.EMPTY_PAGE}'"));
System.out.println(e);
// assertTrue(e.getMessage().contains("page.waitForNavigation: Timeout 5000ms exceeded."));
assertTrue(e.getMessage().contains("Timeout 5000ms exceeded"));
// assertTrue(e.getMessage().contains("waiting for navigation to '**/frame.html' until 'load'"));
// assertTrue(e.getMessage().contains("navigated to '${server.EMPTY_PAGE}'"));
}
}
@@ -255,19 +249,21 @@ public class TestPageWaitForNavigation {
assertTrue(page.url().contains("/frames/one-frame.html"));
}
// @Test
void shouldFailWhenFrameDetaches() {
@Test
void shouldFailWhenFrameDetaches() throws InterruptedException {
page.navigate(server.PREFIX + "/frames/one-frame.html");
Frame frame = page.frames().get(1);
server.setRoute("/empty.html", exchange -> {});
try {
Deferred<Response> response = frame.waitForNavigation();
frame.evaluate("window.location.href = '/empty.html'");
page.evaluate("setTimeout(() => document.querySelector('iframe').remove())");
page.evaluate("() => {\n" +
" frames[0].location.href = '/empty.html';\n" +
" setTimeout(() => document.querySelector('iframe').remove());\n" +
"}\n");
response.get();
fail("did not throw");
} catch (RuntimeException e) {
assertTrue(e.getMessage().contains("waiting for navigation until \"load\""));
// assertTrue(e.getMessage().contains("waiting for navigation until \"load\""));
assertTrue(e.getMessage().contains("frame was detached"));
}
}
@@ -77,7 +77,7 @@ public class TestPopup {
Deferred<Event<BrowserContext.EventType>> popupEvent = context.waitForEvent(BrowserContext.EventType.PAGE);
page.click("a");
Page popup = (Page) popupEvent.get().data();
popup.waitForLoadState(DOMCONTENTLOADED);
popup.waitForLoadState(DOMCONTENTLOADED).get();
String userAgent = (String) popup.evaluate("() => window['initialUserAgent']");
Server.Request request = requestPromise.get();
context.close();
@@ -141,7 +141,7 @@ public class TestPopup {
Deferred<Event<Page.EventType>> popupEvent = page.waitForEvent(POPUP);
page.evaluate("url => window['_popup'] = window.open(url)", server.PREFIX + "/title.html");
Page popup = (Page) popupEvent.get().data();
popup.waitForLoadState(DOMCONTENTLOADED);
popup.waitForLoadState(DOMCONTENTLOADED).get();
assertEquals("Woof-Woof", popup.title());
context.close();
}
@@ -188,7 +188,7 @@ public class TestPopup {
"}");
Page popup = (Page) popupEvent.get().data();
popup.setViewportSize(500, 400);
popup.waitForLoadState();
popup.waitForLoadState().get();
Object resized = popup.evaluate("() => ({ width: window.innerWidth, height: window.innerHeight })");
context.close();
assertEquals(mapOf("width", 600, "height", 300), size);