1
0
mirror of synced 2026-08-05 15:06:54 +00:00

feat: implement ElementHandle.click

This commit is contained in:
Yury Semikhatsky
2020-10-05 14:25:29 -07:00
parent d58eaa5d8d
commit 1f44173b8c
15 changed files with 550 additions and 71 deletions
@@ -464,6 +464,8 @@ class Interface extends TypeDefinition {
"\n" +
"package com.microsoft.playwright;\n";
private static Set<String> allowedBaseInterfaces = new HashSet<>(Arrays.asList("Browser", "JSHandle"));
Interface(JsonObject jsonElement) {
super(null, jsonElement);
@@ -489,7 +491,16 @@ class Interface extends TypeDefinition {
output.add("import java.util.*;");
output.add("import java.util.function.BiConsumer;");
output.add("");
output.add("public interface " + jsonName + " {");
String implementsClause = "";
if (jsonElement.getAsJsonObject().has("extends")) {
String base = jsonElement.getAsJsonObject().get("extends").getAsString();
if (allowedBaseInterfaces.contains(base)) {
implementsClause = " extends " + base;
}
}
output.add("public interface " + jsonName + implementsClause + " {");
offset = " ";
writeSharedTypes(output, offset);
super.writeTo(output, offset);
@@ -19,7 +19,7 @@ package com.microsoft.playwright;
import java.util.*;
import java.util.function.BiConsumer;
public interface ChromiumBrowser {
public interface ChromiumBrowser extends Browser {
class StartTracingOptions {
public String path;
public Boolean screenshots;
@@ -19,7 +19,7 @@ package com.microsoft.playwright;
import java.util.*;
import java.util.function.BiConsumer;
public interface ElementHandle {
public interface ElementHandle extends JSHandle {
enum ElementState { DISABLED, ENABLED, HIDDEN, STABLE, VISIBLE }
class CheckOptions {
public Boolean force;
@@ -19,6 +19,6 @@ package com.microsoft.playwright;
import java.util.*;
import java.util.function.BiConsumer;
public interface FirefoxBrowser {
public interface FirefoxBrowser extends Browser {
}
@@ -19,6 +19,6 @@ package com.microsoft.playwright;
import java.util.*;
import java.util.function.BiConsumer;
public interface WebKitBrowser {
public interface WebKitBrowser extends Browser {
}
@@ -198,11 +198,14 @@ public class Connection {
case "Electron":
// result = new Playwright(parent, type, guid, initializer);
break;
case "ElementHandle":
result = new ElementHandleImpl(parent, type, guid, initializer);
break;
case "Frame":
result = new FrameImpl(parent, type, guid, initializer);
break;
case "JSHandle":
// result = new JSHandle(parent, type, guid, initializer);
result = new JSHandleImpl(parent, type, guid, initializer);
break;
case "Page":
result = new PageImpl(parent, type, guid, initializer);
@@ -0,0 +1,214 @@
/**
* Copyright (c) Microsoft Corporation.
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.ElementHandle;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.JSHandle;
import java.util.List;
import static com.microsoft.playwright.impl.Serialization.toProtocol;
public class ElementHandleImpl extends JSHandleImpl implements ElementHandle {
public ElementHandleImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@Override
public ElementHandle asElement() {
return this;
}
@Override
public ElementHandle querySelector(String selector) {
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
JsonElement json = sendMessage("querySelector", params);
JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
if (element == null) {
return null;
}
return connection.getExistingObject(element.get("guid").getAsString());
}
@Override
public List<ElementHandle> querySelectorAll(String selector) {
return null;
}
@Override
public Object evalOnSelector(String selector, String pageFunction, Object arg) {
return null;
}
@Override
public Object evalOnSelectorAll(String selector, String pageFunction, Object arg) {
return null;
}
@Override
public Object boundingBox() {
return null;
}
@Override
public void check(CheckOptions options) {
}
@Override
public void click(ClickOptions options) {
if (options == null) {
options = new ClickOptions();
}
JsonObject params = new Gson().toJsonTree(options).getAsJsonObject();
params.remove("button");
if (options.button != null) {
params.addProperty("button", toProtocol(options.button));
}
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", toProtocol(options.modifiers));
}
sendMessage("click", params);
}
@Override
public Frame contentFrame() {
return null;
}
@Override
public void dblclick(DblclickOptions options) {
if (options == null) {
options = new DblclickOptions();
}
JsonObject params = new Gson().toJsonTree(options).getAsJsonObject();
params.remove("button");
if (options.button != null) {
params.addProperty("button", toProtocol(options.button));
}
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", toProtocol(options.modifiers));
}
sendMessage("dblclick", params);
}
@Override
public void dispatchEvent(String type, Object eventInit) {
}
@Override
public void fill(String value, FillOptions options) {
}
@Override
public void focus() {
}
@Override
public String getAttribute(String name) {
return null;
}
@Override
public void hover(HoverOptions options) {
}
@Override
public String innerHTML() {
return null;
}
@Override
public String innerText() {
return null;
}
@Override
public Frame ownerFrame() {
return null;
}
@Override
public void press(String key, PressOptions options) {
}
@Override
public byte[] screenshot(ScreenshotOptions options) {
return new byte[0];
}
@Override
public void scrollIntoViewIfNeeded(ScrollIntoViewIfNeededOptions options) {
}
@Override
public List<String> selectOption(String values, SelectOptionOptions options) {
return null;
}
@Override
public void selectText(SelectTextOptions options) {
}
@Override
public void setInputFiles(String files, SetInputFilesOptions options) {
}
@Override
public String textContent() {
return null;
}
@Override
public void type(String text, TypeOptions options) {
}
@Override
public void uncheck(UncheckOptions options) {
}
@Override
public void waitForElementState(ElementState state, WaitForElementStateOptions options) {
}
@Override
public ElementHandle waitForSelector(String selector, WaitForSelectorOptions options) {
return null;
}
}
@@ -17,7 +17,6 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
@@ -70,7 +69,14 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public ElementHandle querySelector(String selector) {
return null;
JsonObject params = new JsonObject();
params.addProperty("selector", selector);
JsonElement json = sendMessage("querySelector", params);
JsonObject element = json.getAsJsonObject().getAsJsonObject("element");
if (element == null) {
return null;
}
return connection.getExistingObject(element.get("guid").getAsString());
}
@Override
@@ -116,32 +122,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
private static String toProtocol(Mouse.Button button) {
switch (button) {
case LEFT: return "left";
case RIGHT: return "right";
case MIDDLE: return "middle";
default: throw new RuntimeException("Unexpected value: " + button);
}
}
private static JsonArray toProtocol(Set<Keyboard.Modifier> modifiers) {
JsonArray result = new JsonArray();
if (modifiers.contains(Keyboard.Modifier.ALT)) {
result.add("Alt");
}
if (modifiers.contains(Keyboard.Modifier.CONTROL)) {
result.add("Control");
}
if (modifiers.contains(Keyboard.Modifier.META)) {
result.add("Meta");
}
if (modifiers.contains(Keyboard.Modifier.SHIFT)) {
result.add("Shift");
}
return result;
}
@Override
public void click(String selector, ClickOptions options) {
if (options == null) {
@@ -152,12 +132,12 @@ public class FrameImpl extends ChannelOwner implements Frame {
params.remove("button");
if (options.button != null) {
params.addProperty("button", toProtocol(options.button));
params.addProperty("button", Serialization.toProtocol(options.button));
}
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", toProtocol(options.modifiers));
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
sendMessage("click", params);
@@ -178,12 +158,12 @@ public class FrameImpl extends ChannelOwner implements Frame {
params.remove("button");
if (options.button != null) {
params.addProperty("button", toProtocol(options.button));
params.addProperty("button", Serialization.toProtocol(options.button));
}
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", toProtocol(options.modifiers));
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
sendMessage("dblclick", params);
@@ -201,7 +181,13 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override
public JSHandle evaluateHandle(String pageFunction, Object arg) {
return null;
JsonObject params = new JsonObject();
params.addProperty("expression", pageFunction);
params.addProperty("world", "main");
params.addProperty("isFunction", isFunctionBody(pageFunction));
params.add("arg", new Gson().toJsonTree(serializeArgument(arg)));
JsonElement json = sendMessage("evaluateExpressionHandle", params);
return connection.getExistingObject(json.getAsJsonObject().getAsJsonObject("handle").get("guid").getAsString());
}
@Override
@@ -0,0 +1,59 @@
/**
* Copyright (c) Microsoft Corporation.
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.ElementHandle;
import com.microsoft.playwright.JSHandle;
import java.util.Map;
public class JSHandleImpl extends ChannelOwner implements JSHandle {
public JSHandleImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
@Override
public ElementHandle asElement() {
return null;
}
@Override
public Object evaluate(String pageFunction, Object arg) {
return null;
}
@Override
public JSHandle evaluateHandle(String pageFunction, Object arg) {
return null;
}
@Override
public Map<String, JSHandle> getProperties() {
return null;
}
@Override
public JSHandle getProperty(String propertyName) {
return null;
}
@Override
public Object jsonValue() {
return null;
}
}
@@ -20,6 +20,7 @@ import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Mouse;
import static com.microsoft.playwright.impl.Serialization.toProtocol;
import static com.microsoft.playwright.impl.Utils.convertViaJson;
class MouseImpl implements Mouse {
@@ -29,15 +30,6 @@ class MouseImpl implements Mouse {
this.page = page;
}
private static String toProtocol(Mouse.Button button) {
switch (button) {
case LEFT: return "left";
case RIGHT: return "right";
case MIDDLE: return "middle";
default: throw new RuntimeException("Unexpected value: " + button);
}
}
@Override
public void click(int x, int y, ClickOptions options) {
if (options == null) {
@@ -106,7 +106,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public void close(CloseOptions options) {
JsonObject params = options == null ? new JsonObject() : (JsonObject) new Gson().toJsonTree(options);
JsonObject params = options == null ? new JsonObject() : new Gson().toJsonTree(options).getAsJsonObject();
sendMessage("close", params);
if (ownedContext != null) {
ownedContext.close();
@@ -205,7 +205,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override
public JSHandle evaluateHandle(String pageFunction, Object arg) {
return null;
return mainFrame.evaluateHandle(pageFunction, arg);
}
@Override
@@ -22,6 +22,7 @@ class Binary {
}
class Channel {
String guid;
}
class Metadata{
@@ -17,13 +17,14 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.microsoft.playwright.JSHandle;
import com.microsoft.playwright.Keyboard;
import com.microsoft.playwright.Mouse;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
class Serialization {
static SerializedError serializeError(Throwable e) {
@@ -38,8 +39,16 @@ class Serialization {
return result;
}
private static SerializedValue serializeValue(Object value) {
private static SerializedValue serializeValue(Object value, List<JSHandleImpl> handles, int depth) {
if (depth > 100) {
throw new RuntimeException("Maximum argument depth exceeded");
}
SerializedValue result = new SerializedValue();
if (value instanceof JSHandleImpl) {
result.h = handles.size();
handles.add((JSHandleImpl) value);
return result;
}
if (value == null)
result.v = "undefined";
else if (value instanceof Double) {
@@ -64,7 +73,7 @@ class Serialization {
else if (value instanceof List) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (List) value)
list.add(serializeValue(o));
list.add(serializeValue(o, handles, depth + 1));
result.a = list.toArray(new SerializedValue[0]);
} else if (value instanceof Map) {
List<SerializedValue.O> list = new ArrayList<>();
@@ -72,7 +81,7 @@ class Serialization {
for (Map.Entry<String, Object> e : map.entrySet()) {
SerializedValue.O o = new SerializedValue.O();
o.k = e.getKey();
o.v = serializeValue(e.getValue());
o.v = serializeValue(e.getValue(), handles, depth + 1);
list.add(o);
}
result.o = list.toArray(new SerializedValue.O[0]);
@@ -83,8 +92,15 @@ class Serialization {
static SerializedArgument serializeArgument(Object arg) {
SerializedArgument result = new SerializedArgument();
result.value = serializeValue(arg);
result.handles = new Channel[0];
List<JSHandleImpl> handles = new ArrayList<>();
result.value = serializeValue(arg, handles, 0);
result.handles = new Channel[handles.size()];
int i = 0;
for (JSHandleImpl handle : handles) {
result.handles[i] = new Channel();
result.handles[i].guid = handle.guid;
++i;
}
return result;
}
@@ -129,4 +145,30 @@ class Serialization {
}
throw new RuntimeException("Unexpected result: " + new Gson().toJson(value));
}
static String toProtocol(Mouse.Button button) {
switch (button) {
case LEFT: return "left";
case RIGHT: return "right";
case MIDDLE: return "middle";
default: throw new RuntimeException("Unexpected value: " + button);
}
}
static JsonArray toProtocol(Set<Keyboard.Modifier> modifiers) {
JsonArray result = new JsonArray();
if (modifiers.contains(Keyboard.Modifier.ALT)) {
result.add("Alt");
}
if (modifiers.contains(Keyboard.Modifier.CONTROL)) {
result.add("Control");
}
if (modifiers.contains(Keyboard.Modifier.META)) {
result.add("Meta");
}
if (modifiers.contains(Keyboard.Modifier.SHIFT)) {
result.add("Shift");
}
return result;
}
}
@@ -31,16 +31,21 @@ import static org.junit.jupiter.api.Assertions.*;
public class TestClick {
private static Playwright playwright;
private static Server server;
private Browser browser;
private boolean isChromium;
private boolean isWebKit;
private boolean headful;
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 createPlaywright() {
static void launchBrowser() {
playwright = Playwright.create();
BrowserType.LaunchOptions options = new BrowserType.LaunchOptions();
browser = playwright.chromium().launch(options);
isChromium = true;
isWebKit = false;
headful = false;
}
@BeforeAll
@@ -50,25 +55,22 @@ public class TestClick {
@AfterAll
static void stopServer() throws IOException {
browser.close();
server.stop();
server = null;
}
@BeforeEach
void setUp() {
// BrowserType.LaunchOptions options = new BrowserType.LaunchOptions().withHeadless(false).withSlowMo(1000);
BrowserType.LaunchOptions options = new BrowserType.LaunchOptions();
browser = playwright.chromium().launch(options);
isChromium = true;
isWebKit = false;
headful = false;
context = browser.newContext();
page = context.newPage();
}
@AfterEach
void tearDown() {
browser.close();
context.close();
context = null;
page = null;
}
@Test
@@ -0,0 +1,169 @@
/**
* Copyright (c) Microsoft Corporation.
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.junit.jupiter.api.Assertions.*;
public class TestElementHandleClick {
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() {
context = browser.newContext();
page = context.newPage();
}
@AfterEach
void tearDown() {
context.close();
context = null;
page = null;
}
@Test
void should_work() {
page.navigate(server.PREFIX + "/input/button.html");
ElementHandle button = page.querySelector("button");
button.click();
assertEquals("Clicked", page.evaluate("() => window['result']"));
}
@Test
void should_work_with_Node_removed() {
page.navigate(server.PREFIX + "/input/button.html");
page.evaluate("() => delete window['Node']");
ElementHandle button = page.querySelector("button");
button.click();
assertEquals("Clicked", page.evaluate("() => window['result']"));
}
@Test
void should_work_for_Shadow_DOM_v1() {
page.navigate(server.PREFIX + "/shadow.html");
ElementHandle buttonHandle = page.evaluateHandle("() => window['button']").asElement();
buttonHandle.click();
assertEquals(true, page.evaluate("clicked"));
}
@Test
void should_work_for_TextNodes() {
page.navigate(server.PREFIX + "/input/button.html");
ElementHandle buttonTextNode = page.evaluateHandle("() => document.querySelector('button').firstChild").asElement();
buttonTextNode.click();
assertEquals("Clicked", page.evaluate("() => window['result']"));
}
@Test
void should_throw_for_detached_nodes() {
page.navigate(server.PREFIX + "/input/button.html");
ElementHandle button = page.querySelector("button");
page.evaluate("button => button.remove()", button);
try {
button.click();
fail("click should throw");
} catch (RuntimeException e) {
assertTrue(e.getMessage().contains("Element is not attached to the DOM"));
}
}
@Test
void should_throw_for_hidden_nodes_with_force() {
page.navigate(server.PREFIX + "/input/button.html");
ElementHandle button = page.querySelector("button");
page.evaluate("button => button.style.display = 'none'", button);
try {
button.click(new ElementHandle.ClickOptions().withForce(true));
fail("click should throw");
} catch (RuntimeException e) {
assertTrue(e.getMessage().contains("Element is not visible"));
}
}
@Test
void should_throw_for_recursively_hidden_nodes_with_force() {
page.navigate(server.PREFIX + "/input/button.html");
ElementHandle button = page.querySelector("button");
page.evaluate("button => button.parentElement.style.display = 'none'", button);
try {
button.click(new ElementHandle.ClickOptions().withForce(true));
fail("click should throw");
} catch (RuntimeException e) {
assertTrue(e.getMessage().contains("Element is not visible"));
}
}
@Test
void should_throw_for__br__elements_with_force() {
page.setContent("hello<br>goodbye");
ElementHandle br = page.querySelector("br");
try {
br.click(new ElementHandle.ClickOptions().withForce(true));
fail("click should throw");
} catch (RuntimeException e) {
assertTrue(e.getMessage().contains("Element is outside of the viewport"));
}
}
@Test
void should_double_click_the_button() {
page.navigate(server.PREFIX + "/input/button.html");
page.evaluate("() => {\n" +
" window['double'] = false;\n" +
" const button = document.querySelector('button');\n" +
" button.addEventListener('dblclick', event => {\n" +
" window['double'] = true;\n" +
" });\n" +
"}");
ElementHandle button = page.querySelector("button");
button.dblclick();
assertEquals(true, page.evaluate("double"));
assertEquals("Clicked", page.evaluate("result"));
}
}