diff --git a/dotnet/src/support/GlobalSuppressions.cs b/dotnet/src/support/GlobalSuppressions.cs index e46ac86aba200..fb60425b837f8 100644 --- a/dotnet/src/support/GlobalSuppressions.cs +++ b/dotnet/src/support/GlobalSuppressions.cs @@ -26,7 +26,6 @@ // "In Project Suppression File". // You do not need to add suppressions to this file manually. -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "OpenQA.Selenium.Support.PageObjects", Justification = "Number of namespace classes is subject to increase.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Scope = "member", Target = "OpenQA.Selenium.Support.Events.WebDriverNavigationEventArgs.#.ctor(OpenQA.Selenium.IWebDriver,System.String)", Justification = "Using string to preserve user input.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Scope = "member", Target = "OpenQA.Selenium.Support.Events.WebDriverNavigationEventArgs.#Url", Justification = "Using string to preserve user input.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "OpenQA.Selenium.Support.Events.EventFiringWebDriver+EventFiringWebElement.#ParentDriver", Justification = "Method must be available for subclasses.")] @@ -72,3 +71,6 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "OpenQA.Selenium.Support.UI.SelectElement.#.ctor(OpenQA.Selenium.IWebElement)", Justification = "WebDriver normalizes strings to lowercase.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations", Scope = "member", Target = "OpenQA.Selenium.Support.UI.SelectElement.#SelectedOption", Justification = "Exception should be thrown in this property.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "OpenQA.Selenium.Support.UI.DefaultWait`1.#Until`1(System.Func`2)", Justification = "Analyzing and handling all exceptions that occur, so catching generic Exception is appropriate.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "IJavaScriptExecutor", Scope = "member", Target = "OpenQA.Selenium.Support.Extensions.WebDriverExtensions.#ExecuteJavaScript`1(OpenQA.Selenium.IWebDriver,System.String,System.Object[])", Justification = "Interface name is correct")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "IHasCapabilities", Scope = "member", Target = "OpenQA.Selenium.Support.Extensions.WebDriverExtensions.#TakeScreenshot(OpenQA.Selenium.IWebDriver)", Justification = "Interface name is correct")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "ITakesScreenshot", Scope = "member", Target = "OpenQA.Selenium.Support.Extensions.WebDriverExtensions.#TakeScreenshot(OpenQA.Selenium.IWebDriver)", Justification = "Interface name is correct")] diff --git a/dotnet/src/support/PageObjects/DefaultElementLocatorFactory.cs b/dotnet/src/support/PageObjects/DefaultElementLocatorFactory.cs new file mode 100644 index 0000000000000..8c8770f9e8680 --- /dev/null +++ b/dotnet/src/support/PageObjects/DefaultElementLocatorFactory.cs @@ -0,0 +1,93 @@ +// +// Copyright 2014 Software Freedom Conservancy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; + +namespace OpenQA.Selenium.Support.PageObjects +{ + /// + /// A default locator for elements for use with the . This locator + /// implements no retry logic for elements not being found, nor for elements being stale. + /// + public class DefaultElementLocatorFactory : IElementLocatorFactory + { + /// + /// Locates an element using the given and list of criteria. + /// + /// The object within which to search for an element. + /// The list of methods by which to search for the element. + /// An which is the first match under the desired criteria. + public IWebElement LocateElement(ISearchContext searchContext, IEnumerable bys) + { + if (searchContext == null) + { + throw new ArgumentNullException("searchContext", "searchContext may not be null"); + } + + if (bys == null) + { + throw new ArgumentNullException("bys", "List of criteria may not be null"); + } + + string errorString = null; + foreach (var by in bys) + { + try + { + return searchContext.FindElement(by); + } + catch (NoSuchElementException) + { + errorString = (errorString == null ? "Could not find element by: " : errorString + ", or: ") + by; + } + } + + throw new NoSuchElementException(errorString); + } + + /// + /// Locates a list of elements using the given and list of criteria. + /// + /// The object within which to search for elements. + /// The list of methods by which to search for the elements. + /// An list of all elements which match the desired criteria. + public ReadOnlyCollection LocateElements(ISearchContext searchContext, IEnumerable bys) + { + if (searchContext == null) + { + throw new ArgumentNullException("searchContext", "searchContext may not be null"); + } + + if (bys == null) + { + throw new ArgumentNullException("bys", "List of criteria may not be null"); + } + + List collection = new List(); + foreach (var by in bys) + { + ReadOnlyCollection list = searchContext.FindElements(by); + collection.AddRange(list); + } + + return collection.AsReadOnly(); + } + } +} diff --git a/dotnet/src/support/PageObjects/FindsByAttribute.cs b/dotnet/src/support/PageObjects/FindsByAttribute.cs index 76223ad3dd8af..94ed0412cccfa 100644 --- a/dotnet/src/support/PageObjects/FindsByAttribute.cs +++ b/dotnet/src/support/PageObjects/FindsByAttribute.cs @@ -32,7 +32,7 @@ namespace OpenQA.Selenium.Support.PageObjects /// to indicate how to find the elements. This attribute can be used to decorate fields and properties /// in your Page Object classes. The of the field or property must be either /// or IList{IWebElement}. Any other type will throw an - /// when is called. + /// when is called. /// /// /// @@ -152,6 +152,11 @@ internal By Finder /// if the first instance is greater than the second; otherwise, . public static bool operator >(FindsByAttribute one, FindsByAttribute two) { + if (one == null) + { + throw new ArgumentNullException("one", "Object to compare cannot be null"); + } + return one.CompareTo(two) > 0; } @@ -163,6 +168,11 @@ internal By Finder /// if the first instance is less than the second; otherwise, . public static bool operator <(FindsByAttribute one, FindsByAttribute two) { + if (one == null) + { + throw new ArgumentNullException("one", "Object to compare cannot be null"); + } + return one.CompareTo(two) < 0; } diff --git a/dotnet/src/support/PageObjects/IElementLocatorFactory.cs b/dotnet/src/support/PageObjects/IElementLocatorFactory.cs new file mode 100644 index 0000000000000..8f9a5ea5cc2a7 --- /dev/null +++ b/dotnet/src/support/PageObjects/IElementLocatorFactory.cs @@ -0,0 +1,46 @@ +// +// Copyright 2014 Software Freedom Conservancy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; + +namespace OpenQA.Selenium.Support.PageObjects +{ + /// + /// Interface describing how elements are to be located by a + /// + public interface IElementLocatorFactory + { + /// + /// Locates an element using the given and list of criteria. + /// + /// The object within which to search for an element. + /// The list of methods by which to search for the element. + /// An which is the first match under the desired criteria. + IWebElement LocateElement(ISearchContext searchContext, IEnumerable bys); + + /// + /// Locates a list of elements using the given and list of criteria. + /// + /// The object within which to search for elements. + /// The list of methods by which to search for the elements. + /// An list of all elements which match the desired criteria. + ReadOnlyCollection LocateElements(ISearchContext searchContext, IEnumerable bys); + } +} diff --git a/dotnet/src/support/PageObjects/PageFactory.cs b/dotnet/src/support/PageObjects/PageFactory.cs index c337bfbf0fd0f..0cd186da4c8da 100644 --- a/dotnet/src/support/PageObjects/PageFactory.cs +++ b/dotnet/src/support/PageObjects/PageFactory.cs @@ -57,6 +57,31 @@ private PageFactory() /// or IList{IWebElement}. /// public static T InitElements(IWebDriver driver) + { + return InitElements(driver, new DefaultElementLocatorFactory()); + } + + /// + /// Initializes the elements in the Page Object with the given type. + /// + /// The of the Page Object class. + /// The instance used to populate the page. + /// The implementation that + /// determines how elements are located. + /// An instance of the Page Object class with the elements initialized. + /// + /// The class used in the argument must have a public constructor + /// that takes a single argument of type . This helps to enforce + /// best practices of the Page Object pattern, and encapsulates the driver into the Page + /// Object so that it can have no external WebDriver dependencies. + /// + /// + /// thrown if no constructor to the class can be found with a single IWebDriver argument + /// -or- + /// if a field or property decorated with the is not of type + /// or IList{IWebElement}. + /// + public static T InitElements(IWebDriver driver, IElementLocatorFactory locatorFactory) { T page = default(T); Type pageClassType = typeof(T); @@ -67,7 +92,7 @@ public static T InitElements(IWebDriver driver) } page = (T)ctor.Invoke(new object[] { driver }); - InitElements(driver, page); + InitElements(driver, page, locatorFactory); return page; } @@ -81,12 +106,35 @@ public static T InitElements(IWebDriver driver) /// or IList{IWebElement}. /// public static void InitElements(ISearchContext driver, object page) + { + InitElements(driver, page, new DefaultElementLocatorFactory()); + } + + /// + /// Initializes the elements in the Page Object. + /// + /// The driver used to find elements on the page. + /// The Page Object to be populated with elements. + /// The implementation that + /// determines how elements are located. + /// + /// thrown if a field or property decorated with the is not of type + /// or IList{IWebElement}. + /// + public static void InitElements(ISearchContext driver, object page, IElementLocatorFactory locatorFactory) { if (page == null) { throw new ArgumentNullException("page", "page cannot be null"); } + if (locatorFactory == null) + { + throw new ArgumentNullException("locatorFactory", "locatorFactory cannot be null"); + } + + // Get a list of all of the fields and properties (public and non-public [private, protected, etc.]) + // in the passed-in page object. Note that we walk the inheritance tree to get superclass members. var type = page.GetType(); var members = new List(); const BindingFlags PublicBindingOptions = BindingFlags.Instance | BindingFlags.Public; @@ -100,6 +148,8 @@ public static void InitElements(ISearchContext driver, object page) type = type.BaseType; } + // Examine each member, and if it is both marked with an appropriate attribute, and of + // the proper type, set the member's value to the appropriate type of proxy object. foreach (var member in members) { List bys = CreateLocatorList(member); @@ -112,7 +162,7 @@ public static void InitElements(ISearchContext driver, object page) var property = member as PropertyInfo; if (field != null) { - proxyObject = CreateProxyObject(field.FieldType, driver, bys, cache); + proxyObject = CreateProxyObject(field.FieldType, driver, bys, cache, locatorFactory); if (proxyObject == null) { throw new ArgumentException("Type of field '" + field.Name + "' is not IWebElement or IList"); @@ -122,7 +172,7 @@ public static void InitElements(ISearchContext driver, object page) } else if (property != null) { - proxyObject = CreateProxyObject(property.PropertyType, driver, bys, cache); + proxyObject = CreateProxyObject(property.PropertyType, driver, bys, cache, locatorFactory); if (proxyObject == null) { throw new ArgumentException("Type of property '" + property.Name + "' is not IWebElement or IList"); @@ -173,16 +223,16 @@ private static bool ShouldCacheLookup(MemberInfo member) return cache; } - private static object CreateProxyObject(Type memberType, ISearchContext driver, List bys, bool cache) + private static object CreateProxyObject(Type memberType, ISearchContext driver, List bys, bool cache, IElementLocatorFactory locatorFactory) { object proxyObject = null; if (memberType == typeof(IList)) { - proxyObject = new WebElementListProxy(driver, bys, cache); + proxyObject = new WebElementListProxy(driver, bys, cache, locatorFactory); } else if (memberType == typeof(IWebElement)) { - proxyObject = new WebElementProxy(driver, bys, cache); + proxyObject = new WebElementProxy(driver, bys, cache, locatorFactory); } return proxyObject; diff --git a/dotnet/src/support/PageObjects/RetryingElementLocatorFactory.cs b/dotnet/src/support/PageObjects/RetryingElementLocatorFactory.cs new file mode 100644 index 0000000000000..b2597094c3741 --- /dev/null +++ b/dotnet/src/support/PageObjects/RetryingElementLocatorFactory.cs @@ -0,0 +1,153 @@ +// +// Copyright 2014 Software Freedom Conservancy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading; + +namespace OpenQA.Selenium.Support.PageObjects +{ + /// + /// A locator for elements for use with the that retries locating + /// the element up to a timeout if the element is not found. + /// + public class RetryingElementLocatorFactory : IElementLocatorFactory + { + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(5); + private static readonly TimeSpan DefaultPollingInterval = TimeSpan.FromMilliseconds(500); + + private TimeSpan timeout; + private TimeSpan pollingInterval; + + /// + /// Initializes a new instance of the class. + /// + public RetryingElementLocatorFactory() + : this(DefaultTimeout) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The indicating how long the locator should + /// retry before timing out. + public RetryingElementLocatorFactory(TimeSpan timeout) + : this(timeout, DefaultPollingInterval) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The indicating how long the locator should + /// retry before timing out. + /// The indicating how often to poll + /// for the existence of the element. + public RetryingElementLocatorFactory(TimeSpan timeout, TimeSpan pollingInterval) + { + this.timeout = timeout; + this.pollingInterval = pollingInterval; + } + + /// + /// Locates an element using the given and list of criteria. + /// + /// The object within which to search for an element. + /// The list of methods by which to search for the element. + /// An which is the first match under the desired criteria. + public IWebElement LocateElement(ISearchContext searchContext, IEnumerable bys) + { + if (searchContext == null) + { + throw new ArgumentNullException("searchContext", "searchContext may not be null"); + } + + if (bys == null) + { + throw new ArgumentNullException("bys", "List of criteria may not be null"); + } + + string errorString = null; + DateTime endTime = DateTime.Now.Add(this.timeout); + bool timeoutReached = DateTime.Now > endTime; + while (!timeoutReached) + { + foreach (var by in bys) + { + try + { + return searchContext.FindElement(by); + } + catch (NoSuchElementException) + { + errorString = (errorString == null ? "Could not find element by: " : errorString + ", or: ") + by; + } + } + + timeoutReached = DateTime.Now > endTime; + if (!timeoutReached) + { + Thread.Sleep(this.pollingInterval); + } + } + + throw new NoSuchElementException(errorString); + } + + /// + /// Locates a list of elements using the given and list of criteria. + /// + /// The object within which to search for elements. + /// The list of methods by which to search for the elements. + /// An list of all elements which match the desired criteria. + public ReadOnlyCollection LocateElements(ISearchContext searchContext, IEnumerable bys) + { + if (searchContext == null) + { + throw new ArgumentNullException("searchContext", "searchContext may not be null"); + } + + if (bys == null) + { + throw new ArgumentNullException("bys", "List of criteria may not be null"); + } + + List collection = new List(); + DateTime endTime = DateTime.Now.Add(this.timeout); + bool timeoutReached = DateTime.Now > endTime; + while (!timeoutReached) + { + foreach (var by in bys) + { + ReadOnlyCollection list = searchContext.FindElements(by); + collection.AddRange(list); + } + + timeoutReached = collection.Count == 0 && DateTime.Now > endTime; + if (!timeoutReached) + { + Thread.Sleep(this.pollingInterval); + } + } + + return collection.AsReadOnly(); + } + } +} diff --git a/dotnet/src/support/PageObjects/WebElementListProxy.cs b/dotnet/src/support/PageObjects/WebElementListProxy.cs index 5c9f00b4e93fb..0a29f4f592fbe 100644 --- a/dotnet/src/support/PageObjects/WebElementListProxy.cs +++ b/dotnet/src/support/PageObjects/WebElementListProxy.cs @@ -29,6 +29,7 @@ namespace OpenQA.Selenium.Support.PageObjects /// internal class WebElementListProxy : IList { + private readonly IElementLocatorFactory locatorFactory; private readonly ISearchContext searchContext; private readonly IEnumerable bys; private readonly bool cache; @@ -40,8 +41,11 @@ internal class WebElementListProxy : IList /// The driver used to search for elements. /// The list of methods by which to search for the elements. /// to cache the lookup to the element; otherwise, . - internal WebElementListProxy(ISearchContext searchContext, IEnumerable bys, bool cache) + /// The implementation that + /// determines how elements are located. + internal WebElementListProxy(ISearchContext searchContext, IEnumerable bys, bool cache, IElementLocatorFactory locatorFactory) { + this.locatorFactory = locatorFactory; this.searchContext = searchContext; this.bys = bys; this.cache = cache; @@ -77,11 +81,7 @@ private List ElementList if (!this.cache || this.collection == null) { this.collection = new List(); - foreach (var by in this.bys) - { - ReadOnlyCollection list = this.searchContext.FindElements(by); - this.collection.AddRange(list); - } + this.collection.AddRange(this.locatorFactory.LocateElements(this.searchContext, this.bys)); } return this.collection; diff --git a/dotnet/src/support/PageObjects/WebElementProxy.cs b/dotnet/src/support/PageObjects/WebElementProxy.cs index f97503b269bf2..c0f9491a8158f 100644 --- a/dotnet/src/support/PageObjects/WebElementProxy.cs +++ b/dotnet/src/support/PageObjects/WebElementProxy.cs @@ -31,6 +31,7 @@ namespace OpenQA.Selenium.Support.PageObjects /// internal class WebElementProxy : IWebElement, ILocatable, IWrapsElement { + private readonly IElementLocatorFactory locatorFactory; private readonly ISearchContext searchContext; private readonly IEnumerable bys; private readonly bool cache; @@ -42,8 +43,11 @@ internal class WebElementProxy : IWebElement, ILocatable, IWrapsElement /// The driver used to search for elements. /// The list of methods by which to search for the element. /// to cache the lookup to the element; otherwise, . - internal WebElementProxy(ISearchContext searchContext, IEnumerable bys, bool cache) + /// The implementation that + /// determines how elements are located. + internal WebElementProxy(ISearchContext searchContext, IEnumerable bys, bool cache, IElementLocatorFactory locatorFactory) { + this.locatorFactory = locatorFactory; this.searchContext = searchContext; this.bys = bys; this.cache = cache; @@ -147,26 +151,12 @@ public IWebElement WrappedElement { get { - if (this.cache && this.cachedElement != null) + if (!this.cache || this.cachedElement == null) { - return this.cachedElement; + this.cachedElement = this.locatorFactory.LocateElement(this.searchContext, this.bys); } - string errorString = null; - foreach (var by in this.bys) - { - try - { - this.cachedElement = this.searchContext.FindElement(by); - return this.cachedElement; - } - catch (NoSuchElementException) - { - errorString = (errorString == null ? "Could not find element by: " : errorString + ", or: ") + by; - } - } - - throw new NoSuchElementException(errorString); + return this.cachedElement; } } diff --git a/dotnet/src/support/WebDriver.Support.csproj b/dotnet/src/support/WebDriver.Support.csproj index 17fc1d97ca6e0..98bdadb6ae277 100644 --- a/dotnet/src/support/WebDriver.Support.csproj +++ b/dotnet/src/support/WebDriver.Support.csproj @@ -81,9 +81,12 @@ + + +