diff --git a/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.csproj b/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.csproj index e1af7991..d6ae52f1 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.csproj +++ b/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.csproj @@ -25,6 +25,7 @@ + @@ -49,6 +50,7 @@ + diff --git a/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml b/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml index e975055f..67878770 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml +++ b/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml @@ -1233,21 +1233,9 @@ search context. - - - Finds element in the shadow root of the current element. - - Type of the target element that has to implement . - Locator of the target element. - Note that some browsers don't support XPath locator for shadow elements (e.g. Chrome). - Name of the target element. - Delegate that defines constructor of element. - State of the target element. - Instance of element. - - Perfroms click on element and waits for page is loaded. + Performs click on element and waits for page is loaded. @@ -1728,6 +1716,15 @@ Dictionary where key is interface and value is its implementation. + + + Generates locator for target element + + locator of parent element + target element + index of target element + target element's locator + Generates xpath locator for target element @@ -1931,24 +1928,6 @@ Key for sending. - - - Expands shadow root. - - search context. - - - - Finds element in the shadow root of the current element. - - Type of the target element that has to implement . - Locator of the target element. - Note that some browsers don't support XPath locator for shadow elements. - Name of the target element. - Delegate that defines constructor of element. - State of the target element. - Instance of element. - Defines the interface used to create the elements. @@ -2055,6 +2034,55 @@ True if checked and false otherwise. + + + Shadow Root expander. + + + + + Expands shadow root. + + ShadowRoot search context. + + + + Extensions for Shadow Root expander (like element or JS Actions). + + + + + Provides to find elements in the shadow root of the current element. + + + + + Finds element in the shadow root of the current element. + + Type of the target element that has to implement . + Current instance of the Shadow root expander. + Locator of the target element. + Note that some browsers don't support XPath locator for shadow elements (e.g. Chrome). + Name of the target element. + Delegate that defines constructor of element. + State of the target element. + Instance of element. + + + + Finds elements in the shadow root of the current element. + + Type of the target elements that has to implement . + Current instance of the Shadow root expander. + Locator of target elements. + Note that some browsers don't support XPath locator for shadow elements. + Therefore, we suggest to use CSS selectors + Name of target elements. + Delegate that defines constructor of element. + Expected number of elements that have to be found (zero, more then zero, any). + State of target elements. + List of found elements. + Describes behavior of TextBox UI element. diff --git a/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScript.cs b/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScript.cs index be986b4c..df8dd8bf 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScript.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScript.cs @@ -18,6 +18,7 @@ public enum JavaScript GetElementByXPath, GetElementText, GetElementXPath, + GetElementCssSelector, GetTextFirstChild, IsPageLoaded, MouseHover, diff --git a/Aquality.Selenium/src/Aquality.Selenium/Elements/Actions/JsActions.cs b/Aquality.Selenium/src/Aquality.Selenium/Elements/Actions/JsActions.cs index 61768994..d5988627 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Elements/Actions/JsActions.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Elements/Actions/JsActions.cs @@ -5,7 +5,6 @@ using System.Linq; using Aquality.Selenium.Browsers; using Aquality.Selenium.Configurations; -using Aquality.Selenium.Core.Elements; using Aquality.Selenium.Core.Localization; using Aquality.Selenium.Core.Utilities; using Aquality.Selenium.Elements.Interfaces; @@ -16,7 +15,7 @@ namespace Aquality.Selenium.Elements.Actions /// /// Allows to perform actions on elements via JavaScript. /// - public class JsActions + public class JsActions : IShadowRootExpander { private readonly IElement element; private readonly string elementType; @@ -47,25 +46,7 @@ public ShadowRoot ExpandShadowRoot() } /// - /// Finds element in the shadow root of the current element. - /// - /// Type of the target element that has to implement . - /// Locator of the target element. - /// Note that some browsers don't support XPath locator for shadow elements (e.g. Chrome). - /// Name of the target element. - /// Delegate that defines constructor of element. - /// State of the target element. - /// Instance of element. - public T FindElementInShadowRoot(By locator, string name, ElementSupplier supplier = null, ElementState state = ElementState.Displayed) - where T : IElement - { - var shadowRootRelativeFinder = new RelativeElementFinder(Logger, AqualityServices.ConditionalWait, ExpandShadowRoot); - var shadowRootFactory = new ElementFactory(AqualityServices.ConditionalWait, shadowRootRelativeFinder, AqualityServices.Get()); - return shadowRootFactory.Get(locator, name, supplier, state); - } - - /// - /// Perfroms click on element and waits for page is loaded. + /// Performs click on element and waits for page is loaded. /// public void ClickAndWait() { diff --git a/Aquality.Selenium/src/Aquality.Selenium/Elements/Element.cs b/Aquality.Selenium/src/Aquality.Selenium/Elements/Element.cs index 2af48bd4..d77af92a 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Elements/Element.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Elements/Element.cs @@ -16,6 +16,7 @@ using ICoreElementFactory = Aquality.Selenium.Core.Elements.Interfaces.IElementFactory; using ICoreElementFinder = Aquality.Selenium.Core.Elements.Interfaces.IElementFinder; using ICoreElementStateProvider = Aquality.Selenium.Core.Elements.Interfaces.IElementStateProvider; +using System.Collections.Generic; namespace Aquality.Selenium.Elements { @@ -134,13 +135,5 @@ public ShadowRoot ExpandShadowRoot() var shadowRoot = (ShadowRoot)GetElement().GetShadowRoot(); return shadowRoot; } - - public T FindElementInShadowRoot(By locator, string name, ElementSupplier supplier = null, ElementState state = ElementState.Displayed) - where T : IElement - { - var shadowRootRelativeFinder = new RelativeElementFinder(LocalizedLogger, ConditionalWait, ExpandShadowRoot); - var shadowRootFactory = new ElementFactory(ConditionalWait, shadowRootRelativeFinder, LocalizationManager); - return shadowRootFactory.Get(locator, name, supplier, state); - } } } diff --git a/Aquality.Selenium/src/Aquality.Selenium/Elements/ElementFactory.cs b/Aquality.Selenium/src/Aquality.Selenium/Elements/ElementFactory.cs index 71b85325..affb36ee 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Elements/ElementFactory.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Elements/ElementFactory.cs @@ -113,6 +113,26 @@ protected override IDictionary ElementTypesMap } } + /// + /// Generates locator for target element + /// + /// locator of parent element + /// target element + /// index of target element + /// target element's locator + protected override By GenerateLocator(By baseLocator, IWebElement webElement, int elementIndex) + { + try + { + return GenerateXpathLocator(baseLocator, webElement, elementIndex); + } + catch (WebDriverException ex) + { + return By.CssSelector(ConditionalWait.WaitFor(driver => driver.ExecuteJavaScript( + JavaScript.GetElementCssSelector.GetScript(), webElement), message: $"{ex.Message}. CSS selector generation failed too.")); + } + } + /// /// Generates xpath locator for target element /// @@ -122,10 +142,16 @@ protected override IDictionary ElementTypesMap /// target element's locator protected override By GenerateXpathLocator(By baseLocator, IWebElement webElement, int elementIndex) { - return IsLocatorSupportedForXPathExtraction(baseLocator) - ? base.GenerateXpathLocator(baseLocator, webElement, elementIndex) - : By.XPath(ConditionalWait.WaitFor(driver => driver.ExecuteJavaScript( - JavaScript.GetElementXPath.GetScript(), webElement), message: "XPath generation failed")); + if (IsLocatorSupportedForXPathExtraction(baseLocator)) + { + var locator = base.GenerateXpathLocator(baseLocator, webElement, elementIndex); + if (ElementFinder.FindElements(locator).Count == 1) + { + return locator; + } + } + return By.XPath(ConditionalWait.WaitFor(driver => driver.ExecuteJavaScript( + JavaScript.GetElementXPath.GetScript(), webElement), message: "XPath generation failed")); } /// diff --git a/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IElement.cs b/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IElement.cs index c3745270..9fa1997d 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IElement.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IElement.cs @@ -1,6 +1,4 @@ -using Aquality.Selenium.Core.Elements; -using Aquality.Selenium.Elements.Actions; -using OpenQA.Selenium; +using Aquality.Selenium.Elements.Actions; using ICoreElement = Aquality.Selenium.Core.Elements.Interfaces.IElement; namespace Aquality.Selenium.Elements.Interfaces @@ -8,7 +6,7 @@ namespace Aquality.Selenium.Elements.Interfaces /// /// Describes behavior of any UI element. /// - public interface IElement : ICoreElement + public interface IElement : ICoreElement, IShadowRootExpander { /// /// Gets JavaScript actions that can be performed with an element. @@ -74,24 +72,5 @@ public interface IElement : ICoreElement /// /// Key for sending. void SendKey(Key key); - - /// - /// Expands shadow root. - /// - /// search context. - ShadowRoot ExpandShadowRoot(); - - /// - /// Finds element in the shadow root of the current element. - /// - /// Type of the target element that has to implement . - /// Locator of the target element. - /// Note that some browsers don't support XPath locator for shadow elements. - /// Name of the target element. - /// Delegate that defines constructor of element. - /// State of the target element. - /// Instance of element. - T FindElementInShadowRoot(By locator, string name, ElementSupplier supplier = null, ElementState state = ElementState.Displayed) - where T : IElement; } } diff --git a/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IShadowRootExpander.cs b/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IShadowRootExpander.cs new file mode 100644 index 00000000..1b924b24 --- /dev/null +++ b/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IShadowRootExpander.cs @@ -0,0 +1,71 @@ +using Aquality.Selenium.Browsers; +using Aquality.Selenium.Core.Elements; +using Aquality.Selenium.Core.Localization; +using OpenQA.Selenium; +using System.Collections.Generic; + +namespace Aquality.Selenium.Elements.Interfaces +{ + /// + /// Shadow Root expander. + /// + public interface IShadowRootExpander + { + /// + /// Expands shadow root. + /// + /// ShadowRoot search context. + ShadowRoot ExpandShadowRoot(); + } + + /// + /// Extensions for Shadow Root expander (like element or JS Actions). + /// + public static class ShadowRootExpanderExtensions + { + /// + /// Provides to find elements in the shadow root of the current element. + /// + public static IElementFactory GetShadowRootElementFactory(this IShadowRootExpander shadowRootExpander) + { + var shadowRootRelativeFinder = new RelativeElementFinder(AqualityServices.LocalizedLogger, AqualityServices.ConditionalWait, shadowRootExpander.ExpandShadowRoot); + return new ElementFactory(AqualityServices.ConditionalWait, shadowRootRelativeFinder, AqualityServices.Get()); + } + + /// + /// Finds element in the shadow root of the current element. + /// + /// Type of the target element that has to implement . + /// Current instance of the Shadow root expander. + /// Locator of the target element. + /// Note that some browsers don't support XPath locator for shadow elements (e.g. Chrome). + /// Name of the target element. + /// Delegate that defines constructor of element. + /// State of the target element. + /// Instance of element. + public static T FindElementInShadowRoot(this IShadowRootExpander shadowRootExpander, By locator, string name, ElementSupplier supplier = null, ElementState state = ElementState.Displayed) + where T : IElement + { + return shadowRootExpander.GetShadowRootElementFactory().Get(locator, name, supplier, state); + } + + /// + /// Finds elements in the shadow root of the current element. + /// + /// Type of the target elements that has to implement . + /// Current instance of the Shadow root expander. + /// Locator of target elements. + /// Note that some browsers don't support XPath locator for shadow elements. + /// Therefore, we suggest to use CSS selectors + /// Name of target elements. + /// Delegate that defines constructor of element. + /// Expected number of elements that have to be found (zero, more then zero, any). + /// State of target elements. + /// List of found elements. + public static IList FindElementsInShadowRoot(this IShadowRootExpander shadowRootExpander, By locator, string name = null, ElementSupplier supplier = null, ElementsCount expectedCount = ElementsCount.Any, ElementState state = ElementState.Displayed) + where T : IElement + { + return shadowRootExpander.GetShadowRootElementFactory().FindElements(locator, name, supplier, expectedCount, state); + } + } +} diff --git a/Aquality.Selenium/src/Aquality.Selenium/Resources/JavaScripts/GetElementCssSelector.js b/Aquality.Selenium/src/Aquality.Selenium/Resources/JavaScripts/GetElementCssSelector.js new file mode 100644 index 00000000..b1d21207 --- /dev/null +++ b/Aquality.Selenium/src/Aquality.Selenium/Resources/JavaScripts/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/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/ShadowRootTests.cs b/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/ShadowRootTests.cs index 37180d0e..599fff58 100644 --- a/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/ShadowRootTests.cs +++ b/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/ShadowRootTests.cs @@ -1,6 +1,7 @@ using Aquality.Selenium.Elements.Interfaces; using Aquality.Selenium.Tests.Integration.TestApp.Browser.Forms; using NUnit.Framework; +using System.Linq; namespace Aquality.Selenium.Tests.Integration { @@ -17,18 +18,38 @@ public void OpenDownloads() [Test] public void Should_ExpandShadowRoot_FromElement() { - Assert.IsNotNull(form.ExpandShadowRoot(), "Should be possible to expand shadow root and get Selenium native ShadowRoot object"); + Assert.IsNotNull(form.ExpandShadowRoot(), "Should be possible to expand shadow root and get Selenium native ShadowRoot object"); + } + + [Test] + public void Should_BePossibleTo_FindElement_InShadowRoot() + { Assert.IsNotNull(form.DownloadsToolbarLabel.GetElement(), "Should be possible do get the element hidden under the shadow"); Assert.IsNotNull(form.DownloadsToolbarLabel.FindElementInShadowRoot(ChromeDownloadsForm.NestedShadowRootLocator, "More actions menu").GetElement(), - "Should be possible to expand the nested shadow root and get the element from it"); + "Should be possible to expand the nested shadow root and get the element from it"); Assert.IsTrue(form.MainContainerLabel.State.IsDisplayed, "Should be possible to check that element under the shadow is displayed"); } + [Test] + public void Should_BePossibleTo_FindElements_InShadowRoot() + { + var elementLabels = form.DivElementLabels; + Assert.That(elementLabels, Has.Count.GreaterThan(1), "Should be possible to find multiple elements hidden under the shadow"); + Assert.That(elementLabels.First().Locator.Mechanism, Contains.Substring("css"), "Unique locator of correct type should be generated"); + Assert.That(elementLabels.First().GetElement().TagName, Is.EqualTo("div"), "Should be possible to work with one of found elements"); + Assert.That(form.MainContainerLabels.First().GetElement().TagName, Is.EqualTo("div"), "Should be possible to work with one of found elements found by id"); + } + [Test] public void ShouldBePossibleTo_ExpandShadowRoot_ViaJs() { Assert.IsNotNull(form.ExpandShadowRootViaJs(), "Should be possible to expand shadow root and get Selenium native ShadowRoot object"); Assert.IsNotNull(form.DownloadsToolbarLabelFromJs.GetElement(), "Should be possible do get the element hidden under the shadow"); + var elementLabels = form.DivElementLabelsFromJs; + Assert.That(elementLabels, Has.Count.GreaterThan(1), "Should be possible to find multiple elements hidden under the shadow"); + Assert.That(elementLabels.First().Locator.Mechanism, Contains.Substring("css"), "Unique locator of correct type should be generated"); + Assert.That(elementLabels.First().GetElement().TagName, Is.EqualTo("div"), "Should be possible to work with one of found elements"); + Assert.That(form.MainContainerLabelsFromJs.First().GetElement().TagName, Is.EqualTo("div"), "Should be possible to work with one of found elements found by id"); Assert.IsNotNull(form.DownloadsToolbarLabelFromJs.JsActions.FindElementInShadowRoot(ChromeDownloadsForm.NestedShadowRootLocator, "More actions menu").GetElement(), "Should be possible to expand the nested shadow root and get the element from it"); Assert.IsTrue(form.MainContainerLabelFromJs.State.IsDisplayed, "Should be possible to check that element under the shadow is displayed"); diff --git a/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/TestApp/Browser/ChromeDownloadsForm.cs b/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/TestApp/Browser/ChromeDownloadsForm.cs index b86af3a7..b370d487 100644 --- a/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/TestApp/Browser/ChromeDownloadsForm.cs +++ b/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/TestApp/Browser/ChromeDownloadsForm.cs @@ -2,6 +2,7 @@ using Aquality.Selenium.Elements.Interfaces; using Aquality.Selenium.Forms; using OpenQA.Selenium; +using System.Collections.Generic; namespace Aquality.Selenium.Tests.Integration.TestApp.Browser.Forms { @@ -11,8 +12,12 @@ internal class ChromeDownloadsForm : Form public static By NestedShadowRootLocator => By.Id("moreActionsMenu"); public ILabel DownloadsToolbarLabel => FormElement.FindElementInShadowRoot(By.CssSelector("downloads-toolbar"), "Downloads toolbar"); + public IList DivElementLabels => FormElement.FindElementsInShadowRoot(By.CssSelector("div"), "div"); + public IList MainContainerLabels => FormElement.FindElementsInShadowRoot(By.Id("mainContainer"), "main container"); public ILabel MainContainerLabel => FormElement.FindElementInShadowRoot(By.Id("mainContainer"), "main container"); public ILabel DownloadsToolbarLabelFromJs => FormElement.JsActions.FindElementInShadowRoot(By.CssSelector("downloads-toolbar"), "Downloads toolbar"); + public IList DivElementLabelsFromJs => FormElement.JsActions.FindElementsInShadowRoot(By.CssSelector("div"), "div"); + public IList MainContainerLabelsFromJs => FormElement.JsActions.FindElementsInShadowRoot(By.Id("mainContainer"), "main container"); public ILabel MainContainerLabelFromJs => FormElement.JsActions.FindElementInShadowRoot(By.Id("mainContainer"), "Main container"); public ChromeDownloadsForm() : base(By.TagName("downloads-manager"), "Chrome downloads manager")