From 3f81284a7a7a26cc446624dd96e26a92862c327a Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 21 Oct 2020 14:34:02 -0700 Subject: [PATCH] feat: ElementHandle.boundingBox (#31) --- .../playwright/tools/ApiGenerator.java | 9 ++ .../com/microsoft/playwright/tools/Types.java | 1 + .../microsoft/playwright/ElementHandle.java | 9 +- .../playwright/impl/ElementHandleImpl.java | 8 +- .../TestElementHandleBoundingBox.java | 146 ++++++++++++++++++ 5 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestElementHandleBoundingBox.java 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 1ae93b34..88e7bfdb 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 @@ -623,6 +623,15 @@ class Interface extends TypeDefinition { output.add(offset + "}"); break; } + case "ElementHandle": { + output.add(offset + "class BoundingBox {"); + output.add(offset + " public double x;"); + output.add(offset + " public double y;"); + output.add(offset + " public double width;"); + output.add(offset + " public double height;"); + output.add(offset + "}"); + break; + } default: return; } output.add(""); 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 0a2b5002..194fa1be 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 @@ -168,6 +168,7 @@ class Types { // Return structures add("ConsoleMessage.location", "Object", "Location"); + add("ElementHandle.boundingBox", "Promise", "BoundingBox", new Empty()); add("Page.waitForRequest", "Promise", "Deferred"); add("Page.waitForResponse", "Promise", "Deferred"); diff --git a/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java b/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java index 91b5e3e6..f974401c 100644 --- a/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java +++ b/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java @@ -19,6 +19,13 @@ package com.microsoft.playwright; import java.util.*; public interface ElementHandle extends JSHandle { + class BoundingBox { + public double x; + public double y; + public double width; + public double height; + } + enum ElementState { DISABLED, ENABLED, HIDDEN, STABLE, VISIBLE } class CheckOptions { public Boolean force; @@ -369,7 +376,7 @@ public interface ElementHandle extends JSHandle { return evalOnSelectorAll(selector, pageFunction, null); } Object evalOnSelectorAll(String selector, String pageFunction, Object arg); - Object boundingBox(); + BoundingBox boundingBox(); default void check() { check(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 11d7f04d..05bb2716 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java @@ -91,8 +91,12 @@ class ElementHandleImpl extends JSHandleImpl implements ElementHandle { } @Override - public Object boundingBox() { - return null; + public BoundingBox boundingBox() { + JsonObject json = sendMessage("boundingBox", new JsonObject()).getAsJsonObject(); + if (!json.has("value")) { + return null; + } + return new Gson().fromJson(json.get("value"), BoundingBox.class); } @Override diff --git a/playwright/src/test/java/com/microsoft/playwright/TestElementHandleBoundingBox.java b/playwright/src/test/java/com/microsoft/playwright/TestElementHandleBoundingBox.java new file mode 100644 index 00000000..ddc0e803 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestElementHandleBoundingBox.java @@ -0,0 +1,146 @@ +/** + * 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 java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestElementHandleBoundingBox extends TestBase { + @Test + void shouldWork() { + // TODO: test.fail(browserName === "firefox" && headful); + page.setViewportSize(500, 500); + page.navigate(server.PREFIX + "/grid.html"); + ElementHandle elementHandle = page.querySelector(".box:nth-of-type(13)"); + ElementHandle.BoundingBox box = elementHandle.boundingBox(); + assertEquals(100, box.x); + assertEquals(50, box.y); + assertEquals(50, box.width); + assertEquals(50, box.height); + } + + @Test + void shouldHandleNestedFrames() { + page.setViewportSize(500, 500); + page.navigate(server.PREFIX + "/frames/nested-frames.html"); + Frame nestedFrame = page.frameByName("dos"); + assertNotNull(nestedFrame); + ElementHandle elementHandle = nestedFrame.querySelector("div"); + ElementHandle.BoundingBox box = elementHandle.boundingBox(); + assertEquals(24, box.x); + assertEquals(224, box.y); + assertEquals(268, box.width); + assertEquals(18, box.height); + } + + @Test + void shouldReturnNullForInvisibleElements() { + page.setContent("
hi
"); + ElementHandle element = page.querySelector("div"); + assertNull(element.boundingBox()); + } + + @Test + void shouldForceALayout() { + page.setViewportSize(500, 500); + page.setContent("
hello
"); + ElementHandle elementHandle = page.querySelector("div"); + page.evaluate("element => element.style.height = '200px'", elementHandle); + ElementHandle.BoundingBox box = elementHandle.boundingBox(); + assertEquals(box.x, 8); + assertEquals(box.y, 8); + assertEquals(box.width, 100); + assertEquals(box.height, 200); + } + + @Test + void shouldWorkWithSVGNodes() { + page.setContent("\n" + + "\n" + + ""); + ElementHandle element = page.querySelector("#therect"); + ElementHandle.BoundingBox pwBoundingBox = element.boundingBox(); + Map webBoundingBox = (Map) page.evaluate("e => {\n" + + " const rect = e.getBoundingClientRect();\n" + + " return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };\n" + + "}", element); + assertEquals(webBoundingBox.get("x").doubleValue(), pwBoundingBox.x); + assertEquals(webBoundingBox.get("y").doubleValue(), pwBoundingBox.y); + assertEquals(webBoundingBox.get("width").doubleValue(), pwBoundingBox.width); + assertEquals(webBoundingBox.get("height").doubleValue(), pwBoundingBox.height); + } + + @Test + void shouldWorkWithPageScale() { + // TODO: test.skip(browserName === "firefox"); + BrowserContext context = browser.newContext(new Browser.NewContextOptions() + .withViewport(400, 400).withIsMobile(true)); + Page page = context.newPage(); + page.navigate(server.PREFIX + "/input/button.html"); + ElementHandle button = page.querySelector("button"); + button.evaluate("button => {\n" + + " document.body.style.margin = '0';\n" + + " button.style.borderWidth = '0';\n" + + " button.style.width = '200px';\n" + + " button.style.height = '20px';\n" + + " button.style.marginLeft = '17px';\n" + + " button.style.marginTop = '23px';\n" + + "}"); + ElementHandle.BoundingBox box = button.boundingBox(); + assertEquals(17 * 100, round(box.x * 100)); + assertEquals(23 * 100, round(box.y * 100)); + assertEquals(200 * 100, round(box.width * 100)); + assertEquals(20 * 100, round(box.height * 100)); + context.close(); + } + + static long round(Object o) { + if (o instanceof Integer) { + return ((Integer) o).longValue(); + } + return Math.round((Double) o); + } + + @Test + void shouldWorkWhenInlineBoxChildIsOutsideOfViewport() { + page.setContent("\n" + + "woofdoggo"); + ElementHandle handle = page.querySelector("span"); + ElementHandle.BoundingBox pwBoundingBox = handle.boundingBox(); + Map webBoundingBox = (Map) handle.evaluate("e => {\n" + + " const rect = e.getBoundingClientRect();\n" + + " return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };\n" + + "}"); + + assertEquals(round(webBoundingBox.get("x")), round(pwBoundingBox.x)); + assertEquals(round(webBoundingBox.get("y")), round(pwBoundingBox.y)); + assertEquals(round(webBoundingBox.get("width")), round(pwBoundingBox.width)); + assertEquals(round(webBoundingBox.get("height")), round(pwBoundingBox.height)); + } +}