diff --git a/src/main/java/org/brit/driver/PlaywrightiumDriver.java b/src/main/java/org/brit/driver/PlaywrightiumDriver.java index 9315090..8bc35a8 100644 --- a/src/main/java/org/brit/driver/PlaywrightiumDriver.java +++ b/src/main/java/org/brit/driver/PlaywrightiumDriver.java @@ -1,6 +1,5 @@ package org.brit.driver; -import com.codeborne.selenide.impl.WebElementSource; import com.microsoft.playwright.*; import com.microsoft.playwright.options.AriaRole; import com.microsoft.playwright.options.Geolocation; @@ -9,6 +8,7 @@ import lombok.Getter; import lombok.SneakyThrows; import org.apache.commons.text.CaseUtils; +import org.brit.driver.adapters.JsExecutionAdapter; import org.brit.element.PlaywrightWebElement; import org.brit.emulation.Device; import org.brit.locators.ArialSearchOptions; @@ -44,7 +44,7 @@ public class PlaywrightiumDriver extends RemoteWebDriver implements TakesScreens private Frame mainFrameCopy = null; private PlaywrightiumOptions options; - + private final static JsExecutionAdapter jsExecutionAdapter = new JsExecutionAdapter(); @SneakyThrows public PlaywrightiumDriver() { @@ -632,8 +632,7 @@ public WebDriver defaultContent() { @Override public WebElement activeElement() { - ElementHandle element = page.evaluateHandle("() => document.activeElement").asElement(); - return getPlaywrightElement(element); + return (WebElement) PlaywrightiumDriver.this.executeScript("() => document.activeElement"); } @Override @@ -876,107 +875,12 @@ public void perform(Collection actions) { @Override public Object executeScript(String script, Object... args) { - script = script.trim().startsWith("return") ? script.replaceFirst("return", "") : script; - if (args.length > 0) { - var arguments = transformArguments(args); - JSHandle jsHandle = page.evaluateHandle("(arguments) => " + script, arguments); - if (Boolean.parseBoolean(jsHandle.evaluate("node => node instanceof HTMLCollection").toString())) { - int length = (int) page.evaluate("node => node.length", jsHandle); - List list = new ArrayList<>(); - for (int i = 0; i < length; i++) { - list.add(getPlaywrightElement(page.evaluateHandle("node => node.item(%s)".formatted(i), jsHandle).asElement())); - } - return list; - } else if (Boolean.parseBoolean(jsHandle.evaluate("node => node instanceof HTMLElement").toString())) { - return getPlaywrightElement(jsHandle.asElement()); - } else if (Boolean.parseBoolean(jsHandle.evaluate("node => node instanceof Array").toString())) { - int length = (int) page.evaluate("node => node.length", jsHandle); - List list = new ArrayList<>(); - for (int i = 0; i < length; i++) { - Object evaluate = page.evaluate("node => node[%s]".formatted(i), jsHandle); - list.add(evaluate != null ? evaluate.toString() : null); - } - return list; - } else if (Boolean.parseBoolean(jsHandle.evaluate("node => node instanceof Object").toString())) { - return jsHandle.jsonValue(); - } else { - return jsHandle.toString(); - } - } else { - return page.evaluate(script); - } - // return Map.of(); - } - - private List transformArguments(Object... args) { - List result = new LinkedList<>(); - for (Object arg : args) { - ElementHandle elementHandle = null; - if (arg instanceof WebElementSource) { - elementHandle = ((PlaywrightWebElement) (((WebElementSource) arg).getWebElement())).getLocator().elementHandle(); - } else if (arg instanceof WrapsElement) { - elementHandle = ((PlaywrightWebElement) (((WrapsElement) arg).getWrappedElement())).getElementHandle(); - } else if (arg instanceof WebElement) { - Locator locator = ((PlaywrightWebElement) arg).getLocator(); - elementHandle = locator.elementHandle(); - } else if (arg instanceof Collection) { - ArrayList arrayList = new ArrayList<>((Collection) arg); - if (!arrayList.isEmpty() && arrayList.get(0) instanceof WebElement) { - List list = arrayList.stream().map(e -> { - Locator locator = ((PlaywrightWebElement) e).getLocator(); - return locator.elementHandle(); - }).toList(); - result.add(list); - continue; - } - } - if (elementHandle != null) { - result.add(elementHandle); - } else { - if (arg instanceof Collection) { - ArrayList objects = new ArrayList<>((Collection) arg); - result.add(objects); - } else { - result.add(arg); - } - } - } - return result; + return jsExecutionAdapter.executeScript(page, script, args); } @Override public Object executeAsyncScript(String script, Object... args) { - if (args.length > 0) { - return page.evaluate("async () => {" + script.replace("return", "") + "}", transformArguments(args)); - } else { - return page.evaluate("async () => {" + script.replace("return", "") + "}"); - } - } - - - public PlaywrightWebElement getPlaywrightElement(ElementHandle node) { - String string = page.evaluate(""" - node => - { - names = []; - do { - index = 0; - cursorElement = node; - while (cursorElement !== null) { - ++index; - cursorElement = cursorElement.previousElementSibling; - } - names.unshift(node.tagName + ":nth-child(" + index + ")"); - node = node.parentElement; - } while (node !== null); - - return names.join(" > "); - } - """, node).toString(); - - Locator locator = page.locator(string); - return new PlaywrightWebElement(locator); - + return jsExecutionAdapter.executeAsyncScript(page, script, args); } public PlaywrightiumOptions getOptions() { diff --git a/src/main/java/org/brit/driver/adapters/JsExecutionAdapter.java b/src/main/java/org/brit/driver/adapters/JsExecutionAdapter.java new file mode 100644 index 0000000..18919c3 --- /dev/null +++ b/src/main/java/org/brit/driver/adapters/JsExecutionAdapter.java @@ -0,0 +1,141 @@ +package org.brit.driver.adapters; + +import com.codeborne.selenide.impl.WebElementSource; +import com.microsoft.playwright.ElementHandle; +import com.microsoft.playwright.JSHandle; +import com.microsoft.playwright.Page; +import org.brit.element.converters.ElementHandleConverter; +import org.brit.element.PlaywrightWebElement; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.WrapsElement; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.IntStream; + +public class JsExecutionAdapter { + private static final ElementHandleConverter converter = new ElementHandleConverter(); + + /** + * Executes a JavaScript script on a Playwright page. + * + * @param page The Playwright page object where the script will be executed. + * @param script The JavaScript code to be executed. + * @param args The arguments to pass to the script. + * @return The result of executing the script. + */ + public Object executeScript(Page page, String script, Object... args) { + String modifiedScript = removeReturnKeyword(script); + + var arguments = args.length > 0 ? transformArguments(args) : List.of(); + JSHandle jsHandle = page.evaluateHandle("(arguments) => " + modifiedScript, arguments); + String type = jsHandle.evaluate( + """ + (node) => { + if (node instanceof HTMLCollection) return 'HTMLCollection'; + if (node instanceof HTMLElement) return 'HTMLElement'; + if (Array.isArray(node)) return 'Array'; + return 'Other'; + } + """).toString(); + + switch (type) { + case "HTMLCollection": + return processHtmlCollection(page, jsHandle); + case "HTMLElement": + return converter.toPwElement(page, jsHandle.asElement()); + case "Array": + return processArray(page, jsHandle); + default: + return jsHandle.jsonValue(); + } + } + + /** + * Executes an asynchronous JavaScript script on a Playwright page. + * + * @param page The Playwright page object where the script will be executed. + * @param script The JavaScript code to be executed. + * @param args The arguments to pass to the script. + * @return A promise of the result of executing the asynchronous script. + */ + public Object executeAsyncScript(Page page, String script, Object... args) { + String modifiedScript = removeReturnKeyword(script); + if (args.length > 0) { + return page.evaluate("async () => {%s}".formatted(modifiedScript), transformArguments(args)); + } else { + return page.evaluate("async () => {%s}".formatted(modifiedScript)); + } + } + + private String removeReturnKeyword(String script) { + return script.replaceFirst("^return", "").trim(); + } + + private List processHtmlCollection(Page page, JSHandle jsHandle) { + int length = (int) page.evaluate("node => node.length", jsHandle); + return IntStream.range(0, length) + .mapToObj(i -> converter.toPwElement(page, page.evaluateHandle("node => node.item(%s)".formatted(i), jsHandle).asElement())) + .toList(); + } + + private List processArray(Page page, JSHandle jsHandle) { + int length = (int) page.evaluate("node => node.length", jsHandle); + return IntStream.range(0, length) + .mapToObj(i -> Optional.ofNullable(page.evaluate("node => node['%s']".formatted(i), jsHandle)).map(Object::toString).orElse(null)) + .toList(); + } + + private List transformArguments(Object... args) { + return Arrays.stream(args) + .map(this::transformArgument) + .toList(); + } + + private final Map, Function> argumentTransformers = Map.of( + WebElementSource.class, arg -> getElementHandleFrom((WebElementSource) arg), + WrapsElement.class, arg -> getElementHandleFrom((WrapsElement) arg), + WebElement.class, arg -> getElementHandleFrom((WebElement) arg), + Collection.class, arg -> transformCollection((Collection) arg) + ); + + private Object transformArgument(Object arg) { + if (arg instanceof WebElementSource) { + return getElementHandleFrom((WebElementSource) arg); + } else if (arg instanceof WrapsElement) { + return getElementHandleFrom((WrapsElement) arg); + } else if (arg instanceof WebElement) { + return getElementHandleFrom((WebElement) arg); + } else if (arg instanceof Collection) { + return transformCollection((Collection) arg); + } + return arg; + } + + private Object transformCollection(Collection collection) { + if (collection.isEmpty()) return List.of(); + + if (collection.iterator().next() instanceof WebElement) { + return collection.stream() + .map(e -> ((PlaywrightWebElement) e).getLocator().elementHandle()) + .toList(); + } + return List.copyOf(collection); + } + + private ElementHandle getElementHandleFrom(WebElementSource source) { + return ((PlaywrightWebElement) source.getWebElement()).getLocator().elementHandle(); + } + + private ElementHandle getElementHandleFrom(WrapsElement wrapsElement) { + return ((PlaywrightWebElement) wrapsElement.getWrappedElement()).getElementHandle(); + } + + private ElementHandle getElementHandleFrom(WebElement webElement) { + return ((PlaywrightWebElement) webElement).getLocator().elementHandle(); + } +} diff --git a/src/main/java/org/brit/element/converters/ElementHandleConverter.java b/src/main/java/org/brit/element/converters/ElementHandleConverter.java new file mode 100644 index 0000000..fb741f3 --- /dev/null +++ b/src/main/java/org/brit/element/converters/ElementHandleConverter.java @@ -0,0 +1,37 @@ +package org.brit.element.converters; + +import com.microsoft.playwright.ElementHandle; +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; +import org.brit.element.PlaywrightWebElement; + +/*** + * Converts ElementHandle to PlaywrightWebElement + */ +public class ElementHandleConverter { + + public PlaywrightWebElement toPwElement(Page page, ElementHandle node) { + // JS code calculates a unique CSS selector path for the node. + // It does this by constructing a path from the node itself to the root of the document. + String string = page.evaluate(""" + node => + { + names = []; + do { + index = 0; + cursorElement = node; + while (cursorElement !== null) { + ++index; + cursorElement = cursorElement.previousElementSibling; + } + names.unshift(node.tagName + ":nth-child(" + index + ")"); + node = node.parentElement; + } while (node !== null); + + return names.join(" > "); + } + """, node).toString(); + + return new PlaywrightWebElement(page.locator(string)); + } +}