Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#102 Fix js script execution #107

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public void scrollFromWebElement_deltaX_isNegative_deltaY_isNegative_xOffset_not
private Boolean isElementVisibleInViewPort(SelenideElement element) {
File script = new File(getClass().getClassLoader().getResource("is-element-visible.js").getPath());
try {
return Boolean.parseBoolean(executeJavaScript(FileUtils.readFileToString(script, Charset.defaultCharset()), element));
return executeJavaScript(FileUtils.readFileToString(script, Charset.defaultCharset()), element);
} catch (IOException e) {
throw new RuntimeException(e);
}
Expand Down