Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Highlighting Elements on Action — Test Automation Framework Extensibility With the Observer Design Pattern

DZone 's Guide to

Highlighting Elements on Action — Test Automation Framework Extensibility With the Observer Design Pattern

Learn how to set up element highlighting in full-stack test automation frameworks.

· DevOps Zone ·
Free Resource

As you know, in past articles from the Design and Architecture Series I wrote about the 5th generation test automation frameworks, or as I like to call them, Full-stack Test Automation Frameworks. In the articles about the generations, we defined the most important qualities of these new frameworks. I think maybe the most important of them is the extensibility. Your framework will be used by different teams which operate in different contexts from one another. Meaning that no matter how smart you are and how ultra-generic you build your framework, there will be cases where it won't work out-of-the-box. Because of that, it is essential for the users to give them ways to extend and customize it. In this article, I am going to show you how to use event-delegates (or Observer Design Pattern implementation in .NET) to extend UI components. The goal is to create a plugin for highlighting elements after a particular action is performed which can ease debugging and troubleshooting through videos/screenshots. I am not going to go in details how the observer design pattern or even/delegates work since you can read about them in my past articles.

Creating Extensibility Points

Our first job is to create the so-called hooks or extensibility points where people can add their logic. Considering the problem, we try to solve — highlighting elements — the most appropriate place to put such a point is where we find elements or ElementFinderService. Also, we are going to extend only the WebDriver engine part of the Hybrid Framework.

public class ElementFinderService
{
    public static event EventHandler<NativeElementActionEventArgs> ReturningWrappedElement;
    private readonly IUnityContainer _container;
    public ElementFinderService(IUnityContainer container)
    {
        _container = container;
    }
    public TElement Find<TElement>(ISearchContext searchContext, Core.By by)
        where TElement : class, Core.Controls.IElement
    {
        var element = searchContext.FindElement(by.ToSeleniumBy());
        ReturningWrappedElement?.Invoke(this, new NativeElementActionEventArgs(element));
        var result = ResolveElement<TElement>(searchContext, element);
        return result;
    }
    public IEnumerable<TElement> FindAll<TElement>(ISearchContext searchContext, Core.By by) 
        where TElement : class, Core.Controls.IElement
    {
        var elements = searchContext.FindElements(by.ToSeleniumBy());
        var resolvedElements = new List<TElement>();
        foreach (var currentElement in elements)
        {
            var result = ResolveElement<TElement>(searchContext, currentElement);
            resolvedElements.Add(result);
        }
        return resolvedElements;
    }
    public bool IsElementPresent(ISearchContext searchContext, Core.By by)
    {
        var element = Find<Element>(searchContext, by);
        return element.IsVisible;
    }
    private TElement ResolveElement<TElement>(ISearchContext searchContext, IWebElement currentElement)
        where TElement : class, Core.Controls.IElement
    {
        var result = _container.Resolve<TElement>(new ResolverOverride[]
        {
            new ParameterOverride("driver", searchContext),
            new ParameterOverride("webElement", currentElement),
            new ParameterOverride("container", _container)
        });
        return result;
    }
}

The important part is placed in the method Find where we invoke the static event ReturningWrappedElement which means that in this particular moment the logic from all subscribers to this event will be executed.

public TElement Find<TElement>(ISearchContext searchContext, Core.By by)
    where TElement : class, Core.Controls.IElement
{
    var element = searchContext.FindElement(by.ToSeleniumBy());
    ReturningWrappedElement?.Invoke(this, new NativeElementActionEventArgs(element));
    var result = ResolveElement<TElement>(searchContext, element);
    return result;
}

All events in C# should accept arguments. I created special arguments for this one holding the wrapped WebDriver element.

public class NativeElementActionEventArgs
{
    public NativeElementActionEventArgs(IWebElement element)
        => Element = element;
    public IWebElement Element { get; }
}
Also, to ease the process of extensibility, I wrote a base class for all plugins. 
public abstract class ElementFinderServiceEvenHandlers
{
    public virtual void SubscribeToAll()
    {
        ElementFinderService.ReturningWrappedElement 
            += ReturningWrappedElementEventHandler;
    }
    public virtual void UnsubscribeToAll()
    {
        ElementFinderService.ReturningWrappedElement 
            -= ReturningWrappedElementEventHandler;
    }
    protected virtual void ReturningWrappedElementEventHandler(
        object sender, 
        NativeElementActionEventArgs arg)
    {
    }
}

If you want to create a plugin using the native WebDriver element you inherit this class and override the ReturningWrappedElementEventHandler method where you can put the logic that you want to be executed when the element is found.

By the way, the plugin was invented as part of the development of our 5th generation test automation framework- Bellatrix. This is one of the many features for easiness of troubleshooting. First, you can use it when you run tests locally on your machine to see what some particular test do. After that, you can enable it during CI execution together with screenshots on test failure or videos on test failure. Once there are generated the elements will be highlighted which will help you to identify faster in which particular area of your page the test failed.

Creating Highlight Element Plugin

To create the highlight plugin as mentioned, we need to inherit the abstract class- ElementFinderServiceEvenHandlers.

public static class ElementHighlighter
{
    private static readonly IJavaScriptInvoker JavaScriptExecutor;
    static ElementHighlighter()
    {
        JavaScriptExecutor = UnityContainerFactory.GetContainer().Resolve<IJavaScriptInvoker>();
    }
    public static void Highlight(this IWebElement nativeElement, int waitBeforeUnhighlightMiliSeconds = 100, string color = "yellow")
    {
        try
        {
            var originalElementBorder = (string)JavaScriptExecutor.ExecuteScript("return arguments[0].style.background", nativeElement);
            JavaScriptExecutor.ExecuteScript($"arguments[0].style.background='{color}'; return;", nativeElement);
            if (waitBeforeUnhighlightMiliSeconds >= 0)
            {
                if (waitBeforeUnhighlightMiliSeconds > 1000)
                {
                    var backgroundWorker = new BackgroundWorker();
                    backgroundWorker.DoWork += (obj, e) 
                        => Unhighlight(nativeElement, originalElementBorder, waitBeforeUnhighlightMiliSeconds);
                    backgroundWorker.RunWorkerAsync();
                }
                else
                {
                    Unhighlight(nativeElement, originalElementBorder, waitBeforeUnhighlightMiliSeconds);
                }
            }
        }
        catch (Exception)
        {
            // ignored
        }
    }
    private static void Unhighlight(IWebElement nativeElement, string border, int waitBeforeUnhighlightMiliSeconds)
    {
        try
        {
            Thread.Sleep(waitBeforeUnhighlightMiliSeconds);
            JavaScriptExecutor.ExecuteScript("arguments[0].style.background='" + border + "'; return;", nativeElement);
        }
        catch (Exception)
        {
            // ignored
        }
    }
}

First, we use the ServiceLocator design pattern to get the current instance of the JavaScriptInvoker which internally calls WebDriver to execute the JavaScript code. You can write something similar using WebDriver interfaces directly if you wish.

How Does the Highlight Method Work?

var originalElementBorder = (string)JavaScriptExecutor.ExecuteScript("return arguments[0].style.background", nativeElement);
JavaScriptExecutor.ExecuteScript($"arguments[0].style.background='{color}'; return;", nativeElement);

First, we get the original border of the element though JS code. Then, again through JS code, we set the specified new color as a new background.

var backgroundWorker = new BackgroundWorker();
backgroundWorker.DoWork += (obj, e) 
=> Unhighlight(nativeElement, originalElementBorder, waitBeforeUnhighlightMiliSeconds);
backgroundWorker.RunWorkerAsync();

In the next part, we create C# BackgroundWorker which create a new process which waits the specified amount of milliseconds before unhighlighting the element.

How Does the Unhighlight Method Work?

Thread.Sleep(waitBeforeUnhighlightMiliSeconds);
JavaScriptExecutor.ExecuteScript("arguments[0].style.background='" + border + "'; return;", nativeElement);

To unhighlight the element we just set the background to its original color. That's it.

Highlighting Elements in Tests


[TestClass,
ExecutionEngineAttribute(ExecutionEngineType.WebDriver, Browsers.Chrome),
VideoRecordingAttribute(VideoRecordingMode.DoNotRecord)]
public class BingTests : BaseTest
{
    [TestMethod]
    public void SearchForAutomateThePlanet()
    {
        var bingMainPage = Container.Resolve<BingMainPage>();
        bingMainPage.Navigate();
        bingMainPage.Search("Automate The Planet");
        bingMainPage.AssertResultsCountIsAsExpected("15,800,000");
    }
}

As part of the resolution of the execution engine, we call the SubscribeToAll method of the Highlight plugin. You can read more about this resolution engine in the article Create Hybrid Test Framework – Improved Execution Engine.

Topics:
devops ,tutorial ,test automation ,observer design pattern ,observer pattern

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}