chore: rework waitFor* to use Waitable (#17)
This commit is contained in:
@@ -85,6 +85,15 @@ class ChannelOwner {
|
||||
return result;
|
||||
}
|
||||
|
||||
<T> Deferred<T> toDeferred(Waitable waitable) {
|
||||
return () -> {
|
||||
while (!waitable.isDone()) {
|
||||
connection.processOneMessage();
|
||||
}
|
||||
return (T) waitable.get();
|
||||
};
|
||||
}
|
||||
|
||||
<T> T waitForCompletion(CompletableFuture<T> future) {
|
||||
while (!future.isDone()) {
|
||||
connection.processOneMessage();
|
||||
|
||||
@@ -28,6 +28,7 @@ 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;
|
||||
@@ -40,7 +41,8 @@ public class FrameImpl extends ChannelOwner implements Frame {
|
||||
FrameImpl parentFrame;
|
||||
Set<FrameImpl> childFrames = new LinkedHashSet<>();
|
||||
private final Set<LoadState> loadStates = new HashSet<>();
|
||||
private final List<WaitForNavigationHelper> eventHelpers = new ArrayList<>();
|
||||
enum InternalEventType { NAVIGATED, LOADSTATE };
|
||||
private final ListenerCollection<InternalEventType> internalListeners = new ListenerCollection<>();
|
||||
PageImpl page;
|
||||
boolean isDetached;
|
||||
|
||||
@@ -438,71 +440,118 @@ public class FrameImpl extends ChannelOwner implements Frame {
|
||||
if (state == null) {
|
||||
state = LOAD;
|
||||
}
|
||||
while (!loadStates.contains(state)) {
|
||||
// TODO: support timeout!
|
||||
WaitForLoadStateHelper helper = new WaitForLoadStateHelper(state);
|
||||
while (!helper.isDone()) {
|
||||
connection.processOneMessage();
|
||||
}
|
||||
}
|
||||
|
||||
enum State { WAITING_FOR_NAVIGATION, WAITING_FOR_LOAD_STATE, DONE };
|
||||
private class WaitForLoadStateHelper implements Waitable, Listener<InternalEventType> {
|
||||
private final LoadState expectedState;
|
||||
private boolean isDone;
|
||||
|
||||
// TODO: switch to listeners or something else less convoluted.
|
||||
private class WaitForNavigationHelper implements Deferred<Response> {
|
||||
private final CompletableFuture<Response> result = new CompletableFuture<>();
|
||||
private final UrlMatcher matcher;
|
||||
private final LoadState loadState;
|
||||
private State state = State.WAITING_FOR_NAVIGATION;
|
||||
private RequestImpl request;
|
||||
|
||||
WaitForNavigationHelper(UrlMatcher matcher, LoadState loadState) {
|
||||
this.matcher = matcher;
|
||||
this.loadState = loadState;
|
||||
eventHelpers.add(this);
|
||||
WaitForLoadStateHelper(LoadState state) {
|
||||
expectedState = state;
|
||||
isDone = loadStates.contains(state);
|
||||
if (!isDone) {
|
||||
internalListeners.add(InternalEventType.LOADSTATE, this);
|
||||
}
|
||||
}
|
||||
|
||||
void handleEvent(String name, JsonObject params) {
|
||||
if (state == State.WAITING_FOR_NAVIGATION) {
|
||||
if (!"navigated".equals(name)) {
|
||||
return;
|
||||
}
|
||||
if (!matcher.test(params.get("url").getAsString())) {
|
||||
return;
|
||||
}
|
||||
if (params.has("error")) {
|
||||
result.completeExceptionally(new RuntimeException(params.get("error").getAsString()));
|
||||
state = State.DONE;
|
||||
} else {
|
||||
if (params.has("newDocument")) {
|
||||
JsonObject jsonReq = params.getAsJsonObject("newDocument").getAsJsonObject("request");
|
||||
if (jsonReq != null) {
|
||||
request = connection.getExistingObject(jsonReq.get("guid").getAsString());
|
||||
}
|
||||
}
|
||||
state = State.WAITING_FOR_LOAD_STATE;
|
||||
}
|
||||
@Override
|
||||
public void handle(Event<InternalEventType> event) {
|
||||
assert event.type() == InternalEventType.LOADSTATE;
|
||||
if (expectedState.equals(event.data())) {
|
||||
isDone = true;
|
||||
dispose();
|
||||
}
|
||||
if (state == State.WAITING_FOR_LOAD_STATE) {
|
||||
if (loadStates.contains(loadState)) {
|
||||
state = State.DONE;
|
||||
if (request == null) {
|
||||
result.complete(null);
|
||||
} else {
|
||||
result.complete(request.finalRequest().response());
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
internalListeners.remove(InternalEventType.LOADSTATE, this);
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return isDone;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class WaitForNavigationHelper implements Waitable, Listener<InternalEventType> {
|
||||
private final UrlMatcher matcher;
|
||||
private final LoadState expectedLoadState;
|
||||
private WaitForLoadStateHelper loadStateHelper;
|
||||
|
||||
private RequestImpl request;
|
||||
private RuntimeException exception;
|
||||
|
||||
WaitForNavigationHelper(UrlMatcher matcher, LoadState expectedLoadState) {
|
||||
this.matcher = matcher;
|
||||
this.expectedLoadState = expectedLoadState;
|
||||
internalListeners.add(InternalEventType.NAVIGATED, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Event<InternalEventType> event) {
|
||||
assert InternalEventType.NAVIGATED == event.type();
|
||||
JsonObject params = (JsonObject) event.data();
|
||||
if (!matcher.test(params.get("url").getAsString())) {
|
||||
return;
|
||||
}
|
||||
eventHelpers.remove(this);
|
||||
if (params.has("error")) {
|
||||
exception = new RuntimeException(params.get("error").getAsString());
|
||||
} else {
|
||||
if (params.has("newDocument")) {
|
||||
JsonObject jsonReq = params.getAsJsonObject("newDocument").getAsJsonObject("request");
|
||||
if (jsonReq != null) {
|
||||
request = connection.getExistingObject(jsonReq.get("guid").getAsString());
|
||||
}
|
||||
}
|
||||
loadStateHelper = new WaitForLoadStateHelper(expectedLoadState);
|
||||
}
|
||||
internalListeners.remove(InternalEventType.NAVIGATED, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
internalListeners.remove(InternalEventType.NAVIGATED, this);
|
||||
if (loadStateHelper != null) {
|
||||
loadStateHelper.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
if (exception != null) {
|
||||
return true;
|
||||
}
|
||||
if (loadStateHelper != null) {
|
||||
return loadStateHelper.isDone();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response get() {
|
||||
return waitForCompletion(result);
|
||||
while (!isDone()) {
|
||||
connection.processOneMessage();
|
||||
}
|
||||
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
return request.finalRequest().response();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Deferred<Response> waitForNavigation(WaitForNavigationOptions options) {
|
||||
if (options == null) {
|
||||
@@ -510,7 +559,20 @@ public class FrameImpl extends ChannelOwner implements Frame {
|
||||
options.url = "**";
|
||||
options.waitUntil = LOAD;
|
||||
}
|
||||
return new WaitForNavigationHelper(new UrlMatcher(options.url), options.waitUntil);
|
||||
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(page.createWaitForCloseHelper());
|
||||
if (options.timeout != null) {
|
||||
waitables.add(new WaitableTimeout(options.timeout.intValue()));
|
||||
}
|
||||
return toDeferred(new WaitableRace(waitables));
|
||||
}
|
||||
|
||||
private static String toProtocol(WaitForSelectorOptions.State state) {
|
||||
@@ -540,14 +602,17 @@ public class FrameImpl extends ChannelOwner implements Frame {
|
||||
|
||||
@Override
|
||||
public void waitForTimeout(int timeout) {
|
||||
|
||||
// return toDeferred(new WaitableTimeout(timeout));
|
||||
toDeferred(new WaitableTimeout(timeout)).get();
|
||||
}
|
||||
|
||||
protected void handleEvent(String event, JsonObject params) {
|
||||
if ("loadstate".equals(event)) {
|
||||
JsonElement add = params.get("add");
|
||||
if (add != null) {
|
||||
loadStates.add(loadStateFromProtocol(add.getAsString()));
|
||||
LoadState state = loadStateFromProtocol(add.getAsString());
|
||||
loadStates.add(state);
|
||||
internalListeners.notify(InternalEventType.LOADSTATE, state);
|
||||
}
|
||||
JsonElement remove = params.get("remove");
|
||||
if (remove != null) {
|
||||
@@ -556,13 +621,10 @@ public class FrameImpl extends ChannelOwner implements Frame {
|
||||
} else if ("navigated".equals(event)) {
|
||||
url = params.get("url").getAsString();
|
||||
name = params.get("name").getAsString();
|
||||
// liste
|
||||
if (!params.has("error") && page != null) {
|
||||
page.frameNavigated(this);
|
||||
}
|
||||
}
|
||||
for (WaitForNavigationHelper h : new ArrayList<>(eventHelpers)) {
|
||||
h.handleEvent(event, params);
|
||||
internalListeners.notify(InternalEventType.NAVIGATED, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,16 +661,17 @@ public class PageImpl extends ChannelOwner implements Page {
|
||||
}
|
||||
}
|
||||
|
||||
private class WaitEventHelper<R> implements Deferred<R>, Listener<EventType> {
|
||||
private final CompletableFuture<Event<EventType>> result = new CompletableFuture<>();
|
||||
private final EventType type;
|
||||
private final Predicate<Event<EventType>> predicate;
|
||||
private final List<EventType> subscribedEvents;
|
||||
|
||||
WaitEventHelper(EventType type, Predicate<Event<EventType>> predicate) {
|
||||
this.type = type;
|
||||
this.predicate = predicate;
|
||||
subscribedEvents = Arrays.asList(type, EventType.CLOSE, EventType.CRASH);
|
||||
Waitable createWaitForCloseHelper() {
|
||||
return new WaitablePageClose();
|
||||
}
|
||||
|
||||
class WaitablePageClose implements Waitable, Listener<EventType> {
|
||||
private final List<EventType> subscribedEvents;
|
||||
private RuntimeException exception;
|
||||
|
||||
WaitablePageClose() {
|
||||
subscribedEvents = Arrays.asList(EventType.CLOSE, EventType.CRASH);
|
||||
for (EventType e : subscribedEvents) {
|
||||
addListener(e, this);
|
||||
}
|
||||
@@ -678,44 +679,101 @@ public class PageImpl extends ChannelOwner implements Page {
|
||||
|
||||
@Override
|
||||
public void handle(Event<EventType> event) {
|
||||
if (type.equals(event.type()) && predicate.test(event)) {
|
||||
result.complete(event);
|
||||
} else if (EventType.CLOSE.equals(event.type())) {
|
||||
result.completeExceptionally(new RuntimeException("Page closed"));
|
||||
if (EventType.CLOSE.equals(event.type())) {
|
||||
exception = new RuntimeException("Page closed");
|
||||
} else if (EventType.CRASH.equals(event.type())) {
|
||||
result.completeExceptionally(new RuntimeException("Page crashed"));
|
||||
exception = new RuntimeException("Page crashed");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
return exception != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get() {
|
||||
throw exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
for (EventType e : subscribedEvents) {
|
||||
removeListener(e, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public R get() {
|
||||
Event<EventType> r = waitForCompletion(result);
|
||||
return (R) r.data();
|
||||
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) {
|
||||
return new WaitEventHelper<>(EventType.REQUEST, e -> {
|
||||
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(createWaitForCloseHelper());
|
||||
if (options != null && options.timeout != null) {
|
||||
waitables.add(new WaitableTimeout(options.timeout.intValue()));
|
||||
}
|
||||
return toDeferred(new WaitableRace(waitables));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Deferred<Response> waitForResponse(String urlOrPredicate, WaitForResponseOptions options) {
|
||||
return new WaitEventHelper<>(EventType.RESPONSE, e -> {
|
||||
List<Waitable> waitables = new ArrayList<>();
|
||||
waitables.add(new WaitableEvent(EventType.RESPONSE, e -> {
|
||||
if (urlOrPredicate == null) {
|
||||
return true;
|
||||
}
|
||||
return urlOrPredicate.equals(((Response) e.data()).url());
|
||||
});
|
||||
}));
|
||||
waitables.add(createWaitForCloseHelper());
|
||||
if (options != null && options.timeout != null) {
|
||||
waitables.add(new WaitableTimeout(options.timeout.intValue()));
|
||||
}
|
||||
return toDeferred(new WaitableRace(waitables));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
interface Waitable {
|
||||
boolean isDone();
|
||||
Object get();
|
||||
void dispose();
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
class WaitableRace implements Waitable {
|
||||
private final Collection<Waitable> waitables;
|
||||
|
||||
WaitableRace(Waitable... waitables) {
|
||||
this(Arrays.asList(waitables));
|
||||
}
|
||||
|
||||
WaitableRace(Collection<Waitable> waitables) {
|
||||
this.waitables = waitables;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
for (Waitable w : waitables) {
|
||||
if (w.isDone()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get() {
|
||||
assert isDone();
|
||||
dispose();
|
||||
for (Waitable w : waitables) {
|
||||
if (w.isDone()) {
|
||||
return w.get();
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("At least one element must be ready");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
for (Waitable w : waitables) {
|
||||
w.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
class WaitableTimeout implements Waitable {
|
||||
private final long deadline;
|
||||
private final int timeout;
|
||||
|
||||
WaitableTimeout(int millis) {
|
||||
timeout = millis;
|
||||
deadline = System.nanoTime() + millis * 1_000_000;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
return System.nanoTime() > deadline;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get() {
|
||||
throw new RuntimeException("Timeout " + timeout + "ms exceeded");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* 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 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 org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class TestPageWaitForNavigation {
|
||||
private static Playwright playwright;
|
||||
private static Server server;
|
||||
private static Browser browser;
|
||||
private static boolean isChromium;
|
||||
private static boolean isWebKit;
|
||||
private static boolean headful;
|
||||
private BrowserContext context;
|
||||
private Page page;
|
||||
|
||||
@BeforeAll
|
||||
static void launchBrowser() {
|
||||
playwright = Playwright.create();
|
||||
BrowserType.LaunchOptions options = new BrowserType.LaunchOptions();
|
||||
browser = playwright.chromium().launch(options);
|
||||
isChromium = true;
|
||||
isWebKit = false;
|
||||
headful = false;
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
static void startServer() throws IOException {
|
||||
server = new Server(8907);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopServer() throws IOException {
|
||||
browser.close();
|
||||
server.stop();
|
||||
server = null;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
server.reset();
|
||||
context = browser.newContext();
|
||||
page = context.newPage();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
context.close();
|
||||
context = null;
|
||||
page = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWork() {
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
Deferred<Response> response = page.waitForNavigation();
|
||||
page.evaluate("url => window.location.href = url", server.PREFIX + "/grid.html");
|
||||
assertTrue(response.get().ok());
|
||||
assertTrue(response.get().url().contains("grid.html"));
|
||||
}
|
||||
|
||||
// @Test
|
||||
// TODO: timeout
|
||||
void shouldRespectTimeout() {
|
||||
Deferred<Response> promise = page.waitForNavigation(new Page.WaitForNavigationOptions().withUrl("**/frame.html").withTimeout(5000));
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
try {
|
||||
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}'"));
|
||||
}
|
||||
}
|
||||
|
||||
// Skipped in sync API.
|
||||
void shouldWorkWithBothDomcontentloadedAndLoad() {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithClickingOnAnchorLinks() {
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
page.setContent("<a href='#foobar'>foobar</a>");
|
||||
Deferred<Response> response = page.waitForNavigation();
|
||||
page.click("a");
|
||||
assertNull(response.get());
|
||||
assertEquals(server.EMPTY_PAGE + "#foobar", page.url());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithClickingOnLinksWhichDoNotCommitNavigation() {
|
||||
// TODO: https server
|
||||
// page.navigate(server.EMPTY_PAGE);
|
||||
// page.setContent("<a href='" + httpsServer.EMPTY_PAGE + "'>foobar</a>");
|
||||
// try {
|
||||
// page.waitForNavigation();
|
||||
// page.click("a");
|
||||
// fail("did not throw");
|
||||
// } catch (RuntimeException e) {
|
||||
// assertTrue(e.getMessage().contains(expectedSSLError(browserName)));
|
||||
// }
|
||||
}
|
||||
|
||||
@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>");
|
||||
Deferred<Response> response = page.waitForNavigation();
|
||||
page.click("a");
|
||||
assertNull(response.get());
|
||||
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>");
|
||||
Deferred<Response> response = page.waitForNavigation();
|
||||
page.click("a");
|
||||
assertNull(response.get());
|
||||
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());
|
||||
|
||||
Deferred<Response> backResponse = page.waitForNavigation();
|
||||
page.click("a#back");
|
||||
assertNull(backResponse.get());
|
||||
assertEquals(server.PREFIX + "/first.html", page.url());
|
||||
|
||||
Deferred<Response> forwardResponse = page.waitForNavigation();
|
||||
page.click("a#forward");
|
||||
assertNull(forwardResponse.get());
|
||||
assertEquals(server.PREFIX + "/second.html", page.url());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWhenSubframeIssuesWindowStop() {
|
||||
server.setRoute("/frames/style.css", exchange -> {});
|
||||
boolean[] frameWindowStopCalled = {false};
|
||||
page.addListener(Page.EventType.FRAMEATTACHED, event -> {
|
||||
Frame frame = (Frame) event.data();
|
||||
page.addListener(FRAMENAVIGATED, event1 -> {
|
||||
if (frame.equals(event1.data())) {
|
||||
frame.evaluate("window.stop()");
|
||||
frameWindowStopCalled[0] = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
page.navigate(server.PREFIX + "/frames/one-frame.html");
|
||||
assertTrue(frameWindowStopCalled[0]);
|
||||
}
|
||||
|
||||
// @Test
|
||||
void shouldWorkWithUrlMatch() {
|
||||
// TODO: predicate
|
||||
Deferred<Response> response1 = page.waitForNavigation(new Page.WaitForNavigationOptions().withUrl("/one-style.html/"));
|
||||
Deferred<Response> response2 = page.waitForNavigation(new Page.WaitForNavigationOptions().withUrl("/frame.html/"));
|
||||
Deferred<Response> response3 = page.waitForNavigation(new Page.WaitForNavigationOptions().withUrl("url => url.searchParams.get(\"foo\") === \"bar\""));
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
page.navigate(server.PREFIX + "/frame.html");
|
||||
assertNotNull(response2.get());
|
||||
page.navigate(server.PREFIX + "/one-style.html");
|
||||
assertNotNull(response1.get());
|
||||
page.navigate(server.PREFIX + "/frame.html?foo=bar");
|
||||
assertNotNull(response3.get());
|
||||
page.navigate(server.PREFIX + "/empty.html");
|
||||
assertEquals(server.PREFIX + "/one-style.html", response1.get().url());
|
||||
assertEquals(server.PREFIX + "/frame.html", response2.get().url());
|
||||
assertEquals(server.PREFIX + "/frame.html?foo=bar", response3.get().url());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithUrlMatchForSameDocumentNavigations() {
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
// TODO: use regex
|
||||
Deferred<Response> waitPromise = page.waitForNavigation(new Page.WaitForNavigationOptions().withUrl("**/third.html"));
|
||||
page.evaluate("() => {\n" +
|
||||
" history.pushState({}, '', '/first.html');\n" +
|
||||
"}");
|
||||
page.evaluate("() => {\n" +
|
||||
" history.pushState({}, '', '/second.html');\n" +
|
||||
"}");
|
||||
page.evaluate("() => {\n" +
|
||||
" history.pushState({}, '', '/third.html');\n" +
|
||||
"}");
|
||||
assertNull(waitPromise.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkForCrossProcessNavigations() {
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
Deferred<Response> waitPromise = page.waitForNavigation(new Page.WaitForNavigationOptions().withWaitUntil(Frame.LoadState.DOMCONTENTLOADED));
|
||||
String url = server.CROSS_PROCESS_PREFIX + "/empty.html";
|
||||
page.navigate(url);
|
||||
Response response = waitPromise.get();
|
||||
assertEquals(url, response.url());
|
||||
assertEquals(url, page.url());
|
||||
assertEquals(url, page.evaluate("document.location.href"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkOnFrame() {
|
||||
page.navigate(server.PREFIX + "/frames/one-frame.html");
|
||||
Frame frame = page.frames().get(1);
|
||||
Deferred<Response> response = frame.waitForNavigation();
|
||||
frame.evaluate("url => window.location.href = url", server.PREFIX + "/grid.html");
|
||||
assertTrue(response.get().ok());
|
||||
assertTrue(response.get().url().contains("grid.html"));
|
||||
assertEquals(frame, response.get().frame());
|
||||
assertTrue(page.url().contains("/frames/one-frame.html"));
|
||||
}
|
||||
|
||||
// @Test
|
||||
void shouldFailWhenFrameDetaches() {
|
||||
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())");
|
||||
response.get();
|
||||
fail("did not throw");
|
||||
} catch (RuntimeException e) {
|
||||
assertTrue(e.getMessage().contains("waiting for navigation until \"load\""));
|
||||
assertTrue(e.getMessage().contains("frame was detached"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user