My name is Michal Sporek. I am a contractor in IT (software development). I am available for IT contracting jobs working remotely (telecommuting).

Please visit my site to learn more...

Experiment using Selenium with Google Chrome and Firefox (C# .NET)

Recently I've been doing research on one of my interests - automating actions in web browsers to allow advanced testing of web solutions. Once you decide to automate testing done with Internet Explorer - there are numerous libraries available on the market and you can decide which one suits you the best. But if you would like to automate Google Chrome browser, well - that's not the case anymore. I had made some research on the best ways to automate Chrome with C# .NET code and what I found is that it's the best to use Selenium. Selenium is a tool basically used for automating testing on the web and it comes with support of the largest browser vendors and it has a convenient API.

I tried to see how good Selenium really is when working on doing some automation with websites which have not that clear structure, and trying to do tricky things. My idea for an experiment was - let's see if I can write a small piece of C# .NET code which will allow me (with the help of Selenium) to send emails via the gmail web interface. Assuming - for simplicity - that the user is already authenticated in the Chrome, what I have to do is just to open a new Chrome webbrowser, navigate to gmail.com, force it to compose a new email in the webbrowser, and send it via the web interface... Actually I have managed to do it and bundled my functionality into a class I called GmailHandler. This is how I use it:

// Creating a new instance of the ChromeBrowser class.
// Making it use the current user profile, and utilizing the deferred (lazy) initialization.
using (ChromeBrowser chrome = new ChromeBrowser(true, true))
{
    // Creating a new instance of my GmailHandler class, passing the newly-created ChromeBrowser inside.
    GmailHandler gmailHandler = new GmailHandler(chrome);
    gmailHandler.OpenGmail();
    gmailHandler.SendEmail("msporek@gmail.com", "chrome test", "This is\n\rChrome body\n\r");
}

This simple example above shows the usage of my class. It uses both GmailHandler which is a class I have specifically created for opening gmail, composing a new email message, and sending it, as well as ChromeBrowser which is a class I have created a wrapper around the Selenium browser representedy by the RemoteWebDriver class.

Now let's have a look at the implementation of my GmailHandler class:

public class GmailHandler
{
    // Method opens gmail and checks whether we can find the "COMPOSE" button.
    // If we cannot find the button, then we throw an exception.
    public void OpenGmail()
    {
        browser.Driver.Navigate().GoToUrl("http://www.gmail.com");
    
        GetDivCOMPOSEThrowException();
    }

    // Method sends email of given subject and body to given email address.
    public void SendEmail(string toAddresses, string subject, string body)
    {
        // Getting the "COMPOSE" button...
        IWebElement divCompose = GetDivCOMPOSEThrowException();
        try
        {
            // We need to click "COMPOSE" in the webbrowser, to open the window where we put email contents.
            divCompose.Click();
        }
        catch (Exception ex)
        {
            throw new GmailException(string.Format("Error on clicking the \"Compose\" with {0}.", browser.ToString()), ex);
        }

        System.Threading.Thread.Sleep(5000);

        // Getting the email recipient field. If there is no such a field - throw an exception.
        IWebElement toField = browser.Driver.FindElementByName("to");
        if (toField != null)
        {
            try
            {
                // Injecting the recipient addresses into this field - we can inject multiple addresses separated with a semicolon.
                toField.SendKeys(toAddresses);
            }
            catch (Exception ex)
            {
                throw new GmailException(string.Format("Error on setting value in the \"To\" with {0}.", browser.ToString()), ex);
            }
        }
        else
        {
            throw new GmailException(string.Format("Cannot find the \"To\" with {0}.", browser.ToString()));
        }

        // Finding the subject field and setting the subject...
        IWebElement subjectField = browser.Driver.FindElementByName("subjectbox");
        if (subjectField != null)
        {
            try
            {
                subjectField.SendKeys(subject);
            }
            catch (Exception ex)
            {
                throw new GmailException(string.Format("Error on setting value in the \"Subject\" with {0}.", browser.ToString()), ex);
            }
        }
        else
        {
            throw new GmailException(string.Format("Cannot find the \"Subject\" with {0}.", browser.ToString()));
        }

        // Moving to body with a TAB, and entering the body contents...
        try
        {
            browser.Driver.Keyboard.SendKeys(Keys.Tab);
            browser.Driver.Keyboard.SendKeys(body);
        }
        catch (Exception ex)
        {
            throw new GmailException(string.Format("Error on setting value in the \"Body\" with {0}.", browser.ToString()), ex);
        }

        // Sending the email with a CTRL + ENTER combination which sends the message...
        try
        {
            browser.Driver.Keyboard.SendKeys(string.Concat(Keys.Control, Keys.Enter));
        }
        catch (Exception ex)
        {
            throw new GmailException(string.Format("Error on sending the email at gmail.com with {0}.", browser.ToString()), ex);
        }
    }

    protected virtual IWebElement GetDivCOMPOSE()
    {
        IWebElement divOverCompose = browser.Driver.FindElementByClassName("z0");
        return divOverCompose.FindElement(By.TagName("div"));
    }

    // Method fetches the main DIV which corresponds to the "Compose" button.
    // Once this button is clicked, then we can compose a new email.
    protected virtual IWebElement GetDivCOMPOSEThrowException()
    {
        IWebElement divCompose = null;
        try
        {
            divCompose = GetDivCOMPOSE();
        }
        catch (Exception ex)
        {
            throw new GmailException(string.Format("The user is not logged into gmail with {0}.", browser.ToString()), ex);
        }

        if (divCompose == null)
        {
            throw new GmailException(string.Format("The user is not logged into gmail with {0}.", browser.ToString()));
        }

        return divCompose;
    }

    // Passing the browser as a parameter. The ISeleniumBrowser is the interface which I have introduced
    // to be implemented by browser wrappers I implement.
    public GmailHandler(ISeleniumBrowser browser)
    {
        browser = browser;
    }

    // We use this browser to handle all the actions against gmail.
    protected ISeleniumBrowser browser = null;
}


What the SendEmail method does is basically it opens the gmail.com website, it assumes that the user is already logged into gmail (I do not implement authentication here), and then it finds the "Compose" button which you can easily find when you open gmail.com website being logged-in. The method clicks on the "Compose" button in the browser (which opens the email popup), and it then fills up all the email details. Once this is done, we send a CTRL + ENTER combination to the webbrowser which activates the "Send" button in the gmail email composing popup.

And finally here is the implementation of my ChromeBrowser class together with the ISeleniumBrowser interface:

// The class works as a wrapper over the Selenium Browser, it inherits from my SeleniumBrowserBase class.
public class ChromeBrowser : SeleniumBrowserBase, IDisposable
{
    protected override RemoteWebDriver InitializeNewDriver()
    {
        ChromeOptions options = new ChromeOptions();

        // If we should use the profile of the currently logged in user, then need to
        // open the Local Application Dat aFolder, get the chrome profile location, and pass the
        // user-data-dir parameter as an input to Chrome startup options.         if (useProfileOfCurrentUser)
        {
            string localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            string chromeProfileLocation = Path.Combine(localApplicationData, chromeUserDataRelativePath);
            options.AddArguments(string.Concat("user-data-dir=", chromeProfileLocation));
        }

        // Creating a new Chrome Driver Service,         // making it invisible so that we don't have too many windows running on the screen.         string dirWithChromeDriver = Directory.GetCurrentDirectory();
        chromeDriverService = ChromeDriverService.CreateDefaultService(dirWithChromeDriver);
        chromeDriverService.HideCommandPromptWindow = true;

        if (useProfileOfCurrentUser)
        {
            // If we are supposed to use the profile of the current user, then we need to kill all the running Chrome instances,
            // because they are probably using the profile...
            new List(Process.GetProcessesByName("chrome")).ForEach(p => p.Kill());
        }
    
        var newDriver = new ChromeDriver(chromeDriverService, options);

        return newDriver;
    }

    public override void Dispose()
    {
        base.Dispose();

        ChromeDriverService service = chromeDriverService;         if (service != null)
        {
            service.Dispose();
        }
        chromeDriverService = null;
    }

    public ChromeBrowser(bool useProfileOfCurrentUser, bool lazyInitialize)
    {
        useProfileOfCurrentUser = useProfileOfCurrentUser;
        lazyInitialize = lazyInitialize;

        if (!lazyInitialize)
        {
            remoteDriver = InitializeNewDriver();
        }
    }

    protected ChromeDriverService chromeDriverService = null;

    protected bool useProfileOfCurrentUser = false;
    protected bool lazyInitialize = false;

    private static readonly string chromeUserDataRelativePath = @"Google\Chrome\User Data";
}

public abstract class SeleniumBrowserBase : ISeleniumBrowser, IDisposable
{
    #region IDisposable Members

    public virtual void Dispose()
    {
        RemoteWebDriver driver = remoteDriver;
        if (driver != null)
        {
            driver.Close();
            driver.Dispose();
        }
        remoteDriver = null;
    }

    #endregion

    #region ISeleniumBrowser Members

    public RemoteWebDriver Driver
    {
        get
        {
            if (remoteDriver == null)
            {
             remoteDriver = InitializeNewDriver();
            }

            return remoteDriver;
        }
    }

    #endregion

    protected abstract RemoteWebDriver InitializeNewDriver();

    protected RemoteWebDriver remoteDriver = null;
}

public interface ISeleniumBrowser
{
    RemoteWebDriver Driver { get; }
}

My ChromeBrowser wrapper class presented above uses the ChromeDriverService class to create a new instance of ChromeDriver which derives from the RemoteWebDriver interface. The ChromeDriverService,ChromeDriver and RemoteWebDriver classes are provided by Selenium.

You can try this example yourself to see how it opens a new Chrome browser, then opens a gmail.com website within it, and providing that the current user is authenticated with gmail, the code will compose and send a simple email message. This approach - although it's just an experiment - could be used to automate sending emails with gmail web interface (of course it is easier to accomplish that by connecting directly to Google's SMTP server).

Selenium has appeared to be a very flexible tool when it comes to web tasks automation, and I believe I will use it for creating automated tests of the web applications I develop.


A post scriptum...

The way I have written the GmailHandler class is very flexible as it operates on the instance of ISeleniumBrowser injected. The GmailHandler is not bound to a specific implementation of a webbrowser, so the same functionality should be able to work with other browsers too. I have concluded that thanks to that, it should be an relatively easy task to provide a Firefox implementation of the ISeleniumBrowser and have the same functionality to work smoothly with Firefox.

Here is the my Firefox browser implementation:

public class FirefoxBrowser : SeleniumBrowserBase, IDisposable
{
    protected override RemoteWebDriver InitializeNewDriver()
    {
    // Getting the path to the Firefox profiles. They are stored in the subfolder of APPDATA folder.
    string pathToCurrentUserProfiles = Environment.ExpandEnvironmentVariables("%APPDATA%") + @"\Mozilla\Firefox\Profiles";

    FirefoxDriver newDriver = null;
    // Getting profiles folders.
    string[] pathsToProfiles = Directory.GetDirectories(pathToCurrentUserProfiles, "*.default", SearchOption.TopDirectoryOnly);
    if (pathsToProfiles.Length != 0)
    {
        // Taking the first of them.
        FirefoxProfile profile = new FirefoxProfile(pathsToProfiles[0]);
        // Creating a new instance of FirefoxDriver (Selenium class with the profile of the currently-logged in user).
        newDriver = new FirefoxDriver(new FirefoxBinary(), profile);
    }
    else
    {
        newDriver = new FirefoxDriver();
    }

    return newDriver;
    }

    public FirefoxBrowser(bool lazyInitialize)
    {
        lazyInitialize = lazyInitialize;

        if (!lazyInitialize)
        {
            remoteDriver = InitializeNewDriver();
        }
    }

    protected bool lazyInitialize = false;
}

The FirefoxBrowser is following the same concept as the ChromeBrowser shown earlier. It tries to fetch the Firefox profile of the currently-logged-in user, and once it gets it - I create a new Firefox browser controller by Selenium with the right profile. Getting the profile is required as well as having the current user already logged in to Gmail.

You can easily try it and the few lines of code below proves that it works like a sharm:

using (FirefoxBrowser firefox = new FirefoxBrowser(true))
{
    GmailHandler gmailHandler = new GmailHandler(firefox);
    gmailHandler.OpenGmail();
    gmailHandler.SendEmail("msporek@gmail.com", "firefox test", "This is\n\rFirefox body\n\r");
}

Posted by Michal Sporek