From 3157da00c3bfbec4c529d232fe63ba12135b7178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alaksiej=20Miale=C5=A1ka?= Date: Mon, 31 Jan 2022 12:48:41 +0300 Subject: [PATCH] [Selenium 4] ShadowRoot functionality wrapper (#211) +semver: feature * Implemented ShadowRoot functionality wrapper +semver: feature --- .../Aquality.Selenium.csproj | 2 + .../Aquality.Selenium/Aquality.Selenium.xml | 55 +++++++++++++++++-- .../Aquality.Selenium/Browsers/JavaScript.cs | 3 +- .../Elements/Actions/JsActions.cs | 30 ++++++++++ .../src/Aquality.Selenium/Elements/Element.cs | 19 ++++++- .../Elements/ElementFactory.cs | 38 ++++++++++--- .../Elements/Interfaces/IElement.cs | 31 +++++++++-- .../Elements/Interfaces/IElementFactory.cs | 11 ++++ .../Resources/JavaScripts/ExpandShadowRoot.js | 1 + .../Resources/Localization/be.json | 4 +- .../Resources/Localization/en.json | 4 +- .../Resources/Localization/ru.json | 4 +- .../Integration/ShadowRootTests.cs | 37 +++++++++++++ .../AutomationPractice/Helpers/SiteLoader.cs | 16 +++--- .../TestApp/Browser/ChromeDownloadsForm.cs | 37 +++++++++++++ 15 files changed, 263 insertions(+), 29 deletions(-) create mode 100644 Aquality.Selenium/src/Aquality.Selenium/Resources/JavaScripts/ExpandShadowRoot.js create mode 100644 Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/ShadowRootTests.cs create mode 100644 Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/TestApp/Browser/ChromeDownloadsForm.cs diff --git a/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.csproj b/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.csproj index 55e453a1..239de7cb 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.csproj +++ b/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.csproj @@ -24,6 +24,7 @@ + @@ -43,6 +44,7 @@ + diff --git a/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml b/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml index c6aba0d9..b9605f34 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml +++ b/Aquality.Selenium/src/Aquality.Selenium/Aquality.Selenium.xml @@ -755,6 +755,24 @@ Allows to perform actions on elements via JavaScript. + + + 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 (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. @@ -1368,7 +1386,7 @@ Gets element text. - Should the element be hightlighted or not. + Should the element be highlighted or not. Default value is from configuration: String representation of element text. @@ -1376,8 +1394,8 @@ Gets element attribute value by its name. - Name of attrbiute - Should the element be hightlighted or not. + Name of attribute + Should the element be highlighted or not. Default value is from configuration: Value of element attribute. @@ -1386,7 +1404,7 @@ Gets css value of the element. Name of css property - Should the element be hightlighted or not. + Should the element be highlighted or not. Default value is from configuration: Value of element attribute. @@ -1417,6 +1435,24 @@ 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. @@ -1485,6 +1521,17 @@ Element state Instance of element that implements ITextBox interface + + + Creates element that implements interface. + + Type of child element that has to implement IElement. + Base elements locator. + Elements name. + Delegate that defines constructor of element in case of custom element. + Elements state. + + Describes behavior of Label UI element. diff --git a/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScript.cs b/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScript.cs index e1f08583..3c138541 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScript.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Browsers/JavaScript.cs @@ -33,7 +33,8 @@ public enum JavaScript SetValue, GetViewPortCoordinates, OpenNewTab, - OpenInNewTab + OpenInNewTab, + ExpandShadowRoot } /// diff --git a/Aquality.Selenium/src/Aquality.Selenium/Elements/Actions/JsActions.cs b/Aquality.Selenium/src/Aquality.Selenium/Elements/Actions/JsActions.cs index e85d5155..79312a57 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Elements/Actions/JsActions.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Elements/Actions/JsActions.cs @@ -5,9 +5,11 @@ 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; +using OpenQA.Selenium; namespace Aquality.Selenium.Elements.Actions { @@ -34,6 +36,34 @@ public JsActions(IElement element, string elementType, ILocalizedLogger logger, protected ILocalizedLogger Logger { get; } + /// + /// Expands shadow root. + /// + /// search context. + public ShadowRoot ExpandShadowRoot() + { + LogElementAction("loc.shadowroot.expand.js"); + return ExecuteScript(JavaScript.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. /// diff --git a/Aquality.Selenium/src/Aquality.Selenium/Elements/Element.cs b/Aquality.Selenium/src/Aquality.Selenium/Elements/Element.cs index 96442b23..2af48bd4 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Elements/Element.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Elements/Element.cs @@ -46,7 +46,9 @@ protected Element(By locator, string name, ElementState state) : base(locator, n protected virtual IElementFactory CustomFactory => AqualityServices.Get(); - protected override ICoreElementFinder Finder => AqualityServices.Get(); + protected internal virtual ICoreElementFinder CustomFinder { get; internal set; } = AqualityServices.Get(); + + protected override ICoreElementFinder Finder => CustomFinder; protected override IElementCacheConfiguration CacheConfiguration => AqualityServices.Get(); @@ -125,5 +127,20 @@ public void SendKey(Key key) .FirstOrDefault(field => field.Name == key.ToString())?.GetValue(null).ToString(); DoWithRetry(() => GetElement().SendKeys(keysString)); } + + public ShadowRoot ExpandShadowRoot() + { + LogElementAction("loc.shadowroot.expand"); + 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 dc0fc06d..71b85325 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Elements/ElementFactory.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Elements/ElementFactory.cs @@ -34,37 +34,61 @@ public ElementFactory(IConditionalWait conditionalWait, IElementFinder elementFi public IButton GetButton(By locator, string name, ElementState state = ElementState.Displayed) { - return new Button(locator, name, state); + return ResolveSupplier()(locator, name, state); } public ICheckBox GetCheckBox(By locator, string name, ElementState state = ElementState.Displayed) { - return new CheckBox(locator, name, state); + return ResolveSupplier()(locator, name, state); } public IComboBox GetComboBox(By locator, string name, ElementState state = ElementState.Displayed) { - return new ComboBox(locator, name, state); + return ResolveSupplier()(locator, name, state); } public ILabel GetLabel(By locator, string name, ElementState state = ElementState.Displayed) { - return new Label(locator, name, state); + return ResolveSupplier()(locator, name, state); } public ILink GetLink(By locator, string name, ElementState state = ElementState.Displayed) { - return new Link(locator, name, state); + return ResolveSupplier()(locator, name, state); } public IRadioButton GetRadioButton(By locator, string name, ElementState state = ElementState.Displayed) { - return new RadioButton(locator, name, state); + return ResolveSupplier()(locator, name, state); } public ITextBox GetTextBox(By locator, string name, ElementState state = ElementState.Displayed) { - return new TextBox(locator, name, state); + return ResolveSupplier()(locator, name, state); + } + + public T Get(By locator, string name, ElementSupplier supplier = null, ElementState state = ElementState.Displayed) where T : Interfaces.IElement + { + return ResolveSupplier(supplier)(locator, name, state); + } + + private ElementSupplier ResolveSupplier() where T : Interfaces.IElement + { + return ResolveSupplier(null); + } + + protected override ElementSupplier ResolveSupplier(ElementSupplier supplier) + { + var baseSupplier = base.ResolveSupplier(supplier); + return (loc, name, state) => + { + var element = baseSupplier.Invoke(loc, name, state); + if (element is Element baseElement) + { + baseElement.CustomFinder = ElementFinder; + } + return element; + }; } /// diff --git a/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IElement.cs b/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IElement.cs index 75255afa..c3745270 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IElement.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IElement.cs @@ -1,4 +1,6 @@ -using Aquality.Selenium.Elements.Actions; +using Aquality.Selenium.Core.Elements; +using Aquality.Selenium.Elements.Actions; +using OpenQA.Selenium; using ICoreElement = Aquality.Selenium.Core.Elements.Interfaces.IElement; namespace Aquality.Selenium.Elements.Interfaces @@ -23,7 +25,7 @@ public interface IElement : ICoreElement /// /// Gets element text. /// - /// Should the element be hightlighted or not. + /// Should the element be highlighted or not. /// Default value is from configuration: /// String representation of element text. string GetText(HighlightState highlightState = HighlightState.Default); @@ -31,8 +33,8 @@ public interface IElement : ICoreElement /// /// Gets element attribute value by its name. /// - /// Name of attrbiute - /// Should the element be hightlighted or not. + /// Name of attribute + /// Should the element be highlighted or not. /// Default value is from configuration: /// Value of element attribute. string GetAttribute(string attr, HighlightState highlightState = HighlightState.Default); @@ -41,7 +43,7 @@ public interface IElement : ICoreElement /// Gets css value of the element. /// /// Name of css property - /// Should the element be hightlighted or not. + /// Should the element be highlighted or not. /// Default value is from configuration: /// Value of element attribute. string GetCssValue(string propertyName, HighlightState highlightState = HighlightState.Default); @@ -72,5 +74,24 @@ 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/IElementFactory.cs b/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IElementFactory.cs index 33cfa571..320e53fb 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IElementFactory.cs +++ b/Aquality.Selenium/src/Aquality.Selenium/Elements/Interfaces/IElementFactory.cs @@ -71,5 +71,16 @@ public interface IElementFactory : ICoreElementFactory /// Element state /// Instance of element that implements ITextBox interface ITextBox GetTextBox(By locator, string name, ElementState state = ElementState.Displayed); + + /// + /// Creates element that implements interface. + /// + /// Type of child element that has to implement IElement. + /// Base elements locator. + /// Elements name. + /// Delegate that defines constructor of element in case of custom element. + /// Elements state. + /// + T Get(By locator, string name, ElementSupplier supplier = null, ElementState state = ElementState.Displayed) where T : IElement; } } diff --git a/Aquality.Selenium/src/Aquality.Selenium/Resources/JavaScripts/ExpandShadowRoot.js b/Aquality.Selenium/src/Aquality.Selenium/Resources/JavaScripts/ExpandShadowRoot.js new file mode 100644 index 00000000..af2e83ed --- /dev/null +++ b/Aquality.Selenium/src/Aquality.Selenium/Resources/JavaScripts/ExpandShadowRoot.js @@ -0,0 +1 @@ +return arguments[0].shadowRoot; diff --git a/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/be.json b/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/be.json index cf370281..d27d8ae2 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/be.json +++ b/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/be.json @@ -84,5 +84,7 @@ "loc.browser.network.handler.response.add": "Дадаем апрацоўшчык сеткавых адказаў", "loc.browser.network.handler.response.clear": "Ачышчаем апрацоўшчыкі сеткавых адказаў", "loc.browser.network.monitoring.start": "Пачынаем сеткавы маніторынг", - "loc.browser.network.monitoring.stop": "Спыняем сеткавы маніторынг" + "loc.browser.network.monitoring.stop": "Спыняем сеткавы маніторынг", + "loc.shadowroot.expand": "Разварочваем дрэва схаваных элементаў", + "loc.shadowroot.expand.js": "Разварочваем дрэва схаваных элементаў праз JavaScript" } diff --git a/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/en.json b/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/en.json index c89d481c..8e94c7cf 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/en.json +++ b/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/en.json @@ -84,5 +84,7 @@ "loc.browser.network.handler.response.add": "Adding Network Response handler", "loc.browser.network.handler.response.clear": "Clearing Network Response handler", "loc.browser.network.monitoring.start": "Starting Network Monitoring", - "loc.browser.network.monitoring.stop": "Stopping Network Monitoring" + "loc.browser.network.monitoring.stop": "Stopping Network Monitoring", + "loc.shadowroot.expand": "Expanding the Shadow Root", + "loc.shadowroot.expand.js": "Expanding the Shadow Root via JavaScript" } diff --git a/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/ru.json b/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/ru.json index c94adf89..30d55e97 100644 --- a/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/ru.json +++ b/Aquality.Selenium/src/Aquality.Selenium/Resources/Localization/ru.json @@ -84,5 +84,7 @@ "loc.browser.network.handler.response.add": "Добавление обработчика сетевых ответов", "loc.browser.network.handler.response.clear": "Очистка обработчиков сетевых ответов", "loc.browser.network.monitoring.start": "Начинаем сетевой мониторинг", - "loc.browser.network.monitoring.stop": "Останавливаем сетевой мониторинг" + "loc.browser.network.monitoring.stop": "Останавливаем сетевой мониторинг", + "loc.shadowroot.expand": "Разворачиваем дерево скрытых элементов", + "loc.shadowroot.expand.js": "Разворачиваем дерево скрытых элементов посредством JavaScript" } diff --git a/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/ShadowRootTests.cs b/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/ShadowRootTests.cs new file mode 100644 index 00000000..37180d0e --- /dev/null +++ b/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/ShadowRootTests.cs @@ -0,0 +1,37 @@ +using Aquality.Selenium.Elements.Interfaces; +using Aquality.Selenium.Tests.Integration.TestApp.Browser.Forms; +using NUnit.Framework; + +namespace Aquality.Selenium.Tests.Integration +{ + internal class ShadowRootTests : UITest + { + private static readonly ChromeDownloadsForm form = new ChromeDownloadsForm(); + + [SetUp] + public void OpenDownloads() + { + ChromeDownloadsForm.Open(); + } + + [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.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"); + Assert.IsTrue(form.MainContainerLabel.State.IsDisplayed, "Should be possible to check that element under the shadow is displayed"); + } + + [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"); + 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/AutomationPractice/Helpers/SiteLoader.cs b/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/TestApp/AutomationPractice/Helpers/SiteLoader.cs index 433f5364..5ba9a196 100644 --- a/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/TestApp/AutomationPractice/Helpers/SiteLoader.cs +++ b/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/TestApp/AutomationPractice/Helpers/SiteLoader.cs @@ -1,7 +1,7 @@ -using Aquality.Selenium.Elements.Interfaces; +using Aquality.Selenium.Browsers; +using Aquality.Selenium.Elements.Interfaces; using OpenQA.Selenium; using System; -using static Aquality.Selenium.Browsers.AqualityServices; namespace Aquality.Selenium.Tests.Integration.TestApp.AutomationPractice.Helpers { @@ -9,16 +9,16 @@ internal static class SiteLoader { public static void OpenAutomationPracticeSite(string customUrl = null) { - var resourceLimitLabel = Get() + var resourceLimitLabel = AqualityServices.Get() .GetLabel(By.XPath("//h1[.='Resource Limit Is Reached']"), "Resource Limit Is Reached"); - Browser.GoTo(customUrl ?? Constants.UrlAutomationPractice); - Browser.WaitForPageToLoad(); - ConditionalWait.WaitForTrue(() => + AqualityServices.Browser.GoTo(customUrl ?? Constants.UrlAutomationPractice); + AqualityServices.Browser.WaitForPageToLoad(); + AqualityServices.ConditionalWait.WaitForTrue(() => { if (resourceLimitLabel.State.IsDisplayed) { - Browser.Refresh(); - Browser.WaitForPageToLoad(); + AqualityServices.Browser.Refresh(); + AqualityServices.Browser.WaitForPageToLoad(); return false; } return true; 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 new file mode 100644 index 00000000..b86af3a7 --- /dev/null +++ b/Aquality.Selenium/tests/Aquality.Selenium.Tests/Integration/TestApp/Browser/ChromeDownloadsForm.cs @@ -0,0 +1,37 @@ +using Aquality.Selenium.Browsers; +using Aquality.Selenium.Elements.Interfaces; +using Aquality.Selenium.Forms; +using OpenQA.Selenium; + +namespace Aquality.Selenium.Tests.Integration.TestApp.Browser.Forms +{ + internal class ChromeDownloadsForm : Form + { + private const string Address = "chrome://downloads/"; + + public static By NestedShadowRootLocator => By.Id("moreActionsMenu"); + public ILabel DownloadsToolbarLabel => FormElement.FindElementInShadowRoot(By.CssSelector("downloads-toolbar"), "Downloads toolbar"); + public ILabel MainContainerLabel => FormElement.FindElementInShadowRoot(By.Id("mainContainer"), "main container"); + public ILabel DownloadsToolbarLabelFromJs => FormElement.JsActions.FindElementInShadowRoot(By.CssSelector("downloads-toolbar"), "Downloads toolbar"); + public ILabel MainContainerLabelFromJs => FormElement.JsActions.FindElementInShadowRoot(By.Id("mainContainer"), "Main container"); + + public ChromeDownloadsForm() : base(By.TagName("downloads-manager"), "Chrome downloads manager") + { + } + + public static void Open() + { + AqualityServices.Browser.GoTo(Address); + } + + public ShadowRoot ExpandShadowRoot() + { + return FormElement.ExpandShadowRoot(); + } + + public ShadowRoot ExpandShadowRootViaJs() + { + return FormElement.JsActions.ExpandShadowRoot(); + } + } +}