On qTrace Defect Submission (Part 2) – Submitting via Browser Automation

In last week’s post, I posed a challenge regarding qTrace’s defect tracker integration ability.  On the one hand, we don’t want to belabor on replicating 100% every single feature of each and every supported defect tracker from within qTrace because that is hardly a scalable approach for our small team.  On the other hand, if we don’t have 100% functionality built in, some users at some point of time will have to perform many manual tasks between capturing their defect and having it submitted to their tracker.  It’s a dilemma.  This post presents a possible solution to this headache.  Enter browser automation.

Basically this is how it works.  After capturing a defect with qTrace, testers can choose to submit it via browser automation mode.  Then qTrace will fire up a browser instance, redirect user to the ticket submission page, and populate the page with information such as title, description, environment information and output attachments.  Now, testers can then make necessary adjustment, like selecting an assignee, priority etc. before submitting the defect.  In order words, qTrace takes care of data information and facilitates the population of data into a new defect while the defect tracker itself takes care of its business logic and workflow.

This post will toy with this very idea by building a custom qTrace integration adapter which uses Selenium WebDriver to submit tickets to Assembla.  Make sure you read this article on how to build a custom integration adapter before moving on because I will go quickly and skip most things already covered by that article.

Create a Library project in Visual Studio .NET.  After that, install the following NuGet packages:

  • Selenium WebDriver
  • Selenium WebDriver Support Classes

The NuGet Package Manager will download the following assemblies and reference them in your project.

Besides, reference the following assemblies in your project:

  • qTrace.Trackers.Contracts (shipped with qTrace)
  • System.ComponentModel.Composition
  • System.Xaml
  • System.WindowsBase
  • PresentationCore
  • PresentationFramework

Note that only the first 2 assemblies are required to build a custom adapter.  However, as you will see later, we need the other assemblies to build a WPF window.  Go ahead creating a class with the following implementation.

namespace SeleniumSubmitter
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Windows;
    using Trackers.Contracts;

    [Export(typeof(IBugTracker))]
    public class Submitter : IBugTracker
    {
        public string DisplayName
        {
            get { return "Selenium Submitter"; }
        }

        public string IconUri
        {
            get { return "/SeleniumSubmitter;component/icon.png"; }
        }

        public string Verify(BugTrackerAccount bugTrackerAccount,
                             int timeoutInMillis,
                             IDictionary settings)
        {
            throw new NotImplementedException();
        }

        public SubmittedDefect Submit(Window owner,
                                      BugTrackerAccount bugTrackerAccount,
                                      int timeoutInMillis,
                                      Defect defect,
                                      Func> attachmentsFunc,
                                      IDictionary settings)
        {
            throw new NotImplementedException();
        }
    }
}

Edit Post-build Action to have VS.NET copy all dependent private assemblies to
[qTrace Installation Folder]TrackersSelenium

Next, let’s implement the Verify() method.  We will simply send a request to Assembla REST API to request the list of spaces using the provided credentials.  If the credentials are valid, Assembla REST API will return the list of space in XML format to us. If not, there will be an authorization exception.  Kind of a hack you might say, but there seems to be no operation in the Assembla REST API that allows us to just authenticate a user without actually querying or modifying some kind of resources.  Notice that I factor out a method to create the request so that I can reuse it later.

public string Verify(BugTrackerAccount bugTrackerAccount,
                int timeoutInMillis,
                IDictionary settings)
{
    var request = CreateLoadSpacesRequest(bugTrackerAccount, timeoutInMillis);
    using (request.GetResponse()) return null;
}

private static HttpWebRequest CreateLoadSpacesRequest(
    BugTrackerAccount bugTrackerAccount,
    int timeoutInMillis)
{
    string url = string.Format("{0}/spaces/my_spaces", bugTrackerAccount.Url);
    var request = (HttpWebRequest) WebRequest.Create(url);
    request.Timeout = timeoutInMillis;
    request.Accept = "application/xml";
    request.ContentType = "application/xml";
    request.Credentials = new NetworkCredential(
        bugTrackerAccount.UserName,
        bugTrackerAccount.Password);
    return request;
}

Build the project, launch qTrace, go to Settings screen and observe that our custom adapter does show up.  Input the connection URL and credentials and click Test Connection.  If you provide valid information, qTrace should let us know that it can connect to the server.

Okay, now the interesting part, browser automation.  The approach could be summarized like this:

  1. When users click Submit, qTrace display a form allowing them to choose the Assembla space to submit to
  2. Users select a space, then click Submit
  3. qTrace launch Firefox (we could instruct WebDriver to use another browser instead), navigate to the login page, login (using the provided credentials), navigate to the ticket submission page, populate the summary, description and attachment areas
  4. Users fill in necessary details about the defect and click Submit on the browser

First, we need a screen to prompt users for an Assembla space.  Go ahead creating a WPF User Control.  We are supposed to create a WPF Window here, but because we started with the Library project, VS.NET doesn’t show the WPF Window template.  But that is no big deal, it only takes minor XAML edit to turn our control to a window.

Use the following XAML for the control.  Basically this turns it into a WPF window and adds a couple of UI controls to it.  To keep things simple, I’ll bind the combo box to the Spaces property of the window class instead of doing the extra steps of creating a view model and connecting it to the view.
Our window should look like this.

Let’s write code for the Space class, modeling an Assembla space, and the code-behind.

namespace SeleniumSubmitter
{
    public class Space
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }
}

namespace SeleniumSubmitter
{
    using System.Collections.ObjectModel;
    using System.ComponentModel;

    public partial class SpacePromptView : INotifyPropertyChanged
    {
        public SpacePromptView()
        {
            InitializeComponent();
        }

        private ObservableCollection _spaces;
        public ObservableCollection Spaces
        {
            get { return _spaces; }
            set
            {
                _spaces = value;
                if (_spaces.Count > 0)
                    SelectedSpace = _spaces[0];
                NotifyChange("Spaces");
            }
        }

        private Space _selectedSpace;
        public Space SelectedSpace
        {
            get { return _selectedSpace; }
            set
            {
                _selectedSpace = value;
                NotifyChange("SelectedSpace");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyChange(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private void Submit(object sender, System.Windows.RoutedEventArgs e)
        {
            DialogResult = true;
            Close();
        }

        private void Cancel(object sender, System.Windows.RoutedEventArgs e)
        {
            DialogResult = false;
            Close();
        }
    }
}

Now, we need a method to display the Window to user and get back the selected space.  The code basically starts by using Assembla REST API to query all the spaces, convert the response XML to a collection which is used to populate the Spaces property of the window class.

private Space SelectSpace(BugTrackerAccount bugTrackerAccount, int timeoutInMillis)
{
    var view = new SpacePromptView
                    {
                        Spaces = new ObservableCollection(
                            LoadSpaces(bugTrackerAccount, timeoutInMillis))
                    };
    return view.ShowDialog() == true ? view.SelectedSpace : null;
}

private List LoadSpaces(BugTrackerAccount bugTrackerAccount, int timeoutInMillis)
{
    var request = CreateLoadSpacesRequest(bugTrackerAccount, timeoutInMillis);
    using (var response = request.GetResponse())
    using (var responseStream = response.GetResponseStream())
    {
        var document = XDocument.Load(responseStream);
        return (from space in document.Root.Elements("space")
                select new Space
                {
                    Id = space.Element("wiki-name").Value,
                    Name = space.Element("name").Value
                }).ToList();
    }
}

Okay, now that we have allowed users to select space.  Next is the interesting part, we’ll automate the browser.  Selenium WebDriver is a very straightforward API.  We just need to examine the HTML of the pages we work with in order to decide how we can interact with UI elements within those pages.  Fortunately, Assembla’s HTML is very well formatted.  The only not so straightforward part is that in order to add attachment, user need to click on a link which will then display a file upload control.  So we need to identify that link and similar a click on it via WebDriver.  As can be seen in the screenshot below, all it takes is querying the link with the item-attachment CSS class and click it.

After that, the DOM is modified with the addition of a file upload control and some additional textboxes.  Another HTML peak reveals the name of the control.  Clicking Add attachments many times result in multiple file upload controls each with a predictable name with a sequence number starting from 1.  That’s all we need to know to interact with these controls.

Let’s go ahead implementing the Submit() method.  Note we also need a helper method FormatSummary() which turns some qTrace makers into the necessary text format used by Assembla.

public SubmittedDefect Submit(Window owner,
                                BugTrackerAccount bugTrackerAccount,
                                int timeoutInMillis,
                                Defect defect,
                                Func> attachmentsFunc,
                                IDictionary settings)
{
    // Prompt users to select a space
    var space = SelectSpace(bugTrackerAccount, timeoutInMillis);
    if (space == null)
        return null;
    Mouse.OverrideCursor = Cursors.Wait;
    try
    {
        // Generate the attachments in UI thread
        IDictionary attachments = attachmentsFunc();

        // Automate browser in a worker thread to avoid blocking the UI
        Task.Factory.StartNew(() =>
        {
            // Launch a FireFox instance
            var driver = new FirefoxDriver();

            // How long WebDriver will wait for an element to exist in the DOM before timeout
            driver.Manage().Timeouts().ImplicitlyWait(new TimeSpan(0, 0, 20));

            // Go to login page, fill in credentials and click Login
            driver.Navigate().GoToUrl(string.Format("{0}/login", bugTrackerAccount.Url));
            driver.FindElement(By.Name("user[login]")).SendKeys(bugTrackerAccount.UserName);
            driver.FindElement(By.Name("user[password]")).SendKeys(bugTrackerAccount.Password);
            driver.FindElement(By.Name("commit")).Click();

            // Go to ticket page, fill in summary and description
            driver.Navigate().GoToUrl(string.Format("{0}/spaces/{1}/tickets/new",
                                            bugTrackerAccount.Url, space.Id));
            driver.FindElement(By.Name("ticket[summary]")).SendKeys(defect.Title);
            driver.FindElement(By.Name("ticket[description]")).SendKeys(FormatSummary(defect.Summary));

            // Add attachments
            int i = 0;
            foreach (var attachment in attachments)
            {
                // Click the "Add attachments" link
                driver.FindElement(By.ClassName("item-attachment")).Click();

                // Fill file path to the file upload control
                driver.FindElement(By.Name(string.Format("ticket[new_attachment_attributes][{0}][file]", ++i)))
                        .SendKeys(attachment.Key);
            }
        });
    }
    finally
    {
        Mouse.OverrideCursor = null;
    }
    return null;
}

private string FormatSummary(string summary)
{
    return summary.Replace(Markers.Heading1, "h1. ")
                    .Replace(Markers.Heading2, "* ")
                    .Replace(Markers.Heading3, "* ")
                    .Replace(Markers.Note,     "!");
}

Build the project again and launch qTrace.  Try to capture a defect and click Submit.  Choose a space and click Submit again.  You should see qTrace fire up Firefox and interact with it to login and create a ticket.  The whole VS.NET project is also available in GitHub.

Conclusion
We have addressed a dilemma in qTrace.  Browser automation makes it possible to support any kind of logic and workflow a defect tracker needs (because the submission process happens inside the tracker itself) while not requiring users to perform many error-prone and manual tasks.  And the best thing is that the effort it takes to automate a tracker system is only a fraction of what is required to bake 100% of the tracker’s functionality into qTrace.

Having said that, what we have built so far is a very rough implementation of the idea.  In order for it to be production ready, much more designing needs doing.  For example WebDriver appears to always launch a completely new instance of a browser for every run.  That might be useful for test environment because testers might want a clean browser session to start with.  But it does not deem necessary in our scenario while making users have to wait for a browser instance to be launched.  It should be better if qTrace can navigate to the pages in an existing browser session, if it exists.

On the other hand, should we somehow combine the built-in submission of qTrace with browser automation or should we always treat them as separate submission scenarios?  How are users supposed to configure the browser automation submission?  Should qTrace, as shown in this article, ask for credentials or should it require users to manually log in? How to deal with captcha if it is turned on in some trackers? Should the same adapter provide both submission types and allow user to opt for a submission strategy?  All kinds of things to figure out, but they are part of what makes building qTrace exciting.

NEVER MISS AN UPDATE

Subscribe to Our Newsletter

Leave a Reply

Your email address will not be published. Required fields are marked *

More Great Content

Get Started with QASymphony