Posts
114
Comments
126
Trackbacks
10
May 2010 Entries
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===<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; }
        }
    }
}
Posted On Monday, May 31, 2010 12:18 PM | Comments (6)
Azure Boot Camp

Belated thanks to Perficient for sponsoring (and providing lunch, which was a nice unadvertised surprise) and to Avichal Jain and Brian Blanchard for presenting at the St. Louis Azure Boot Camp May 13-14.

There was a little more upfront discussion of “What is Cloud Computing and Why is it important?” than I thought necessary (I would think that people signing up for a two-day Azure event would already be convinced that it’s a worthwhile thing), but we put on our boots and fired up Visual Studio soon enough.

The good news for developers, as with most of Microsoft’s recent initiatives (e.g Silverlight and Windows Phone 7 development), is that you can leverage the skills you already have. If you’ve developed service-oriented applications, you’ve got a big head start.

If a free Azure Boot Camp event is coming to your area (here’s the schedule), be sure to check it out. If not, you can download the slides and labs from their web site and “throw your own”.

Posted On Monday, May 31, 2010 8:03 AM | Comments (0)
Tag Cloud