WatiN screenshot saver

Posted by Brian Schroer on Geeks with Blogs See other posts from Geeks with Blogs or by Brian Schroer
Published on Mon, 31 May 2010 10:18:58 GMT Indexed on 2010/05/31 17:34 UTC
Read the original article Hit count: 1121

Filed under:

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

© Geeks with Blogs or respective owner