Skip to content

Commit e0f84d7

Browse files
authored
Support Finding of Multiple elements from ShadowRoot (#236)
* 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
1 parent b388421 commit e0f84d7

File tree

11 files changed

+236
-89
lines changed

11 files changed

+236
-89
lines changed

Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
<ItemGroup>
2727
<None Remove="Resources\JavaScripts\ExpandShadowRoot.js" />
28+
<None Remove="Resources\JavaScripts\GetElementCssSelector.js" />
2829
<None Remove="Resources\JavaScripts\SetAttribute.js" />
2930
<None Remove="Resources\Localization\be.json" />
3031
<None Remove="Resources\Localization\en.json" />
@@ -49,6 +50,7 @@
4950
<EmbeddedResource Include="Resources\JavaScripts\GetElementByXPath.js" />
5051
<EmbeddedResource Include="Resources\JavaScripts\ExpandShadowRoot.js" />
5152
<EmbeddedResource Include="Resources\JavaScripts\GetElementText.js" />
53+
<EmbeddedResource Include="Resources\JavaScripts\GetElementCssSelector.js" />
5254
<EmbeddedResource Include="Resources\JavaScripts\GetElementXPath.js" />
5355
<EmbeddedResource Include="Resources\JavaScripts\GetTextFirstChild.js" />
5456
<EmbeddedResource Include="Resources\JavaScripts\GetViewPortCoordinates.js" />

Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml

Lines changed: 59 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScript.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public enum JavaScript
1818
GetElementByXPath,
1919
GetElementText,
2020
GetElementXPath,
21+
GetElementCssSelector,
2122
GetTextFirstChild,
2223
IsPageLoaded,
2324
MouseHover,

Aquality.Selenium/src/Aquality.Selenium/Elements/Actions/JsActions.cs

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Linq;
66
using Aquality.Selenium.Browsers;
77
using Aquality.Selenium.Configurations;
8-
using Aquality.Selenium.Core.Elements;
98
using Aquality.Selenium.Core.Localization;
109
using Aquality.Selenium.Core.Utilities;
1110
using Aquality.Selenium.Elements.Interfaces;
@@ -16,7 +15,7 @@ namespace Aquality.Selenium.Elements.Actions
1615
/// <summary>
1716
/// Allows to perform actions on elements via JavaScript.
1817
/// </summary>
19-
public class JsActions
18+
public class JsActions : IShadowRootExpander
2019
{
2120
private readonly IElement element;
2221
private readonly string elementType;
@@ -47,25 +46,7 @@ public ShadowRoot ExpandShadowRoot()
4746
}
4847

4948
/// <summary>
50-
/// Finds element in the shadow root of the current element.
51-
/// </summary>
52-
/// <typeparam name="T">Type of the target element that has to implement <see cref="IElement"/>.</typeparam>
53-
/// <param name="locator">Locator of the target element.
54-
/// Note that some browsers don't support XPath locator for shadow elements (e.g. Chrome).</param>
55-
/// <param name="name">Name of the target element.</param>
56-
/// <param name="supplier">Delegate that defines constructor of element.</param>
57-
/// <param name="state">State of the target element.</param>
58-
/// <returns>Instance of element.</returns>
59-
public T FindElementInShadowRoot<T>(By locator, string name, ElementSupplier<T> supplier = null, ElementState state = ElementState.Displayed)
60-
where T : IElement
61-
{
62-
var shadowRootRelativeFinder = new RelativeElementFinder(Logger, AqualityServices.ConditionalWait, ExpandShadowRoot);
63-
var shadowRootFactory = new ElementFactory(AqualityServices.ConditionalWait, shadowRootRelativeFinder, AqualityServices.Get<ILocalizationManager>());
64-
return shadowRootFactory.Get(locator, name, supplier, state);
65-
}
66-
67-
/// <summary>
68-
/// Perfroms click on element and waits for page is loaded.
49+
/// Performs click on element and waits for page is loaded.
6950
/// </summary>
7051
public void ClickAndWait()
7152
{

Aquality.Selenium/src/Aquality.Selenium/Elements/Element.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using ICoreElementFactory = Aquality.Selenium.Core.Elements.Interfaces.IElementFactory;
1717
using ICoreElementFinder = Aquality.Selenium.Core.Elements.Interfaces.IElementFinder;
1818
using ICoreElementStateProvider = Aquality.Selenium.Core.Elements.Interfaces.IElementStateProvider;
19+
using System.Collections.Generic;
1920

2021
namespace Aquality.Selenium.Elements
2122
{
@@ -134,13 +135,5 @@ public ShadowRoot ExpandShadowRoot()
134135
var shadowRoot = (ShadowRoot)GetElement().GetShadowRoot();
135136
return shadowRoot;
136137
}
137-
138-
public T FindElementInShadowRoot<T>(By locator, string name, ElementSupplier<T> supplier = null, ElementState state = ElementState.Displayed)
139-
where T : IElement
140-
{
141-
var shadowRootRelativeFinder = new RelativeElementFinder(LocalizedLogger, ConditionalWait, ExpandShadowRoot);
142-
var shadowRootFactory = new ElementFactory(ConditionalWait, shadowRootRelativeFinder, LocalizationManager);
143-
return shadowRootFactory.Get(locator, name, supplier, state);
144-
}
145138
}
146139
}

Aquality.Selenium/src/Aquality.Selenium/Elements/ElementFactory.cs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,26 @@ protected override IDictionary<Type, Type> ElementTypesMap
113113
}
114114
}
115115

116+
/// <summary>
117+
/// Generates locator for target element
118+
/// </summary>
119+
/// <param name="baseLocator">locator of parent element</param>
120+
/// <param name="webElement">target element</param>
121+
/// <param name="elementIndex">index of target element</param>
122+
/// <returns>target element's locator</returns>
123+
protected override By GenerateLocator(By baseLocator, IWebElement webElement, int elementIndex)
124+
{
125+
try
126+
{
127+
return GenerateXpathLocator(baseLocator, webElement, elementIndex);
128+
}
129+
catch (WebDriverException ex)
130+
{
131+
return By.CssSelector(ConditionalWait.WaitFor(driver => driver.ExecuteJavaScript<string>(
132+
JavaScript.GetElementCssSelector.GetScript(), webElement), message: $"{ex.Message}. CSS selector generation failed too."));
133+
}
134+
}
135+
116136
/// <summary>
117137
/// Generates xpath locator for target element
118138
/// </summary>
@@ -122,10 +142,16 @@ protected override IDictionary<Type, Type> ElementTypesMap
122142
/// <returns>target element's locator</returns>
123143
protected override By GenerateXpathLocator(By baseLocator, IWebElement webElement, int elementIndex)
124144
{
125-
return IsLocatorSupportedForXPathExtraction(baseLocator)
126-
? base.GenerateXpathLocator(baseLocator, webElement, elementIndex)
127-
: By.XPath(ConditionalWait.WaitFor(driver => driver.ExecuteJavaScript<string>(
128-
JavaScript.GetElementXPath.GetScript(), webElement), message: "XPath generation failed"));
145+
if (IsLocatorSupportedForXPathExtraction(baseLocator))
146+
{
147+
var locator = base.GenerateXpathLocator(baseLocator, webElement, elementIndex);
148+
if (ElementFinder.FindElements(locator).Count == 1)
149+
{
150+
return locator;
151+
}
152+
}
153+
return By.XPath(ConditionalWait.WaitFor(driver => driver.ExecuteJavaScript<string>(
154+
JavaScript.GetElementXPath.GetScript(), webElement), message: "XPath generation failed"));
129155
}
130156

131157
/// <summary>

Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IElement.cs

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
using Aquality.Selenium.Core.Elements;
2-
using Aquality.Selenium.Elements.Actions;
3-
using OpenQA.Selenium;
1+
using Aquality.Selenium.Elements.Actions;
42
using ICoreElement = Aquality.Selenium.Core.Elements.Interfaces.IElement;
53

64
namespace Aquality.Selenium.Elements.Interfaces
75
{
86
/// <summary>
97
/// Describes behavior of any UI element.
108
/// </summary>
11-
public interface IElement : ICoreElement
9+
public interface IElement : ICoreElement, IShadowRootExpander
1210
{
1311
/// <summary>
1412
/// Gets JavaScript actions that can be performed with an element.
@@ -74,24 +72,5 @@ public interface IElement : ICoreElement
7472
/// </summary>
7573
/// <param name="key"> Key for sending.</param>
7674
void SendKey(Key key);
77-
78-
/// <summary>
79-
/// Expands shadow root.
80-
/// </summary>
81-
/// <returns><see cref="ShadowRoot"/> search context.</returns>
82-
ShadowRoot ExpandShadowRoot();
83-
84-
/// <summary>
85-
/// Finds element in the shadow root of the current element.
86-
/// </summary>
87-
/// <typeparam name="T">Type of the target element that has to implement <see cref="IElement"/>.</typeparam>
88-
/// <param name="locator">Locator of the target element.
89-
/// Note that some browsers don't support XPath locator for shadow elements.</param>
90-
/// <param name="name">Name of the target element.</param>
91-
/// <param name="supplier">Delegate that defines constructor of element.</param>
92-
/// <param name="state">State of the target element.</param>
93-
/// <returns>Instance of element.</returns>
94-
T FindElementInShadowRoot<T>(By locator, string name, ElementSupplier<T> supplier = null, ElementState state = ElementState.Displayed)
95-
where T : IElement;
9675
}
9776
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using Aquality.Selenium.Browsers;
2+
using Aquality.Selenium.Core.Elements;
3+
using Aquality.Selenium.Core.Localization;
4+
using OpenQA.Selenium;
5+
using System.Collections.Generic;
6+
7+
namespace Aquality.Selenium.Elements.Interfaces
8+
{
9+
/// <summary>
10+
/// Shadow Root expander.
11+
/// </summary>
12+
public interface IShadowRootExpander
13+
{
14+
/// <summary>
15+
/// Expands shadow root.
16+
/// </summary>
17+
/// <returns>ShadowRoot search context.</returns>
18+
ShadowRoot ExpandShadowRoot();
19+
}
20+
21+
/// <summary>
22+
/// Extensions for Shadow Root expander (like element or JS Actions).
23+
/// </summary>
24+
public static class ShadowRootExpanderExtensions
25+
{
26+
/// <summary>
27+
/// Provides <see cref="IElementFactory"/> to find elements in the shadow root of the current element.
28+
/// </summary>
29+
public static IElementFactory GetShadowRootElementFactory(this IShadowRootExpander shadowRootExpander)
30+
{
31+
var shadowRootRelativeFinder = new RelativeElementFinder(AqualityServices.LocalizedLogger, AqualityServices.ConditionalWait, shadowRootExpander.ExpandShadowRoot);
32+
return new ElementFactory(AqualityServices.ConditionalWait, shadowRootRelativeFinder, AqualityServices.Get<ILocalizationManager>());
33+
}
34+
35+
/// <summary>
36+
/// Finds element in the shadow root of the current element.
37+
/// </summary>
38+
/// <typeparam name="T">Type of the target element that has to implement <see cref="IElement"/>.</typeparam>
39+
/// <param name="shadowRootExpander">Current instance of the Shadow root expander.</param>
40+
/// <param name="locator">Locator of the target element.
41+
/// Note that some browsers don't support XPath locator for shadow elements (e.g. Chrome).</param>
42+
/// <param name="name">Name of the target element.</param>
43+
/// <param name="supplier">Delegate that defines constructor of element.</param>
44+
/// <param name="state">State of the target element.</param>
45+
/// <returns>Instance of element.</returns>
46+
public static T FindElementInShadowRoot<T>(this IShadowRootExpander shadowRootExpander, By locator, string name, ElementSupplier<T> supplier = null, ElementState state = ElementState.Displayed)
47+
where T : IElement
48+
{
49+
return shadowRootExpander.GetShadowRootElementFactory().Get(locator, name, supplier, state);
50+
}
51+
52+
/// <summary>
53+
/// Finds elements in the shadow root of the current element.
54+
/// </summary>
55+
/// <typeparam name="T">Type of the target elements that has to implement <see cref="IElement"/>.</typeparam>
56+
/// <param name="shadowRootExpander">Current instance of the Shadow root expander.</param>
57+
/// <param name="locator">Locator of target elements.
58+
/// Note that some browsers don't support XPath locator for shadow elements.
59+
/// Therefore, we suggest to use CSS selectors</param>
60+
/// <param name="name">Name of target elements.</param>
61+
/// <param name="supplier">Delegate that defines constructor of element.</param>
62+
/// <param name="expectedCount">Expected number of elements that have to be found (zero, more then zero, any).</param>
63+
/// <param name="state">State of target elements.</param>
64+
/// <returns>List of found elements.</returns>
65+
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)
66+
where T : IElement
67+
{
68+
return shadowRootExpander.GetShadowRootElementFactory().FindElements(locator, name, supplier, expectedCount, state);
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)