If you have some experience writing Selenium tests, you’ve probably ran into issues where your tests occasionally fail due to what appears to be missing elements on pages being tested. Typically the way to solve that problem is to “wait” for the missing element to be present in the DOM before issuing statements that interact with it (eg. clicks etc.). This cycle includes the following steps:
- Visit a page
- Wait for an element to be present on the page
- Take an action on the element or on the page
Most new users of Selenium omit step 2, usually due to the fact that their tests pass just fine without it. Unfortunately it turns out Selenium tests can often be flakey, and page (and DOM) load times for the same tests can be inconsistent.
The remedy is simple enough, with a block of code like the following, you can always get your tests to execute a wait step as a precursor to interacting with an element.
var wait = new WebDriverWait(Driver.Instance, TimeSpan.FromSeconds(waitTime));
wait.Until(d => d.FindElement(By.Id(someElement)));
Easily refactored out into a utility method and used in tests as follows:
somePage.GoTo();
Utility.WaitForElement(somePage.Element);
somePage.Element.Click();
Assert.IsTrue(someCondition, "Condition failed");
Although the above works just fine, the idea of multiple wait statements scattered throughout tests didn’t seem ideal. As part of a larger effort to refactor our entire test suite, I wanted our team to write tests that were intuitive and implemented a good degree of cohesion for common page behaviors. The following code segment meets some of these criteria:
var page = new Somepage()
page.GoTo()
page.OnReady().TakeSomeAction();
Assert.IsTrue(page.IsSomeCondition(), "Condition failed");
The key concepts in the above code are
- A page somehow knowing when it’s ready to be interacted with based on a check for an element, or a given set of elements
- Being able to immediately follow up with an action once a page signals that it is “interaction-ready” (this is where method chaining comes in handy)
Since undoubtedly all pages accessed by your test suite will require the same basic capabilities, it makes sense to use a base class that defines the common behaviors. The code for the base class looks like the following:
public abstract class BasePage<T> : ITestPage where T : BasePage<T>
{
public virtual By[] IdentifyingElements => new By[] { By.CssSelector("#some_element_id") };
protected virtual string BaseUrl
{
get {
return Configs.SomeUrl;
}
}
public virtual string Path { get; protected set; }
public virtual void GoTo()
{
Driver.Instance.Navigate().GoToUrl($"https://{BaseUrl}{this.Path}");
}
public T OnReady()
{
return OnReady(Utility.DefaultWaitTime);
}
public T OnReady(int secondsToWait)
{
return OnElementPresent(IdentifyingElements, secondsToWait);
}
public T OnElementPresent(By[] elementsToWaitFor, int secondsToWait)
{
Utility.WaitForAnyElement(secondsToWait, elementsToWaitFor);
return (T)this;
}
public virtual bool IsLoaded(int waitTime = waitTime)
{
try
{
Utility.WaitForAnyElement(waitTime, IdentifyingElements);
return true;
}
catch (Exception)
{
return false;
}
}
}
The OnReady() function calls OnElementPresent() which checks if any element in a page’s set of “IdentifyingElements” is loaded, and then returns the instance on which it was called to a caller so it can take a subsequent action. This is made possible by the where clause in the generic type constraint used in the abstract class definition.
The reason why we allow the options to check for multiple elements goes back to the previously mentioned flakiness that comes with Selenium tests and perhaps tests that execute against live web pages in general — elements change, relocate or load differently from one test to the next so you need a safeguard against that unpredictability.
Pages that inherit from this base class will elect which base properties (eg. Path) to override and can override the IdentifyingElements array with a set of elements that are present in their own DOM, or just use the default established in the parent eg.
public class SomePage : BasePage<SomePage>
{
public override string Path { get { return "/somepath"; } }
public override By[] IdentifyingElements => new By[] { By.CssSelector(".js-custom-selector") };
public void TakeSomeAction()
{
// page interaction logic here
}
public bool IsSomeCondition()
{
// verification logic here
}
}
Note: it’s also good to encapsulate page behaviors within classes like the one above.
This pattern is cleaner, more intuitive and ought to cut down significantly on failing tests resulting from missing elements.