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 @@
+
+
+