HTTP Error Codes in WatiN 1.3
One of the biggest surprises when I started working with WatiN was
the omission of a mechanism to check for error conditions. A partial
solution using a subclass has been posted
before, but it doesn’t quite cover all the bases. Specifically, it’s
missing a mechanism to attach existing Internet Explorer instances to
objects of the enhanced subtype. Depending on the site under test’s use
of pop-ups, this could be a rather severe limitation. So let’s see how
we can fix it.
As WatiN is open source, one option is to just patch the existing implementation to include the desired behavior. I’ve uploaded a patch with tests here, but the gist of the patch is quite similar to the solution referenced above:
protected void AttachEventHandlers()
{
ie.BeforeNavigate2 += (object pDisp, ref object URL, ref object Flags, ref object TargetFrameName, ref object PostData, ref object Headers, ref bool Cancel) =>
{
ErrorCode = null;
};
ie.NavigateError += (object pDisp, ref object URL, ref object Frame, ref object StatusCode, ref bool Cancel) =>
{
ErrorCode = (HttpStatusCode)StatusCode;
};
}
/// <summary>
/// HTTP Status Code of last error, or null if the last request was successful
/// </summary>
public HttpStatusCode? ErrorCode
{
get;
private set;
}
Before every request we clear out the error code, with errors captured as an enum value borrowed from System.Net.
We complete the patch by placing calls to our AttachEventHandlers()
method in two places:
- The constructor that accepts an existing SHDocVw.InternetExplorer handle.
- The CreateNewIEAndGoToUri() method used by every other constructor.
At this point we can now assert success:
using (IE ie = new IE("http://solutionizing.net/"))
{
Assert.That(ie.ErrorCode, Is.Null);
}
Or specific kinds of failure:
using (IE ie = new IE("http://solutionizing.net/4040404040404"))
{
Assert.That(ie.ErrorCode, Is.EqualTo(HttpStatusCode.NotFound));
}
See the patch above for a more complete set of example tests.
Private Strikes Again
It’s wonderful that we have the option to make our own patched build
with the desired behavior, but what if we would rather use the binary
distribution? Well through the magic of inheritance we can get most of
the way there pretty easily:
public class MyIE : IE
{
public MyIE()
{
Initialize();
}
public MyIE(object shDocVwInternetExplorer)
: base(shDocVwInternetExplorer)
{
Initialize();
}
public MyIE(string url)
: base(url)
{
Initialize();
}
// Remaining c'tors left as an exercise
// Property named ie for consistency with the private field in the parent
protected InternetExplorer ie
{
get { return (InternetExplorer)InternetExplorer; }
}
protected void Initialize()
{
AttachEventHandlers();
}
// AttachEventHandlers() and ErrorCode as defined above
}
But as I suggested before, this is where we run into a bit of a snag. The IE
class also provides a set of static AttachToIE()
methods that, as their name suggests, return an IE
object for an existing Internet Explorer window. These static methods
have the downside that they are hard-coded to return objects of type IE
, not our enhanced MyIE
type. And because all the relevant helper methods are private and not
designed for reuse, we have no choice but to pull them into our
subclass in their entirety:
public new static MyIE AttachToIE(BaseConstraint findBy)
{
return findIE(findBy, Settings.AttachToIETimeOut, true);
}
public new static MyIE AttachToIE(BaseConstraint findBy, int timeout)
{
return findIE(findBy, timeout, true);
}
public new static MyIE AttachToIENoWait(BaseConstraint findBy)
{
return findIE(findBy, Settings.AttachToIETimeOut, false);
}
public new static MyIE AttachToIENoWait(BaseConstraint findBy, int timeout)
{
return findIE(findBy, timeout, false);
}
private static MyIE findIE(BaseConstraint findBy, int timeout, bool waitForComplete)
{
SHDocVw.InternetExplorer internetExplorer = findInternetExplorer(findBy, timeout);
if (internetExplorer != null)
{
MyIE ie = new MyIE(internetExplorer);
if (waitForComplete)
{
ie.WaitForComplete();
}
return ie;
}
throw new IENotFoundException(findBy.ConstraintToString(), timeout);
}
protected static SHDocVw.InternetExplorer findInternetExplorer(BaseConstraint findBy, int timeout)
{
Logger.LogAction("Busy finding Internet Explorer matching constriant " + findBy.ConstraintToString());
SimpleTimer timeoutTimer = new SimpleTimer(timeout);
do
{
Thread.Sleep(500);
SHDocVw.InternetExplorer internetExplorer = findInternetExplorer(findBy);
if (internetExplorer != null)
{
return internetExplorer;
}
} while (!timeoutTimer.Elapsed);
return null;
}
private static SHDocVw.InternetExplorer findInternetExplorer(BaseConstraint findBy)
{
ShellWindows allBrowsers = new ShellWindows();
int browserCount = allBrowsers.Count;
int browserCounter = 0;
IEAttributeBag attributeBag = new IEAttributeBag();
while (browserCounter < browserCount)
{
attributeBag.InternetExplorer = (SHDocVw.InternetExplorer) allBrowsers.Item(browserCounter);
if (findBy.Compare(attributeBag))
{
return attributeBag.InternetExplorer;
}
browserCounter++;
}
return null;
}
The original version of the first findInternetExplorer()
is private. Were it protected instead, we would only have had to implement our own findIE()
to wrap the found InternetExplorer
object in our subtype.
I won’t go so far as to say private methods are a code smell, but they certainly can make the O in OCP more difficult to achieve.
So there you have it, two different techniques for accessing HTTP
error codes in WatiN 1.3. At some point I’ll look at adding similar
functionality to 2.0, if it’s not already there. And if someone on the
project team see this, feel free to run with it.