Creating an internal WebDriverFactory

Creating an internal WebDriverFactory

This entry is part 4 of 9 in the series Launching WebDrivers in .NET Core the easy way

Last time I showed how to launch a WebDriver instance from .NET core so that we can test on any platform. When creating a framework however I like to have a factory to handle the driver creation and setup for me. I don’t wan’t to have to mess around setting things up in my Test class every time.

My .NET core WebDriverFactory design criteria:

  • It should have really simple syntax, especially for creating a ‘default’ webdriver.
  • It should handle local WebDriver instances that I might use for writing and debugging as well as calling for remote webdrivers from a Selenium grid.
  • It should handle the supported Desktop browsers. (Caveat here – as Opera has for some time used the same Blink browser Engine as Chrome, I don’t see the purpose in testing against both so I don’t explicitly support Opera)
  • By default it should launch a browser at 1366 x 768 to test usuability on (sadly) the most prevalent cheap laptop screen resolution: It should also provide support for 1080p (1920 x 1080) as well as full screen and ‘un-modified’ launch resolution.
  • Headless mode should be trivial to launch for Firefox and Chrome.
  • Exceptions should provide as much information about probable problems to make starting a project simple.
  • For now I am not touching on mobile, but extensibility should be a consideration.

Why the default screen size?

Depending on the project that you are testing, screen size may be a critical factor. You should probably test at both HD (1366 x 768) and FHD (1920 x 1080) for any application that you expect to be used primarily on PCs. Mac users are more challenging as the screens come in a huge range of sizes from HD up. My admittedly limited experience suggests that if your app works well at both of the above, that is sufficient for automation. (Of course you did perform thorough manual testing at launch on a range of window sizes didn’t you?)

Given that, in most cases you cannot set the browser window size at creation time, you could quite reasonably argue that this does not belong in the WebDriverFactory. Given its importance and role as the first change I would make after launch I will leave it in here for now: I may pull it out to a more general utility class later. It just requires a single method (that could equally be called at any later point) and some values. I will specifiy my screen sizes using an emun:

namespace AlexanderOnTest.WebDriverFactory
{
    public enum WindowSize
    {
        Hd,
        Fhd,
        Maximise,
        Unchanged
    }
}

Adding additional sizes requires adding a new value here as well as making the addition to any factory class that uses it. Oh how I miss my Java enums (sob!)

Notes:

  • WindowSize.Unchanged is particularly unhelpful for automation as it not only changes for each machine, but may even change over time.
  • WindowSize.Maximise should of course at least be consistent on the same machine, but I suspect that most developers work on significantly better displays than most users, and not necessarily the same as their CI build server.
  • When used on a headless browser; in my testing, WindowSize.Maximise  produces an 800 x 600 size window in Chrome and a 1366 x768 window in Firefox.
  • Calls to make a larger browser than the active display size will generally produce a browser that fills the screen. Only Firefox (on screen and headless) and headless Chrome permit windows of arbitrary size.
  • I still have no Mac so I am unsure of Safari window size behaviour.

WebDriverFactory method signatures

IWebDriver GetLocalWebDriver(Browser browser, bool headless = false);

and

IWebDriver GetRemoteWebDriver(Browser browser, Uri gridUrl, PlatformType platformType = PlatformType.Any)

In each case the method should return a WebDriver instance at 1366 x 768 resolution and for convenience include a resize method

IWebDriver SetWindowSize(IWebDriver driver, WindowSize windowSize)

I’m happy to expose other alternative signatures to provide options, but they are the goals. So lets get straight to it….

namespace AlexanderOnTest.WebDriverFactory
{
    public enum Browser
    {
        Firefox,
        Chrome,
        InternetExplorer,
        Edge,
        Safari
    }
}

and the StaticWebDriverFactory:

using System;
using System.Drawing;
using System.IO;
using System.Reflection;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Edge;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.IE;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Safari;

namespace AlexanderOnTest.WebDriverFactory
{
    public static class StaticWebDriverFactory
    {
        private static string DriverPath => Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

        public static IWebDriver GetLocalWebDriver(Browser browser, bool headless = false)
        {
            if (headless && !(browser == Browser.Chrome || browser == Browser.Firefox))
            {
                throw new ArgumentException($"Headless mode is not currently supported for {browser}.");
            }
            switch (browser)
            {
                case Browser.Firefox:
                    return GetLocalWebDriver(StaticDriverOptionsFactory.GetFirefoxOptions(headless));

                case Browser.Chrome:
                    return GetLocalWebDriver(StaticDriverOptionsFactory.GetChromeOptions(headless));

                case Browser.InternetExplorer:
                    return GetLocalWebDriver(StaticDriverOptionsFactory.GetInternetExplorerOptions());

                case Browser.Edge:
                    return GetLocalWebDriver(StaticDriverOptionsFactory.GetEdgeOptions());

                case Browser.Safari:
                    return GetLocalWebDriver(StaticDriverOptionsFactory.GetSafariOptions());

                default:
                    throw new PlatformNotSupportedException($"{browser} is not currently supported.");
            }
        }

        public static IWebDriver GetLocalWebDriver(ChromeOptions options, WindowSize windowSize = WindowSize.Hd)
        {
            IWebDriver driver = new ChromeDriver(DriverPath, options);
            return SetWindowSize(driver, windowSize);
        }

        public static IWebDriver GetLocalWebDriver(FirefoxOptions options, WindowSize windowSize = WindowSize.Hd)
        {
            IWebDriver driver = new FirefoxDriver(DriverPath, options);
            return SetWindowSize(driver, windowSize);
        }

        public static IWebDriver GetLocalWebDriver(EdgeOptions options, WindowSize windowSize = WindowSize.Hd)
        {
            if (!Platform.CurrentPlatform.IsPlatformType(PlatformType.WinNT))
            {
                throw new PlatformNotSupportedException("Microsoft Edge is only available on Microsoft Windows.");
            }

            IWebDriver driver = new EdgeDriver(options);
            return SetWindowSize(driver, windowSize);
        }


        public static IWebDriver GetLocalWebDriver(InternetExplorerOptions options, WindowSize windowSize = WindowSize.Hd)
        {
            if (!Platform.CurrentPlatform.IsPlatformType(PlatformType.WinNT))
            {
                throw new PlatformNotSupportedException("Microsoft Internet Explorer is only available on Microsoft Windows.");
            }

            IWebDriver driver = new InternetExplorerDriver(DriverPath, options);
            return SetWindowSize(driver, windowSize);
        }

        public static IWebDriver GetLocalWebDriver(SafariOptions options, WindowSize windowSize = WindowSize.Hd)
        {
            if (!Platform.CurrentPlatform.IsPlatformType(PlatformType.Mac))
            {
                throw new PlatformNotSupportedException("Safari is only available on Mac Os.");
            }
            
            // I suspect that the SafariDriver is already on the path as it is within the Safari executable.
            // I currently have no means to test this
            IWebDriver driver = new SafariDriver(options);
            return SetWindowSize(driver, windowSize);
        }

        public static IWebDriver GetRemoteWebDriver(
            DriverOptions options,
            Uri gridUrl,
            WindowSize windowSize = WindowSize.Hd)
        {
            IWebDriver driver = new RemoteWebDriver(gridUrl, options);
            return SetWindowSize(driver, windowSize);
        }

        public static IWebDriver GetRemoteWebDriver(
            Browser browser,
            Uri gridUrl,
            PlatformType platformType = PlatformType.Any)
        {
            switch (browser)
            {
                case Browser.Firefox:
                    return GetRemoteWebDriver(StaticDriverOptionsFactory.GetFirefoxOptions(platformType), gridUrl);

                case Browser.Chrome:
                    return GetRemoteWebDriver(StaticDriverOptionsFactory.GetChromeOptions(platformType), gridUrl);

                case Browser.InternetExplorer:
                    return GetRemoteWebDriver(StaticDriverOptionsFactory.GetInternetExplorerOptions(platformType), gridUrl);

                case Browser.Edge:
                    return GetRemoteWebDriver(StaticDriverOptionsFactory.GetEdgeOptions(platformType), gridUrl);

                case Browser.Safari:
                    return GetRemoteWebDriver(StaticDriverOptionsFactory.GetSafariOptions(platformType), gridUrl);

                default:
                    throw new PlatformNotSupportedException($"{browser} is not currently supported.");
            }
        }

        public static IWebDriver SetWindowSize(IWebDriver driver, WindowSize windowSize)
        {
            switch (windowSize)
            {
                case WindowSize.Unchanged:
                    return driver;

                case WindowSize.Maximise:
                    driver.Manage().Window.Maximize();
                    return driver;

                case WindowSize.Hd:
                    driver.Manage().Window.Position = Point.Empty;
                    driver.Manage().Window.Size = new Size(1366, 768);
                    return driver;

                case WindowSize.Fhd:
                    driver.Manage().Window.Position = Point.Empty;
                    driver.Manage().Window.Size = new Size(1920, 1080);
                    return driver;

                case WindowSize.Uhd4K:
                    driver.Manage().Window.Position = Point.Empty;
                    driver.Manage().Window.Size = new Size(3840, 2160);
                    return driver;

                default:
                    return driver;
            }
        }
    }
}

Finally I need a factory to provide my default DriverOptions:

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Edge;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.IE;
using OpenQA.Selenium.Safari;

namespace AlexanderOnTest.WebDriverFactory
{
    public static class StaticDriverOptionsFactory
    {
        public static ChromeOptions GetChromeOptions(PlatformType platformType = PlatformType.Any)
        {
            return GetChromeOptions(false, platformType);
        }

        public static ChromeOptions GetChromeOptions(bool headless = false, PlatformType platformType = PlatformType.Any)
        {
            ChromeOptions options = new ChromeOptions();
            options.AddArguments("disable-infobars", "test-type");
            if (headless)
            {
                options.AddArgument("headless");
            }

            SetPlatform(options, platformType);
            return options;
        }

        public static FirefoxOptions GetFirefoxOptions(PlatformType platformType = PlatformType.Any)
        {
            return GetFirefoxOptions(false, platformType);
        }

        public static FirefoxOptions GetFirefoxOptions(bool headless = false, PlatformType platformType = PlatformType.Any)
        {
            FirefoxOptions options = new FirefoxOptions
            {
                AcceptInsecureCertificates = true
            };

            if (headless)
            {
                options.AddArgument("--headless");
            }

            SetPlatform(options, platformType);
            return options;
        }

        public static EdgeOptions GetEdgeOptions(PlatformType platformType = PlatformType.Any)
        {
            EdgeOptions options =  new EdgeOptions();
            SetPlatform(options, platformType);
            return options;
        }

        public static InternetExplorerOptions GetInternetExplorerOptions(PlatformType platformType = PlatformType.Any)
        {
            InternetExplorerOptions options = new InternetExplorerOptions
            {
                EnablePersistentHover = true,
                IgnoreZoomLevel = true,
                UnhandledPromptBehavior = UnhandledPromptBehavior.Dismiss,
            };

            SetPlatform(options, platformType);
            return options;
        }

        public static SafariOptions GetSafariOptions(PlatformType platformType = PlatformType.Any)
        {
            SafariOptions options = new SafariOptions();
            SetPlatform(options, platformType);
            return options;
        }

        public static T SetPlatform<T>(T options, PlatformType platformType) where T : DriverOptions
        {
            switch (platformType)
            {
                case PlatformType.Any:
                    return options;

                case PlatformType.Windows:
                    options.PlatformName = "WINDOWS";
                    return options;

                case PlatformType.Linux:
                    options.PlatformName = "LINUX";
                    return options;

                case PlatformType.Mac:
                    options.PlatformName = "MAC";
                    return options;

                default:
                    return options;
            }
        }
    }
}

Of course we need to test this too. The nature of the factory means that it requires integration rather than unit tests, and so for Windows:

using System;
using System.Drawing;
using FluentAssertions;
using FluentAssertions.Execution;
using NUnit.Framework;
using OpenQA.Selenium;

namespace AlexanderOnTest.WebDriverFactory.WindowsTests
{
    [TestFixture]
    public class StaticWebDriverFactoryTests
    {
        private IWebDriver Driver { get; set; }
        private readonly PlatformType thisPlatformType = PlatformType.Windows;

        [OneTimeSetUp]
        public void CheckForValidPlatform()
        {
            Assume.That(() => Platform.CurrentPlatform.IsPlatformType(thisPlatformType));
        }

        [Test]
        [TestCase(Browser.Firefox)]
        [TestCase(Browser.InternetExplorer)]
        [TestCase(Browser.Edge)]
        [TestCase(Browser.Chrome)]
        public void LocalWebDriverCanBeLaunchedAndLoadExampleDotCom(Browser browser)
        {
            Driver = StaticWebDriverFactory.GetLocalWebDriver(browser);
            Driver.Url = "https://example.com/";
            Driver.Title.Should().Be("Example Domain");
        }

        [Test]
        [TestCase(Browser.Safari)]
        public void RequestingUnsupportedWebDriverThrowsInformativeException(Browser browser)
        {
            Action act = () => StaticWebDriverFactory.GetLocalWebDriver(browser);
            act.Should()
                .Throw<PlatformNotSupportedException>($"because {browser} is not supported on {thisPlatformType}.")
                .WithMessage("*is only available on*");
        }

        [Test]
        [TestCase(Browser.Firefox)]
        [TestCase(Browser.Chrome)]
        public void HeadlessBrowsersCanBeLaunched(Browser browser)
        {
            Driver = StaticWebDriverFactory.GetLocalWebDriver(browser, true);
            Driver.Url = "https://example.com/";
            Driver.Title.Should().Be("Example Domain");
        }

        [Test]
        [TestCase(Browser.Edge)]
        [TestCase(Browser.InternetExplorer)]
        [TestCase(Browser.Safari)]
        public void RequestingUnsupportedHeadlessBrowserThrowsInformativeException(Browser browser)
        {
            Action act = () => StaticWebDriverFactory.GetLocalWebDriver(browser, true);
            act.Should()
                .ThrowExactly<ArgumentException>($"because headless mode is not supported on {browser}.")
                .WithMessage($"Headless mode is not currently supported for {browser}.");
        }

        [Test]
        public void HdBrowserIsOfRequestedSize()
        {
            Driver = StaticWebDriverFactory.GetLocalWebDriver(StaticDriverOptionsFactory.GetFirefoxOptions(true), WindowSize.Hd);

            using (new AssertionScope())
            {
                Size size = Driver.Manage().Window.Size;
                size.Width.Should().Be(1366);
                size.Height.Should().Be(768);
            };
        }

        [Test]
        public void FhdBrowserIsOfRequestedSize()
        {
            Driver = StaticWebDriverFactory.GetLocalWebDriver(StaticDriverOptionsFactory.GetFirefoxOptions(true), WindowSize.Fhd);

            using (new AssertionScope())
            {
                Size size = Driver.Manage().Window.Size;
                size.Height.Should().Be(1080);
                size.Width.Should().Be(1920);
            };
        }

        [Test]
        public void Uhd4KBrowserIsOfRequestedSize()
        {
            Driver = StaticWebDriverFactory.GetLocalWebDriver(StaticDriverOptionsFactory.GetFirefoxOptions(true), WindowSize.Uhd4K);

            using (new AssertionScope())
            {
                Size size = Driver.Manage().Window.Size;
                size.Height.Should().Be(2160);
                size.Width.Should().Be(3840);
            };
        }

        [Test]
        [TestCase(Browser.Chrome, PlatformType.Windows)]
        [TestCase(Browser.Edge, PlatformType.Windows)]
        [TestCase(Browser.Firefox, PlatformType.Windows)]
        [TestCase(Browser.InternetExplorer, PlatformType.Windows)]
        [TestCase(Browser.Chrome, PlatformType.Linux)]
        [TestCase(Browser.Firefox, PlatformType.Linux)]
        public void RemoteWebDriverCanBeLaunched(Browser browser, PlatformType platformType)
        {
            Driver = StaticWebDriverFactory.GetRemoteWebDriver(browser, new Uri("http://192.168.0.200:4444/wd/hub"), platformType);
            Driver.Url = "https://example.com/";
            Driver.Title.Should().Be("Example Domain");
        }


        [TearDown]
        public void Teardown()
        {
            Driver?.Quit();
        }
    }
}

And what do you know?

Looks good to me
It Works!
Works on Linux too

Progress made:

  • Working Static WebDriverFactory
  • Separate working test projects for Windows and Linux
  • Replaced hacked together Linux WLS System for a full Ubuntu Linux node on a new grid.

Lessons learnt:

  • I must be careful in WordPress to not press the wrong button and publish too early! Doh!
  • Verifying cross platform working takes much longer than writing just for my system.

A reminder:

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

Next time I’ll look at extending this to include an overidable Factory instance and package it for nuget.org. (Less code: more observations I promise)

[Edit 26/6/19 Moved into the new series: Launching WebDrivers in .NET Core the easy way]
Series Navigation<< Launching WebDrivers in .net CoreTestability, interfaces and unit tests for Dummies (and junior SDETs) >>

5 thoughts on “Creating an internal WebDriverFactory

  1. Oh man, you rock! I used to be a C# developer, years ago I switched to Java for a “temporal” project, now that I want to “duplicate” my Java Framework into C# I found it is a pain because I’m in the middle of nothing. I just found things like PageFactory is deprecated and stuff like that. I’m rusty 🙁

  2. Hi Alexander,

    I’m so glad I have found your blog. I am a beginner in test automation and I am using Selenium in C#. Your blog is so useful for me and I learned a lot from it. I hope on your next blog, you will share your experience with OOP approach to testing in Selenium C#. I am very confused and would greatly appreciate hearing more from you.

    1. Hi Kathleen, I am glad you find it useful. My intention is to build up an object oriented framework using my version of the page factory.

      As this is written in my spare time it will take a while, but I find that blogging about it helps me remember what I did. It is worth thinking about doing something similar yourself.

Comments are closed.

Comments are closed.