WatiN is my tool of choice for automating Internet Explorer, but I need to automate Firefox as well. I'm not happy with any of my options, and I've been wanting to explore automation with MUIA for the last several weeks, so I decided to play around with it a bit this evening.
Attaching to Firefox
The first thing I need to do is attach to a running instance of Firefox. Being a good TDD'er, I write my test first. Here it is:
[Fact]
public void firefox_attach_will_find_the_correct_automation_element()
{
StartFirefox();
IBrowser browser = Firefox.Attach("Mozilla Firefox");
Assert.NotNull(((Firefox)browser).Instance);
Assert.Equal("MozillaUIWindowClass", ((Firefox)browser).Instance.Current.ClassName);
}
I know the Firefox window's class name from a previous automation effort, so I use it for my assertion (I imagine it will be hard to find good assertions going forward, as so much of the validation will be visual). Now I can create the Attach method of my Firefox class. But where should I begin? I need to know what Firefox's automation element tree looks like. Using UISpy.exe would make that easy, but I'm having trouble getting it installed. I decide to just hack at the MUIA namespace (System.Windows.Automation) and see where it gets me.
I know I'll need to reference the MUIA assembly. I expect it to be System.Windows.Automation, but I'm wrong. A quick web search shows me what I need: UIAutomationClient and UIAutomationProvider. While I'm in the references dialog, I also notice UIAutomationTypes. I add all three for good measure.
I know from previous reading that AutomationElement.RootElement provides access to the top of the automation element tree. It's the Windows Desktop. But trying to loop through its children would be slow (and stupid). My test provides the window's title, so I decide to search on that. I give this code a run:
public static IBrowser Attach(string windowTitle)
{
AutomationElement firefox = AutomationElement.RootElement.FindFirst(
TreeScope.Children,
new PropertyCondition(
AutomationElement.NameProperty,
windowTitle));
if (firefox == null)
throw new InvalidOperationException("Unable to find the specified Firefox instance.");
return new Firefox(firefox);
}
Success! My test passes, and on the first try (that's not common for me). Before I move on, I'll explain the meat of this method, the FindFirst invocation.
The TreeScope.Children argument tells the FindFirst method to search only the immediate children of the Windows Desktop (as opposed to TreeScope.Descendants, which searches everything down the tree). The property condition specifies the search criteria. In this case, I searched by the name property. There are many other options as well (AutomationIdProperty and IsInvokePatternAvailableProperty are both good ones).
Maximizing the Browser
I'm pretty happy. I've made good progress for 10 or so minutes. I want to try one other thing, also very simple. I want to automate maximizing the browser window. Here is my unit test:
[Fact]
public void show_window_will_maximize_firefox()
{
StartFirefox();
IBrowser browser = Firefox.Attach("Mozilla Firefox");
browser.ShowWindow();
Assert.NotNull(((Firefox)browser).Instance);
Assert.Equal(
AutomationElement.RootElement.Current.BoundingRectangle,
((Firefox)browser).Instance.Current.BoundingRectangle);
}
My ShowWindow method should simulate the user clicking the maximize button (if you are wondering why I named a maximize method ShowWindow, it's because it will do more later; I plan to keep this tool as source compatible with WatiN as I can). In MUIA, clicking buttons is done with an InvokePattern. And I happen to remember reading that the maximize button has an AutomationID property of "Maximize". So, I create this ShowWindow method:
public void ShowWindow()
{
AutomationElement maximizeButton = instance.FindFirst(
TreeScope.Descendants,
new PropertyCondition(AutomationElement.AutomationIdProperty, "Maximize"));
if (maximizeButton != null)
((InvokePattern)maximizeButton.GetCurrentPattern(InvokePattern.Pattern)).Invoke();
}
The code is pretty simple. Try to get the maximize button, and if I get it invoke it. It's important to note that the maximize button is only present when the window isn't already maximized. If the window is maximized, it's a restore button instead.
I run my tests. Uh oh, the new test fails:
TestCase 'BrowserAutomator.UnitTests.FirefoxTests.show_window_will_maximize_firefox2'
failed: Assert.Equal() Failure
Expected: 0,0,1920,1106
Actual: -4,-4,1928,1054
Because I set the initial size for my Firefox window to 1024x768, I know that the browser was actually maximized. But comparing the bounding rectangles of the Windows Desktop and Firefox won't work. So, I go to "Plan B," and changed my test to this:
[Fact]
public void show_window_will_maximize_firefox()
{
StartFirefox();
IBrowser browser = Firefox.Attach("Mozilla Firefox");
browser.ShowWindow();
Assert.NotNull(((Firefox)browser).Instance);
Native.Types.WindowPlacement windowPlacement = new Native.Types.WindowPlacement();
Native.Methods.GetWindowPlacement(
new IntPtr(((Firefox)browser).Instance.Current.NativeWindowHandle),
ref windowPlacement);
Assert.Equal(Native.Constants.ShowCommandMaximize, windowPlacement.ShowCommand);
}
I run this test and it passes. I'm done for the night.
Afterthoughts
There are some real problems with my code as it currently is. For instance, if Firefox is already running, my unit tests fail. If Firefox was previously closed unexpectedly, my unit tests hang on the "Restore Previous Session" dialog.
I foresee other difficulties as well. An example is waiting for the page to finish loading. In a tool like WatiN, the browser's object model can be queried to determine whether the page is ready. I'm hoping I can query the text of the status bar, which always reads "Done" when the page is ready. We'll see. I'm sure there will be many problems such as this ahead.
Next Time
In part two of my Firefox automation adventures I will automate a Google search. It seems that most of the browser automation tools use this as their canonical example, so I might as well get it out of the way. More importantly, typing text and clicking buttons are simple but very useful steps forward.
Regards, DM