From 5e02d0ae4905ba5c798ac8cedd75bd5b4f19d328 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 21 Oct 2020 11:02:40 -0700 Subject: [PATCH] feat: ElementHandle basic functionality (#29) --- .../com/microsoft/playwright/tools/Types.java | 1 + .../microsoft/playwright/ElementHandle.java | 6 +- .../playwright/impl/ElementHandleImpl.java | 132 +++++++++++---- .../TestElementHandleWaitForElementState.java | 153 ++++++++++++++++++ 4 files changed, 262 insertions(+), 30 deletions(-) create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestElementHandleWaitForElementState.java diff --git a/api-generator/src/main/java/com/microsoft/playwright/tools/Types.java b/api-generator/src/main/java/com/microsoft/playwright/tools/Types.java index eb9cd1d4..0a2b5002 100644 --- a/api-generator/src/main/java/com/microsoft/playwright/tools/Types.java +++ b/api-generator/src/main/java/com/microsoft/playwright/tools/Types.java @@ -183,6 +183,7 @@ class Types { add("Page.waitForTimeout", "Promise", "Deferred", new Empty()); add("Frame.waitForFunction", "Promise", "Deferred", new Empty()); add("Page.waitForFunction", "Promise", "Deferred", new Empty()); + add("ElementHandle.waitForElementState", "Promise", "Deferred", new Empty()); // Custom options add("Page.pdf.options.margin.top", "string|number", "String"); diff --git a/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java b/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java index 89c1a774..91b5e3e6 100644 --- a/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java +++ b/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java @@ -434,10 +434,10 @@ public interface ElementHandle extends JSHandle { uncheck(null); } void uncheck(UncheckOptions options); - default void waitForElementState(ElementState state) { - waitForElementState(state, null); + default Deferred waitForElementState(ElementState state) { + return waitForElementState(state, null); } - void waitForElementState(ElementState state, WaitForElementStateOptions options); + Deferred waitForElementState(ElementState state, WaitForElementStateOptions options); default Deferred waitForSelector(String selector) { return waitForSelector(selector, null); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java index c843784d..70e120f0 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java @@ -20,17 +20,16 @@ import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.microsoft.playwright.Deferred; -import com.microsoft.playwright.ElementHandle; -import com.microsoft.playwright.Frame; +import com.microsoft.playwright.*; import java.util.ArrayList; import java.util.List; -import static com.microsoft.playwright.impl.Serialization.toProtocol; +import static com.microsoft.playwright.impl.Serialization.*; +import static com.microsoft.playwright.impl.Utils.isFunctionBody; -public class ElementHandleImpl extends JSHandleImpl implements ElementHandle { - public ElementHandleImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { +class ElementHandleImpl extends JSHandleImpl implements ElementHandle { + ElementHandleImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); } @@ -69,12 +68,26 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle { @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); + SerializedValue value = new Gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class); + return deserialize(value); } @Override public Object evalOnSelectorAll(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("evalOnSelectorAll", params); + SerializedValue value = new Gson().fromJson(json.getAsJsonObject().get("value"), SerializedValue.class); + return deserialize(value); } @Override @@ -99,12 +112,12 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle { JsonObject params = new Gson().toJsonTree(options).getAsJsonObject(); 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); @@ -124,12 +137,12 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle { JsonObject params = new Gson().toJsonTree(options).getAsJsonObject(); 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); @@ -137,47 +150,71 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle { @Override public void dispatchEvent(String type, Object eventInit) { - + JsonObject params = new JsonObject(); + params.addProperty("type", type); + params.add("eventInit", new Gson().toJsonTree(serializeArgument(eventInit))); + sendMessage("dispatchEvent", params).getAsJsonObject(); } @Override public void fill(String value, FillOptions options) { - + if (options == null) { + options = new FillOptions(); + } + JsonObject params = new Gson().toJsonTree(options).getAsJsonObject(); + params.addProperty("value", value); + sendMessage("fill", params); } @Override public void focus() { - + sendMessage("focus", new JsonObject()); } @Override public String getAttribute(String name) { - return null; + JsonObject params = new JsonObject(); + params.addProperty("name", name); + JsonObject json = sendMessage("getAttribute", params).getAsJsonObject(); + return json.has("value") ? json.get("value").getAsString() : null; } @Override public void hover(HoverOptions options) { - + JsonObject params = new Gson().toJsonTree(options).getAsJsonObject(); + params.remove("modifiers"); + if (options.modifiers != null) { + params.add("modifiers", Serialization.toProtocol(options.modifiers)); + } + sendMessage("hover", params); } @Override public String innerHTML() { - return null; + JsonObject json = sendMessage("innerHTML", new JsonObject()).getAsJsonObject(); + return json.get("value").getAsString(); } @Override public String innerText() { - return null; + JsonObject json = sendMessage("innerText", new JsonObject()).getAsJsonObject(); + return json.get("value").getAsString(); } @Override public Frame ownerFrame() { - return null; + JsonObject json = sendMessage("ownerFrame", new JsonObject()).getAsJsonObject(); + return connection.getExistingObject(json.getAsJsonObject("frame").get("guid").getAsString()); } @Override public void press(String key, PressOptions options) { - + if (options == null) { + options = new PressOptions(); + } + JsonObject params = new Gson().toJsonTree(options).getAsJsonObject(); + params.addProperty("key", key); + sendMessage("press", params); } @Override @@ -187,17 +224,26 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle { @Override public void scrollIntoViewIfNeeded(ScrollIntoViewIfNeededOptions options) { - + if (options == null) { + options = new ScrollIntoViewIfNeededOptions(); + } + JsonObject params = new Gson().toJsonTree(options).getAsJsonObject(); + sendMessage("scrollIntoViewIfNeeded", params); } @Override public List selectOption(String values, SelectOptionOptions options) { + // TODO: return null; } @Override public void selectText(SelectTextOptions options) { - + if (options == null) { + options = new SelectTextOptions(); + } + JsonObject params = new Gson().toJsonTree(options).getAsJsonObject(); + sendMessage("selectText", params); } @Override @@ -207,12 +253,18 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle { @Override public String textContent() { - return null; + JsonObject json = sendMessage("textContent", new JsonObject()).getAsJsonObject(); + return json.has("value") ? json.get("value").getAsString() : null; } @Override public void type(String text, TypeOptions options) { - + if (options == null) { + options = new TypeOptions(); + } + JsonObject params = new Gson().toJsonTree(options).getAsJsonObject(); + params.addProperty("text", text); + sendMessage("type", params); } @Override @@ -225,12 +277,38 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle { } @Override - public void waitForElementState(ElementState state, WaitForElementStateOptions options) { + public Deferred waitForElementState(ElementState state, WaitForElementStateOptions options) { + if (options == null) { + options = new WaitForElementStateOptions(); + } + JsonObject params = new Gson().toJsonTree(options).getAsJsonObject(); + params.addProperty("state", toProtocol(state)); + return toDeferred(sendMessageAsync("waitForElementState", params).apply(json -> null)); + } + private static String toProtocol(ElementState state) { + if (state == null) { + throw new IllegalArgumentException("State cannot by null"); + } + return state.toString().toLowerCase(); } @Override public Deferred waitForSelector(String selector, WaitForSelectorOptions options) { - return null; + if (options == null) { + options = new WaitForSelectorOptions(); + } + JsonObject params = new Gson().toJsonTree(options).getAsJsonObject(); + params.remove("state"); + params.addProperty("state", toProtocol(options.state)); + params.addProperty("selector", selector); + return toDeferred(sendMessageAsync("waitForElementState", params).apply(json -> null)); + } + + private static String toProtocol(WaitForSelectorOptions.State state) { + if (state == null) { + state = WaitForSelectorOptions.State.VISIBLE; + } + return state.toString().toLowerCase(); } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestElementHandleWaitForElementState.java b/playwright/src/test/java/com/microsoft/playwright/TestElementHandleWaitForElementState.java new file mode 100644 index 00000000..076f85be --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestElementHandleWaitForElementState.java @@ -0,0 +1,153 @@ +/** + * 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.Test; + +import static com.microsoft.playwright.ElementHandle.ElementState.*; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class TestElementHandleWaitForElementState extends TestBase { + + static void giveItAChanceToResolve(Page page) { + for (int i = 0; i < 5; i++) { + page.evaluate("() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))"); + } + } + + @Test + void shouldWaitForVisible() { + page.setContent("
content
"); + ElementHandle div = page.querySelector("div"); + Deferred promise = div.waitForElementState(VISIBLE); + giveItAChanceToResolve(page); + div.evaluate("div => div.style.display = 'block'"); + promise.get(); + } + + @Test + void shouldWaitForAlreadyVisible() { + page.setContent("
content
"); + ElementHandle div = page.querySelector("div"); + div.waitForElementState(VISIBLE); + } + + @Test + void shouldTimeoutWaitingForVisible() { + page.setContent("
content
"); + ElementHandle div = page.querySelector("div"); + Deferred result = div.waitForElementState(VISIBLE, new ElementHandle.WaitForElementStateOptions().withTimeout(1000)); + try { + result.get(); + fail("did not throw"); + } catch (RuntimeException e) { + assertTrue(e.getMessage().contains("Timeout 1000ms exceeded")); + } + } + + @Test + void shouldThrowWaitingForVisibleWhenDetached() { + page.setContent("
content
"); + ElementHandle div = page.querySelector("div"); + Deferred promise = div.waitForElementState(VISIBLE); + div.evaluate("div => div.remove()"); + try { + promise.get(); + fail("did not throw"); + } catch (RuntimeException e) { + assertTrue(e.getMessage().contains("Element is not attached to the DOM")); + } + } + + @Test + void shouldWaitForHidden() { + page.setContent("
content
"); + ElementHandle div = page.querySelector("div"); + Deferred promise = div.waitForElementState(HIDDEN); + giveItAChanceToResolve(page); + div.evaluate("div => div.style.display = 'none'"); + promise.get(); + } + + @Test + void shouldWaitForAlreadyHidden() { + page.setContent("
"); + ElementHandle div = page.querySelector("div"); + Deferred result = div.waitForElementState(HIDDEN); + result.get(); + } + + @Test + void shouldWaitForHiddenWhenDetached() { + page.setContent("
content
"); + ElementHandle div = page.querySelector("div"); + Deferred promise = div.waitForElementState(HIDDEN); + giveItAChanceToResolve(page); + div.evaluate("div => div.remove()"); + promise.get(); + } + + @Test + void shouldWaitForEnabledButton() { + page.setContent(""); + ElementHandle span = page.querySelector("text=Target"); + Deferred promise = span.waitForElementState(ENABLED); + giveItAChanceToResolve(page); + span.evaluate("span => span.parentElement.disabled = false"); + promise.get(); + } + + @Test + void shouldThrowWaitingForEnabledWhenDetached() { + page.setContent(""); + ElementHandle button = page.querySelector("button"); + Deferred promise = button.waitForElementState(ENABLED); + button.evaluate("button => button.remove()"); + try { + promise.get(); + fail("did not throw"); + } catch (RuntimeException e) { + assertTrue(e.getMessage().contains("Element is not attached to the DOM")); + } + } + + @Test + void shouldWaitForDisabledButton() { + page.setContent(""); + ElementHandle span = page.querySelector("text=Target"); + Deferred promise = span.waitForElementState(DISABLED); + giveItAChanceToResolve(page); + span.evaluate("span => span.parentElement.disabled = true"); + promise.get(); + } + + @Test + void shouldWaitForStablePosition() { + // TODO: test.fixme(browserName === "firefox" && platform === "linux"); + page.navigate(server.PREFIX + "/input/button.html"); + ElementHandle button = page.querySelector("button"); + page.evalOnSelector("button", "button => {\n" + + " button.style.transition = 'margin 10000ms linear 0s';\n" + + " button.style.marginLeft = '20000px';\n" + + "}"); + Deferred promise = button.waitForElementState(STABLE); + giveItAChanceToResolve(page); + button.evaluate("button => button.style.transition = ''"); + promise.get(); + } +}