Skip to content

Commit

Permalink
britka#102 Fix js script execution. Refactor and move js execution im…
Browse files Browse the repository at this point in the history
…plementation to separate class.
  • Loading branch information
BorisOsipov committed Nov 12, 2024
1 parent 968f804 commit 93794ee
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 101 deletions.
106 changes: 5 additions & 101 deletions src/main/java/org/brit/driver/PlaywrightiumDriver.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -876,107 +875,12 @@ public void perform(Collection<Sequence> 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<PlaywrightWebElement> 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<String> 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<Object> transformArguments(Object... args) {
List<Object> 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<Object> arrayList = new ArrayList<>((Collection<?>) arg);
if (!arrayList.isEmpty() && arrayList.get(0) instanceof WebElement) {
List<ElementHandle> 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<Object> 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() {
Expand Down
141 changes: 141 additions & 0 deletions src/main/java/org/brit/driver/adapters/JsExecutionAdapter.java
Original file line number Diff line number Diff line change
@@ -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<PlaywrightWebElement> 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<String> 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<Object> transformArguments(Object... args) {
return Arrays.stream(args)
.map(this::transformArgument)
.toList();
}

private final Map<Class<?>, Function<Object, Object>> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}

0 comments on commit 93794ee

Please sign in to comment.