Capturing screenshots is annoying, but most of time it has to be done and usually it's you - The Developer - who has to do it. Taking a few screenshots isn't too bad, but lets say for example that now you are working on a website using Responsive Web Design and you have to take five times as many screenshots at various viewports. That one, two second annoying task has now started eating into your lunch time.
Intro
Today I am going to run through writing a quick and simple script to take some screenshots of any site at various viewports and save the images to disc. This is something I first saw at mediaqueri.es and started implementing it into my build process. It's not ideal for real testing, as it acts more like a sanity test and provides a good overview for anything I might be working on involving Responsive Web.
When a build is run, a small script using PhantomJS and CasperJS can go and grab a screenshot at various viewports which I've defined and give me a quick overview of any desired page. It's particularly good when you are working within a larger team and you might not have the time before each build to go and check every single page and module for how it looks at various resolutions. It will also give you something to possibly show the client in regular intervals, to display how their site flexes at various viewports.
Note : PhantomJS is a headless WebKit browser and anything rendered would be using WebKit rendering, so it's not an actual representation of how the site would render on various devices that might run different browsers and whilst you are able to change the User Agent string sent to the site on-load, this doesn't change the rendering engine.
PhantomJS has a great JavaScript API built with testing in mind. For a great introduction to PhantomJS and using it for testing there is a tutorial right here on nettuts and be sure to check the official site and documentation.
CasperJS is a toolkit that sits on-top of PhantomJS and eases the process of writing Phantom scripts by providing functions, methods, and syntactic sugar.
Installation
You might already have Phantom installed, especially if you are already testing your client-side code, if not, it's pretty straight forward and you can get more detailed instructions on the official site
For Windows users, there's an executable to download and run.
For Mac users, there's both the binary or you can install using Homebrew:
brew update && brew install phantomjs
For Linux users, there's a 64-bit binary or you have an option to compile PhantomJS from source.
Once installed, you can open a terminal and check that everything is OK by running:
phantomjs --version
which should return:
1.8.0
Once Phantom is installed you can go ahead and install CasperJS too. For Mac users, you can again use Homebrew:
brew install casperjs
For Windows users, you need to append your PATH
file with ";C:\casperjs\batchbin"
(Modify this path depending on where you want to store CasperJS). Within the batchbin
directory, there is a batch file called casperjs.bat
, this will be the script that runs your Casper scripts without having to need Ruby or Python installed to run it. Whenever you need to run the Casper script, just use casperjs.bat scriptname.js
rather than casperjs scriptname.js
.
Then check that:casperjs --version
returns:1.0.0
Both of these version numbers are up-to-date as of the time of writing this article.
Hello PhantomJS
Now that we have both of these running, let's do a couple of quick Hello Worlds to make sure that both Phantom and Casper are running as expected.
Make a new directory and inside of it, make two JavaScript files, hellophantom.js
and hellocasper.js
. Open these up in the editor of your choice and lets start with making sure Phantom is actually running correctly.
We are going to start in the hellophantom.js
file and write a quick test to grab the title of a webpage. I'm not going to cover the PhantomJS API in-detail, this will just give you a quick introduction and test our installation. If you have PhantomJS running already, you can skip this part.
First we need to set up a couple of variables, one which instantiates the 'webpage' Module and another just as a 'URL' variable.
var page = require('webpage').create(), url = "http://net.tutsplus.com";
Next we can create the function that navigates to the webpage, we pass in the URL as an argument, and a callback function. We receive a status in our callback (success or fail) on the open
method.
page.open(url, function(status) { });
Now we can call the evaluate function to get the title of the page. We can return the result, to a variable, by assigning the function to it:
page.open(url, function(status) { var title = page.evaluate(function () { return document.title; }); });
Lastly, we are just going to log this out so we can see the result in the terminal and then exit out of the Phantom process.
console.log('Hello, World! The Page title on '+ url +' is ' + title); phantom.exit();
Our finished script will look something like this.
var page = require('webpage').create(), url = "http://net.tutsplus.com"; page.open(url, function (status) { var title = page.evaluate(function () { return document.title; }); console.log('Hello, World! The Page title on '+ url +' is ' + title); phantom.exit(); });
cd into the directory where this script is located and you can run it using the following command:
phantomjs hellophantom.js
After a few seconds, you will get the following result in your terminal:
Hello, World! The Page title on http://net.tutsplus.com is Web development tutorials, from beginner to advanced | Nettuts+
Thats great, but before we move on, we can just make this code a little more flexible with a quick re-factor. There are a few modules that are available for us to use and one of them is the system module. Some of the properties of the system module give you access to things such as the Process ID that PhantomJS is running on or the OS being used, but the one we are interested in is the args
property.
The args
property returns an array of the command line arguments. The first item in the array is always the script name, but we can pass any number of arguments from the command line and use them in our script. So we can pass the URL that we want to open, with phantom, in the command line, so we can re-use the script anytime without needing to edit it every time.
We just need to first require the system module and then change the url
variable to be the argument we pass through:
system = require('system'), url = system.args[1];
and now we can run the script with the following command:
phantomjs hellophantom.js http://net.tutsplus.com
Hello Casper
Now that we know that Phantom is working, we can move on to testing out Casper. We will replicate the same test script, only this time we will use the Casper API.
First we need to instantiate a casper instance:
var casper = require("casper").create();
and then grab the URL from one of the arguments passed from the terminal. Casper has its own command-line parser that sits on-top of the one that comes with Phantom and we can get access to any arguments passed through from the command-line just as we did previously. The only difference being, that our first argument will be the first one that we pass through and not the script name (as it was with Phantom)
var url = casper.cli.args[0];
The Casper CLI API can also take named options as well as positional arguments, we can use this if we wanted to set up some options or be more verbose with our script, for example:
casperjs hellocasper.js argumentOne argumentTwo --option1=this --option2=that
and we can get these named options using cli.get('optionName')
, so we could do something like the following, to pass in both arguments and options (if we had some config options that needed to be set):
var argumentOne = casper.cli.args[0]; var argumentTwo = casper.cli.args[1]; var thisOption = casper.cli.get('option'); var thatOption = casper.cli.get('option2');
For now, I am just going to use the positional argument to get the URL. Next we are going to run the start()
method in order to do any sort of navigation. The start method takes a string URL and a callback function.
casper.start(url, function() { this.echo('Hello, World! The Page title on '+ url +' is '); });
If you don't want to have all your functionality, you can use the then()
method. Each then()
method call gets added as a step in the stack and is executed in a linear fashion, so rather than the above, you could have:
casper.start(url); casper.then(function(){ this.echo('Hello, World! The Page title on '+ url +' is '); });
I prefer to use then()
, as I find it easier to read, but either is acceptable and really it's all just a matter of taste.
In order to get the title of the page there is already a getTitle()
method available to us, so we can just use that in our echo
.
casper.start(url); casper.then(function(){ this.echo('Hello, World! The Page title on '+ url +' is ' + this.getTitle()); });
Finally, we run our steps with the run()
method, which is a mandatory method, needed in order for your Casper script to run. This method can also have an optional onComplete
callback to run once all steps are complete. If you utilized the callback, you need to make sure that you exit the Casper process by using the exit()
method. Here's an example of both:
//this doesn't need to use the exit method. casper.run(); //OR //this needs the exit method casper.run(function(){ this.echo('Everything in the stack has ended'); this.exit(); })
Alternatively, you could just chain the exit method after the echo:
casper.run(function(){ this.echo('Everything in the stack has ended').exit(); })
Again, just a matter of taste.
Now our complete HelloCasper.js script should look like this:
var casper = require("casper").create(); var url = casper.cli.args[0]; casper.start(url, function(){ this.echo('Hello, World! The Page title on '+ url +' is ' + this.getTitle()); }); casper.run(function() { this.echo('Everything in the stack has ended.').exit(); });
We can now run the Casper script with the following command:
casperjs hellocasper.js http://net.tutsplus.com
It doesn't do anything different than what we were already doing with Phantom, Casper just gives us a nice API (with some added extras) to sit on top of Phantom and makes the code we write a little more verbose and readable, this is particularly helpful when you get into writing scripts that have to navigate a site.
Lets now dive into saving some snapshots of our screen.
Snapshot Basics
I'm going to start off with a file called casperscreens.js and instantiate Casper. Then set up an array that will contain our desired viewport widths that we want to capture screenshots at. Each item in the array will consist of another array which will have the width and height we want to set.
viewportSizes = [ [320,480], [320,568], [600,1024], [1024,768], [1280,800], [1440,900] ]
I'm also going to set a var for getting the URL from the command line and then I want to run a regex on the URL to create a directory to save the screenshots in. I'm just going to remove the http://
part and replace the periods with hyphens. Then we are going to run casper.start()
.
saveDir = url.replace(/[^a-zA-Z0-9]/gi, '-').replace(/^https?-+/, ''); casper.start();
Now we are going to use a loop and for each viewport size, grab a screenshot of the specified URL. We are going to set the viewport to the sizes defined in the array item that we are on - open the URL - wait 5000 milliseconds to ensure the page has loaded - and then capture two types of screenshots.
The first one is for the actual height and width defined, for this we use the capture()
method which takes two arguments - a string for the output file and an object argument for setting what part of the page to clip. The second is for a complete page screenshot with just the defined width and we do this using the captureSelector()
method which captures the area within the defined selector, in our case we're just using body
and this method takes two arguments, the first being the filename and the second being the selector.
Whilst the actual defined screenshot is useful, I've found that it's helpful to also have a full page chromeless screenshot, so that you can see how the whole page flows.
casper.each(viewportSizes, function(self, viewportSize, i) { // set two vars for the viewport height and width as we loop through each item in the viewport array var width = viewportSize[0], height = viewportSize[1]; //give some time for the page to load casper.wait(5000, function() { //set the viewport to the desired height and width this.viewport(width, height); casper.thenOpen(url, function() { this.echo('Opening at ' + width); //Set up two vars, one for the fullpage save, one for the actual viewport save var FPfilename = saveDir + '/fullpage-' + width + ".png"; var ACfilename = saveDir + '/' + width + '-' + height + ".png"; //Capture selector captures the whole body this.captureSelector(FPfilename, 'body'); //capture snaps a defined selection of the page this.capture(ACfilename,{top: 0,left: 0,width: width, height: height}); this.echo('snapshot taken'); }); }); });
Finally we call the run()
method and in the callback function I am just going to echo out that the capturing has finished.
casper.run(function() { this.echo('Finished captures for ' + url).exit(); });
The full script should now look like this:
var casper = require("casper").create(), viewportSizes = [ [320,480], [320,568], [600,1024], [1024,768], [1280,800], [1440,900] ], url = casper.cli.args[0], saveDir = url.replace(/[^a-zA-Z0-9]/gi, '-').replace(/^https?-+/, ''); casper.start(); casper.each(viewportSizes, function(self, viewportSize, i) { // set two vars for the viewport height and width as we loop through each item in the viewport array var width = viewportSize[0], height = viewportSize[1]; //give some time for the page to load casper.wait(5000, function() { //set the viewport to the desired height and width this.viewport(width, height); casper.thenOpen(url, function() { this.echo('Opening at ' + width); //Set up two vars, one for the fullpage save, one for the actual viewport save var FPfilename = saveDir + '/fullpage-' + width + ".png"; var ACfilename = saveDir + '/' + width + '-' + height + ".png"; //Capture selector captures the whole body this.captureSelector(FPfilename, 'body'); //capture snaps a defined selection of the page this.capture(ACfilename,{top: 0,left: 0,width: width, height: height}); this.echo('snapshot taken'); }); }); }); casper.run(function() { this.echo('Finished captures for ' + url).exit(); });
And now we can run this script using the following command:
casperjs casperscreens.js http://todomvc.com
I've chosen to capture some screens from todomvc.com simply because it's a responsive site that can display the sort of results we are looking for.
Now, if you navigate to the directory where the script was run from, you will see a new directory has been created and inside are all of your PNGs.
Wrap Up
So we've managed to write a fairly small bit of JavaScript that will save a lot of hassle, next time the boss or the client wants a bunch of screenshots, while also providing an additional script we can add to our toolbox when doing some testing. Sure, this only shows us a WebKit rendering, but for many, that's good enough.
Now try integrating this into your build process, run it alongside your other tests and use the screen capture functionality to not only test the responsiveness of your site, but how a users journey might look on different size screens. Also, check out the Grunt plugin grunt-casper if Grunt is part of your build process.
If you are a fan of CoffeeScript, you can even try re-writing this script in the CoffeeScript syntax, just ensure that your file ends with the .coffee
extension:
casperjs casperscreen.coffee http://example.com
And you don't even have to worry about pre-compiling your CoffeeScript, Casper scripts.
There's so much more to both CasperJS and PhantomJS, so check out their respective sites and see how they can help with your testing.
Comments