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 customise 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 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 colour 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 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 colour. That’s it.
For more detailed overview and usage of many more design patterns and best practices in automated testing, check my book “Design Patterns for High-Quality Automated Tests, C# Edition, High-Quality Tests Attributes, and Best Practices”. You can read part of three of the chapters:
Defining High-Quality Test Attributes for Automated Tests
Benchmarking for Assessing Automated Test Components Performance
Generic Repository Design Pattern- Test Data Preparation
Highlighting Elements in Tests
[TestClass,
ExecutionEngineAttribute(ExecutionEngineType.WebDriver, Browsers.Chrome),
VideoRecordingAttribute(VideoRecordingMode.DoNotRecord)]
public class BingTests : BaseTest
{
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.
