From 6eb4aa2b5f3f390c4f8314f9d9f86b520018f1e3 Mon Sep 17 00:00:00 2001 From: MiriamKyoseva Date: Thu, 1 May 2025 12:25:52 +0300 Subject: [PATCH 1/7] Fixed shadow dom getHtml logic in case of the current element being ShadowRoot - due to changes in inShadowContext() method, the getHtml never returns the actual shadow dom html if the element is ShadowRoot --- .../bellatrix/web/components/WebComponent.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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); - } } } From 650db550b8b3474877351b4bf130eb15dabaff5e Mon Sep 17 00:00:00 2001 From: MiriamKyoseva Date: Thu, 1 May 2025 12:36:04 +0300 Subject: [PATCH 2/7] Moved retry logic from createAll to only create in ShadowDomService As per the logic in Selenium: - It is a valid case to return an empty list of components - the user should add retry logic to createAll if they intend to wait for some element to appear - the user should write themselves logic to throw an exception if finding 0 elements is unacceptable - an exception should be thrown in case one searches for 1 element and it is not found after retrying for the specified time - the user should try-catch if it happens to be a valid case to not find any element matching the locator --- .../shadowdom/ShadowDomService.java | 77 ++++++++++--------- 1 file changed, 42 insertions(+), 35 deletions(-) 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..03e83ace 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 @@ -20,6 +20,7 @@ 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 +44,16 @@ 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); + if (Wait.retry(() -> { + List foundElements = createAllFromShadowRoot(componentClass, parentComponent, findStrategy); + + if (foundElements.size() == 0) throw new IllegalArgumentException(); + + }, Duration.ofSeconds(ConfigurationService.get(WebSettings.class).getTimeoutSettings().getElementWaitTimeout()), Duration.ofSeconds(1), false)) { + return createAllFromShadowRoot(componentClass, parentComponent, findStrategy).get(0); + } else { + throw new IllegalArgumentException("No elements inside the shadow DOM were found with the findStrategy: " + findStrategy.toString()); + } } public static List createAllFromShadowRoot(Class componentClass, ShadowRoot parentComponent, TFindStrategy findStrategy) { @@ -69,7 +79,16 @@ public static TComponent createInShadowContext(Class componentClass, WebComponent parentComponent, TFindStrategy findStrategy) { - return createAllInShadowContext(componentClass, parentComponent, findStrategy).get(0); + if (Wait.retry(() -> { + List foundElements = createAllInShadowContext(componentClass, parentComponent, findStrategy); + + if (foundElements.size() == 0) throw new IllegalArgumentException(); + + }, Duration.ofSeconds(ConfigurationService.get(WebSettings.class).getTimeoutSettings().getElementWaitTimeout()), Duration.ofSeconds(1), false)) { + return createAllInShadowContext(componentClass, parentComponent, findStrategy).get(0); + } else { + throw new IllegalArgumentException("No elements inside the shadow DOM were found with the findStrategy: " + findStrategy.toString()); + } } public static List createAllInShadowContext(Class componentClass, WebComponent parentComponent, TFindStrategy findStrategy) { @@ -103,7 +122,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 +132,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 +213,7 @@ private static String retraceParentShadowRoots(WebComponent component) { } StringBuilder finalCss = new StringBuilder(); - while(!findStrategies.isEmpty()) { + while (!findStrategies.isEmpty()) { finalCss.append(findStrategies.pop()); } @@ -338,11 +345,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 +361,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 +374,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 +388,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 +411,7 @@ function clone(element, tag) { temporaryDiv.appendChild(clone(element, "redundant-el")); temporaryDiv = temporaryDiv.querySelector("redundant-el"); } - + return temporaryDiv.innerHTML; } """; From 6ca723a3438ac0421b2bba07d11a801d07026ca3 Mon Sep 17 00:00:00 2001 From: MiriamKyoseva Date: Thu, 1 May 2025 12:57:12 +0300 Subject: [PATCH 3/7] Added tests for shadow dom --- .../controls/shadowdom/ShadowDomTests.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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..d6546c53 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 @@ -107,5 +107,24 @@ 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(IllegalArgumentException.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()) + ); + } + // TODO: Test Relative Finding of Elements } From 645305246821ced0a9eb05ea3ee18592ac05259c Mon Sep 17 00:00:00 2001 From: MiriamKyoseva Date: Thu, 1 May 2025 12:57:44 +0300 Subject: [PATCH 4/7] Refactored retry logic to appear only once in ShadowDomService --- .../shadowdom/ShadowDomService.java | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) 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 03e83ace..a30bb362 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 @@ -44,16 +44,7 @@ public static String getShadowHtml(WebComponent shadowComponent, boolean isShado } public static TComponent createFromShadowRoot(Class componentClass, ShadowRoot parentComponent, TFindStrategy findStrategy) { - if (Wait.retry(() -> { - List foundElements = createAllFromShadowRoot(componentClass, parentComponent, findStrategy); - - if (foundElements.size() == 0) throw new IllegalArgumentException(); - - }, Duration.ofSeconds(ConfigurationService.get(WebSettings.class).getTimeoutSettings().getElementWaitTimeout()), Duration.ofSeconds(1), false)) { - return createAllFromShadowRoot(componentClass, parentComponent, findStrategy).get(0); - } else { - throw new IllegalArgumentException("No elements inside the shadow DOM were found with the findStrategy: " + findStrategy.toString()); - } + return retryFindingSingleComponent(() -> createAllFromShadowRoot(componentClass, parentComponent, findStrategy), findStrategy); } public static List createAllFromShadowRoot(Class componentClass, ShadowRoot parentComponent, TFindStrategy findStrategy) { @@ -79,16 +70,7 @@ public static TComponent createInShadowContext(Class componentClass, WebComponent parentComponent, TFindStrategy findStrategy) { - if (Wait.retry(() -> { - List foundElements = createAllInShadowContext(componentClass, parentComponent, findStrategy); - - if (foundElements.size() == 0) throw new IllegalArgumentException(); - - }, Duration.ofSeconds(ConfigurationService.get(WebSettings.class).getTimeoutSettings().getElementWaitTimeout()), Duration.ofSeconds(1), false)) { - return createAllInShadowContext(componentClass, parentComponent, findStrategy).get(0); - } else { - throw new IllegalArgumentException("No elements inside the shadow DOM were found with the findStrategy: " + findStrategy.toString()); - } + return retryFindingSingleComponent(() -> createAllInShadowContext(componentClass, parentComponent, findStrategy), findStrategy); } public static List createAllInShadowContext(Class componentClass, WebComponent parentComponent, TFindStrategy findStrategy) { @@ -261,6 +243,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.size() == 0) 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 IllegalArgumentException("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 = " > "; From 4cdb3534f3c543f5bd1ffb6e0498fe4201bb60b3 Mon Sep 17 00:00:00 2001 From: MiriamKyoseva Date: Thu, 1 May 2025 12:59:51 +0300 Subject: [PATCH 5/7] Changed what exception is thrown when no element is found in the shadow dom --- .../bellatrix/web/components/shadowdom/ShadowDomService.java | 3 ++- .../src/test/java/controls/shadowdom/ShadowDomTests.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 a30bb362..a9b6353c 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,6 +15,7 @@ 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; @@ -261,7 +262,7 @@ private static TComponent retryFindingSingleCo throw new RuntimeException(e); } } else { - throw new IllegalArgumentException("No element inside the shadow DOM was found with the findStrategy: " + findStrategy.toString()); + throw new NoSuchElementException("No element inside the shadow DOM was found with the findStrategy: " + findStrategy.toString()); } } 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 d6546c53..ce3c1e56 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,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openqa.selenium.NoSuchElementException; import solutions.bellatrix.core.configuration.ConfigurationService; import solutions.bellatrix.web.components.Anchor; import solutions.bellatrix.web.components.Div; @@ -112,7 +113,7 @@ public void exceptionThrown_when_tryingToFindNonExistentElement() { var shadowHost = app().create().byId(Div.class, "complexShadowHost"); var shadowRoot = shadowHost.getShadowRoot(); - Assertions.assertThrows(IllegalArgumentException.class, () -> shadowRoot.createByXPath(Div.class, "//nonExistentElement")); + Assertions.assertThrows(NoSuchElementException.class, () -> shadowRoot.createByXPath(Div.class, "//nonExistentElement")); } @Test From ab3077dbe2d567f310428e71d2c28fceb1c23abb Mon Sep 17 00:00:00 2001 From: MiriamKyoseva <133047546+MiriamKyoseva@users.noreply.github.com> Date: Thu, 8 May 2025 14:57:25 +0300 Subject: [PATCH 6/7] added 2 new tests for shadow dom --- .../controls/shadowdom/ShadowDomTests.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) 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 ce3c1e56..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 @@ -5,6 +5,7 @@ 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; @@ -12,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; @@ -127,5 +129,37 @@ public void returnedEmptyList_when_tryingToFindNonExistentElements() { ); } + @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 } From 86f553cf96aafe16b6aed1d895a06d3e3c278bc9 Mon Sep 17 00:00:00 2001 From: MiriamKyoseva <133047546+MiriamKyoseva@users.noreply.github.com> Date: Thu, 8 May 2025 14:58:21 +0300 Subject: [PATCH 7/7] refactored if-statement in ShadowDomService --- .../bellatrix/web/components/shadowdom/ShadowDomService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a9b6353c..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 @@ -253,7 +253,7 @@ private static TComponent retryFindingSingleCo throw new RuntimeException(e); } - if (foundElements.size() == 0) throw new IllegalArgumentException(); + if (foundElements.isEmpty()) throw new IllegalArgumentException(); }, Duration.ofSeconds(ConfigurationService.get(WebSettings.class).getTimeoutSettings().getElementWaitTimeout()), Duration.ofSeconds(1), false)) { try {