Skip to content

Commit

Permalink
Implementing pluggable element locator factories for .NET PageFactory
Browse files Browse the repository at this point in the history
This change allows the user to specify a custom IElementLocatorFactory
for locating elements when used with the PageFactory. This gives much more
control over the algorithm used to locate elements, and allows the
incorporation of things like retries or handling of specific exceptions.
  • Loading branch information
jimevans committed Aug 4, 2014
1 parent 8f988e0 commit aac4d59
Show file tree
Hide file tree
Showing 9 changed files with 379 additions and 32 deletions.
4 changes: 3 additions & 1 deletion dotnet/src/support/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.")]
Expand Down Expand Up @@ -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<!0,!!0>)", 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")]
93 changes: 93 additions & 0 deletions dotnet/src/support/PageObjects/DefaultElementLocatorFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// <copyright file="DefaultElementLocatorFactory.cs" company="WebDriver Committers">
// 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.
// </copyright>

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;

namespace OpenQA.Selenium.Support.PageObjects
{
/// <summary>
/// A default locator for elements for use with the <see cref="PageFactory"/>. This locator
/// implements no retry logic for elements not being found, nor for elements being stale.
/// </summary>
public class DefaultElementLocatorFactory : IElementLocatorFactory
{
/// <summary>
/// Locates an element using the given <see cref="ISearchContext"/> and list of <see cref="By"/> criteria.
/// </summary>
/// <param name="searchContext">The <see cref="ISearchContext"/> object within which to search for an element.</param>
/// <param name="bys">The list of methods by which to search for the element.</param>
/// <returns>An <see cref="IWebElement"/> which is the first match under the desired criteria.</returns>
public IWebElement LocateElement(ISearchContext searchContext, IEnumerable<By> 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);
}

/// <summary>
/// Locates a list of elements using the given <see cref="ISearchContext"/> and list of <see cref="By"/> criteria.
/// </summary>
/// <param name="searchContext">The <see cref="ISearchContext"/> object within which to search for elements.</param>
/// <param name="bys">The list of methods by which to search for the elements.</param>
/// <returns>An list of all elements which match the desired criteria.</returns>
public ReadOnlyCollection<IWebElement> LocateElements(ISearchContext searchContext, IEnumerable<By> 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<IWebElement> collection = new List<IWebElement>();
foreach (var by in bys)
{
ReadOnlyCollection<IWebElement> list = searchContext.FindElements(by);
collection.AddRange(list);
}

return collection.AsReadOnly();
}
}
}
12 changes: 11 additions & 1 deletion dotnet/src/support/PageObjects/FindsByAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <see cref="Type"/> of the field or property must be either
/// <see cref="IWebElement"/> or IList{IWebElement}. Any other type will throw an
/// <see cref="ArgumentException"/> when <see cref="PageFactory.InitElements"/> is called.
/// <see cref="ArgumentException"/> when <see cref="PageFactory.InitElements(ISearchContext, object)"/> is called.
/// </para>
/// <para>
/// <code>
Expand Down Expand Up @@ -152,6 +152,11 @@ internal By Finder
/// <returns><see langword="true"/> if the first instance is greater than the second; otherwise, <see langword="false"/>.</returns>
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;
}

Expand All @@ -163,6 +168,11 @@ internal By Finder
/// <returns><see langword="true"/> if the first instance is less than the second; otherwise, <see langword="false"/>.</returns>
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;
}

Expand Down
46 changes: 46 additions & 0 deletions dotnet/src/support/PageObjects/IElementLocatorFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// <copyright file="IElementLocatorFactory.cs" company="WebDriver Committers">
// 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.
// </copyright>

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;

namespace OpenQA.Selenium.Support.PageObjects
{
/// <summary>
/// Interface describing how elements are to be located by a <see cref="PageFactory"/>
/// </summary>
public interface IElementLocatorFactory
{
/// <summary>
/// Locates an element using the given <see cref="ISearchContext"/> and list of <see cref="By"/> criteria.
/// </summary>
/// <param name="searchContext">The <see cref="ISearchContext"/> object within which to search for an element.</param>
/// <param name="bys">The list of methods by which to search for the element.</param>
/// <returns>An <see cref="IWebElement"/> which is the first match under the desired criteria.</returns>
IWebElement LocateElement(ISearchContext searchContext, IEnumerable<By> bys);

/// <summary>
/// Locates a list of elements using the given <see cref="ISearchContext"/> and list of <see cref="By"/> criteria.
/// </summary>
/// <param name="searchContext">The <see cref="ISearchContext"/> object within which to search for elements.</param>
/// <param name="bys">The list of methods by which to search for the elements.</param>
/// <returns>An list of all elements which match the desired criteria.</returns>
ReadOnlyCollection<IWebElement> LocateElements(ISearchContext searchContext, IEnumerable<By> bys);
}
}
62 changes: 56 additions & 6 deletions dotnet/src/support/PageObjects/PageFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,31 @@ private PageFactory()
/// <see cref="IWebElement"/> or IList{IWebElement}.
/// </exception>
public static T InitElements<T>(IWebDriver driver)
{
return InitElements<T>(driver, new DefaultElementLocatorFactory());
}

/// <summary>
/// Initializes the elements in the Page Object with the given type.
/// </summary>
/// <typeparam name="T">The <see cref="Type"/> of the Page Object class.</typeparam>
/// <param name="driver">The <see cref="IWebDriver"/> instance used to populate the page.</param>
/// <param name="locatorFactory">The <see cref="IElementLocatorFactory"/> implementation that
/// determines how elements are located.</param>
/// <returns>An instance of the Page Object class with the elements initialized.</returns>
/// <remarks>
/// The class used in the <typeparamref name="T"/> argument must have a public constructor
/// that takes a single argument of type <see cref="IWebDriver"/>. 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.
/// </remarks>
/// <exception cref="ArgumentException">
/// thrown if no constructor to the class can be found with a single IWebDriver argument
/// <para>-or-</para>
/// if a field or property decorated with the <see cref="FindsByAttribute"/> is not of type
/// <see cref="IWebElement"/> or IList{IWebElement}.
/// </exception>
public static T InitElements<T>(IWebDriver driver, IElementLocatorFactory locatorFactory)
{
T page = default(T);
Type pageClassType = typeof(T);
Expand All @@ -67,7 +92,7 @@ public static T InitElements<T>(IWebDriver driver)
}

page = (T)ctor.Invoke(new object[] { driver });
InitElements(driver, page);
InitElements(driver, page, locatorFactory);
return page;
}

Expand All @@ -81,12 +106,35 @@ public static T InitElements<T>(IWebDriver driver)
/// <see cref="IWebElement"/> or IList{IWebElement}.
/// </exception>
public static void InitElements(ISearchContext driver, object page)
{
InitElements(driver, page, new DefaultElementLocatorFactory());
}

/// <summary>
/// Initializes the elements in the Page Object.
/// </summary>
/// <param name="driver">The driver used to find elements on the page.</param>
/// <param name="page">The Page Object to be populated with elements.</param>
/// <param name="locatorFactory">The <see cref="IElementLocatorFactory"/> implementation that
/// determines how elements are located.</param>
/// <exception cref="ArgumentException">
/// thrown if a field or property decorated with the <see cref="FindsByAttribute"/> is not of type
/// <see cref="IWebElement"/> or IList{IWebElement}.
/// </exception>
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<MemberInfo>();
const BindingFlags PublicBindingOptions = BindingFlags.Instance | BindingFlags.Public;
Expand All @@ -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<By> bys = CreateLocatorList(member);
Expand All @@ -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<IWebElement>");
Expand All @@ -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<IWebElement>");
Expand Down Expand Up @@ -173,16 +223,16 @@ private static bool ShouldCacheLookup(MemberInfo member)
return cache;
}

private static object CreateProxyObject(Type memberType, ISearchContext driver, List<By> bys, bool cache)
private static object CreateProxyObject(Type memberType, ISearchContext driver, List<By> bys, bool cache, IElementLocatorFactory locatorFactory)
{
object proxyObject = null;
if (memberType == typeof(IList<IWebElement>))
{
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;
Expand Down
Loading

0 comments on commit aac4d59

Please sign in to comment.