In part two, we created the foundation of our framework, by setting up the classes that allow us to setup and dispose of our driver instances. But we’re not going to be doing much in the way of testing by just opening an empty window and closing it. Next, we’re going to create some wrapper methods around basic selenium functionality that will allow us to interact with our new browser window, as well as any elements on the pages we visit.
If you remember from the last article, we created a class called Driver
, which we have decided to split across several different files using the partial
keyword. This allows us to create one super class but keep the code readability high by creating smaller and more maintainable files that use this class.
Our Dispose
methods were in a partial Driver
class, and our next three classes will also be partial
classes. These will be called DriverElement
, DriverWindow
and DriverScript
. Let’s have a look at our DriverWindow
class first:
public static partial class Driver
{
#region Properties
public static string Title()
{
try
{
return DriverBase.Instance.Title;
}
catch(WebDriverException)
{
throw new WebDriverException
("Failed to retrieve the title of the current webdriver window");
}
}
public static string Url()
{
try
{
return DriverBase.Instance.Url;
}
catch (WebDriverException)
{
throw new WebDriverException
("Failed to retrieve the url of the current webdriver window");
}
}
#endregion
#region Navigate Methods
public static void NavigateTo(string url)
{
try
{
DriverBase.Instance.Navigate().GoToUrl(url);
}
catch (WebDriverException)
{
throw new WebDriverException($"Failed to navigate to the url: {url}");
}
}
public static void NavigateTo(Uri url)
{
try
{
DriverBase.Instance.Navigate().GoToUrl(url);
}
catch (WebDriverException)
{
throw new WebDriverException($"Failed to navigate to the url: {url}");
}
}
public static void NavigateForward()
{
try
{
DriverBase.Instance.Navigate().Forward();
}
catch (WebDriverException)
{
throw new WebDriverException($"Failed to navigate forward in the current window");
}
}
public static void NavigateBack()
{
try
{
DriverBase.Instance.Navigate().Back();
}
catch (WebDriverException)
{
throw new WebDriverException($"Failed to navigate back in the current window");
}
}
public static void Refresh()
{
try
{
DriverBase.Instance.Navigate().Refresh();
}
catch (WebDriverException)
{
throw new WebDriverException($"Failed to refresh the current window");
}
}
#endregion
#region Alert Methods
public static IAlert WaitAndGetAlert()
{
return WaitAndGetAlert(TimeOut, PollingInterval);
}
public static IAlert WaitAndGetAlert(TimeSpan timeout, TimeSpan pollInterval)
{
DriverBase.Instance.Manage().Timeouts().ImplicitWait = timeout;
var wait = new WebDriverWait(DriverBase.Instance, timeout)
{
PollingInterval = pollInterval
};
try
{
return wait.Until(d =>
{
try
{
return DriverBase.Instance.SwitchTo().Alert();
}
catch (WebDriverException)
{
return null;
}
});
}
catch(WebDriverException)
{
DriverBase.Instance.Manage().Timeouts().ImplicitWait = TimeOut;
throw new WebDriverException
("Failed to wait and get an alert in the current browser instance");
}
}
public static void WaitAndAcceptAlert(TimeSpan timeout, TimeSpan pollInterval)
{
try
{
var alert = WaitAndGetAlert(timeout, pollInterval);
alert.Accept();
}
catch(WebDriverException)
{
throw new WebDriverException("Failed to accept alert in the current browser window");
}
}
#endregion
#region Switch Methods
public static void SwitchToWindow(string windowName)
{
try
{
DriverBase.Instance.SwitchTo().Window(windowName);
}
catch (WebDriverException)
{
throw new WebDriverException($"Unable to switch to window: {windowName}");
}
}
#endregion
}
Immediately, you might notice that the methods within our class are very similar to look at. All use good exception handling via a try catch
block, with our simple code inside each of the methods' try
statement. The other thing you might notice is that a lot of our methods are simply calling the existing WebDriver
functionality from within Selenium using our WebDriver
Instance variable. And as that is the case, why are we bothering and not simply calling that code directly?
Two reasons. The first being the exception handling. Doing it this way allows us to create our own exception messages that we can use to debug any potential issues, as well as specifying the type of exception that is thrown. This will all make our lives easier when it comes to fixing code later on and pinpointing issues with our tests if there are issues at the framework level.
Secondly, our basic framework is designed around using a static WebDriver
instance that we use across our framework project. By creating these wrapper methods, we are able to call any of these methods across either our Driver
classes, or later on our Page Objects
classes without having to initialise a WebDriver
instance for each class. This helps you write more simple code.
So, going back to the class, using regions, we’ve split our class into four areas: Properties
, Navigate
, Alert
and Switch
. Properties
will allow us to return any information we might need for the browser instance, such as the page title or the current URL of the page we are looking at. Navigate
methods are for getting to our desired page, and if needed, simulating the user pressing back or forward in the window. We can even refresh the page at this point.
You might notice that we have an overloaded method for NavigateTo
. One taking a string
URL, and the other a URI. If we were to create a URI, it simply allows us to have some kind of validation of our URL and making sure it is well formed. It’s recommended we use this one where possible, but having the string
version allows us a quicker yet more error prone way of navigating to a URL.
Our Alert
methods allow us to deal with any browser specific alert messages that may appear. These are different to popups that are done at the site level. Which leads on to our final area, our Windows
methods. These are to deal with site level popups such as login windows or possibly even sites opening pages in a new window as opposed to a new tab.
Our next class is our Elements
class, so let’s take a look:
public static partial class Driver
{
#region Properties Methods
public static string ReturnElementUrl(this By by)
{
return DriverBase.Instance.FindElement(by).Location.ToString();
}
#endregion
#region Bool Methods
public static bool DoesElementExist(this By by)
{
return by.DoesElementExist(TimeOut, PollingInterval);
}
public static bool DoesElementExist(this By by, TimeSpan timeout, TimeSpan pollInterval)
{
try
{
by.WaitUntilElementIsVisible(timeout, pollInterval);
}
catch(Exception)
{
return false;
}
return true;
}
public static bool IsElementVisible(this By by, bool cssVisibleOnly = false)
{
IWebElement element;
try
{
element = by.FindElement();
}
catch(Exception)
{
return false;
}
if(!cssVisibleOnly)
{
return true;
}
try
{
return element.Displayed;
}
catch(StaleElementReferenceException)
{
return by.FindElement().Displayed;
}
}
#endregion
#region Find Methods
public static IWebElement FindElement(this By by)
{
try
{
return WaitUntilElementIsVisible(by);
}
catch (WebDriverException)
{
throw new WebDriverException($"Failed to find element by '{by}'");
}
}
#endregion
#region Wait Methods
public static IWebElement WaitUntilElementIsVisible(this By by)
{
return WaitUntilElementIsVisible(by, TimeOut, PollingInterval);
}
public static IWebElement WaitUntilElementIsVisible
(this By by, TimeSpan timeout, TimeSpan pollInterval)
{
IWebElement element;
try
{
var wait = new WebDriverWait(DriverBase.Instance, timeout)
{ PollingInterval = pollInterval };
wait.IgnoreExceptionTypes(typeof(StaleElementReferenceException));
wait.Until(ExpectedConditions.ElementIsVisible(by));
element = DriverBase.Instance.FindElement(by);
}
catch(Exception exception) when (exception is WebDriverException ||
exception is StaleElementReferenceException)
{
DriverBase.Instance.Manage().Timeouts().ImplicitWait = TimeOut;
throw new Exception($"Timeout after {timeout.TotalSeconds}
seconds of waiting for element {by} to become visible");
}
return element;
}
public static void WaitAndClickElement(this By by)
{
WaitAndClickElement(by, TimeOut, PollingInterval);
}
public static void WaitAndClickElement(this By by, TimeSpan timeout, TimeSpan pollInterval)
{
try
{
var elementToClick = WaitUntilElementIsVisible(by, timeout, pollInterval);
try
{
elementToClick.Click();
}
catch(StaleElementReferenceException)
{
var wait = new WebDriverWait(DriverBase.Instance, timeout)
{ PollingInterval = pollInterval };
wait.Until(ExpectedConditions.StalenessOf(DriverBase.Instance.FindElement(by)));
DriverBase.Instance.FindElement(by);
}
}
catch(Exception exception) when (exception is WebDriverException ||
exception is InvalidOperationException)
{
DriverBase.Instance.Manage().Timeouts().ImplicitWait = TimeOut;
throw new Exception($"Unable to click element {by}
due to the following error: {exception.Message}");
}
}
#endregion
}
The code in here is slightly more complicated than the Window
class in that we aren’t simply wrapping existing functionality. Instead, we are going to add functionality to Selenium in the form of extension methods.
Extension methods allow you to add new methods to existing classes or types. In this context, we’re adding extension methods to deal with elements that may or may not be visible, or elements that may not even exist at all.
Adding methods like this allow us to decide how we will deal with these kinds of events. Without these extension methods, if our tests encounter an element that doesn’t exist, it will just crash and throw an exception. However, there may be times when we are expecting an element not to be visible, in which case we don’t want to crash at all.
Extension methods can be identified by their first parameter having the ‘this
’ keyword at the start, so ‘this By by
’. This tells our code that we want to add a method to the By
type. So you can see in this class that we are adding a lot of extension methods to By
, which massively improves the way in which we can interact with our elements.
The code itself, other than the extension methods isn’t doing anything too complicated, and follows our previous code in that it does a lot of thorough exception handling, the only new code I’d like to cover is this:
Our wait
variable we are declaring allows us to add certain conditions to our wait
s that determine how long we are waiting for, or a certain event. We can even set events to be ignored. In this example, we are ignoring StaleElementReferenceExceptions
. Stale elements are effectively old elements, this could be as simple as a text box that we’ve tried to manipulate too quickly, and the location of the text box in the DOM has changed. We might be expecting an event like this to happen when waiting for element, so we choose to ignore it.
Whereas you can see in another part of the code, we’ve chosen not to ignore it. This is because we are actually interacting with the element, via a click. We definitely don’t want to ignore any possible issues with stale elements.
The trigger for our wait
to stop is done using the Until
method. Here, we pass it a special variable type of ExpectedConditions
, one particular type of ExpectedConditions
is the ElementIsVisible
. As soon as our code picks up that this element is now visible, it will stop the wait and proceed, however, if it times out or there is an issue, we will catch and throw an exception.
{
public static partial class Driver
{
public static string InvokeJavaScript(string script)
{
return ((IJavaScriptExecutor)DriverBase.Instance).ExecuteScript(script).ToString();
}
}
}
Our final class is the script
class. This is simple in that its purpose is to simply allow us to execute any JavaScript on the web pages we visit. Why might this be useful? Well, there are usually scripts on a page that allow us to check if a page has finished loading, which we can use to wait when we navigate to a new page instead of using something lazy like a Thread.Sleep
. There are other scripts that can be available depending on the page and the developers that have coded it.
So, there we have it, we now have our Driver
class. We can now launch a driver
instance, manipulate the window and interact with any elements on our pages. We also have great exception handling that will allow us to deal with any issues that might occur.
In the next article, we’re going to expand upon our element interaction and add a control interface that will improve how we initialise and interact with different types of elements.
The post Framework – Your First Framework – Part 3 appeared first on Learn Automation.