Capturing screenshots with Mink, Sahi and PhantomJS

Following on from my previous post Headless Behat/Mink testing with Sahi and PhantomJS, I wanted to complete the final part of Ryan’s post concerning capturing a screenshot of the page which has failed a test. I had hoped this would be fairly simple, but the march of technology has made Ryan’s post almost obsolete. Also, I am not  fan of his technique, with involves injecting an element into the page from the Phantom setup script. This causes problems if your tests want to move between pages.

Setting up Phantom

The additions required to the Phantom script I used in the last post are fairly minimal, just the last four lines:

if (phantom.args.length === 0) {
    console.log('Usage: sahi.js ');
    phantom.exit();
} else {
    var address = phantom.args[0];
    console.log('Loading ' + address);
    var page = new WebPage();
    page.open(address, function(status) {
        if (status === 'success') {
            var title = page.evaluate(function() {
                return document.title;
	    });
            console.log('Page title is ' + title);
        } else {
            console.log('FAIL to load the address');
        }
    });
    // add callback listener to catch window.callPhantom() in the page
    page.onCallback = function(data) {
        page.render('/home/shanethehat/fail.jpg');
        phantom.exit();
    };
}

In my opinion this is much cleaner than injecting an element into the page, although it does come with the caveat that the onCallback event is still considered experimental in PhantomJS 1.7. The onCallback handler is triggered when the loaded page makes a call to window.callPhantom(), which is going to be done when Mink fails a step. The call to page.render() will generate an image of the page at the point of the fail. It is no longer necessary to set an arbitrary viewport size first, the image will be the correct size for the page.

Hooking up Mink

As Ryan did, I created a hook in Mink to fire after a step completes. Hooks in Behat are now implemented as functions, with an annotation to set the target point.

public function afterStep(StepEvent $event)
{
    /**
     * @AfterStep
     */
    $context = $event->getContext();
    if ($context->getMinkParameter('browser_name') == 'phantomjs' && $event->getResult() == StepEvent::FAILED) {
        $context->getSession()->executeScript('window.callPhantom()');
    }
}

This is a bit different to Ryan’s code, although the intention is the same. The event object no longer contains an environment; instead I grab a reference to the context, which is the instance of the FeatureContext class. If PhantomJS is the browser and the step failed then executeScript() calls Sahi’s _call() function to trigger the call the Phantom from the page.

Now all it takes is to write a failing test and watch your screenshot appear.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>