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 ceee3c8f..aae81ff8 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 @@ -1081,18 +1081,16 @@ protected String defaultGetWidthAttribute() { } protected String defaultGetInnerHtmlAttribute() { - if (!this.inShadowContext()) { + if (this instanceof ShadowRoot) { + return ShadowDomService.getShadowHtml(this, true); + } else if (this.inShadowContext()) { + return ShadowDomService.getShadowHtml(this, false); + } else { 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 ebee3405..c16d00f1 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 @@ -15,11 +15,13 @@ import lombok.experimental.UtilityClass; import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; import solutions.bellatrix.core.configuration.ConfigurationService; 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.components.contracts.Component; import solutions.bellatrix.web.configuration.WebSettings; import solutions.bellatrix.web.findstrategies.CssFindStrategy; import solutions.bellatrix.web.findstrategies.FindStrategy; @@ -43,7 +45,7 @@ public static String getShadowHtml(WebComponent shadowComponent, boolean isShado } public static TComponent createFromShadowRoot(Class componentClass, ShadowRoot parentComponent, TFindStrategy findStrategy) { - return createAllFromShadowRoot(componentClass, parentComponent, findStrategy).get(0); + return retryFindingSingleComponent(() -> createAllFromShadowRoot(componentClass, parentComponent, findStrategy), findStrategy); } public static List createAllFromShadowRoot(Class componentClass, ShadowRoot parentComponent, TFindStrategy findStrategy) { @@ -69,7 +71,7 @@ public static TComponent createInShadowContext(Class componentClass, WebComponent parentComponent, TFindStrategy findStrategy) { - return createAllInShadowContext(componentClass, parentComponent, findStrategy).get(0); + return retryFindingSingleComponent(() -> createAllInShadowContext(componentClass, parentComponent, findStrategy), findStrategy); } public static List createAllInShadowContext(Class componentClass, WebComponent parentComponent, TFindStrategy findStrategy) { @@ -103,7 +105,7 @@ private static String[] getAbsoluteCss(ShadowRoot shadowRoot, String locator) { shadowRoot.findElement(), locator, null).toArray(String[]::new); }; - return getCss(js, locator); + return getCss(js); } private static String[] getRelativeCss(ShadowRoot shadowRoot, String locator, String parentLocator) { @@ -113,30 +115,18 @@ private static String[] getRelativeCss(ShadowRoot shadowRoot, String locator, St shadowRoot.findElement(), locator, parentLocator).toArray(String[]::new); }; - return getCss(js, locator); + return getCss(js); } - private static String[] getCss(Callable callable, String locator) { - if (Wait.retry(() -> { - String[] foundElements; - try { - foundElements = callable.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 callable.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } else { - throw new IllegalArgumentException("No elements inside the shadow DOM were found with the locator: " + locator); + private static String[] getCss(Callable callable) { + String[] foundElements; + try { + foundElements = callable.call(); + } catch (Exception e) { + throw new RuntimeException(e); } + + return foundElements; } private static TComponent buildMissingShadowRootsAndCreate(Class clazz, ShadowRoot parentComponent, Ref fullCss) { @@ -206,7 +196,7 @@ private static String retraceParentShadowRoots(WebComponent component) { } StringBuilder finalCss = new StringBuilder(); - while(!findStrategies.isEmpty()) { + while (!findStrategies.isEmpty()) { finalCss.append(findStrategies.pop()); } @@ -254,6 +244,28 @@ private static String convertToCssOrXpath(FindStrategy findStrategy) { return null; } + private static TComponent retryFindingSingleComponent(Callable> callable, FindStrategy findStrategy) { + if (Wait.retry(() -> { + List foundElements; + try { + foundElements = callable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + if (foundElements.isEmpty()) throw new IllegalArgumentException(); + + }, Duration.ofSeconds(ConfigurationService.get(WebSettings.class).getTimeoutSettings().getElementWaitTimeout()), Duration.ofSeconds(1), false)) { + try { + return callable.call().get(0); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + throw new NoSuchElementException("No element inside the shadow DOM was found with the findStrategy: " + findStrategy.toString()); + } + } + private static final String javaScript = /* lang=js */ """ function (element, locator, relativeElementCss) { const child_combinator = " > "; @@ -338,11 +350,11 @@ function getAbsoluteCss(xpath) { } 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); @@ -354,12 +366,12 @@ function getAbsoluteCss(xpath) { } else { elements = Array.from(startPoint.querySelectorAll(locator)); } - + let finalLocators = []; elements.forEach((el) => { finalLocators.push(getAbsoluteCss(getAbsoluteXpath(el))); }); - + return finalLocators; }"""; @@ -367,7 +379,7 @@ function getAbsoluteCss(xpath) { function (element, isShadowRoot) { const child_combinator = " > "; const node = "/"; - + function clone(element, tag) { let cloneElement; if (element instanceof ShadowRoot && !tag) { @@ -381,20 +393,20 @@ function clone(element, tag) { 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)); @@ -404,7 +416,7 @@ function clone(element, tag) { temporaryDiv.appendChild(clone(element, "redundant-el")); temporaryDiv = temporaryDiv.querySelector("redundant-el"); } - + return temporaryDiv.innerHTML; } """; diff --git a/framework-tests/bellatrix.web.tests/src/test/java/controls/shadowdom/ShadowDomTests.java b/framework-tests/bellatrix.web.tests/src/test/java/controls/shadowdom/ShadowDomTests.java index e4df5642..ae5f5c71 100644 --- a/framework-tests/bellatrix.web.tests/src/test/java/controls/shadowdom/ShadowDomTests.java +++ b/framework-tests/bellatrix.web.tests/src/test/java/controls/shadowdom/ShadowDomTests.java @@ -4,6 +4,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openqa.selenium.NoSuchElementException; +import org.testng.asserts.Assertion; import solutions.bellatrix.core.configuration.ConfigurationService; import solutions.bellatrix.web.components.Anchor; import solutions.bellatrix.web.components.Div; @@ -11,6 +13,7 @@ import solutions.bellatrix.web.components.advanced.grid.Grid; import solutions.bellatrix.web.components.advanced.grid.GridCell; import solutions.bellatrix.web.components.shadowdom.ShadowRoot; +import solutions.bellatrix.web.configuration.WebSettings; import solutions.bellatrix.web.infrastructure.Browser; import solutions.bellatrix.web.infrastructure.ExecutionBrowser; import solutions.bellatrix.web.infrastructure.Lifecycle; @@ -107,5 +110,56 @@ public void findingElementByAnotherElementInNestedShadowRoot_withCss() { Assertions.assertEquals("edit", edit.getText()); } + @Test + public void exceptionThrown_when_tryingToFindNonExistentElement() { + var shadowHost = app().create().byId(Div.class, "complexShadowHost"); + var shadowRoot = shadowHost.getShadowRoot(); + + Assertions.assertThrows(NoSuchElementException.class, () -> shadowRoot.createByXPath(Div.class, "//nonExistentElement")); + } + + @Test + public void returnedEmptyList_when_tryingToFindNonExistentElements() { + var shadowHost = app().create().byId(Div.class, "complexShadowHost"); + var shadowRoot = shadowHost.getShadowRoot(); + + Assertions.assertAll( + () -> Assertions.assertDoesNotThrow(() -> shadowRoot.createAllByXPath(Div.class, "//nonExistentElement")), + () -> Assertions.assertTrue(shadowRoot.createAllByXPath(Div.class, "//nonExistentElement").isEmpty()) + ); + } + + @Test + public void waitedTimeout_when_tryingToFindNonExistentElement() { + var shadowHost = app().create().byId(Div.class, "complexShadowHost"); + var shadowRoot = shadowHost.getShadowRoot(); + + long startTime = System.currentTimeMillis(); + try { + shadowRoot.createByXPath(Div.class, "//nonExistentElement"); + } catch (NoSuchElementException ignored) { + var elapsedTime = System.currentTimeMillis() - startTime; + + Assertions.assertTrue(elapsedTime > ConfigurationService.get(WebSettings.class).getTimeoutSettings().getElementWaitTimeout()*1000); + } + } + + @Test + public void returnedEmptyListWithoutWaiting_when_tryingToFindNonExistentElements() { + var shadowHost = app().create().byId(Div.class, "complexShadowHost"); + var shadowRoot = shadowHost.getShadowRoot(); + + long startTime = System.currentTimeMillis(); + + var isEmpty = shadowRoot.createAllByXPath(Div.class, "//nonExistentElement").isEmpty(); + + var elapsedTime = System.currentTimeMillis() - startTime; + + Assertions.assertAll( + () -> Assertions.assertTrue(isEmpty), + () -> Assertions.assertTrue(elapsedTime < ConfigurationService.get(WebSettings.class).getTimeoutSettings().getElementWaitTimeout()*1000) + ); + } + // TODO: Test Relative Finding of Elements }