WatiN screenshot saver

Technorati Tags:,,

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===

{0}:
<img src=\"{1}\" />
===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); }

///

/// 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). /// 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; } } } }

(http://11011.net/software/vspaste)

This article is part of the GWB Archives. Original Author: Brian Schroer

New on Geeks with Blogs