[EN] Use PhantomJS to take screenshots of your webapp for you
Problem
You’ve built a web application and it’s time to write the user documentation. Your app is localized and you ship new features every three months.
In your documentation, you want screenshots of specific parts of your application: the login box, the collapsed Help panel, the main data grid…
You need to take screenshots in 4 languages.
Do you want to take them manually? You could probably define a bunch of rectangular zones and automate the process but the login box is bigger in French than in English since the text is longer. And in six months, you’ll ship a feature which will require a change in the global application layout.
Do you want to define your rectangular zones 4 times? And redefine them (4 times) whenever you resize/move a component?
Solution: component based screenshots
What if you could tell: “Take a screenshot of this div” instead of “Take a screenshot of this app and cut here, here and there”?
Even better: “Take a screenshot of this <insert your favorite UI framework> component”.
With PhantomJS, you can.
Installation
There are builds for Windows and MacOS X available on the download page, there is brew install phantomjs, there is a PPA for Ubuntu, and there are several git repositories.
I chose to compile it on my 32 bits Ubuntu computer:
sudo apt-get install libqt4-dev libqtwebkit-dev qt4-qmake git clone git://github.com/ariya/phantomjs.git cd phantomjs git checkout 1.4.1 qmake-qt4 make |
There is no install target in the Makefile so do whatever you usually do in this case (move/symlink the binary to /usr/bin/, add the bin/ directory in your $PATH, use the full path when invoking phantomjs…). The PhantomJS binary is bin/phantomjs.
Usage
phantomjs your_script.js [parameters]
Examples
You can find some examples on the site. Here are some of them:
Load a web page:
var page = new WebPage(); page.open(url, function (status) { // do something }); |
Take a screenshot a a page:
var page = new WebPage(), address, output, size; if (phantom.args.length < 2 || phantom.args.length > 3) { console.log('Usage: rasterize.js URL filename'); phantom.exit(); } else { address = phantom.args[0]; output = phantom.args[1]; page.viewportSize = { width: 600, height: 600 }; page.open(address, function (status) { if (status !== 'success') { console.log('Unable to load the address!'); } else { window.setTimeout(function () { page.render(output); phantom.exit(); }, 200); } }); } |
Evaluate some JS code in the context of the web page:
var page = new WebPage(); page.open(url, function (status) { var title = page.evaluate(function () { return document.title; }); console.log('Page title is ' + title); }); |
Combine of these possibilities and you have a nice screenshotting solution!
Component screenshotting
With PhantomJS, this is really simple:
- Load your app
- Find your divs / Find your components
- Get their bounding boxes (top, left, width, height)
- Render with clipRect
Example with an ExtJS demo app:
This is a RSS feed viewer and I want screenshots of:
- The left panel
- The preview button
- The whole application, with the left panel collapsed
Here it is:
var page = new WebPage(), address = 'http://dev.sencha.com/deploy/ext-4.0.7-gpl/examples/feed-viewer/feed-viewer.html'; page.viewportSize = { width : 800, height : 600 }; // define the components we want to capture var components = [{ output : 'feed-viewer-left.png', //ExtJS has a nice component query engine selector : 'feedpanel' },{ output : 'feed-viewer-preview-btn.png', selector : 'feeddetail > feedgrid > toolbar > cycle' },{ output : 'feed-viewer-collapsed.png', //executed before the rendering before : function(){ var panel = Ext.ComponentQuery.query('feedpanel')[0]; panel.animCollapse = false; // cancel animation, no need to wait before capture panel.collapse(); }, selector : 'viewport' }]; page.open(address, function (status) { if (status !== 'success') { console.log('Unable to load the address!'); } else { /* * give some time to ExtJS to * - render the application * - load asynchronous data */ window.setTimeout(function () { components.forEach(function(component){ //execute the before function component.before && page.evaluate(component.before); // get the rectangular area to capture /* * page.evaluate() is sandboxed * so that 'component' is not defined. * * It should be possible to pass variables in phantomjs 1.5 * but for now, workaround! */ eval('function workaround(){ window.componentSelector = "' + component.selector + '";}') page.evaluate(workaround); var rect = page.evaluate(function(){ // find the component var comp = Ext.ComponentQuery.query(window.componentSelector)[0]; // get its bounding box var box = comp.el.getBox(); // box is {x, y, width, height} // we want {top, left, width, height} box.top = box.y; box.left = box.x; return box; }); page.clipRect = rect; page.render(component.output); }); // job done, exit phantom.exit(); }, 2000); } }); |
Result
And here are the screenshots:
Conclusion
Now, we can automate screenshotting of specific components, even if we don’t know their position or size. Relaunch this script with your application in Spanish and voilà: 3 Spanish screenshots in 5 seconds, even if the button is bigger (or smaller, I don’t speak Spanish)!
Ideas:
- Components to capture defined in the app itself
- Browser extension/Bookmarlet to define the components to capture just by clicking on it
- Use it for unit testing, with image comparison
- Make a product of this, make profit
I hope you liked it!
Commentaires
Commentaire de NiKo
le 7 January 2012, 10:54
You should probably have a look at casperjs n1k0.github.com/casperjs
Commentaire de Florian Cargoet
le 7 January 2012, 13:35
CasperJS looks awesome! Thanks!
Commentaire de molokoloco
le 7 January 2012, 16:36
Nice job Florian ! It’s a great idea, i will try to enhance the next documentation i produce
Commentaire de Jay
le 3 September 2012, 15:44
Very good idea. The problem in that case is that if you want to detect browser specific issues you only will get the phantom js rendering. Try http://usersnap.com this will generate a screenshot only with a few lines of js-code.
le 7 January 2012, 0:22
Thanks for posting, nice implementation