Framework – Your First Framework – Part 3
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 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 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 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 in to 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 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 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 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 we are adding a lot of extension methods to By, which massively improves the way 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 waits 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.
Where as 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 simple 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.