From 7b9649d356b8e0ffb4c7bda5b9dfd23cbe2fd04f Mon Sep 17 00:00:00 2001 From: Aleksey2 Meleshko Date: Tue, 9 May 2023 19:12:20 +0200 Subject: [PATCH 1/2] Support Finding of Multiple elements from ShadowRoot - add JavaScript to generate CSS selector from element - try to generate CSS selector if XPath generation fails - necessary for ShadowRoot elements since XPath doesn't work for them Related to https://github.com/aquality-automation/aquality-selenium-dotnet/issues/235 --- .../aquality/selenium/browser/JavaScript.java | 1 + .../selenium/elements/ElementFactory.java | 25 +++++++++--- .../resources/js/getElementCssSelector.js | 40 +++++++++++++++++++ src/test/java/forms/ChromeDownloadsForm.java | 26 +++++++++++- .../java/tests/usecases/ShadowRootTests.java | 21 ++++++++++ 5 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/js/getElementCssSelector.js diff --git a/src/main/java/aquality/selenium/browser/JavaScript.java b/src/main/java/aquality/selenium/browser/JavaScript.java index b9a4c19..196a1cf 100644 --- a/src/main/java/aquality/selenium/browser/JavaScript.java +++ b/src/main/java/aquality/selenium/browser/JavaScript.java @@ -22,6 +22,7 @@ public enum JavaScript { GET_COMBOBOX_SELECTED_TEXT("getCmbText.js"), GET_COMBOBOX_TEXTS("getCmbValues.js"), GET_ELEMENT_BY_XPATH("getElementByXpath.js"), + GET_ELEMENT_CSS_SELECTOR("getElementCssSelector.js"), GET_ELEMENT_XPATH("getElementXPath.js"), GET_ELEMENT_TEXT("getElementText.js"), GET_TEXT_FIRST_CHILD("getTextFirstChild.js"), diff --git a/src/main/java/aquality/selenium/elements/ElementFactory.java b/src/main/java/aquality/selenium/elements/ElementFactory.java index f63d5ec..d7f3cb6 100644 --- a/src/main/java/aquality/selenium/elements/ElementFactory.java +++ b/src/main/java/aquality/selenium/elements/ElementFactory.java @@ -1,6 +1,5 @@ package aquality.selenium.elements; -import aquality.selenium.browser.AqualityServices; import aquality.selenium.browser.JavaScript; import aquality.selenium.core.elements.interfaces.IElementFinder; import aquality.selenium.core.elements.interfaces.IElementSupplier; @@ -8,23 +7,26 @@ import aquality.selenium.core.waitings.IConditionalWait; import aquality.selenium.elements.interfaces.*; import com.google.inject.Inject; -import org.openqa.selenium.By; +import org.openqa.selenium.*; import org.openqa.selenium.By.ByClassName; import org.openqa.selenium.By.ById; import org.openqa.selenium.By.ByName; -import org.openqa.selenium.WebElement; +import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.support.ByIdOrName; import java.util.HashMap; import java.util.Map; +import java.util.Objects; public class ElementFactory extends aquality.selenium.core.elements.ElementFactory implements IElementFactory { + private final IConditionalWait conditionalWait; private final IElementFinder elementFinder; @Inject public ElementFactory(IConditionalWait conditionalWait, IElementFinder elementFinder, ILocalizationManager localizationManager) { super(conditionalWait, elementFinder, localizationManager); + this.conditionalWait = conditionalWait; this.elementFinder = elementFinder; } @@ -60,9 +62,20 @@ protected Map ((RemoteWebDriver) Objects.requireNonNull(driver)) + .executeScript(JavaScript.GET_ELEMENT_XPATH.getScript(), webElement), "XPath generation failed")); + } + catch (InvalidArgumentException | JavascriptException ex) { + return By.cssSelector((String) conditionalWait.waitFor(driver -> ((RemoteWebDriver) Objects.requireNonNull(driver)) + .executeScript(JavaScript.GET_ELEMENT_CSS_SELECTOR.getScript(), webElement), ex.getMessage() + ". CSS selector generation failed too.")); + } } /** diff --git a/src/main/resources/js/getElementCssSelector.js b/src/main/resources/js/getElementCssSelector.js new file mode 100644 index 0000000..b1d2120 --- /dev/null +++ b/src/main/resources/js/getElementCssSelector.js @@ -0,0 +1,40 @@ +function previousElementSibling (element) { + if (element.previousElementSibling !== 'undefined') { + return element.previousElementSibling; + } else { + // Loop through ignoring anything not an element + while (element = element.previousSibling) { + if (element.nodeType === 1) { + return element; + } + } + } +} +function getCssPath (element) { + // Empty on non-elements + if (!(element instanceof HTMLElement)) { return ''; } + let path = []; + while (element.nodeType === Node.ELEMENT_NODE) { + let selector = element.nodeName; + if (element.id) { selector += ('#' + element.id); } + else { + // Walk backwards until there is no previous sibling + let sibling = element; + // Will hold nodeName to join for adjacent selection + let siblingSelectors = []; + while (sibling !== null && sibling.nodeType === Node.ELEMENT_NODE) { + siblingSelectors.unshift(sibling.nodeName); + sibling = previousElementSibling(sibling); + } + // :first-child does not apply to HTML + if (siblingSelectors[0] !== 'HTML') { + siblingSelectors[0] = siblingSelectors[0] + ':first-child'; + } + selector = siblingSelectors.join(' + '); + } + path.unshift(selector); + element = element.parentNode; + } + return path.join(' > '); +} +return getCssPath(arguments[0]); diff --git a/src/test/java/forms/ChromeDownloadsForm.java b/src/test/java/forms/ChromeDownloadsForm.java index 28ff07e..8ff19ac 100644 --- a/src/test/java/forms/ChromeDownloadsForm.java +++ b/src/test/java/forms/ChromeDownloadsForm.java @@ -6,9 +6,15 @@ import org.openqa.selenium.By; import org.openqa.selenium.SearchContext; +import java.util.List; + public class ChromeDownloadsForm extends Form { private static final String ADDRESS = "chrome://downloads/"; public static final By NESTED_SHADOW_ROOT_LOCATOR = By.id("moreActionsMenu"); + public static final By DIV_ELEMENTS_LOCATOR = By.cssSelector("div"); + + private final ILabel lblDownloadsToolbar = getFormLabel().findElementInShadowRoot(By.cssSelector("downloads-toolbar"), "Downloads toolbar", ILabel.class); + private final ILabel lblMainContainer = getFormLabel().findElementInShadowRoot(By.id("mainContainer"), "Main container", ILabel.class); private final ILabel lblDownloadsToolbarFromJs = getFormLabel().getJsActions().findElementInShadowRoot(By.cssSelector("downloads-toolbar"), "Downloads toolbar", ILabel.class); private final ILabel lblMainContainerFromJs = getFormLabel().getJsActions().findElementInShadowRoot(By.id("mainContainer"), "Main container", ILabel.class); @@ -31,11 +37,11 @@ public SearchContext expandShadowRootViaJs() { } public ILabel getDownloadsToolbarLabel() { - return getFormLabel().findElementInShadowRoot(By.cssSelector("downloads-toolbar"), "Downloads toolbar", ILabel.class); + return lblDownloadsToolbar; } public ILabel getMainContainerLabel() { - return getFormLabel().findElementInShadowRoot(By.id("mainContainer"), "main container", ILabel.class); + return lblMainContainer; } public ILabel getDownloadsToolbarLabelFromJs() { @@ -45,4 +51,20 @@ public ILabel getDownloadsToolbarLabelFromJs() { public ILabel getMainContainerLabelFromJs() { return lblMainContainerFromJs; } + + public List getDivElementLabels() { + return getFormLabel().findElementsInShadowRoot(DIV_ELEMENTS_LOCATOR, "div", ILabel.class); + } + + public List getDivElementLabelsFromJs() { + return getFormLabel().getJsActions().findElementsInShadowRoot(DIV_ELEMENTS_LOCATOR, "div", ILabel.class); + } + + public List getMainContainerLabels() { + return getFormLabel().findElementsInShadowRoot(lblMainContainer.getLocator(), lblMainContainer.getName(), ILabel.class); + } + + public List getMainContainerLabelsFromJs() { + return getFormLabel().getJsActions().findElementsInShadowRoot(lblMainContainer.getLocator(), lblMainContainer.getName(), ILabel.class); + } } diff --git a/src/test/java/tests/usecases/ShadowRootTests.java b/src/test/java/tests/usecases/ShadowRootTests.java index 3422ced..cd257b9 100644 --- a/src/test/java/tests/usecases/ShadowRootTests.java +++ b/src/test/java/tests/usecases/ShadowRootTests.java @@ -2,11 +2,14 @@ import aquality.selenium.elements.interfaces.ILabel; import forms.ChromeDownloadsForm; +import org.openqa.selenium.By; import org.testng.Assert; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import tests.BaseTest; +import java.util.List; + public class ShadowRootTests extends BaseTest { private static final ChromeDownloadsForm form = new ChromeDownloadsForm(); @@ -19,16 +22,34 @@ public void beforeMethod() { @Test public void testExpandShadowRoot() { Assert.assertNotNull(form.expandShadowRoot(), "Should be possible to expand shadow root and get Selenium native ShadowRoot object"); + } + + @Test + public void testFindElementInShadowRoot() { Assert.assertNotNull(form.getDownloadsToolbarLabel().getElement(), "Should be possible do get the element hidden under the shadow"); Assert.assertNotNull(form.getDownloadsToolbarLabel().findElementInShadowRoot(ChromeDownloadsForm.NESTED_SHADOW_ROOT_LOCATOR, "More actions menu", ILabel.class).getElement(), "Should be possible to expand the nested shadow root and get the element from it"); Assert.assertTrue(form.getMainContainerLabel().state().isDisplayed(), "Should be possible to check that element under the shadow is displayed"); } + @Test + public void testFindElementsInShadowRoot() { + List elementLabels = form.getDivElementLabels(); + Assert.assertTrue(elementLabels.size() > 1, "Should be possible to find multiple elements hidden under the shadow"); + Assert.assertTrue(elementLabels.get(0).getLocator() instanceof By.ByCssSelector, "Unique locator of correct type should be generated"); + Assert.assertEquals(elementLabels.get(0).getElement().getTagName(), "div", "Should be possible to work with one of found elements"); + Assert.assertEquals(form.getMainContainerLabels().get(0).getElement().getTagName(), "div", "Should be possible to work with one of found elements found by id"); + } + @Test public void testExpandShadowRootViaJs() { Assert.assertNotNull(form.expandShadowRootViaJs(), "Should be possible to expand shadow root and get Selenium native ShadowRoot object"); Assert.assertNotNull(form.getDownloadsToolbarLabelFromJs().getElement(), "Should be possible do get the element hidden under the shadow"); + List elementLabels = form.getDivElementLabelsFromJs(); + Assert.assertTrue(elementLabels.size() > 1, "Should be possible to find multiple elements hidden under the shadow"); + Assert.assertTrue(elementLabels.get(0).getLocator() instanceof By.ByCssSelector, "Unique locator of correct type should be generated"); + Assert.assertEquals(elementLabels.get(0).getElement().getTagName(), "div", "Should be possible to work with one of found elements"); + Assert.assertEquals(form.getMainContainerLabelsFromJs().get(0).getElement().getTagName(), "div", "Should be possible to work with one of found elements found by id"); Assert.assertNotNull(form.getDownloadsToolbarLabelFromJs().findElementInShadowRoot(ChromeDownloadsForm.NESTED_SHADOW_ROOT_LOCATOR, "More actions menu", ILabel.class).getElement(), "Should be possible to expand the nested shadow root and get the element from it"); Assert.assertTrue(form.getMainContainerLabelFromJs().state().isDisplayed(), "Should be possible to check that element under the shadow is displayed"); From f1025fb9ab59857eebe69b848039503315cf1645 Mon Sep 17 00:00:00 2001 From: Aliaksej Mialeshka Date: Thu, 25 Jan 2024 18:41:48 +0100 Subject: [PATCH 2/2] Update ElementFactory to use generate CSS locator logic in generateLocator method instead of generateXPathLocator --- pom.xml | 2 +- .../selenium/elements/ElementFactory.java | 34 +++++++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index a8d9318..ac2724c 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,7 @@ com.github.aquality-automation aquality-selenium-core - 3.1.1 + 3.1.2 org.apache.commons diff --git a/src/main/java/aquality/selenium/elements/ElementFactory.java b/src/main/java/aquality/selenium/elements/ElementFactory.java index d7f3cb6..ed99699 100644 --- a/src/main/java/aquality/selenium/elements/ElementFactory.java +++ b/src/main/java/aquality/selenium/elements/ElementFactory.java @@ -61,23 +61,35 @@ protected Map ((RemoteWebDriver) Objects.requireNonNull(driver)) - .executeScript(JavaScript.GET_ELEMENT_XPATH.getScript(), webElement), "XPath generation failed")); - } - catch (InvalidArgumentException | JavascriptException ex) { + return generateXpathLocator(multipleElementsLocator, webElement, elementIndex); + } catch (InvalidArgumentException | JavascriptException ex) { return By.cssSelector((String) conditionalWait.waitFor(driver -> ((RemoteWebDriver) Objects.requireNonNull(driver)) .executeScript(JavaScript.GET_ELEMENT_CSS_SELECTOR.getScript(), webElement), ex.getMessage() + ". CSS selector generation failed too.")); } } + /** + * Generates xpath locator for target element. + * + * @param multipleElementsLocator locator used to find elements. + * @param webElement target element. + * @param elementIndex index of target element. + * @return target element's locator + */ + @Override + protected By generateXpathLocator(By multipleElementsLocator, WebElement webElement, int elementIndex) { + if (isLocatorSupportedForXPathExtraction(multipleElementsLocator)) { + By locator = super.generateXpathLocator(multipleElementsLocator, webElement, elementIndex); + if (elementFinder.findElements(locator).size() == 1) { + return locator; + } + } + return By.xpath((String) conditionalWait.waitFor(driver -> ((RemoteWebDriver) Objects.requireNonNull(driver)) + .executeScript(JavaScript.GET_ELEMENT_XPATH.getScript(), webElement), "XPath generation failed")); + } + /** * Defines is the locator can be transformed to xpath or not. *