In addition to my automated unit, system and integration tests for ASP.NET projects, I like to give my customers something pretty that they can look at and visually see that the web site is behaving properly. I use the Gallio test runner to produce a pretty HTML report, and WatiN (Web Application Testing In .NET) to test the UI and create screenshots. I have a couple of issues with WatiN’s “CaptureWebPageToFile” method, though: It blew up the first (and only) time I tried it, possibly because… It scrolls down to capture the entire web page (I tried it on a very long page), and I usually don’t need that Also, sometimes I don’t need a picture of the whole browser window - I just want a picture of the element that I'm testing (for example, proving that a button has the correct caption). I wrote a WatiN screenshot saver helper class with these methods: SaveBrowserWindowScreenshot(Watin.Core.IE ie) / SaveBrowserWindowScreenshot(Watin.Core.Element element) saves a screenshot of the browser window SaveBrowserWindowScreenshotWithHighlight(Watin.Core.Element element) saves a screenshot of the browser window, with the specified element scrolled into view and highlighted SaveElementScreenshot(Watin.Core.Element element) saves a picture of only the specified element The element highlighting improves on the built-in WatiN method (which just gives the element a yellow background, and makes the element pretty much unreadable when you have a light foreground color) by adding the ability to specify a HighlightCssClassName that points to a style in your site’s stylesheet. This code is specifically for testing with Internet Explorer (‘cause that’s what I have to test with at work), but you’re welcome to take it and do with it what you want… using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using SHDocVw;
using WatiN.Core;
using mshtml;
namespace BrianSchroer.TestHelpers
{
public static class WatinScreenshotSaver
{
public static void SaveBrowserWindowScreenshotWithHighlight
(Element element, string screenshotName)
{
HighlightElement(element, true);
SaveBrowserWindowScreenshot(element, screenshotName);
HighlightElement(element, false);
}
public static void SaveBrowserWindowScreenshotWithHighlight(Element element)
{
HighlightElement(element, true);
SaveBrowserWindowScreenshot(element);
HighlightElement(element, false);
}
public static void SaveBrowserWindowScreenshot(Element element, string screenshotName)
{
SaveScreenshot(GetIe(element), screenshotName, SaveBitmapForCallbackArgs);
}
public static void SaveBrowserWindowScreenshot(Element element)
{
SaveScreenshot(GetIe(element), null, SaveBitmapForCallbackArgs);
}
public static void SaveBrowserWindowScreenshot(IE ie, string screenshotName)
{
SaveScreenshot(ie, screenshotName, SaveBitmapForCallbackArgs);
}
public static void SaveBrowserWindowScreenshot(IE ie)
{
SaveScreenshot(ie, null, SaveBitmapForCallbackArgs);
}
public static void SaveElementScreenshot(Element element, string screenshotName)
{
// TODO: Figure out how to get browser window "chrome" size and not have to go to full screen:
var iex = (InternetExplorerClass) GetIe(element).InternetExplorer;
bool fullScreen = iex.FullScreen;
if (!fullScreen) iex.FullScreen = true;
ScrollIntoView(element);
SaveScreenshot(GetIe(element), screenshotName, args =>
SaveElementBitmapForCallbackArgs(element, args));
iex.FullScreen = fullScreen;
}
public static void SaveElementScreenshot(Element element)
{
SaveElementScreenshot(element, null);
}
private static void SaveScreenshot(IE browser, string screenshotName,
Action<ScreenshotCallbackArgs> screenshotCallback)
{
string fileName = string.Format("{0:000}{1}{2}.jpg",
++_screenshotCount,
(string.IsNullOrEmpty(screenshotName)) ? "" : " ",
screenshotName);
string path = Path.Combine(ScreenshotDirectoryName, fileName);
Console.WriteLine();
// Gallio HTML-encodes the following display, but I have a utility program to
// remove the "HTML===" and "===HTML" and un-encode the rest to show images in the Gallio report:
Console.WriteLine("HTML===<div><b>{0}:</br></b><img src=\"{1}\" /></div>===HTML",
screenshotName, new Uri(path).AbsoluteUri);
MakeBrowserWindowTopmost(browser);
try
{
var args = new ScreenshotCallbackArgs
{
InternetExplorerClass = (InternetExplorerClass)browser.InternetExplorer,
ScreenshotPath = path
};
Thread.Sleep(100);
screenshotCallback(args);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
public static void HighlightElement(Element element, bool doHighlight)
{
if (!element.Exists) return;
if (string.IsNullOrEmpty(HighlightCssClassName))
{
element.Highlight(doHighlight);
return;
}
string jsRef = element.GetJavascriptElementReference();
if (string.IsNullOrEmpty(jsRef)) return;
var sb = new StringBuilder("try { ");
sb.AppendFormat(" {0}.scrollIntoView(false);", jsRef);
string format = (doHighlight)
? "{0}.className += ' {1}'"
: "{0}.className = {0}.className.replace(' {1}', '')";
sb.AppendFormat(" " + format + ";", jsRef, HighlightCssClassName);
sb.Append("} catch(e) {}");
string script = sb.ToString();
GetIe(element).RunScript(script);
}
public static void ScrollIntoView(Element element)
{
string jsRef = element.GetJavascriptElementReference();
if (string.IsNullOrEmpty(jsRef)) return;
var sb = new StringBuilder("try { ");
sb.AppendFormat(" {0}.scrollIntoView(false);", jsRef);
sb.Append("} catch(e) {}");
string script = sb.ToString();
GetIe(element).RunScript(script);
}
public static void MakeBrowserWindowTopmost(IE ie)
{
ie.BringToFront();
SetWindowPos(ie.hWnd, HWND_TOPMOST, 0, 0, 0, 0, TOPMOST_FLAGS);
}
public static string HighlightCssClassName { get; set; }
private static int _screenshotCount;
private static string _screenshotDirectoryName;
public static string ScreenshotDirectoryName
{
get
{
if (_screenshotDirectoryName == null)
{
var asm = Assembly.GetAssembly(typeof(WatinScreenshotSaver));
var uri = new Uri(asm.CodeBase);
var fileInfo = new FileInfo(uri.LocalPath);
string directoryName = fileInfo.DirectoryName;
_screenshotDirectoryName = Path.Combine(
directoryName,
string.Format("Screenshots_{0:yyyyMMddHHmm}", DateTime.Now));
Console.WriteLine("Screenshot folder: {0}", _screenshotDirectoryName);
Directory.CreateDirectory(_screenshotDirectoryName);
}
return _screenshotDirectoryName;
}
set
{
_screenshotDirectoryName = value;
_screenshotCount = 0;
}
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
int X, int Y, int cx, int cy, uint uFlags);
private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
private const UInt32 SWP_NOSIZE = 0x0001;
private const UInt32 SWP_NOMOVE = 0x0002;
private const UInt32 TOPMOST_FLAGS = SWP_NOMOVE | SWP_NOSIZE;
private static IE GetIe(Element element)
{
if (element == null) return null;
var container = element.DomContainer;
while (container as IE == null)
container = container.DomContainer;
return (IE)container;
}
private static void SaveBitmapForCallbackArgs(ScreenshotCallbackArgs args)
{
InternetExplorerClass iex = args.InternetExplorerClass;
SaveBitmap(args.ScreenshotPath, iex.Left, iex.Top, iex.Width, iex.Height);
}
private static void SaveElementBitmapForCallbackArgs(Element element, ScreenshotCallbackArgs args)
{
InternetExplorerClass iex = args.InternetExplorerClass;
Rectangle bounds = GetElementBounds(element);
SaveBitmap(args.ScreenshotPath,
iex.Left + bounds.Left,
iex.Top + bounds.Top,
bounds.Width,
bounds.Height);
}
/// <summary>
/// This method is used instead of element.NativeElement.GetElementBounds because that
/// method has a bug (http://sourceforge.net/tracker/?func=detail&aid=2994660&group_id=167632&atid=843727).
/// </summary>
private static Rectangle GetElementBounds(Element element)
{
var ieElem = element.NativeElement as WatiN.Core.Native.InternetExplorer.IEElement;
IHTMLElement elem = ieElem.AsHtmlElement;
int left = elem.offsetLeft;
int top = elem.offsetTop;
for (IHTMLElement parent = elem.offsetParent; parent != null; parent = parent.offsetParent)
{
left += parent.offsetLeft;
top += parent.offsetTop;
}
return new Rectangle(left, top, elem.offsetWidth, elem.offsetHeight);
}
private static void SaveBitmap(string path, int left, int top, int width, int height)
{
using (var bitmap = new Bitmap(width, height))
{
using (Graphics g = Graphics.FromImage(bitmap))
{
g.CopyFromScreen(
new Point(left, top),
Point.Empty,
new Size(width, height)
);
}
bitmap.Save(path, ImageFormat.Jpeg);
}
}
private class ScreenshotCallbackArgs
{
public InternetExplorerClass InternetExplorerClass { get; set; }
public string ScreenshotPath { get; set; }
}
}
}