C# PageFactory – Wrap your WebDriver calls

C# PageFactory – Wrap your WebDriver calls

This entry is part 4 of 5 in the series Building a .Net Core PageFactory Framework

In my attempts to keep things simple I am going to explain my approach one thing at a time. To start with I will show a simple approach to making WebDriver calls.

Starting point

Last time I defined a PageController class that handled basics like Url and PageTitle. I’m not really concerned with those here, but lets merge them with my code from the previous post avoiding using properties.

using System;
using OpenQA.Selenium;

namespace QuickPageFactory
{
    public class FooBarPageNew : Page, IFooBarPage
    {
        private const string BarElementCssSelector = "#bar";
        
        private readonly IWebDriver driver;

        public FooBarPageNew(IWebDriver driver) : base(driver) => this.driver = driver;

        public override string GetExpectedPageTitle() => "FooBar Test";

        public override string GetExpectedUri() => $"file:///{AppDomain.CurrentDomain.BaseDirectory}FooBar.html";

        private IWebElement GetFooElement() => this.driver.FindElement(By.Id("foo"));

        private IWebElement GetBarElement() => this.driver.FindElement(By.CssSelector(BarElementCssSelector));

        public void ClickFoo() => GetFooElement().Click();

        public string GetBarText() => GetBarElement().Text;
    }
}

Refactoring

As my goal is not to change behaviour, the best way to ensure that there are no external changes is to ensure that there is appropriate unit (or higher level) test coverage. Get tests passing and then you can refactor in safety. I’ll spare you the details here and focus on the code, but don’t skip this step.

Wrap WebDriver calls

In a simple class like the above, there is little need, but we can define our own internal method signatures for tasks such as finding elements, waiting etc. I used two methods here.

By defining the By locator object in the method – private IWebElement GetFooElement() => this.driver.FindElement(By.Id("foo"));

By defining a CssSelector string in a private const and referencing that in the method – private IWebElement GetBarElement() => this.driver.FindElement(By.CssSelector(BarElementCssSelector));

In an ideal world, I could define a By in a const, but as they cannot be constructed at compile time that is not possible. We could use properties to return Bys for using, but this feels overly complex and there is a short cut that we can use for most locators.

The power of CssSelector

There are 8 locator strategies defined in By Locators. If you look at the W3C WebDriver protocol documentation however there are only 5. How come?

Well it turns out that in fact all but 3 of the By locators can be defined with a CssSelector and if you check out the dotnet WebDriver source code you will see that this is what happens internally.

The 8 Locator strategies:

Convertible to CssSelectors

  • By.CssSelector("locatorString") is already a By.CssSelector
  • By.TagName("tagName") is equivalent to By.CssSelector("tagname")
  • By.ClassName("class") is equivalent to By.CssSelector(".class")
  • By.Id("id") is equivalent to By.CssSelector("#id")
  • By.Name("NAME") is equivalent to By.CssSelector("*[name=\"NAME\"]")

Cannot be converted to use CssSelectors

  • By.XPath("XPath")
  • By.LinkText("fullLinkText")
  • By.PartialLinkText("partialLinkText")

For sure, these have their uses, especially the link text variants, but I try to avoid them as much as posssible. They are far too fragile for use unless you have no other choice. e.g.

  • For the LinkText versions, when the product owner decides to change the link text, your test code will need changing
  • For the XPath, when a developer has to add an extra div to fix a formatting problem your locator will be broken and you will need to fix it in your test code

Wrapped calls

So out of preference I will assign a private string const xxxxCssSelector field at the top of the class for each element that the controller needs to access.

That leaves me then with two method signatures, one passing in the CssSelectorString directly (My preferred option) and a more general method passing in a By locator.

        protected IWebElement FindElement(string cssSelector)
        {
            return this.driver.FindElement(By.CssSelector(cssSelector));
        }

        protected IWebElement FindElement(By by)
        {
            return this.driver.FindElement(by);
        }

As these are now totally generic I can pull them out into my base Controller class as protected methods to call as required. It doesn’t reduce code much at this point, but its importance will become clearer in a later post once I start to look at Block Controllers.

Of course, as well as the single IWebElement FindElement() methods we also want the ReadOnlyCollection<IWebElement> FindElements() versions too.

That leaves us with our PageController base class:

using System.Collections.ObjectModel;
using OpenQA.Selenium;

namespace QuickPageFactory
{
    public class PageController : IPageController
    {
        private readonly IWebDriver driver;

        protected PageController(IWebDriver driver)
        {
            this.driver = driver;
        }
        
        public string GetActualPageTitle()
        {
            return driver.Title;
        }
        
        public string GetActualUri()
        {
            return driver.Url;
        }
        
        protected IWebElement FindElement(string cssSelector)
        {
            return this.driver.FindElement(By.CssSelector(cssSelector));
        }

        protected IWebElement FindElement(By by)
        {
            return this.driver.FindElement(by);
        }
        
        protected ReadOnlyCollection<IWebElement> FindElements(string cssSelector)
        {
            return this.driver.FindElements(By.CssSelector(cssSelector));
        }

        protected ReadOnlyCollection<IWebElement> FindElements(By by)
        {
            return this.driver.FindElements(by);
        }
    }
}

And the refactored FooBarPageNew class that inherits the PageController through the Page base class

using System;
using OpenQA.Selenium;

namespace QuickPageFactory
{
    public class FooBarPageNew : Page, IFooBarPage
    {
        private readonly Uri pageUri = new Uri(
            new Uri(AppDomain.CurrentDomain.BaseDirectory),
            new Uri("FooBar.html", UriKind.Relative));
            
        private const string BarElementCssSelector = "#bar";
        
        private readonly IWebDriver driver;

        public FooBarPageNew(IWebDriver driver) : base(driver) => this.driver = driver;

        public override string GetExpectedPageTitle() => "FooBar Test";

        public override string GetExpectedUri() => pageUri.ToString();

        public void ClickFoo() => GetFooElement().Click();

        public string GetBarText() => GetBarElement().Text;

        private IWebElement GetFooElement() => FindElement(By.Id("foo"));

        private IWebElement GetBarElement() => FindElement(BarElementCssSelector);
    }
}

Progress made:

  • Wrapped my WebDriver calls as protected FindElement and FindElements methods in a base controller class.
  • Included standard (By By.LocatorStrategy(“locatorString”)) signatures as well as (string cssSelectorString)

Lessons learnt:

  • Most non fragile locators have an equivalent By.CssSelector() that can be used instead
  • XPath, LinkText and PartialLinkText are potentially very fragile locator strategies, avoid using them where possible.
  • By locators cannot be assigned to a const field in C#, but a CssSelector string can

A reminder:

If you want to ask me a question, Twitter is undoubtedly the fastest place to get a response: My Username is @AlexanderOnTest so I am easy to find. My DMs are always open for questions, and I publicise my new blog posts there too.

Series Navigation<< C# PageFactory – Starting with ControllersWebDrivers with culture – Testing internationalisation >>

One thought on “C# PageFactory – Wrap your WebDriver calls

  1. Thanks for the comment vurtil opmer.

    Could you clarify what problem you are seeing in Internet Explorer please? I am not seeing any issues in IE11.

    I would also politely point out that Internet Explorer is not the market leader today. according to https://gs.statcounter.com/browser-market-share/desktop/worldwide it is down to 4.5% of total desktop usage. That’s around half of Firefox and Safari, equal to Edge and not in the same ballpark as Chrome at 69%.

Comments are closed.

Comments are closed.