WatiN screenshot saver

Technorati Tags: .NET,WatiN,testing

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; }
    }
}

}