Monday 29 July 2013

Driving an Embedded WPF browser with Selenium (2) Web Driver

Recently our project team was tasked with something a bit strange.  A little history first.

The application we work on is a WPF application for performing simple business functions.  It "integrates" with a large external vendor system that has a user interface of it's own done over the web for the most part; and not enough of the API is exposed to rebuild our own user interfaces.  When doing a cost analysis it was deemed much cheaper to build fragile DOM parsed linkages and include the HTML user interface into our application then to have the vendor build the appropriate APIs (face palms following are welcomed, not a great story, but it's the world I'm stuck with and helps us arrive at our problem).

What is that problem?  How do you automate a test when a huge chunk of the application's functionality is build into a browser window?  Without these tests how do we determine when those fragile DOM linkages are broken (likely because the vendor changed the DOM structure or content)... before the current team of developers took over the project it was a manual process for the testers of checking each function of the system by hand that was possible given a release timeline.  In other words; we simply never caught everything before the code was pushed into production.  Subsequently every release would include 2 or 3 emergency patches mostly surrounding DOM integration and the embedded web component.

A little more history?  The previous development team had automated the pieces of the application that didn't involve the embedded browser using the White framework.  I was relatively happy with White and saw no reason to get rid of it; albeit since that framework had been created until today they were a little out of date obviously.  So a better title for this article may have even been "How to drive a WPF application using White and Selenium" or some such thing!

One more short digression...  I sent a few messages off to the Selenium team looking to see if there was interest in having an official patch and support for this.  I never got a response, so I'm not sure if the right people never saw it or if they aren't interested, but I refuse to spend time creating a well designed and supported extension only to have it rejected.  This is why a blog entry and not an official patch!

The first thing you'll need to know before we go any further; you will need to be comfortable compiling C++ (Selenium); mainly the Internet Explorer Server Driver component.  You'll also need to know a little about how Windows Handles work in Win32 programming and be comfortable with working around them.

Selenium is broken in half (for the purposes of this discussion): The .NET binary that is the API and satellite executable programs which actually integrate with and drive the browser.  For the hack we are about to orchestrate we really only needed to change the satellite Internet Explorer driver since we can hardcode how we locate the window.

Before: Driver creates a new instance of Internet Explorer and latched onto the process.  It proceeds to locate a windows handle within the process named "Internet Explorer_Server".  Once located some magic occurs that is irrelevant to what we care about and the application has control of that instance.

The goal here was to replace the IEDriverServer.exe with a version that no longer creates a new instance of Internet Explorer.  Instead we want to locate that windows handle within the WPF application and allow the magic to continue.  When we are done we also don't want to kill a process that never was created so there is some shutdown code to modify (or we would be stuck with a console window on our automation computer).  We will use the White framework to shutdown the application we are driving so really we need absolutely no shutdown code from Selenium at all.

So here is what changed (ignoring the solution and project file, they changed only because I was lazy and ignored a few things to compile):


1) Don't launch a new version of IE (IECommandExecutor.cpp)

Function: IECommandExecutor::CreateNewBrowser()

Remove the code using the factory to spawn a new browser process.

I also hard coded the dwProcessId to NULL just to be safe.

2) Don't search for the Windows Handle by process anymore (BrowserFactory.cpp)

Function: BrowserFactory::AttachToBrowser()

Obviously we don't have a process ID anymore.  Since this is a quick hack rather then a real solution lets just make this search out our WPF window by caption then sub-window search recursively until we locate the appropriate HWND for Internet Explorer.  The sample code here was what I used to located the default WPF MainWindow with a <WebBrowser> filling the window (adjustments are needed for your own implementation assuming you don't want to have this information or a more robust solution occurring at an API level in the C# side of the world).


**Note: The implementations for finding a window are fairly standard using EnumWindows and EnumChildWindows.  It feels pedantic to include it here.
I looked around briefly on request in comments; I don't have the implementation for the window searching above.  You can kind of get an idea for how the searching was implemented from the definition above, but you will need Google if you aren't familiar with searching for windows in the WinApi.

3) Shutdown (IESession.cpp & Browser.cpp)... Optional if you don't require Quit() at the API

Function: Browser::Close()
Function: IESession::Shutdown()

When you try to .Quit() at the API if you haven't followed these directions two things will happen:
  1. The console window will hang then eventually time-out and crash
  2. The WPF application will act very strange as the browser attempts to actually close out from whatever drives it
Basically we just need to remove all the shutdown code as follows.


**Note: I kept the section about event firing being shut off.  It actually appears to do something useful even in this context!

Profit?

The last thing you need to do is go set it all up.  We used NuGet to install Selenium WebDriver into our testing projects which already were setup with White.  There were a series of tests that already drove our application with White to eventually bring up a screen that had a web browser embedded in them.  When that screen needed to be interacted with an InternetExplorerDriver() was instanced which will spawn IEDriverServer.exe (which we patched with our hacked version) and that will attach to the WPF instance and voila... follow standard Selenium design and syntax to automate your WPF browser.