diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/HtmlService.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/HtmlService.java index 306e62cc..72899827 100644 --- a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/HtmlService.java +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/HtmlService.java @@ -93,6 +93,7 @@ public T getAttribute(Element element, String attributeName, Class clazz) } } + @Deprecated public static String convertAbsoluteXpathToCss(String xpath) { String cssSelector = xpath.replace(NODE, CHILD_COMBINATOR); @@ -115,6 +116,7 @@ public static String convertAbsoluteXpathToCss(String xpath) { return semiFinalLocator; } + @Deprecated public static String removeDanglingChildCombinatorsFromCss(String css) { // convert to array by splitting the css by the child combinator // and remove from that array empty steps diff --git a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/WebComponent.java b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/WebComponent.java index 7fa92546..cc9e7418 100644 --- a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/WebComponent.java +++ b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/WebComponent.java @@ -42,6 +42,7 @@ import solutions.bellatrix.playwright.components.options.actions.HoverOptions; import solutions.bellatrix.playwright.components.options.actions.UncheckOptions; import solutions.bellatrix.playwright.components.options.states.BoundingBoxOptions; +import solutions.bellatrix.playwright.components.shadowdom.ShadowDomService; import solutions.bellatrix.playwright.components.shadowdom.ShadowRoot; import solutions.bellatrix.playwright.configuration.WebSettings; import solutions.bellatrix.playwright.findstrategies.*; @@ -325,7 +326,16 @@ protected String defaultGetWidthAttribute() { } protected String defaultGetInnerHtmlAttribute() { - return Optional.ofNullable(findLocator().innerHTML()).orElse(""); + if (!(this.inShadowContext())) { + return Optional.ofNullable(findLocator().innerHTML()).orElse(""); + } else { + if (this instanceof ShadowRoot) { + return ShadowDomService.getShadowHtml(this, true); + } else { + return ShadowDomService.getShadowHtml(this, false); + } + } + } protected String defaultGetForAttribute() { @@ -1388,4 +1398,15 @@ public List shadowRootCreateAllByI public List shadowRootCreateAllByInnerTextContaining(Class componentClass, String innerText) { return create().allBy(componentClass, new InnerTextContainingFindStrategy(innerText)); } + + private boolean inShadowContext() { + var component = this; + + while (component != null) { + if (component instanceof ShadowRoot) return true; + component = component.getParentComponent(); + } + + return false; + } } diff --git a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/advanced/grid/Grid.java b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/advanced/grid/Grid.java index abec7ee5..1046ce80 100644 --- a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/advanced/grid/Grid.java +++ b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/advanced/grid/Grid.java @@ -24,6 +24,7 @@ import solutions.bellatrix.playwright.findstrategies.XpathFindStrategy; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; @@ -137,12 +138,8 @@ public void forEachRow(Consumer action) { public GridCell getCell(int row, int column) { String xpath = HtmlService.getAbsoluteXpath(getTableService().getCell(row, column)); -// if (innerXpath.startsWith(".")) innerXpath = innerXpath.substring(1); -// String outerXpath = getCurrentElementXPath(); -// -// String fullXpath = Objects.requireNonNullElse(outerXpath, ".") + innerXpath; - GridCell cell = this.create().byXpath(GridCell.class, xpath); + GridCell cell = this.create().byXpath(GridCell.class, "." + xpath); setCellMetaData(cell, row, column); return cell; @@ -310,8 +307,13 @@ public void assertTable(Class clazz, List e if (!clazz.equals(Object.class)) { entity = castRow(clazz, i, propsNotToCompare); } else { - Method method = this.getClass().getMethod("castRow", int.class, List.class); - entity = (TRowObject)method.invoke(this, i, Arrays.stream(propsNotToCompare).toList()); + Method method = null; + try { + method = this.getClass().getMethod("castRow", int.class, List.class); + entity = (TRowObject)method.invoke(this, i, Arrays.stream(propsNotToCompare).toList()); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } } EntitiesAsserter.areEqual(expectedEntities.get(i), entity, propsNotToCompare); } @@ -355,9 +357,10 @@ public TRowObject castRow(Class clazz, int rowIndex, St } var dto = InstanceFactory.create(clazz); - var fields = clazz.getFields(); + var fields = clazz.getDeclaredFields(); for (var field : fields) { var fieldType = field.getType(); + field.setAccessible(true); var headerInfo = getHeaderNamesService().getHeaderInfoByField(field); @@ -492,7 +495,9 @@ public Gri public Grid setModelColumns(Class clazz) { controlColumnDataCollection = new ArrayList<>(); - for (var field : clazz.getFields()) { + List declaredFields = List.of(clazz.getDeclaredFields()); + for (var field : declaredFields) { + field.setAccessible(true); var headerName = field.isAnnotationPresent(TableHeader.class) ? field.getAnnotation(TableHeader.class).name() : field.getName(); controlColumnDataCollection.add(new ControlColumnData(headerName)); } diff --git a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/common/create/RelativeCreateService.java b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/common/create/RelativeCreateService.java index 861ed6a4..34edf079 100644 --- a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/common/create/RelativeCreateService.java +++ b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/common/create/RelativeCreateService.java @@ -50,7 +50,7 @@ public TCo TComponent newComponent; if (inShadowContext()) { - newComponent = ShadowDomService.createInShadowContext((WebComponent)baseComponent, componentClass, findStrategy); + newComponent = ShadowDomService.createInShadowContext(componentClass, (WebComponent)baseComponent, findStrategy); } else { newComponent = createFromParentComponent(componentClass, findStrategy); } @@ -69,7 +69,7 @@ public Lis List componentList = new ArrayList<>(); if (inShadowContext()) { - componentList = ShadowDomService.createAllInShadowContext((WebComponent)baseComponent, componentClass, findStrategy); + componentList = ShadowDomService.createAllInShadowContext(componentClass, (WebComponent)baseComponent, findStrategy); } else { var elements = findStrategy.convert(baseComponent.getWrappedElement()).all(); diff --git a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/shadowdom/ShadowDomService.java b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/shadowdom/ShadowDomService.java index d2d04058..dbfea39d 100644 --- a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/shadowdom/ShadowDomService.java +++ b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/shadowdom/ShadowDomService.java @@ -13,266 +13,385 @@ package solutions.bellatrix.playwright.components.shadowdom; +import com.microsoft.playwright.Page; +import lombok.SneakyThrows; import lombok.experimental.UtilityClass; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; -import org.openqa.selenium.By; -import solutions.bellatrix.core.utilities.HtmlService; +import solutions.bellatrix.core.configuration.ConfigurationService; import solutions.bellatrix.core.utilities.InstanceFactory; import solutions.bellatrix.core.utilities.Ref; +import solutions.bellatrix.core.utilities.SingletonFactory; +import solutions.bellatrix.core.utilities.Wait; import solutions.bellatrix.playwright.components.WebComponent; -import solutions.bellatrix.playwright.findstrategies.CssFindStrategy; -import solutions.bellatrix.playwright.findstrategies.FindStrategy; -import solutions.bellatrix.playwright.findstrategies.ShadowXpathFindStrategy; -import solutions.bellatrix.playwright.findstrategies.XpathFindStrategy; +import solutions.bellatrix.playwright.configuration.WebSettings; +import solutions.bellatrix.playwright.findstrategies.*; +import solutions.bellatrix.playwright.services.JavaScriptService; +import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Stack; +import java.util.concurrent.Callable; +import java.util.function.Consumer; @UtilityClass public class ShadowDomService { private static final String CHILD_COMBINATOR = " > "; private static final String SHADOW_ROOT_TAG = "shadow-root"; - public static String getShadowHtml(ShadowRoot shadowRoot) { - var function = String.format(""" - el => { - function clone(element, tag) { - let cloneElement; - if (element instanceof ShadowRoot && !tag) { - cloneElement = new DocumentFragment(); - } else if (tag) { - cloneElement = document.createElement(tag); - } - else { - cloneElement = element.cloneNode(false); - if (element.firstChild && element.firstChild.nodeType === 3) { - cloneElement.appendChild(element.firstChild.cloneNode(false)); - } - } - - if (element.shadowRoot) { - cloneElement.appendChild(clone(element.shadowRoot, "%s")); - } - - if (element.children) { - for (const child of element.children) { - cloneElement.appendChild(clone(child, undefined)); - } - } - - return cloneElement; - } - - var temporaryDiv = document.createElement("div"); - temporaryDiv.appendChild(clone(el.shadowRoot, undefined)); - return temporaryDiv.innerHTML; - }; - """, SHADOW_ROOT_TAG); - - return (String)shadowRoot.evaluate(function); + public static String getShadowHtml(WebComponent shadowComponent, boolean isShadowRoot) { + return (String)shadowComponent.evaluate(String.format("(el, [isShadowRoot]) => (%s)(el, isShadowRoot)", getInnerHtmlScript), new Object[] { isShadowRoot }); } - public static TComponent createInShadowContext(WebComponent parentComponent, Class componentClass, TFindStrategy findStrategy) { - var shadowRoots = getShadowRootAncestors(parentComponent); + public static TComponent createFromShadowRoot(Class componentClass, ShadowRoot parentComponent, TFindStrategy findStrategy) { + return createAllFromShadowRoot(componentClass, parentComponent, findStrategy).get(0); + } - ShadowRoot initialShadowRoot = shadowRoots.pop(); + public static List createAllFromShadowRoot(Class componentClass, ShadowRoot parentComponent, TFindStrategy findStrategy) { + var locator = findStrategy.getValue(); - var locatorBuilder = new StringBuilder(); - while (!shadowRoots.isEmpty()) { - locatorBuilder.append(shadowRoots.pop().getFindStrategy().getValue()).append(CHILD_COMBINATOR + SHADOW_ROOT_TAG); - } - CssFindStrategy fullLocator = new CssFindStrategy(locatorBuilder.toString() + parentComponent.getFindStrategy().getValue()); + var foundLocators = getAbsoluteCss(parentComponent, locator); + + List componentList = new ArrayList<>(foundLocators.length); + for (var i = 0; i < foundLocators.length; i++) { + Ref currentCss = new Ref<>(foundLocators[i]); + var component = buildMissingShadowRootsAndCreate(componentClass, parentComponent, currentCss); + + if (findStrategy instanceof XpathFindStrategy) { + component.setFindStrategy(new ShadowXpathFindStrategy(findStrategy.getValue(), currentCss.value)); + } else { + component.setFindStrategy(new CssFindStrategy(currentCss.value)); + } + + component.setWrappedElement(component.getFindStrategy().convert(component.getParentComponent().getWrappedElement()).nth(i)); - var parentElement = getElement(initialShadowRoot, fullLocator); + componentList.add(component); + } - var newElement = getElement(parentElement, findStrategy); + return componentList; + } - return createComponent(componentClass, initialShadowRoot, newElement, findStrategy); + public static TComponent createInShadowContext(Class componentClass, WebComponent parentComponent, TFindStrategy findStrategy) { + return createAllInShadowContext(componentClass, parentComponent, findStrategy).get(0); } - public static List createAllInShadowContext(WebComponent parentComponent, Class componentClass, TFindStrategy findStrategy) { - List componentList = new ArrayList<>(); + public static List createAllInShadowContext(Class componentClass, WebComponent parentComponent, TFindStrategy findStrategy) { + var locator = findStrategy.getValue(); + var parentLocator = retraceParentShadowRoots(parentComponent); - var shadowRoots = getShadowRootAncestors(parentComponent); + var outermostShadowRoot = getOutermostShadowRoot(parentComponent); + var foundLocators = getRelativeCss(outermostShadowRoot, locator, parentLocator); - ShadowRoot initialShadowRoot = shadowRoots.pop(); + List componentList = new ArrayList<>(foundLocators.length); + for (var i = 0; i < foundLocators.length; i++) { + Ref currentCss = new Ref<>(foundLocators[i]); + var component = buildMissingShadowRootsAndCreate(componentClass, outermostShadowRoot, currentCss); - var locatorBuilder = new StringBuilder(); - while (!shadowRoots.isEmpty()) { - locatorBuilder.append(shadowRoots.pop().getFindStrategy().getValue()).append(CHILD_COMBINATOR + SHADOW_ROOT_TAG); - } - CssFindStrategy fullLocator = new CssFindStrategy(locatorBuilder.toString() + parentComponent.getFindStrategy().getValue()); + if (findStrategy instanceof ShadowXpathFindStrategy) { + component.setFindStrategy(new ShadowXpathFindStrategy(findStrategy.getValue(), currentCss.value)); + } else { + component.setFindStrategy(new CssFindStrategy(currentCss.value)); + } - var parentElement = getElement(initialShadowRoot, fullLocator); + component.setWrappedElement(component.getFindStrategy().convert(component.getParentComponent().getWrappedElement()).nth(i)); - for (var element : getElements(parentElement, findStrategy)) { - componentList.add(createComponent(componentClass, initialShadowRoot, element, findStrategy)); + componentList.add(component); } return componentList; } - public static TComponent createFromShadowRoot(ShadowRoot shadowRoot, Class componentClass, TFindStrategy findStrategy) { - return createComponent(componentClass, shadowRoot, getElement(shadowRoot, findStrategy), findStrategy); + private static String[] getAbsoluteCss(ShadowRoot shadowRoot, String locator) { + Callable js = () -> { + return ((ArrayList)shadowRoot + .evaluate(String.format("(el, [locator]) => (%s)(el, locator)", javaScript), new Object[] { locator })) + .toArray(String[]::new); + }; + if (Wait.retry(() -> { + String[] foundElements; + try { + foundElements = js.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + if (foundElements == null || foundElements.length == 0) { + throw new IllegalArgumentException(); + } + }, Duration.ofSeconds(ConfigurationService.get(WebSettings.class).getTimeoutSettings().getElementWaitTimeout()), Duration.ofSeconds(1), false)) { + try { + return js.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + throw new IllegalArgumentException("No elements inside the shadow DOM were found with the locator: " + locator); + } } - public static List createAllFromShadowRoot(ShadowRoot shadowRoot, Class componentClass, TFindStrategy findStrategy) { - List componentList = new ArrayList<>(); - - for (var element : getElements(shadowRoot, findStrategy)) { - componentList.add(createComponent(componentClass, shadowRoot, element, findStrategy)); + private static String[] getRelativeCss(ShadowRoot shadowRoot, String locator, String parentLocator) { + Callable js = () -> { + return ((ArrayList)shadowRoot + .evaluate(String.format("(el, [locator, parentLocator]) => (%s)(el, locator, parentLocator)", javaScript), new Object[] { locator, parentLocator })) + .toArray(String[]::new); + }; + + if(Wait.retry(() -> { + String[] foundElements; + try { + foundElements = js.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + if (foundElements == null || foundElements.length == 0) { + throw new IllegalArgumentException(); + } + }, Duration.ofSeconds(ConfigurationService.get(WebSettings.class).getTimeoutSettings().getElementWaitTimeout()), Duration.ofSeconds(1), false)) { + try { + return js.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + throw new IllegalArgumentException("No elements inside the shadow DOM were found with the locator: " + locator); } - - return componentList; } - private static TComponent createComponent(Class clazz, ShadowRoot initialShadowRoot, Element jsoupNode, FindStrategy findStrategy) { + private static TComponent buildMissingShadowRootsAndCreate(Class clazz, ShadowRoot parentComponent, Ref fullCss) { var component = InstanceFactory.create(clazz); - // passed as a reference, the inside value will change: - Ref shadowRoot = new Ref<>(initialShadowRoot); + String[] fullCssArray = fullCss.value.split(CHILD_COMBINATOR + SHADOW_ROOT_TAG + CHILD_COMBINATOR); - // create missing shadow roots between the element and the initial shadow root; - // chain them; - // return the element's css relative to the innermost shadow root - var jsoupNodeCss = buildMissingShadowRootsAndReturnElementRelativeCss(shadowRoot, jsoupNode); + ShadowRoot parent = parentComponent; - if (findStrategy instanceof XpathFindStrategy) { - component.setFindStrategy(new ShadowXpathFindStrategy(findStrategy.getValue(), jsoupNodeCss)); - } else { - component.setFindStrategy(new CssFindStrategy(jsoupNodeCss)); + // we don't need the last String in the array when building the missing shadow root parents, + // as it is the local css relative to the innermost shadow root parent + for (var i = 0; i < fullCssArray.length - 1; i++) { + var nestedShadowRoot = InstanceFactory.create(ShadowRoot.class); + nestedShadowRoot.setFindStrategy(new CssFindStrategy(fullCssArray[i])); + nestedShadowRoot.setParentComponent(parent); + + parent = nestedShadowRoot; } - component.setParentComponent(shadowRoot.value); - component.setWrappedElement(component.getFindStrategy().convert(component.getParentComponent().getWrappedElement()).first()); + fullCss.value = fullCssArray[fullCssArray.length - 1]; + component.setParentComponent(parent); return component; } - private static String buildMissingShadowRootsAndReturnElementRelativeCss(Ref shadowRoot, Element jsoupNode) { - var nestedShadowRootStack = new Stack(); - - // initial absolute xpath, relative to the outermost shadow root element - var jsoupNodeCss = HtmlService.convertAbsoluteXpathToCss(HtmlService.getAbsoluteXpath(jsoupNode)).split(CHILD_COMBINATOR); - - // if there are elements found between the outermost shadow root and the element - // populate the Stack - if (tryFindNestedShadowRoots(jsoupNode, nestedShadowRootStack)) { - String[] previousCss = null; - - while (!nestedShadowRootStack.isEmpty()) { - var parent = nestedShadowRootStack.pop(); - var css = HtmlService.convertAbsoluteXpathToCss(HtmlService.getAbsoluteXpath(parent)).split(CHILD_COMBINATOR); - if (previousCss != null) { - css = removeRedundantSteps(css, previousCss); - } - - shadowRoot.value = createNestedShadowRoot(shadowRoot.value, cleanFromShadowRootTags(css)); + private static ShadowRoot getOutermostShadowRoot(WebComponent component) { + var parent = component.getParentComponent(); + ShadowRoot outermostShadowRoot = null; - jsoupNodeCss = removeRedundantSteps(jsoupNodeCss, css); - - previousCss = HtmlService.convertAbsoluteXpathToCss(HtmlService.getAbsoluteXpath(parent)).split(CHILD_COMBINATOR); - } + while (parent instanceof ShadowRoot) { + outermostShadowRoot = (ShadowRoot)parent; + parent = parent.getParentComponent(); } - return String.join(CHILD_COMBINATOR, cleanFromShadowRootTags(jsoupNodeCss)); - } - - private static String[] removeRedundantSteps(String[] elementCss, String[] currentCss) { - return Arrays.stream(elementCss) - .skip(currentCss.length) - .toArray(String[]::new); + return outermostShadowRoot; } - private static String[] cleanFromShadowRootTags(String[] css) { - return Arrays.stream(css) - .filter(x -> !x.contains(SHADOW_ROOT_TAG)) - .toArray(String[]::new); - } + private static int getNestedLevel(WebComponent component) { + var parent = component.getParentComponent(); - private static ShadowRoot createNestedShadowRoot(ShadowRoot parent, String[] locator) { - var finalLocator = String.join(CHILD_COMBINATOR, cleanFromShadowRootTags(locator)); + var count = 0; + while (parent instanceof ShadowRoot) { + count++; - ShadowRoot nestedShadowRoot = InstanceFactory.create(ShadowRoot.class); - nestedShadowRoot.setFindStrategy(new CssFindStrategy(finalLocator)); - nestedShadowRoot.setParentComponent(parent); + parent = parent.getParentComponent(); + } - return nestedShadowRoot; + return count; } - private static boolean tryFindNestedShadowRoots(Element jsoupElement, Stack parentShadowRootStack) { - var parent = jsoupElement.parent(); + private static String retraceParentShadowRoots(WebComponent component) { + if (getNestedLevel(component) > 1) { + var parent = component.getParentComponent(); - while (parent != null) { - if (parent.tagName().equals(SHADOW_ROOT_TAG)) { - parentShadowRootStack.push(parent); - } + Stack findStrategies = new Stack<>(); - parent = parent.parent(); - } + checkIfCss(component.getFindStrategy()); - return !parentShadowRootStack.isEmpty(); - } + findStrategies.push(component.getFindStrategy().getValue()); - /** - * Find from root element in shadow root's html. - */ - private static Element getElement(ShadowRoot component, FindStrategy findStrategy) { - var doc = Jsoup.parse(component.getHtml()); + while (parent instanceof ShadowRoot) { + checkIfCss(parent.getFindStrategy()); - return getElement(doc, findStrategy); - } + findStrategies.push(CHILD_COMBINATOR + SHADOW_ROOT_TAG + CHILD_COMBINATOR); + findStrategies.push(parent.getFindStrategy().getValue()); - /** - * Find relatively from element. - */ - private static Element getElement(Element element, FindStrategy findStrategy) { - return getElements(element, findStrategy).get(0); - } + parent = parent.getParentComponent(); + } - /** - * Find from root element in shadow root's html. - */ - private static List getElements(ShadowRoot component, FindStrategy findStrategy) { - var doc = Jsoup.parse(component.getHtml()); + StringBuilder finalCss = new StringBuilder(); + while(!findStrategies.isEmpty()) { + finalCss.append(findStrategies.pop()); + } - return getElements(doc, findStrategy); + return finalCss.toString(); + } else { + return component.getFindStrategy().getValue(); + } } - /** - * Find relatively from element. - */ - private static List getElements(Element element, FindStrategy findStrategy) { - Elements foundElements = null; - var strategyValue = findStrategy.getValue(); - - if (findStrategy instanceof CssFindStrategy) { - foundElements = element.select(strategyValue); - } + private static void checkIfCss(FindStrategy findStrategy) { if (findStrategy instanceof XpathFindStrategy) { - foundElements = element.selectXpath(strategyValue); + throw new IllegalArgumentException("Inside Shadow DOM, there cannot be anything different than CSS locator."); } - - return foundElements; } - private static Stack getShadowRootAncestors(WebComponent initialComponent) { - var component = initialComponent.getParentComponent(); - - var shadowRoots = new Stack(); - - while (component != null) { - if (component instanceof ShadowRoot) { - shadowRoots.push((ShadowRoot)component); + private static final String javaScript = /* lang=js */ """ + function (element, strategy, relativeElementCss) { + const child_combinator = " > "; + const node = "/"; + + function clone(element, tag) { + let cloneElement; + if (element instanceof ShadowRoot && !tag) { + cloneElement = new DocumentFragment(); + } else if (tag) { + cloneElement = document.createElement(tag); + } + else { + cloneElement = element.cloneNode(); + if (element.firstChild && element.firstChild.nodeType === 3) { + cloneElement.appendChild(element.firstChild.cloneNode()); + } + } + + if (element.shadowRoot) { + cloneElement.appendChild(clone(element.shadowRoot, "shadow-root")); + } + + if (element.children) { + for (const child of element.children) { + cloneElement.appendChild(clone(child, undefined)); + } + } + + return cloneElement; + } + + function getAbsoluteXpath(element) { + function indexElement(el) { + let index = 1; + + let previousSibling = el.previousElementSibling; + while (previousSibling) { + if (previousSibling.nodeName.toLowerCase() === el.nodeName.toLowerCase()) { + index++; + } + previousSibling = previousSibling.previousElementSibling; + } + + if (el.tagName.toLowerCase() === "shadow-root") { + return node + el.tagName.toLowerCase(); + } else { + return node + el.tagName.toLowerCase() + "[" + index + "]"; + } + } + + let xpath = []; + + let currentElement = element; + while (currentElement) { + if (currentElement.tagName.toLowerCase() === 'html' || currentElement.tagName.toLowerCase() === 'body' || currentElement.tagName.startsWith() === '#' || currentElement.tagName.toLowerCase() === "temporary-div") { + break; + } + + xpath.unshift(indexElement(currentElement)); + + currentElement = currentElement.parentElement; + } + return xpath.join(""); + } + + function getAbsoluteCss(xpath) { + let regex = new RegExp(node, 'g'); + let cssSelector = xpath.replace(regex, child_combinator); + cssSelector = cssSelector.replace(/\\[(\\d+)\\]/g, ':nth-of-type($1)'); + if (cssSelector.startsWith(child_combinator)) { + cssSelector = cssSelector.substring(child_combinator.length); + } + return cssSelector; + } + + const temporaryDiv = document.createElement("temporary-div"); + if (element.shadowRoot) { + temporaryDiv.appendChild(clone(element.shadowRoot, undefined)); + } else { + temporaryDiv.appendChild(clone(element, "shadow-root")); + } + + let startPoint = temporaryDiv; + + if (relativeElementCss) { + startPoint = temporaryDiv.querySelector(relativeElementCss); + } + + let elements; + if (strategy.startsWith("/") || strategy.startsWith("./") || strategy.startsWith("(")) { + let result = document.evaluate(strategy, startPoint, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); + elements = []; + let node; + while ((node = result.iterateNext())) { + elements.push(node); + } + } else { + elements = Array.from(startPoint.querySelectorAll(strategy)); + } + + let finalLocators = []; + elements.forEach((el) => { + finalLocators.push(getAbsoluteCss(getAbsoluteXpath(el))); + }); + + return finalLocators; + }"""; + + private static final String getInnerHtmlScript = """ + function (element, isShadowRoot) { + const child_combinator = " > "; + const node = "/"; + + function clone(element, tag) { + let cloneElement; + if (element instanceof ShadowRoot && !tag) { + cloneElement = new DocumentFragment(); + } else if (tag) { + cloneElement = document.createElement(tag); + } + else { + cloneElement = element.cloneNode(); + if (element.firstChild && element.firstChild.nodeType === 3) { + cloneElement.appendChild(element.firstChild.cloneNode()); + } + } + + if (element.shadowRoot) { + cloneElement.appendChild(clone(element.shadowRoot, "shadow-root")); + } + + if (element.children) { + for (const child of element.children) { + cloneElement.appendChild(clone(child, undefined)); + } + } + + return cloneElement; + } + + let temporaryDiv = document.createElement("temporary-div"); + if (element.shadowRoot) { + temporaryDiv.appendChild(clone(element.shadowRoot, undefined)); + } else if (isShadowRoot) { + temporaryDiv.appendChild(clone(element, "shadow-root")); + } else { + temporaryDiv.appendChild(clone(element, "redundant-el")); + temporaryDiv = temporaryDiv.querySelector("redundant-el"); + } + + return temporaryDiv.innerHTML; } - component = component.getParentComponent(); - } - - return shadowRoots; - } + """; } diff --git a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/shadowdom/ShadowRoot.java b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/shadowdom/ShadowRoot.java index 555cb04a..413008b7 100644 --- a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/shadowdom/ShadowRoot.java +++ b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/shadowdom/ShadowRoot.java @@ -39,6 +39,6 @@ public ShadowRootCreateService create() { * Returns the innerHTML of the shadowRoot of the shadow host. */ public String getHtml() { - return ShadowDomService.getShadowHtml(this); + return defaultGetInnerHtmlAttribute(); } } diff --git a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/shadowdom/ShadowRootCreateService.java b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/shadowdom/ShadowRootCreateService.java index 369ce7a3..bd68772e 100644 --- a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/shadowdom/ShadowRootCreateService.java +++ b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/components/shadowdom/ShadowRootCreateService.java @@ -37,7 +37,7 @@ public TCo wrappedBrowser().getCurrentPage().waitForLoadState(); - TComponent newComponent = ShadowDomService.createFromShadowRoot((ShadowRoot)baseComponent, componentClass, findStrategy); + TComponent newComponent = ShadowDomService.createFromShadowRoot(componentClass, (ShadowRoot)baseComponent, findStrategy); CREATED.broadcast(new ComponentActionEventArgs((WebComponent)baseComponent)); @@ -50,7 +50,7 @@ public Lis wrappedBrowser().getCurrentPage().waitForLoadState(); - List componentList = ShadowDomService.createAllFromShadowRoot((ShadowRoot)baseComponent, componentClass, findStrategy); + List componentList = ShadowDomService.createAllFromShadowRoot(componentClass, (ShadowRoot)baseComponent, findStrategy); CREATED.broadcast(new ComponentActionEventArgs((WebComponent)baseComponent)); diff --git a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/services/JavaScriptService.java b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/services/JavaScriptService.java index 25a3a9d1..a58899c4 100644 --- a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/services/JavaScriptService.java +++ b/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/services/JavaScriptService.java @@ -23,6 +23,10 @@ @SuppressWarnings("resource") public class JavaScriptService extends WebService { + public T genericExecute(String script, Object... args) { + return (T)performEvaluation(() -> wrappedBrowser().getCurrentPage().evaluate(script, new Object[] { args })); + } + public Object execute(String script) { return performEvaluation(() -> wrappedBrowser().getCurrentPage().evaluate(script)); @@ -37,7 +41,7 @@ public String execute(Frame frame, String script) { } public String execute(String script, Object... args) { - return (String) performEvaluation(() -> wrappedBrowser().getCurrentPage().evaluate(script, args)); + return (String) performEvaluation(() -> wrappedBrowser().getCurrentPage().evaluate(script, new Object[] { args })); } public String execute(String script, TComponent component) { diff --git a/bellatrix.web/src/main/java/solutions/bellatrix/web/components/WebComponent.java b/bellatrix.web/src/main/java/solutions/bellatrix/web/components/WebComponent.java index fd57b34c..98cc513a 100644 --- a/bellatrix.web/src/main/java/solutions/bellatrix/web/components/WebComponent.java +++ b/bellatrix.web/src/main/java/solutions/bellatrix/web/components/WebComponent.java @@ -783,7 +783,7 @@ protected TComponent component; if (inShadowContext()) { - component = ShadowDomService.createInShadowContext(this, componentClass, findStrategy); + component = ShadowDomService.createInShadowContext(componentClass, this, findStrategy); } else { component = InstanceFactory.create(componentClass); component.setFindStrategy(findStrategy); @@ -802,7 +802,7 @@ protected List componentList = new ArrayList<>(); if (inShadowContext()) { - componentList = ShadowDomService.createAllInShadowContext(this, componentClass, findStrategy); + componentList = ShadowDomService.createAllInShadowContext(componentClass, this, findStrategy); } else { var nativeElements = wrappedElement.findElements(findStrategy.convert()); @@ -1055,10 +1055,18 @@ protected String defaultGetWidthAttribute() { } protected String defaultGetInnerHtmlAttribute() { - try { - return Optional.ofNullable(getAttribute("innerHTML")).orElse(""); - } catch (StaleElementReferenceException e) { - return Optional.ofNullable(findElement().getAttribute("innerHTML")).orElse(""); + if (!this.inShadowContext()) { + try { + return Optional.ofNullable(getAttribute("innerHTML")).orElse(""); + } catch (StaleElementReferenceException e) { + return Optional.ofNullable(findElement().getAttribute("innerHTML")).orElse(""); + } + } else { + if (this instanceof ShadowRoot) { + return ShadowDomService.getShadowHtml(this, true); + } else { + return ShadowDomService.getShadowHtml(this, false); + } } } diff --git a/bellatrix.web/src/main/java/solutions/bellatrix/web/components/shadowdom/ShadowDomService.java b/bellatrix.web/src/main/java/solutions/bellatrix/web/components/shadowdom/ShadowDomService.java index 4d33ba48..82dacb79 100644 --- a/bellatrix.web/src/main/java/solutions/bellatrix/web/components/shadowdom/ShadowDomService.java +++ b/bellatrix.web/src/main/java/solutions/bellatrix/web/components/shadowdom/ShadowDomService.java @@ -13,23 +13,29 @@ package solutions.bellatrix.web.components.shadowdom; +import lombok.SneakyThrows; import lombok.experimental.UtilityClass; import org.jsoup.Jsoup; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import org.openqa.selenium.By; +import solutions.bellatrix.core.configuration.ConfigurationService; import solutions.bellatrix.core.utilities.HtmlService; import solutions.bellatrix.core.utilities.InstanceFactory; import solutions.bellatrix.core.utilities.Ref; +import solutions.bellatrix.core.utilities.Wait; import solutions.bellatrix.web.components.WebComponent; +import solutions.bellatrix.web.configuration.WebSettings; import solutions.bellatrix.web.findstrategies.CssFindStrategy; import solutions.bellatrix.web.findstrategies.FindStrategy; import solutions.bellatrix.web.findstrategies.ShadowXPathFindStrategy; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Stack; +import java.util.concurrent.Callable; import java.util.stream.Collectors; @UtilityClass @@ -37,262 +43,397 @@ public class ShadowDomService { private static final String CHILD_COMBINATOR = " > "; private static final String SHADOW_ROOT_TAG = "shadow-root"; - public static String getShadowHtml(ShadowRoot shadowRoot) { - var function = String.format(""" - return (function(element) { - function clone(element, tag) { - let cloneElement; - if (element instanceof ShadowRoot && !tag) { - cloneElement = new DocumentFragment(); - } else if (tag) { - cloneElement = document.createElement(tag); - } - else { - cloneElement = element.cloneNode(); - if (element.firstChild && element.firstChild.nodeType === 3) { - cloneElement.appendChild(element.firstChild.cloneNode()); - } - } - - if (element.shadowRoot) { - cloneElement.appendChild(clone(element.shadowRoot, "%s")); - } - - if (element.children) { - for (const child of element.children) { - cloneElement.appendChild(clone(child)); - } - } - - return cloneElement; - } - - var temporaryDiv = document.createElement("div"); - temporaryDiv.appendChild(clone(element.shadowRoot, undefined)); - return temporaryDiv.innerHTML; - })(arguments[0]); - """, SHADOW_ROOT_TAG); - - return shadowRoot.getJavaScriptService().execute(function, shadowRoot); + public static String getShadowHtml(WebComponent shadowComponent, boolean isShadowRoot) { + return shadowComponent.getJavaScriptService() + .genericExecute(String.format("return (%s)(arguments[0], arguments[1]);", getInnerHtmlScript), + shadowComponent.findElement(), isShadowRoot); } - public static TComponent createInShadowContext(WebComponent parentComponent, Class componentClass, TFindStrategy findStrategy) { - var shadowRoots = getShadowRootAncestors(parentComponent); + public static TComponent createFromShadowRoot(Class componentClass, ShadowRoot parentComponent, TFindStrategy findStrategy) { + return createAllFromShadowRoot(componentClass, parentComponent, findStrategy).get(0); + } - ShadowRoot initialShadowRoot = shadowRoots.pop(); + public static List createAllFromShadowRoot(Class componentClass, ShadowRoot parentComponent, TFindStrategy findStrategy) { + var locator = convertToCssOrXpath(findStrategy); - var locatorBuilder = new StringBuilder(); - while (!shadowRoots.isEmpty()) { - locatorBuilder.append(shadowRoots.pop().getFindStrategy().getValue()).append(CHILD_COMBINATOR + SHADOW_ROOT_TAG); - } - CssFindStrategy fullLocator = new CssFindStrategy(locatorBuilder.toString() + parentComponent.getFindStrategy().getValue()); + var foundLocators = getAbsoluteCss(parentComponent, locator); - var parentElement = getElement(initialShadowRoot, fullLocator); + List componentList = new ArrayList<>(foundLocators.length); + for (var i = 0; i < foundLocators.length; i++) { + Ref currentCss = new Ref<>(foundLocators[i]); + var component = buildMissingShadowRootsAndCreate(componentClass, parentComponent, currentCss); - var newElement = getElement(parentElement, findStrategy); + if (findStrategy.convert() instanceof By.ByXPath) { + component.setFindStrategy(new ShadowXPathFindStrategy(findStrategy.getValue(), currentCss.value)); + } else { + component.setFindStrategy(new CssFindStrategy(currentCss.value)); + } - return createComponent(componentClass, initialShadowRoot, newElement, findStrategy); + componentList.add(component); + } + + return componentList; } - public static List createAllInShadowContext(WebComponent parentComponent, Class componentClass, TFindStrategy findStrategy) { - List componentList = new ArrayList<>(); + public static TComponent createInShadowContext(Class componentClass, WebComponent parentComponent, TFindStrategy findStrategy) { + return createAllInShadowContext(componentClass, parentComponent, findStrategy).get(0); + } - var shadowRoots = getShadowRootAncestors(parentComponent); + public static List createAllInShadowContext(Class componentClass, WebComponent parentComponent, TFindStrategy findStrategy) { + var locator = convertToCssOrXpath(findStrategy); + var parentLocator = retraceParentShadowRoots(parentComponent); - ShadowRoot initialShadowRoot = shadowRoots.pop(); + var outermostShadowRoot = getOutermostShadowRoot(parentComponent); + var foundLocators = getRelativeCss(outermostShadowRoot, locator, parentLocator); - var locatorBuilder = new StringBuilder(); - while (!shadowRoots.isEmpty()) { - locatorBuilder.append(shadowRoots.pop().getFindStrategy().getValue()).append(CHILD_COMBINATOR + SHADOW_ROOT_TAG); - } - CssFindStrategy fullLocator = new CssFindStrategy(locatorBuilder.toString() + parentComponent.getFindStrategy().getValue()); + List componentList = new ArrayList<>(foundLocators.length); + for (var i = 0; i < foundLocators.length; i++) { + Ref currentCss = new Ref<>(foundLocators[i]); + var component = buildMissingShadowRootsAndCreate(componentClass, outermostShadowRoot, currentCss); - var parentElement = getElement(initialShadowRoot, fullLocator); + if (findStrategy.convert() instanceof By.ByXPath) { + component.setFindStrategy(new ShadowXPathFindStrategy(findStrategy.getValue(), currentCss.value)); + } else { + component.setFindStrategy(new CssFindStrategy(currentCss.value)); + } - for (var element : getElements(parentElement, findStrategy)) { - componentList.add(createComponent(componentClass, initialShadowRoot, element, findStrategy)); + componentList.add(component); } return componentList; } - public static TComponent createFromShadowRoot(ShadowRoot shadowRoot, Class componentClass, TFindStrategy findStrategy) { - return createComponent(componentClass, shadowRoot, getElement(shadowRoot, findStrategy), findStrategy); + private static String[] getAbsoluteCss(ShadowRoot shadowRoot, String locator) { + Callable js = () -> { + return shadowRoot.getJavaScriptService() + .>genericExecute(String.format("return (%s)(arguments[0], arguments[1], arguments[2]);", javaScript), + shadowRoot.findElement(), locator, null).toArray(String[]::new); + }; + + if (Wait.retry(() -> { + String[] foundElements; + try { + foundElements = js.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + if (foundElements == null || foundElements.length == 0) { + throw new IllegalArgumentException(); + } + }, Duration.ofSeconds(ConfigurationService.get(WebSettings.class).getTimeoutSettings().getElementWaitTimeout()), Duration.ofSeconds(1), false)) { + try { + return js.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + throw new IllegalArgumentException("No elements inside the shadow DOM were found with the locator: " + locator); + } } - public static List createAllFromShadowRoot(ShadowRoot shadowRoot, Class componentClass, TFindStrategy findStrategy) { - List componentList = new ArrayList<>(); + private static String[] getRelativeCss(ShadowRoot shadowRoot, String locator, String parentLocator) { + Callable js = () -> { + return shadowRoot.getJavaScriptService() + .>genericExecute(String.format("return (%s)(arguments[0], arguments[1], arguments[2]);", javaScript), + shadowRoot.findElement(), locator, parentLocator).toArray(String[]::new); + }; + + if (Wait.retry(() -> { + String[] foundElements; + try { + foundElements = js.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } - for (var element : getElements(shadowRoot, findStrategy)) { - componentList.add(createComponent(componentClass, shadowRoot, element, findStrategy)); + if (foundElements == null || foundElements.length == 0) { + throw new IllegalArgumentException(); + } + }, Duration.ofSeconds(ConfigurationService.get(WebSettings.class).getTimeoutSettings().getElementWaitTimeout()), Duration.ofSeconds(1), false)) { + try { + return js.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + throw new IllegalArgumentException("No elements inside the shadow DOM were found with the locator: " + locator); } - - return componentList; } - private static TComponent createComponent(Class clazz, ShadowRoot initialShadowRoot, Element jsoupNode, FindStrategy findStrategy) { + private static TComponent buildMissingShadowRootsAndCreate(Class clazz, ShadowRoot parentComponent, Ref fullCss) { var component = InstanceFactory.create(clazz); - // passed as a reference, the inside value will change: - Ref shadowRoot = new Ref<>(initialShadowRoot); + String[] fullCssArray = fullCss.value.split(CHILD_COMBINATOR + SHADOW_ROOT_TAG + CHILD_COMBINATOR); - // create missing shadow roots between the element and the initial shadow root; - // chain them; - // return the element's css relative to the innermost shadow root - var jsoupNodeCss = buildMissingShadowRootsAndReturnElementRelativeCss(shadowRoot, jsoupNode); + ShadowRoot parent = parentComponent; - if (findStrategy.convert() instanceof By.ByXPath) { - component.setFindStrategy(new ShadowXPathFindStrategy(findStrategy.getValue(), jsoupNodeCss)); - } else { - component.setFindStrategy(new CssFindStrategy(jsoupNodeCss)); + // we don't need the last String in the array when building the missing shadow root parents, + // as it is the local css relative to the innermost shadow root parent + for (var i = 0; i < fullCssArray.length - 1; i++) { + var nestedShadowRoot = InstanceFactory.create(ShadowRoot.class); + nestedShadowRoot.setFindStrategy(new CssFindStrategy(fullCssArray[i])); + nestedShadowRoot.setParentComponent(parent); + nestedShadowRoot.setParentWrappedElement(parent.getWrappedElement()); + + parent = nestedShadowRoot; } - component.setParentComponent(shadowRoot.value); - component.setParentWrappedElement(shadowRoot.value.getWrappedElement()); + fullCss.value = fullCssArray[fullCssArray.length - 1]; + component.setParentComponent(parent); + component.setParentWrappedElement(parent.getWrappedElement()); return component; } - private static String buildMissingShadowRootsAndReturnElementRelativeCss(Ref shadowRoot, Element jsoupNode) { - var nestedShadowRootStack = new Stack(); - - // initial absolute css, relative to the outermost shadow root element - var jsoupNodeCss = HtmlService.convertAbsoluteXpathToCss(HtmlService.getAbsoluteXpath(jsoupNode)).split(CHILD_COMBINATOR); - // if there are elements found between the outermost shadow root and the element - // populate the Stack - if (tryFindNestedShadowRoots(jsoupNode, nestedShadowRootStack)) { - String[] previousCss = null; + private static ShadowRoot getOutermostShadowRoot(WebComponent component) { + var parent = component.getParentComponent(); + ShadowRoot outermostShadowRoot = null; - while (!nestedShadowRootStack.isEmpty()) { - var parent = nestedShadowRootStack.pop(); - var css = HtmlService.convertAbsoluteXpathToCss(HtmlService.getAbsoluteXpath(parent)).split(CHILD_COMBINATOR); - if (previousCss != null) { - css = removeRedundantSteps(css, previousCss); - } + while (parent instanceof ShadowRoot) { + outermostShadowRoot = (ShadowRoot)parent; + parent = parent.getParentComponent(); + } + return outermostShadowRoot; + } - shadowRoot.value = createNestedShadowRoot(shadowRoot.value, cleanFromShadowRootTags(css)); + private static int getNestedLevel(WebComponent component) { + var parent = component.getParentComponent(); - jsoupNodeCss = removeRedundantSteps(jsoupNodeCss, css); + var count = 0; + while (parent instanceof ShadowRoot) { + count++; - previousCss = HtmlService.convertAbsoluteXpathToCss(HtmlService.getAbsoluteXpath(parent)).split(CHILD_COMBINATOR); - } + parent = parent.getParentComponent(); } - return String.join(CHILD_COMBINATOR, cleanFromShadowRootTags(jsoupNodeCss)); + return count; } - private static String[] removeRedundantSteps(String[] elementCss, String[] currentCss) { - return Arrays.stream(elementCss) - .skip(currentCss.length) - .toArray(String[]::new); - } + private static String retraceParentShadowRoots(WebComponent component) { + if (getNestedLevel(component) > 1) { + var parent = component.getParentComponent(); - private static String[] cleanFromShadowRootTags(String[] css) { - return Arrays.stream(css) - .filter(x -> !x.contains(SHADOW_ROOT_TAG)) - .toArray(String[]::new); - } + Stack findStrategies = new Stack<>(); - private static ShadowRoot createNestedShadowRoot(ShadowRoot parent, String[] locator) { - var finalLocator = String.join(CHILD_COMBINATOR, cleanFromShadowRootTags(locator)); + checkIfCss(component.getFindStrategy()); - ShadowRoot nestedShadowRoot = InstanceFactory.create(ShadowRoot.class); - nestedShadowRoot.setFindStrategy(new CssFindStrategy(finalLocator)); - nestedShadowRoot.setParentComponent(parent); - nestedShadowRoot.setParentWrappedElement(parent.shadowRoot()); + findStrategies.push(component.getFindStrategy().getValue()); - return nestedShadowRoot; - } + while (parent instanceof ShadowRoot) { + checkIfCss(parent.getFindStrategy()); - private static boolean tryFindNestedShadowRoots(Element jsoupElement, Stack parentShadowRootStack) { - var parent = jsoupElement.parent(); + findStrategies.push(CHILD_COMBINATOR + SHADOW_ROOT_TAG + CHILD_COMBINATOR); + findStrategies.push(parent.getFindStrategy().getValue()); - while (parent != null) { - if (parent.tagName().equals(SHADOW_ROOT_TAG)) { - parentShadowRootStack.push(parent); + parent = parent.getParentComponent(); } - parent = parent.parent(); - } - - return !parentShadowRootStack.isEmpty(); - } - - /** - * Find from root element in shadow root's html. - */ - private static Element getElement(ShadowRoot component, FindStrategy findStrategy) { - var doc = Jsoup.parse(component.getHtml()); - - return getElement(doc, findStrategy); - } + StringBuilder finalCss = new StringBuilder(); + while(!findStrategies.isEmpty()) { + finalCss.append(findStrategies.pop()); + } - /** - * Find relatively from element. - */ - private static Element getElement(Element element, FindStrategy findStrategy) { - return getElements(element, findStrategy).get(0); + return finalCss.toString(); + } else { + return component.getFindStrategy().getValue(); + } } - /** - * Find from root element in shadow root's html. - */ - private static List getElements(ShadowRoot component, FindStrategy findStrategy) { - var doc = Jsoup.parse(component.getHtml()); + private static void checkIfCss(FindStrategy findStrategy) { + var strategyType = findStrategy.convert(); - return getElements(doc, findStrategy); + if (strategyType instanceof By.ByLinkText || strategyType instanceof By.ByPartialLinkText) { + throw new IllegalArgumentException("Inside Shadow DOM, there cannot be anything different than CSS locator."); + } } - /** - * Find relatively from element. - */ - private static List getElements(Element element, FindStrategy findStrategy) { - Elements foundElements = null; + private static String convertToCssOrXpath(FindStrategy findStrategy) { var strategyType = findStrategy.convert(); var strategyValue = findStrategy.getValue(); if (strategyType instanceof By.ByCssSelector) { - foundElements = element.select(strategyValue); + return strategyValue; } if (findStrategy.convert() instanceof By.ByXPath) { - foundElements = element.selectXpath(strategyValue); + return strategyValue; } if (findStrategy.convert() instanceof By.ById) { - foundElements = element.select(String.format("[id='%s']", strategyValue)); + return String.format("[id='%s']", strategyValue); } if (strategyType instanceof By.ByName) { - foundElements = element.select(String.format("[name='%s']", strategyValue)); + return String.format("[name='%s']", strategyValue); } if (strategyType instanceof By.ByClassName) { - foundElements = element.select(String.format("[class='%s']", strategyValue)); + return String.format("[class='%s']", strategyValue); } if (strategyType instanceof By.ByLinkText) { - foundElements = element.selectXpath(String.format("//a[text()='%s']", strategyValue)); + return String.format("//a[text()='%s']", strategyValue); } if (strategyType instanceof By.ByTagName) { - foundElements = element.select(strategyValue); + return strategyValue; } if (strategyType instanceof By.ByPartialLinkText) { - foundElements = element.selectXpath(String.format("//a[contains(text(), '%s')]", strategyValue)); + return String.format("//a[contains(text(), '%s')]", strategyValue); } - return foundElements; + return null; } - private static Stack getShadowRootAncestors(WebComponent initialComponent) { - var component = initialComponent.getParentComponent(); - - var shadowRoots = new Stack(); - - while (component != null) { - if (component instanceof ShadowRoot) { - shadowRoots.push((ShadowRoot)component); + private static final String javaScript = /* lang=js */ """ + function (element, locator, relativeElementCss) { + const child_combinator = " > "; + const node = "/"; + + function clone(element, tag) { + let cloneElement; + if (element instanceof ShadowRoot && !tag) { + cloneElement = new DocumentFragment(); + } else if (tag) { + cloneElement = document.createElement(tag); + } + else { + cloneElement = element.cloneNode(); + if (element.firstChild && element.firstChild.nodeType === 3) { + cloneElement.appendChild(element.firstChild.cloneNode()); + } + } + + if (element.shadowRoot) { + cloneElement.appendChild(clone(element.shadowRoot, "shadow-root")); + } + + if (element.children) { + for (const child of element.children) { + cloneElement.appendChild(clone(child, undefined)); + } + } + + return cloneElement; + } + + function getAbsoluteXpath(element) { + function indexElement(el) { + let index = 1; + + let previousSibling = el.previousElementSibling; + while (previousSibling) { + if (previousSibling.nodeName.toLowerCase() === el.nodeName.toLowerCase()) { + index++; + } + previousSibling = previousSibling.previousElementSibling; + } + + if (el.tagName.toLowerCase() === "shadow-root") { + return node + el.tagName.toLowerCase(); + } else { + return node + el.tagName.toLowerCase() + "[" + index + "]"; + } + } + + let xpath = []; + + let currentElement = element; + while (currentElement) { + if (currentElement.tagName.toLowerCase() === 'html' || currentElement.tagName.toLowerCase() === 'body' || currentElement.tagName.startsWith() === '#' || currentElement.tagName.toLowerCase() === "temporary-div") { + break; + } + + xpath.unshift(indexElement(currentElement)); + + currentElement = currentElement.parentElement; + } + return xpath.join(""); + } + + function getAbsoluteCss(xpath) { + let regex = new RegExp(node, 'g'); + let cssSelector = xpath.replace(regex, child_combinator); + cssSelector = cssSelector.replace(/\\[(\\d+)\\]/g, ':nth-of-type($1)'); + if (cssSelector.startsWith(child_combinator)) { + cssSelector = cssSelector.substring(child_combinator.length); + } + return cssSelector; + } + + const temporaryDiv = document.createElement("temporary-div"); + if (element.shadowRoot) { + temporaryDiv.appendChild(clone(element.shadowRoot, undefined)); + } else { + temporaryDiv.appendChild(clone(element, "shadow-root")); + } + + let startPoint = temporaryDiv; + + if (relativeElementCss) { + startPoint = temporaryDiv.querySelector(relativeElementCss); + } + + let elements; + if (locator.startsWith("/") || locator.startsWith("./") || locator.startsWith("(")) { + let result = document.evaluate(locator, startPoint, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); + elements = []; + let node; + while ((node = result.iterateNext())) { + elements.push(node); + } + } else { + elements = Array.from(startPoint.querySelectorAll(locator)); + } + + let finalLocators = []; + elements.forEach((el) => { + finalLocators.push(getAbsoluteCss(getAbsoluteXpath(el))); + }); + + return finalLocators; + }"""; + + private static final String getInnerHtmlScript = """ + function (element, isShadowRoot) { + const child_combinator = " > "; + const node = "/"; + + function clone(element, tag) { + let cloneElement; + if (element instanceof ShadowRoot && !tag) { + cloneElement = new DocumentFragment(); + } else if (tag) { + cloneElement = document.createElement(tag); + } + else { + cloneElement = element.cloneNode(); + if (element.firstChild && element.firstChild.nodeType === 3) { + cloneElement.appendChild(element.firstChild.cloneNode()); + } + } + + if (element.shadowRoot) { + cloneElement.appendChild(clone(element.shadowRoot, "shadow-root")); + } + + if (element.children) { + for (const child of element.children) { + cloneElement.appendChild(clone(child, undefined)); + } + } + + return cloneElement; + } + + let temporaryDiv = document.createElement("temporary-div"); + if (element.shadowRoot) { + temporaryDiv.appendChild(clone(element.shadowRoot, undefined)); + } else if (isShadowRoot) { + temporaryDiv.appendChild(clone(element, "shadow-root")); + } else { + temporaryDiv.appendChild(clone(element, "redundant-el")); + temporaryDiv = temporaryDiv.querySelector("redundant-el"); + } + + return temporaryDiv.innerHTML; } - component = component.getParentComponent(); - } - - return shadowRoots; - } + """; } diff --git a/bellatrix.web/src/main/java/solutions/bellatrix/web/components/shadowdom/ShadowRoot.java b/bellatrix.web/src/main/java/solutions/bellatrix/web/components/shadowdom/ShadowRoot.java index 38286f0b..1e412c27 100644 --- a/bellatrix.web/src/main/java/solutions/bellatrix/web/components/shadowdom/ShadowRoot.java +++ b/bellatrix.web/src/main/java/solutions/bellatrix/web/components/shadowdom/ShadowRoot.java @@ -30,14 +30,14 @@ public class ShadowRoot extends WebComponent implements ComponentHtml { * Returns the innerHTML of the shadowRoot of the shadow host. */ public String getHtml() { - return ShadowDomService.getShadowHtml(this); + return defaultGetInnerHtmlAttribute(); } @Override protected TComponent create(Class componentClass, TFindStrategy findStrategy) { CREATING_ELEMENT.broadcast(new ComponentActionEventArgs(this)); findElement(); - var component = ShadowDomService.createFromShadowRoot(this, componentClass, findStrategy); + var component = ShadowDomService.createFromShadowRoot(componentClass, this, findStrategy); CREATED_ELEMENT.broadcast(new ComponentActionEventArgs(this)); return component; } @@ -46,7 +46,7 @@ protected protected List createAll(Class componentClass, TFindStrategy findStrategy) { CREATING_ELEMENTS.broadcast(new ComponentActionEventArgs(this)); findElement(); - List componentList = ShadowDomService.createAllFromShadowRoot(this, componentClass, findStrategy); + List componentList = ShadowDomService.createAllFromShadowRoot(componentClass, this, findStrategy); CREATED_ELEMENTS.broadcast(new ComponentActionEventArgs(this)); return componentList; } diff --git a/bellatrix.web/src/main/java/solutions/bellatrix/web/services/JavaScriptService.java b/bellatrix.web/src/main/java/solutions/bellatrix/web/services/JavaScriptService.java index 06a12023..f30b785f 100644 --- a/bellatrix.web/src/main/java/solutions/bellatrix/web/services/JavaScriptService.java +++ b/bellatrix.web/src/main/java/solutions/bellatrix/web/services/JavaScriptService.java @@ -25,6 +25,16 @@ public JavaScriptService() { javascriptExecutor = (JavascriptExecutor)getWrappedDriver(); } + public T genericExecute(String script, Object... args) { + try { + T result = (T)javascriptExecutor.executeScript(script, args); + return result; + } catch (Exception ex) { + DebugInformation.printStackTrace(ex); + return null; + } + } + public Object execute(String script) { try { var result = javascriptExecutor.executeScript(script);