Skip to content

Commit

Permalink
Support Finding of Multiple elements from ShadowRoot (#236)
Browse files Browse the repository at this point in the history
* 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

* Replace GetElementCssSelector for old browsers compatibility (like InternetExplorer)

* refactor JavaScript function to generate CSS selector from element

* Refactoring, Introduce IShadowRootExpander interface to reduce duplication

* move common code to IShadowRootExpander extensions to reduce duplications

* Fix locator generation in case when generated XPath was invalid (e.g. when use FindElements with By.Id  or By.ClassName locator)

* Update ElementFactory to use Generate CSS locator logic in GenerateLocator method instead of GenerateXPathLocator
  • Loading branch information
mialeska authored Jan 25, 2024
1 parent b388421 commit e0f84d7
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

<ItemGroup>
<None Remove="Resources\JavaScripts\ExpandShadowRoot.js" />
<None Remove="Resources\JavaScripts\GetElementCssSelector.js" />
<None Remove="Resources\JavaScripts\SetAttribute.js" />
<None Remove="Resources\Localization\be.json" />
<None Remove="Resources\Localization\en.json" />
Expand All @@ -49,6 +50,7 @@
<EmbeddedResource Include="Resources\JavaScripts\GetElementByXPath.js" />
<EmbeddedResource Include="Resources\JavaScripts\ExpandShadowRoot.js" />
<EmbeddedResource Include="Resources\JavaScripts\GetElementText.js" />
<EmbeddedResource Include="Resources\JavaScripts\GetElementCssSelector.js" />
<EmbeddedResource Include="Resources\JavaScripts\GetElementXPath.js" />
<EmbeddedResource Include="Resources\JavaScripts\GetTextFirstChild.js" />
<EmbeddedResource Include="Resources\JavaScripts\GetViewPortCoordinates.js" />
Expand Down
90 changes: 59 additions & 31 deletions Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public enum JavaScript
GetElementByXPath,
GetElementText,
GetElementXPath,
GetElementCssSelector,
GetTextFirstChild,
IsPageLoaded,
MouseHover,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,7 +15,7 @@ namespace Aquality.Selenium.Elements.Actions
/// <summary>
/// Allows to perform actions on elements via JavaScript.
/// </summary>
public class JsActions
public class JsActions : IShadowRootExpander
{
private readonly IElement element;
private readonly string elementType;
Expand Down Expand Up @@ -47,25 +46,7 @@ public ShadowRoot ExpandShadowRoot()
}

/// <summary>
/// Finds element in the shadow root of the current element.
/// </summary>
/// <typeparam name="T">Type of the target element that has to implement <see cref="IElement"/>.</typeparam>
/// <param name="locator">Locator of the target element.
/// Note that some browsers don't support XPath locator for shadow elements (e.g. Chrome).</param>
/// <param name="name">Name of the target element.</param>
/// <param name="supplier">Delegate that defines constructor of element.</param>
/// <param name="state">State of the target element.</param>
/// <returns>Instance of element.</returns>
public T FindElementInShadowRoot<T>(By locator, string name, ElementSupplier<T> 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<ILocalizationManager>());
return shadowRootFactory.Get(locator, name, supplier, state);
}

/// <summary>
/// Perfroms click on element and waits for page is loaded.
/// Performs click on element and waits for page is loaded.
/// </summary>
public void ClickAndWait()
{
Expand Down
9 changes: 1 addition & 8 deletions Aquality.Selenium/src/Aquality.Selenium/Elements/Element.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -134,13 +135,5 @@ public ShadowRoot ExpandShadowRoot()
var shadowRoot = (ShadowRoot)GetElement().GetShadowRoot();
return shadowRoot;
}

public T FindElementInShadowRoot<T>(By locator, string name, ElementSupplier<T> 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);
}
}
}
34 changes: 30 additions & 4 deletions Aquality.Selenium/src/Aquality.Selenium/Elements/ElementFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,26 @@ protected override IDictionary<Type, Type> ElementTypesMap
}
}

/// <summary>
/// Generates locator for target element
/// </summary>
/// <param name="baseLocator">locator of parent element</param>
/// <param name="webElement">target element</param>
/// <param name="elementIndex">index of target element</param>
/// <returns>target element's locator</returns>
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<string>(
JavaScript.GetElementCssSelector.GetScript(), webElement), message: $"{ex.Message}. CSS selector generation failed too."));
}
}

/// <summary>
/// Generates xpath locator for target element
/// </summary>
Expand All @@ -122,10 +142,16 @@ protected override IDictionary<Type, Type> ElementTypesMap
/// <returns>target element's locator</returns>
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<string>(
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<string>(
JavaScript.GetElementXPath.GetScript(), webElement), message: "XPath generation failed"));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
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
{
/// <summary>
/// Describes behavior of any UI element.
/// </summary>
public interface IElement : ICoreElement
public interface IElement : ICoreElement, IShadowRootExpander
{
/// <summary>
/// Gets JavaScript actions that can be performed with an element.
Expand Down Expand Up @@ -74,24 +72,5 @@ public interface IElement : ICoreElement
/// </summary>
/// <param name="key"> Key for sending.</param>
void SendKey(Key key);

/// <summary>
/// Expands shadow root.
/// </summary>
/// <returns><see cref="ShadowRoot"/> search context.</returns>
ShadowRoot ExpandShadowRoot();

/// <summary>
/// Finds element in the shadow root of the current element.
/// </summary>
/// <typeparam name="T">Type of the target element that has to implement <see cref="IElement"/>.</typeparam>
/// <param name="locator">Locator of the target element.
/// Note that some browsers don't support XPath locator for shadow elements.</param>
/// <param name="name">Name of the target element.</param>
/// <param name="supplier">Delegate that defines constructor of element.</param>
/// <param name="state">State of the target element.</param>
/// <returns>Instance of element.</returns>
T FindElementInShadowRoot<T>(By locator, string name, ElementSupplier<T> supplier = null, ElementState state = ElementState.Displayed)
where T : IElement;
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Shadow Root expander.
/// </summary>
public interface IShadowRootExpander
{
/// <summary>
/// Expands shadow root.
/// </summary>
/// <returns>ShadowRoot search context.</returns>
ShadowRoot ExpandShadowRoot();
}

/// <summary>
/// Extensions for Shadow Root expander (like element or JS Actions).
/// </summary>
public static class ShadowRootExpanderExtensions
{
/// <summary>
/// Provides <see cref="IElementFactory"/> to find elements in the shadow root of the current element.
/// </summary>
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<ILocalizationManager>());
}

/// <summary>
/// Finds element in the shadow root of the current element.
/// </summary>
/// <typeparam name="T">Type of the target element that has to implement <see cref="IElement"/>.</typeparam>
/// <param name="shadowRootExpander">Current instance of the Shadow root expander.</param>
/// <param name="locator">Locator of the target element.
/// Note that some browsers don't support XPath locator for shadow elements (e.g. Chrome).</param>
/// <param name="name">Name of the target element.</param>
/// <param name="supplier">Delegate that defines constructor of element.</param>
/// <param name="state">State of the target element.</param>
/// <returns>Instance of element.</returns>
public static T FindElementInShadowRoot<T>(this IShadowRootExpander shadowRootExpander, By locator, string name, ElementSupplier<T> supplier = null, ElementState state = ElementState.Displayed)
where T : IElement
{
return shadowRootExpander.GetShadowRootElementFactory().Get(locator, name, supplier, state);
}

/// <summary>
/// Finds elements in the shadow root of the current element.
/// </summary>
/// <typeparam name="T">Type of the target elements that has to implement <see cref="IElement"/>.</typeparam>
/// <param name="shadowRootExpander">Current instance of the Shadow root expander.</param>
/// <param name="locator">Locator of target elements.
/// Note that some browsers don't support XPath locator for shadow elements.
/// Therefore, we suggest to use CSS selectors</param>
/// <param name="name">Name of target elements.</param>
/// <param name="supplier">Delegate that defines constructor of element.</param>
/// <param name="expectedCount">Expected number of elements that have to be found (zero, more then zero, any).</param>
/// <param name="state">State of target elements.</param>
/// <returns>List of found elements.</returns>
public static IList<T> FindElementsInShadowRoot<T>(this IShadowRootExpander shadowRootExpander, By locator, string name = null, ElementSupplier<T> supplier = null, ElementsCount expectedCount = ElementsCount.Any, ElementState state = ElementState.Displayed)
where T : IElement
{
return shadowRootExpander.GetShadowRootElementFactory().FindElements(locator, name, supplier, expectedCount, state);
}
}
}
Loading

0 comments on commit e0f84d7

Please sign in to comment.