From 06132ebfb2be3b093ad183115aeea313ca0ee740 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 30 Sep 2020 18:25:24 -0700 Subject: [PATCH] feat: add console listener, generate some events --- .../playwright/tools/ApiGenerator.java | 122 ++++++++++++++++-- .../microsoft/playwright/ConsoleMessage.java | 21 ++- .../java/com/microsoft/playwright/Page.java | 17 +-- .../com/microsoft/playwright/Request.java | 7 +- .../playwright/impl/BrowserImpl.java | 1 + .../playwright/impl/ChannelOwner.java | 2 +- .../microsoft/playwright/impl/Connection.java | 1 - .../playwright/impl/ConsoleMessageImpl.java | 24 ++-- .../microsoft/playwright/impl/FrameImpl.java | 13 +- .../microsoft/playwright/impl/PageImpl.java | 42 +++--- .../com/microsoft/playwright/TestClick.java | 57 +++++++- 11 files changed, 229 insertions(+), 78 deletions(-) diff --git a/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java b/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java index ce20fd07..8ffeaeda 100644 --- a/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java +++ b/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java @@ -205,6 +205,84 @@ abstract class TypeDefinition extends Element { } } +class Event extends Element { + private final TypeRef type; + + private enum ApiType {HANDLER, LISTENER, WAIT_FOR} + + private static class Info { + final String typePrefix; + final ApiType apiType; + + Info(String typePrefix, ApiType apiType) { + this.typePrefix = typePrefix; + this.apiType = apiType; + } + } + + private static Map events = new HashMap<>(); + private static void add(String jsonPath, String typePrefix, ApiType apiType) { + events.put(jsonPath, new Info(typePrefix, apiType)); + } + static { + add("Browser.disconnected", "Disconnected", ApiType.WAIT_FOR); + add("BrowserContext.page", "Page", ApiType.WAIT_FOR); + add("Page.console", "Console", ApiType.LISTENER); + add("Page.crash", "Crash", ApiType.WAIT_FOR); + add("Page.dialog", "Dialog", ApiType.HANDLER); + add("Page.domcontentloaded", "DomContentLoaded", ApiType.WAIT_FOR); + add("Page.download", "Download", ApiType.WAIT_FOR); + add("Page.filechooser", "FileChooser", ApiType.HANDLER); + add("Page.frameattached", "FrameAttached", ApiType.WAIT_FOR); + add("Page.framedetached", "FrameDetached", ApiType.WAIT_FOR); + add("Page.framenavigated", "FrameNavigated", ApiType.WAIT_FOR); + add("Page.load", "Load", ApiType.WAIT_FOR); + add("Page.pageerror", "Error", ApiType.LISTENER); + add("Page.popup", "Popup", ApiType.WAIT_FOR); + add("Page.request", "Request", ApiType.WAIT_FOR); + add("Page.requestfailed", "RequestFailed", ApiType.WAIT_FOR); + add("Page.requestfinished", "RequestFinished", ApiType.WAIT_FOR); + add("Page.response", "Response", ApiType.WAIT_FOR); + add("Page.worker", "Worker", ApiType.WAIT_FOR); + add("Worker.close", "Close", ApiType.WAIT_FOR); + add("ChromiumBrowser.disconnected", "Disconnected", ApiType.WAIT_FOR); + add("ChromiumBrowserContext.backgroundpage", "BackgroundPage", ApiType.WAIT_FOR); + add("ChromiumBrowserContext.serviceworker", "ServiceWorker", ApiType.WAIT_FOR); + add("ChromiumBrowserContext.page", "Page", ApiType.WAIT_FOR); + add("FirefoxBrowser.disconnected", "Disconnected", ApiType.WAIT_FOR); + add("WebKitBrowser.disconnected", "Disconnected", ApiType.WAIT_FOR); + } + + Event(Element parent, JsonObject jsonElement) { + super(parent, jsonElement); + type = new TypeRef(this, jsonElement.get("type")); + if (!events.containsKey(jsonPath)) { + throw new RuntimeException("Type mapping is missing for event: " + jsonPath); + } + } + + void writeTo(List output, String offset) { + // TODO: only whitelisted events are generated for now as the API may change. + if (!"Page.console".equals(jsonPath) && + !"Page.popup".equals(jsonPath)) { + return; + } + Info info = events.get(jsonPath); + String templateArg = type.toJava().replace("void", "Void"); + if (info.apiType == ApiType.WAIT_FOR) { + output.add(offset + "Deferred<" + templateArg + "> waitFor" + info.typePrefix + "();"); + return; + } + if (info.apiType == ApiType.LISTENER || info.apiType == ApiType.HANDLER) { + String listenerType = info.typePrefix; + output.add(offset + "void add" + listenerType + "Listener(Listener<" + templateArg + "> listener);"); + output.add(offset + "void remove" + listenerType + "Listener(Listener<" + templateArg + "> listener);"); + return; + } + throw new RuntimeException("Unexpected apiType " + info.apiType + " for: " + jsonPath); + } +} + class Method extends Element { final TypeRef returnType; final List params = new ArrayList<>(); @@ -307,13 +385,20 @@ class Field extends Element { return type.toJava() + " " + name; } - void writeTo(List output, String offset) { - output.add(offset + "public " + toJava() + ";"); + void writeTo(List output, String offset, String access) { + output.add(offset + access + toJava() + ";"); + } + + void writeGetter(List output, String offset) { + output.add(offset + "public " + type.toJava() + " " + name + "() {"); + output.add(offset + " return this." + name + ";"); + output.add(offset + "}"); } } class Interface extends TypeDefinition { private final List methods = new ArrayList<>(); + private final List events = new ArrayList<>(); private static String header = "/**\n" + " * Copyright (c) Microsoft Corporation.\n" + " *\n" + @@ -336,14 +421,15 @@ class Interface extends TypeDefinition { super(null, jsonElement); JsonObject members = jsonElement.get("members").getAsJsonObject(); - for (Map.Entry m : members.entrySet()) - createMember(m.getValue().getAsJsonObject()); - } - - private void createMember(JsonObject json) { - String kind = json.get("kind").getAsString(); - if ("method".equals(kind)) { - methods.add(new Method(this, json)); + for (Map.Entry m : members.entrySet()) { + JsonObject json = m.getValue().getAsJsonObject(); + String kind = json.get("kind").getAsString(); + if ("method".equals(kind)) { + methods.add(new Method(this, json)); + } + if ("event".equals(kind)) { + events.add(new Event(this, json)); + } } } @@ -354,6 +440,9 @@ class Interface extends TypeDefinition { output.add(""); output.add("public interface " + jsonName + " {"); super.writeTo(output, offset + " "); + for (Event e : events) { + e.writeTo(output, offset + " "); + } for (Method m : methods) { m.writeTo(output, offset + " "); } @@ -381,11 +470,20 @@ class NestedClass extends TypeDefinition { output.add(offset + access + "class " + name + " {"); String bodyOffset = offset + " "; super.writeTo(output, bodyOffset); + + boolean isReturnType = parent.parent instanceof Method; + String fieldAccess = isReturnType ? "private " : "public "; for (Field f : fields) { - f.writeTo(output, bodyOffset); + f.writeTo(output, bodyOffset, fieldAccess); } output.add(""); - writeBuilderMethods(output, bodyOffset); + if (isReturnType) { + for (Field f : fields) { + f.writeGetter(output, bodyOffset); + } + } else { + writeBuilderMethods(output, bodyOffset); + } output.add(offset + "}"); } diff --git a/lib/src/main/java/com/microsoft/playwright/ConsoleMessage.java b/lib/src/main/java/com/microsoft/playwright/ConsoleMessage.java index 0842c11d..8bc9a61d 100644 --- a/lib/src/main/java/com/microsoft/playwright/ConsoleMessage.java +++ b/lib/src/main/java/com/microsoft/playwright/ConsoleMessage.java @@ -21,21 +21,18 @@ import java.util.function.BiConsumer; public interface ConsoleMessage { class Location { - public String url; - public int lineNumber; - public int columnNumber; + private String url; + private int lineNumber; + private int columnNumber; - public Location withUrl(String url) { - this.url = url; - return this; + public String url() { + return this.url; } - public Location withLineNumber(int lineNumber) { - this.lineNumber = lineNumber; - return this; + public int lineNumber() { + return this.lineNumber; } - public Location withColumnNumber(int columnNumber) { - this.columnNumber = columnNumber; - return this; + public int columnNumber() { + return this.columnNumber; } } List args(); diff --git a/lib/src/main/java/com/microsoft/playwright/Page.java b/lib/src/main/java/com/microsoft/playwright/Page.java index f2693d04..cc55840e 100644 --- a/lib/src/main/java/com/microsoft/playwright/Page.java +++ b/lib/src/main/java/com/microsoft/playwright/Page.java @@ -664,16 +664,14 @@ public interface Page { } } class PageViewportSize { - public int width; - public int height; + private int width; + private int height; - public PageViewportSize withWidth(int width) { - this.width = width; - return this; + public int width() { + return this.width; } - public PageViewportSize withHeight(int height) { - this.height = height; - return this; + public int height() { + return this.height; } } class WaitForFunctionOptions { @@ -746,6 +744,9 @@ public interface Page { return this; } } + void addConsoleListener(Listener listener); + void removeConsoleListener(Listener listener); + Deferred waitForPopup(); default void close() { close(null); } diff --git a/lib/src/main/java/com/microsoft/playwright/Request.java b/lib/src/main/java/com/microsoft/playwright/Request.java index 93934a69..6b27b26c 100644 --- a/lib/src/main/java/com/microsoft/playwright/Request.java +++ b/lib/src/main/java/com/microsoft/playwright/Request.java @@ -21,11 +21,10 @@ import java.util.function.BiConsumer; public interface Request { class RequestFailure { - public String errorText; + private String errorText; - public RequestFailure withErrorText(String errorText) { - this.errorText = errorText; - return this; + public String errorText() { + return this.errorText; } } class RequestPostDataJSON { diff --git a/lib/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java b/lib/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java index a2678e14..95e72ed1 100644 --- a/lib/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java +++ b/lib/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java @@ -21,6 +21,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.microsoft.playwright.Browser; import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.Deferred; import com.microsoft.playwright.Page; import java.util.List; diff --git a/lib/src/main/java/com/microsoft/playwright/impl/ChannelOwner.java b/lib/src/main/java/com/microsoft/playwright/impl/ChannelOwner.java index 2e9dea66..7412a4c2 100644 --- a/lib/src/main/java/com/microsoft/playwright/impl/ChannelOwner.java +++ b/lib/src/main/java/com/microsoft/playwright/impl/ChannelOwner.java @@ -66,7 +66,7 @@ class ChannelOwner { return connection.sendMessage(guid, method, params); } - void sendMessageNoWait(String method, JsonObject params) { + protected void sendMessageNoWait(String method, JsonObject params) { connection.sendMessageNoWait(guid, method, params); } diff --git a/lib/src/main/java/com/microsoft/playwright/impl/Connection.java b/lib/src/main/java/com/microsoft/playwright/impl/Connection.java index c54b06d3..68278b84 100644 --- a/lib/src/main/java/com/microsoft/playwright/impl/Connection.java +++ b/lib/src/main/java/com/microsoft/playwright/impl/Connection.java @@ -137,7 +137,6 @@ public class Connection { if (message.error == null) { callback.complete(message); } else { - System.out.println(message.error); callback.completeExceptionally(new RuntimeException(message.error.toString())); } return; diff --git a/lib/src/main/java/com/microsoft/playwright/impl/ConsoleMessageImpl.java b/lib/src/main/java/com/microsoft/playwright/impl/ConsoleMessageImpl.java index 3a772c83..1772f589 100644 --- a/lib/src/main/java/com/microsoft/playwright/impl/ConsoleMessageImpl.java +++ b/lib/src/main/java/com/microsoft/playwright/impl/ConsoleMessageImpl.java @@ -18,8 +18,12 @@ package com.microsoft.playwright.impl; import com.google.gson.Gson; import com.google.gson.JsonObject; +import com.microsoft.playwright.ConsoleMessage; +import com.microsoft.playwright.JSHandle; -public class ConsoleMessageImpl extends ChannelOwner { +import java.util.List; + +public class ConsoleMessageImpl extends ChannelOwner implements ConsoleMessage { public ConsoleMessageImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); } @@ -32,21 +36,9 @@ public class ConsoleMessageImpl extends ChannelOwner { return initializer.get("text").getAsString(); } -// args(): JSHandle[] { -// return this._initializer.args.map(JSHandle.from); -// } - - public static class Location { - String url; - int lineNumber; - int columnNumber; - - @Override - public String toString() { - return url + - ":" + lineNumber + - ":" + columnNumber; - } + @Override + public List args() { + return null; } public Location location() { diff --git a/lib/src/main/java/com/microsoft/playwright/impl/FrameImpl.java b/lib/src/main/java/com/microsoft/playwright/impl/FrameImpl.java index 643d41a0..6e63b337 100644 --- a/lib/src/main/java/com/microsoft/playwright/impl/FrameImpl.java +++ b/lib/src/main/java/com/microsoft/playwright/impl/FrameImpl.java @@ -133,8 +133,9 @@ public class FrameImpl extends ChannelOwner implements Frame { JsonObject params = new JsonObject(); params.addProperty("expression", expression); params.addProperty("world", "main"); - if (!isFunctionBody(expression)) + if (!isFunctionBody(expression)) { forceExpression = true; + } params.addProperty("isFunction", !forceExpression); params.add("arg", new Gson().toJsonTree(serializeArgument(arg))); JsonElement json = sendMessage("evaluateExpression", params); @@ -155,7 +156,15 @@ public class FrameImpl extends ChannelOwner implements Frame { @Override public Object evalOnSelector(String selector, String pageFunction, Object arg) { - return null; + JsonObject params = new JsonObject(); + params.addProperty("selector", selector); + params.addProperty("expression", pageFunction); + params.addProperty("isFunction", isFunctionBody(pageFunction)); + params.add("arg", new Gson().toJsonTree(serializeArgument(arg))); + JsonElement json = sendMessage("evalOnSelector", params); +// System.out.println("json = " + new Gson().toJson(json)); + SerializedValue value = new Gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class); + return deserialize(value); } @Override diff --git a/lib/src/main/java/com/microsoft/playwright/impl/PageImpl.java b/lib/src/main/java/com/microsoft/playwright/impl/PageImpl.java index cc0e31ae..432acfa6 100644 --- a/lib/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/lib/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -24,6 +24,7 @@ import com.microsoft.playwright.*; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.Future; import java.util.function.BiConsumer; import java.util.function.Supplier; @@ -31,7 +32,7 @@ import java.util.function.Supplier; public class PageImpl extends ChannelOwner implements Page { private final FrameImpl mainFrame; private final List dialogHandlers = new ArrayList<>(); - private final List consoleListeners = new ArrayList<>(); + private final List> consoleListeners = new ArrayList<>(); PageImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); @@ -39,7 +40,18 @@ public class PageImpl extends ChannelOwner implements Page { mainFrame.page = this; } - public Supplier waitForPopup() { + @Override + public void addConsoleListener(Listener listener) { + consoleListeners.add(listener); + } + + @Override + public void removeConsoleListener(Listener listener) { + consoleListeners.remove(listener); + } + + @Override + public Deferred waitForPopup() { Supplier popupSupplier = waitForProtocolEvent("popup"); return () -> { JsonObject params = popupSupplier.get(); @@ -64,31 +76,21 @@ public class PageImpl extends ChannelOwner implements Page { if ("dialog".equals(event)) { String guid = params.getAsJsonObject("dialog").get("guid").getAsString(); DialogImpl dialog = connection.getExistingObject(guid); - for (DialogHandler handler: new ArrayList<>(dialogHandlers)) + for (DialogHandler handler: new ArrayList<>(dialogHandlers)) { handler.handle(dialog); + } // If no action taken dismiss dialog to not hang. if (!dialog.isHandled()) dialog.dismiss(); } else if ("console".equals(event)) { String guid = params.getAsJsonObject("message").get("guid").getAsString(); ConsoleMessageImpl message = connection.getExistingObject(guid); - for (ConsoleListener listener: new ArrayList<>(consoleListeners)) + for (Listener listener: new ArrayList<>(consoleListeners)) { listener.handle(message); + } } } - public interface ConsoleListener { - void handle(ConsoleMessageImpl m); - } - - public void addConsoleListener(ConsoleListener listener) { - consoleListeners.add(listener); - } - - public void removeConsoleListener(ConsoleListener listener) { - consoleListeners.remove(listener); - } - public T evalTyped(String expression) { return mainFrame.evalTyped(expression); } @@ -100,22 +102,22 @@ public class PageImpl extends ChannelOwner implements Page { @Override public ElementHandle querySelector(String selector) { - return null; + return mainFrame.querySelector(selector); } @Override public List querySelectorAll(String selector) { - return null; + return mainFrame.querySelectorAll(selector); } @Override public Object evalOnSelector(String selector, String pageFunction, Object arg) { - return null; + return mainFrame.evalOnSelector(selector, pageFunction, arg); } @Override public Object evalOnSelectorAll(String selector, String pageFunction, Object arg) { - return null; + return mainFrame.evalOnSelectorAll(selector, pageFunction, arg); } @Override diff --git a/lib/src/test/java/com/microsoft/playwright/TestClick.java b/lib/src/test/java/com/microsoft/playwright/TestClick.java index 344315b1..38ccdd1a 100644 --- a/lib/src/test/java/com/microsoft/playwright/TestClick.java +++ b/lib/src/test/java/com/microsoft/playwright/TestClick.java @@ -19,9 +19,11 @@ package com.microsoft.playwright; import org.junit.jupiter.api.*; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class TestClick { private static Playwright playwright; @@ -155,4 +157,55 @@ public class TestClick { " return textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);\n" + "}")); }; + + @Test + void should_click_offscreen_buttons() { + page.navigate(server.PREFIX + "/offscreenbuttons.html"); + List messages = new ArrayList<>(); + page.addConsoleListener(msg -> messages.add(msg.text())); + for (int i = 0; i < 11; ++i) { + // We might've scrolled to click a button - reset to (0, 0). + page.evaluate("() => window.scrollTo(0, 0)"); + page.click("#btn" + i); + } + assertEquals(Arrays.asList( + "button #0 clicked", + "button #1 clicked", + "button #2 clicked", + "button #3 clicked", + "button #4 clicked", + "button #5 clicked", + "button #6 clicked", + "button #7 clicked", + "button #8 clicked", + "button #9 clicked", + "button #10 clicked" + ), messages); + } + + @Test + void should_waitFor_visible_when_already_visible() { + page.navigate(server.PREFIX + "/input/button.html"); + page.click("button"); + assertEquals("Clicked", page.evaluate("result")); + } + + @Test + void should_not_wait_with_force() { + page.navigate(server.PREFIX + "/input/button.html"); + page.evalOnSelector("button", "b => b.style.display = 'none'"); + Exception exception = null; + try { + page.click("button", new Page.ClickOptions().withForce(true)); + } catch (RuntimeException e) { + exception = e; + } + assertNotNull(exception); + assertTrue(exception.getMessage().contains("Element is not visible")); + assertEquals("Was not clicked", page.evaluate("result")); + } + + // TODO: not supported in sync api + void should_waitFor_display_none_to_be_gone() { + } }