feat: implement Page.waitForRequest/Response

This commit is contained in:
Yury Semikhatsky 2020-10-06 21:48:45 -07:00
parent 9b5765e1f1
commit 3d75d2a836
14 changed files with 393 additions and 55 deletions

View File

@ -93,7 +93,7 @@ class TypeRef extends Element {
}
} else {
if (!mapping.from.equals(jsonName)) {
throw new RuntimeException("Unexpected source type for: " + parentPath);
throw new RuntimeException("Unexpected source type for: " + parentPath +". Expected: " + mapping.from + "; found: " + jsonName);
}
customType = mapping.to;
if (mapping.customMapping != null) {
@ -511,6 +511,10 @@ class Interface extends TypeDefinition {
for (Method m : methods) {
m.writeTo(output, offset);
}
// TODO: fix api.json generator to avoid name clash between close() method and close event.
if ("Page".equals(jsonName)) {
output.add(offset + "Deferred<Void> waitForClose();");
}
output.add("}");
output.add("\n");
}

View File

@ -185,6 +185,10 @@ class Types {
// Return structures
add("ConsoleMessage.location", "Object", "Location");
add("Page.waitForRequest", "Promise<Request>", "Deferred<Request>");
add("Page.waitForResponse", "Promise<Response>", "Deferred<Response>");
add("Page.waitForNavigation", "Promise<null|Response>", "Deferred<Response>");
// Custom options
add("Page.pdf.options.margin.top", "string|number", "String");
add("Page.pdf.options.margin.right", "string|number", "String");

View File

@ -920,23 +920,24 @@ public interface Page {
waitForLoadState(null);
}
void waitForLoadState(LoadState state, WaitForLoadStateOptions options);
default Response waitForNavigation() {
default Deferred<Response> waitForNavigation() {
return waitForNavigation(null);
}
Response waitForNavigation(WaitForNavigationOptions options);
default Request waitForRequest(String urlOrPredicate) {
Deferred<Response> waitForNavigation(WaitForNavigationOptions options);
default Deferred<Request> waitForRequest(String urlOrPredicate) {
return waitForRequest(urlOrPredicate, null);
}
Request waitForRequest(String urlOrPredicate, WaitForRequestOptions options);
default Response waitForResponse(String urlOrPredicate) {
Deferred<Request> waitForRequest(String urlOrPredicate, WaitForRequestOptions options);
default Deferred<Response> waitForResponse(String urlOrPredicate) {
return waitForResponse(urlOrPredicate, null);
}
Response waitForResponse(String urlOrPredicate, WaitForResponseOptions options);
Deferred<Response> waitForResponse(String urlOrPredicate, WaitForResponseOptions options);
default ElementHandle waitForSelector(String selector) {
return waitForSelector(selector, null);
}
ElementHandle waitForSelector(String selector, WaitForSelectorOptions options);
void waitForTimeout(int timeout);
List<Worker> workers();
Deferred<Void> waitForClose();
}

View File

@ -24,6 +24,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.regex.Pattern;
@ -34,7 +35,7 @@ import static com.microsoft.playwright.impl.Utils.isFunctionBody;
class BrowserContextImpl extends ChannelOwner implements BrowserContext {
private final BrowserImpl browser;
private final List<PageImpl> pages = new ArrayList<>();
final List<PageImpl> pages = new ArrayList<>();
private List<RouteInfo> routes = new ArrayList<>();
private boolean isClosedOrClosing;
final Map<String, Page.Binding> bindings = new HashMap<String, Page.Binding>();
@ -206,9 +207,9 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public Deferred<Page> waitForPage() {
Supplier<JsonObject> pageSupplier = waitForProtocolEvent("page");
CompletableFuture<JsonObject> pageFuture = futureForEvent("page");
return () -> {
JsonObject params = pageSupplier.get();
JsonObject params = waitForCompletion(pageFuture);
String guid = params.getAsJsonObject("page").get("guid").getAsString();
return connection.getExistingObject(guid);
};

View File

@ -66,11 +66,11 @@ class ChannelOwner {
return connection.sendMessage(guid, method, params);
}
protected void sendMessageNoWait(String method, JsonObject params) {
void sendMessageNoWait(String method, JsonObject params) {
connection.sendMessageNoWait(guid, method, params);
}
protected Supplier<JsonObject> waitForProtocolEvent(String event) {
CompletableFuture<JsonObject> futureForEvent(String event) {
ArrayList<CompletableFuture<JsonObject>> futures = futureEvents.get(event);
if (futures == null) {
futures = new ArrayList<>();
@ -78,23 +78,27 @@ class ChannelOwner {
}
CompletableFuture<JsonObject> result = new CompletableFuture<>();
futures.add(result);
return () -> {
while (!result.isDone()) {
return result;
}
<T> T waitForCompletion(CompletableFuture<T> future) {
while (!future.isDone()) {
connection.processOneMessage();
}
// TODO: ensure it's been removed from futureEvents
try {
return result.get();
return future.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
};
}
final void onEvent(String event, JsonObject parameters) {
handleEvent(event, parameters);
ArrayList<CompletableFuture<JsonObject>> futures = futureEvents.remove(event);
if (futures == null)
if (futures == null) {
return;
}
for (CompletableFuture<JsonObject> f : futures) {
f.complete(parameters);
}

View File

@ -240,7 +240,11 @@ public class FrameImpl extends ChannelOwner implements Frame {
params.addProperty("waitUntil", toProtocol(options.waitUntil));
}
JsonElement result = sendMessage("goto", params);
return connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("response").get("guid").getAsString());
JsonObject jsonResponse = result.getAsJsonObject().getAsJsonObject("response");
if (jsonResponse == null) {
return null;
}
return connection.getExistingObject(jsonResponse.get("guid").getAsString());
}
@Override

View File

@ -17,12 +17,13 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
@ -32,12 +33,16 @@ public class PageImpl extends ChannelOwner implements Page {
private final FrameImpl mainFrame;
private final KeyboardImpl keyboard;
private final MouseImpl mouse;
private Viewport viewport;
// TODO: do not rely on the frame order in the tests
private final Set<FrameImpl> frames = new LinkedHashSet<>();
private final List<Listener<ConsoleMessage>> consoleListeners = new ArrayList<>();
private final List<Listener<Dialog>> dialogListeners = new ArrayList<>();
private final List<Listener<Page>> closeListeners = new ArrayList<>();
private final List<WaitEventHelper> eventHelpers = new ArrayList<>();
final Map<String, Binding> bindings = new HashMap<String, Binding>();
BrowserContextImpl ownedContext;
private boolean isClosed;
PageImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
@ -59,6 +64,14 @@ public class PageImpl extends ChannelOwner implements Page {
consoleListeners.remove(listener);
}
public void addCloseListener(Listener<Page> listener) {
closeListeners.add(listener);
}
public void removeCloseListener(Listener<Page> listener) {
closeListeners.remove(listener);
}
@Override
public void addDialogListener(Listener<Dialog> listener) {
dialogListeners.add(listener);
@ -71,21 +84,25 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Deferred<Page> waitForPopup() {
Supplier<JsonObject> popupSupplier = waitForProtocolEvent("popup");
CompletableFuture<JsonObject> popupFuture = futureForEvent("popup");
return () -> {
JsonObject params = popupSupplier.get();
JsonObject params = waitForCompletion(popupFuture);
String guid = params.getAsJsonObject("page").get("guid").getAsString();
return connection.getExistingObject(guid);
};
}
private static <T> void notifyListeners(List<Listener<T>> listeners, T subject) {
for (Listener<T> listener: new ArrayList<>(listeners)) {
listener.handle(subject);
}
}
protected void handleEvent(String event, JsonObject params) {
if ("dialog".equals(event)) {
String guid = params.getAsJsonObject("dialog").get("guid").getAsString();
DialogImpl dialog = connection.getExistingObject(guid);
for (Listener<Dialog> listener: new ArrayList<>(dialogListeners)) {
listener.handle(dialog);
}
notifyListeners(dialogListeners, dialog);
// If no action taken dismiss dialog to not hang.
if (!dialog.isHandled()) {
dialog.dismiss();
@ -93,9 +110,7 @@ public class PageImpl extends ChannelOwner implements Page {
} else if ("console".equals(event)) {
String guid = params.getAsJsonObject("message").get("guid").getAsString();
ConsoleMessageImpl message = connection.getExistingObject(guid);
for (Listener<ConsoleMessage> listener: new ArrayList<>(consoleListeners)) {
listener.handle(message);
}
notifyListeners(consoleListeners, message);
} else if ("frameAttached".equals(event)) {
String guid = params.getAsJsonObject("frame").get("guid").getAsString();
FrameImpl frame = connection.getExistingObject(guid);
@ -104,7 +119,7 @@ public class PageImpl extends ChannelOwner implements Page {
if (frame.parentFrame != null) {
frame.parentFrame.childFrames.add(frame);
}
} else if ("'frameDetached'".equals(event)) {
} else if ("frameDetached".equals(event)) {
String guid = params.getAsJsonObject("frame").get("guid").getAsString();
FrameImpl frame = connection.getExistingObject(guid);
frames.remove(frame);
@ -112,6 +127,13 @@ public class PageImpl extends ChannelOwner implements Page {
if (frame.parentFrame != null) {
frame.parentFrame.childFrames.remove(frame);
}
} else if ("close".equals(event)) {
isClosed = true;
browserContext.pages.remove(this);
notifyListeners(closeListeners, this);
}
for (WaitEventHelper h : new ArrayList<>(eventHelpers)) {
h.handleEvent(event, params);
}
}
@ -171,7 +193,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void check(String selector, CheckOptions options) {
mainFrame.check(selector, convertViaJson(options, Frame.CheckOptions.class));
}
@Override
@ -181,7 +203,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public String content() {
return null;
return mainFrame.content();
}
@Override
@ -296,7 +318,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public boolean isClosed() {
return false;
return isClosed;
}
@Override
@ -316,8 +338,12 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public Page opener() {
JsonObject result = sendMessage("opener", new JsonObject()).getAsJsonObject();
if (!result.has("page")) {
return null;
}
return connection.getExistingObject(result.getAsJsonObject("page").get("guid").getAsString());
}
@Override
public byte[] pdf(PdfOptions options) {
@ -376,17 +402,15 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void setViewportSize(int width, int height) {
JsonObject size = new JsonObject();
size.addProperty("width", width);
size.addProperty("height", height);
viewport = new Viewport(width, height);
JsonObject params = new JsonObject();
params.add("viewportSize", size);
params.add("viewportSize", new Gson().toJsonTree(viewport));
sendMessage("setViewportSize", params);
}
@Override
public String textContent(String selector, TextContentOptions options) {
return null;
return mainFrame.textContent(selector, convertViaJson(options, Frame.TextContentOptions.class));
}
@Override
@ -396,12 +420,12 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void type(String selector, String text, TypeOptions options) {
mainFrame.type(selector, text, convertViaJson(options, Frame.TypeOptions.class));
}
@Override
public void uncheck(String selector, UncheckOptions options) {
mainFrame.uncheck(selector, convertViaJson(options, Frame.UncheckOptions.class));
}
@Override
@ -411,19 +435,17 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public String url() {
return null;
return mainFrame.url();
}
@Override
public Viewport viewportSize() {
return null;
return viewport;
}
@Override
public Object waitForEvent(String event, String optionsOrPredicate) {
// TODO: do we want to keep this method ?
Supplier<JsonObject> popupSupplier = waitForProtocolEvent(event);
popupSupplier.get();
return null;
}
@ -438,18 +460,51 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public Response waitForNavigation(WaitForNavigationOptions options) {
public Deferred<Response> waitForNavigation(WaitForNavigationOptions options) {
return null;
}
@Override
public Request waitForRequest(String urlOrPredicate, WaitForRequestOptions options) {
return null;
private class WaitEventHelper<R> implements Deferred<R> {
private final CompletableFuture<R> result = new CompletableFuture<>();
private final String event;
private final String fieldName;
WaitEventHelper(String event, String fieldName) {
this.event = event;
this.fieldName = fieldName;
eventHelpers.add(this);
}
void handleEvent(String name, JsonObject params) {
if (event.equals(name)) {
if (fieldName != null && params.has(fieldName)) {
result.complete(connection.getExistingObject(params.getAsJsonObject(fieldName).get("guid").getAsString()));
} else {
result.complete(null);
}
} else if ("close".equals(name)) {
result.completeExceptionally(new RuntimeException("Page closed"));
} else if ("crash".equals(name)) {
result.completeExceptionally(new RuntimeException("Page crashed"));
} else {
return;
}
eventHelpers.remove(this);
}
public R get() {
return waitForCompletion(result);
}
}
@Override
public Response waitForResponse(String urlOrPredicate, WaitForResponseOptions options) {
return null;
public Deferred<Request> waitForRequest(String urlOrPredicate, WaitForRequestOptions options) {
return new WaitEventHelper<>("request", "request");
}
@Override
public Deferred<Response> waitForResponse(String urlOrPredicate, WaitForResponseOptions options) {
return new WaitEventHelper<>("response", "response");
}
@Override
@ -466,4 +521,9 @@ public class PageImpl extends ChannelOwner implements Page {
public List<Worker> workers() {
return null;
}
@Override
public Deferred<Void> waitForClose() {
return new WaitEventHelper<>("close", null);
}
}

View File

@ -101,6 +101,12 @@ public class Server implements HttpHandler {
routes.put(path, handler);
}
void reset() {
requestSubscribers.clear();
auths.clear();
routes.clear();
}
@Override
public void handle(HttpExchange exchange) throws IOException {
String path = exchange.getRequestURI().getPath();

View File

@ -62,6 +62,7 @@ public class TestClick {
@BeforeEach
void setUp() {
server.reset();
context = browser.newContext();
page = context.newPage();
}

View File

@ -50,6 +50,7 @@ public class TestDialog {
@BeforeEach
void setUp() {
server.reset();
context = browser.newContext();
page = context.newPage();
}

View File

@ -49,6 +49,7 @@ public class TestElementHandleClick {
@BeforeEach
void setUp() {
server.reset();
context = browser.newContext();
page = context.newPage();
}

View File

@ -51,6 +51,7 @@ public class TestFrameNavigate {
@BeforeEach
void setUp() {
server.reset();
context = browser.newContext();
page = context.newPage();
}

View File

@ -0,0 +1,249 @@
/**
* 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 static com.microsoft.playwright.Page.LoadState.DOMCONTENTLOADED;
import static com.microsoft.playwright.Page.LoadState.LOAD;
import static org.junit.jupiter.api.Assertions.*;
public class TestPageBasic {
private static Server server;
private static Browser browser;
private static boolean isChromium;
private static boolean isWebKit;
private BrowserContext context;
private Page page;
@BeforeAll
static void launchBrowser() {
Playwright playwright = Playwright.create();
BrowserType.LaunchOptions options = new BrowserType.LaunchOptions();
browser = playwright.chromium().launch(options);
isChromium = true;
}
@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() {
context = browser.newContext();
page = context.newPage();
}
@AfterEach
void tearDown() {
context.close();
context = null;
page = null;
}
@Test
void shouldRejectAllPromisesWhenPageIsClosed() {
Page newPage = context.newPage();
newPage.close();
try {
newPage.evaluate("() => new Promise(r => {})");
fail("evaluate should throw");
} catch (RuntimeException e) {
assertTrue(e.getMessage().contains("Protocol error"));
}
}
@Test
void shouldNotBeVisibleInContextPages() {
Page newPage = context.newPage();
assertTrue(context.pages().contains(newPage));
newPage.close();
assertFalse(context.pages().contains(newPage));
}
@Test
void shouldRunBeforeunloadIfAskedFor() {
Page newPage = context.newPage();
newPage.navigate(server.PREFIX + "/beforeunload.html");
// We have to interact with a page so that "beforeunload" handlers
// fire.
newPage.click("body");
boolean[] didShowDialog = {false};
newPage.addDialogListener(dialog -> {
didShowDialog[0] = true;
assertEquals("beforeunload", dialog.type());
assertEquals("", dialog.defaultValue());
if (isChromium) {
assertEquals("", dialog.message());
} else if (isWebKit) {
assertEquals("Leave?", dialog.message());
} else {
assertEquals("This page is asking you to confirm that you want to leave - data you have entered may not be saved.", dialog.message());
}
dialog.accept();
});
newPage.close(new Page.CloseOptions().withRunBeforeUnload(true));
// TODO: uncomment once https://github.com/microsoft/playwright/pull/4070 is committed.
// assertTrue(didShowDialog[0]);
}
@Test
void shouldNotRunBeforeunloadByDefault() {
Page newPage = context.newPage();
newPage.navigate(server.PREFIX + "/beforeunload.html");
// We have to interact with a page so that "beforeunload" handlers
// fire.
newPage.click("body");
boolean[] didShowDialog = {false};
newPage.addDialogListener(dialog -> didShowDialog[0] = true);
newPage.close();
assertFalse(didShowDialog[0]);
}
@Test
void shouldSetThePageCloseState() {
Page newPage = context.newPage();
assertEquals(false, newPage.isClosed());
newPage.close();
assertEquals(true, newPage.isClosed());
}
@Test
void shouldTerminateNetworkWaiters() {
Page newPage = context.newPage();
Deferred<Request> request = newPage.waitForRequest(server.EMPTY_PAGE);
Deferred<Response> response = newPage.waitForResponse(server.EMPTY_PAGE);
newPage.close();
try {
request.get();
fail("get() should throw");
} catch (RuntimeException e) {
assertTrue(e.getMessage().contains("Page closed"));
assertFalse(e.getMessage().contains("Timeout"));
}
try {
response.get();
fail("get() should throw");
} catch (RuntimeException e) {
assertTrue(e.getMessage().contains("Page closed"));
assertFalse(e.getMessage().contains("Timeout"));
}
}
@Test
void shouldBeCallableTwice() {
Page newPage = context.newPage();
newPage.close();
newPage.close();
newPage.close();
}
@Test
void shouldFireLoadWhenExpected() {
page.navigate("about:blank");
page.waitForLoadState(LOAD);
}
// TODO: not supported in sync api
void asyncStacksShouldWork() {
}
@Test
void shouldProvideAccessToTheOpenerPage() {
Deferred<Page> popupEvent = page.waitForPopup();
page.evaluate("() => window.open('about:blank')");
Page opener = popupEvent.get().opener();
assertEquals(page, opener);
}
@Test
void shouldReturnNullIfParentPageHasBeenClosed() {
Deferred<Page> popupEvent = page.waitForPopup();
page.evaluate("() => window.open('about:blank')");
Page popup = popupEvent.get();
page.close();
Page opener = popup.opener();
assertEquals(null, opener);
}
@Test
void shouldFireDomcontentloadedWhenExpected() {
page.navigate("about:blank");
page.waitForLoadState(DOMCONTENTLOADED);
}
// TODO: downloads
void shouldFailWithErrorUponDisconnect() {
}
@Test
void pageUrlShouldWork() {
assertEquals("about:blank", page.url());
page.navigate(server.EMPTY_PAGE);
assertEquals(server.EMPTY_PAGE, page.url());
}
@Test
void pageUrlShouldIncludeHashes() {
page.navigate(server.EMPTY_PAGE + "#hash");
assertEquals(server.EMPTY_PAGE + "#hash", page.url());
page.evaluate("() => {\n" +
" window.location.hash = 'dynamic';\n" +
"}");
assertEquals(server.EMPTY_PAGE + "#dynamic", page.url());
}
@Test
void pageTitleShouldReturnThePageTitle() {
page.navigate(server.PREFIX + "/title.html");
assertEquals("Woof-Woof", page.title());
}
@Test
void pageCloseShouldWorkWithWindowClose() {
Deferred<Page> newPagePromise = page.waitForPopup();
page.evaluate("() => window['newPage'] = window.open('about:blank')");
Page newPage = newPagePromise.get();
Deferred<Void> closedPromise = newPage.waitForClose();
page.evaluate("() => window['newPage'].close()");
closedPromise.get();
}
@Test
void pageCloseShouldWorkWithPageClose() {
Page newPage = context.newPage();
Deferred<Void> closedPromise = newPage.waitForClose();
newPage.close();
closedPromise.get();
}
@Test
void pageContextShouldReturnTheCorrectInstance() {
assertEquals(context, page.context());
}
}

View File

@ -54,6 +54,7 @@ public class TestPopup {
@BeforeEach
void setUp() {
server.reset();
context = browser.newContext();
page = context.newPage();
}