Testing JavaScript with PhantomJS

Testing JavaScript with PhantomJS

Tutorial Details
    • Topic: PhantomJS
    • Difficulty: Easy
    • Estimated Completion Time: 30 minutes

I don’t think I need to convince you that testing your JavaScript code is a good idea. But, it can sometimes prove tedious to test JavaScript code that requires a DOM. This means you need to test your code in the browser and can’t use the terminal, right? Wrong, actually: enter PhantomJS.


What exactly is PhantomJS? Well, here’s a blurb from the PhantomJS website:

PhantomJS is a headless WebKit with JavaScript API.

As you know, Webkit is the layout engine that Chrome, Safari, and a few other niche browsers use. So PhantomJS is a browser, but a headless browser. This means that the rendered web pages are never actually displayed. This may sound weird to you; so you can think of it as a programmable browser for the terminal. We’ll look at a simple example in a minute, but we first need to install PhantomJS.


Installing PhantomJS

Installing PhantomJS is actually pretty simple: it’s just a single binary that you download and stick in your terminal path. On the PhantomJS download page, choose your operating system and download the correct package. Then move the binary file from the downloaded package to a directory inside your terminal path (I like to put this kind of thing in ~/bin).

If you’re on Mac OS X, there’s a simpler way to install PhantomJS (and this is actually the method I used). Just use Homebrew, like this:

brew update && brew install phantomjs

You should now have PhantomJS installed. You can double-check your installation by running this:

phantomjs --version

I’m seeing 1.7.0; you?


A Small Example

Let’s start with a small example.

simple.js
console.log("we can log stuff out.");

function add(a, b) {
    return a + b;
}

conslole.log("We can execute regular JS too:", add(1, 2));

phantom.exit();

Go ahead and and run this code by issuing the following command:

phantomjs simple.js

You should see the output from the two console.log lines in your terminal window.

Sure, this is simple, but it makes a good point: PhantomJS can execute JavaScript just like a browser. However, this example doesn’t have any PhantomJS-specific code…well, apart from the last line. That’s an important line for every PhantomJS script because it exits the script. This might not make sense here, but remember that JavaScript doesn’t always execute linearly. For example, you might want to put the exit() call in a callback function.

Let’s look at a more complex example.


Loading Pages

Using the PhantomJS API, we can actually load any URL and work with the page from two perspectives:

  • as JavaScript on the page.
  • as a user looking at the page.

Let’s start by choosing to load a page. Create a new script file and add the following code:

script.js
var page = require('webpage').create();

page.open('http://net.tutsplus.com', function (s) {
    console.log(s);
    phantom.exit();
});

We start by loading PhantomJS’ webpage module and creating a webpage object. We then call the open method, passing it a URL and a callback function; it’s inside this callback function that we can interact with the actual page. In the above example, we just log the status of the request, provided by the callback function’s parameter. If you run this script (with phantomjs script.js), you should get ‘success’ printed in the terminal.

But let’s make this more interesting by loading a page and executing some JavaScript on it. We start with the above code, but we then make a call to page.evaluate:

page.open('http://net.tutsplus.com', function () {
    var title = page.evaluate(function () {
        var posts = document.getElementsByClassName("post");
        posts[0].style.backgroundColor = "#000000";
        return document.title;
    });
    page.clipRect = { top: 0, left: 0, width: 600, height: 700 };
    page.render(title + ".png");
    phantom.exit();
});

PhantomJS is a browser, but a headless browser.

The function that we pass to page.evaluate executes as JavaScript on the loaded web page. In this case, we find all elements with the post class; then, we set the background of the first post to black. Finally, we return the document.title. This is a nice feature, returning a value from our evaluate callback and assigning it to a variable (in this case, title).

Then, we set the clipRect on the page; these are the dimensions for the screenshot we take with the render method. As you can see, we set the top and left values to set the starting point, and we also set a width and height. Finally, we call page.render, passing it a name for the file (the title variable). Then, we end by calling phantom.exit().

Go ahead and run this script, and you should have an image that looks something like this:

You can see both sides of the PhantomJS coin here: we can execute JavaScript from inside the page, and also execute from the outside, on the page instance itself.

This has been fun, but not incredibly useful. Let’s focus on using PhantomJS when testing our DOM-related JavaScript.


Testing with PhantomJS

Yeoman uses PhantomJS in its testing procedure, and it’s virtually seamless.

For a lot of JavaScript code, you can test without needing a DOM, but there are times when your tests need to work with HTML elements. If you’re like me and prefer to run tests on the command line, this is where PhantomJS comes into play.

Of course, PhantomJS is not a testing library, but many of the other popular testing libraries can run on top of PhantomJS. As you can see from the PhantomJS wiki page on headless testing, PhantomJS test runners are available for pretty much every testing library you might want to use. Let’s look at how to use PhantomJS with Jasmine and Mocha.

First, Jasmine and a disclaimer: there isn’t a good PhantomJS runner for Jasmine at this time. If you use Windows and Visual Studio, you should check out Chutzpah, and Rails developers should try guard-jasmine. But other than that, Jasmine+PhantomJS support is sparse.

For this reason, I recommend you use Mocha for DOM-related tests.

HOWEVER.

It’s possible that you already have a project using Jasmine and want to use it with PhantomJS. One project, phantom-jasmine, takes a little bit of work to set up, but it should do the trick.

Let’s begin with a set of JasmineJS tests. Download the code for this tutorial (link at the top), and check out the jasmine-starter folder. You’ll see that we have a single tests.js file that creates a DOM element, sets a few properties, and appends it to the body. Then, we run a few Jasmine tests to ensure that process did indeed work correctly. Here’s the contents of that file:

tests.js
describe("DOM Tests", function () {
    var el = document.createElement("div");
    el.id = "myDiv";
    el.innerHTML = "Hi there!";
    el.style.background = "#ccc";
    document.body.appendChild(el);

    var myEl = document.getElementById('myDiv');
    it("is in the DOM", function () {
        expect(myEl).not.toBeNull();
    });

    it("is a child of the body", function () {
        expect(myEl.parentElement).toBe(document.body);
    });

    it("has the right text", function () {
        expect(myEl.innerHTML).toEqual("Hi there!");
    });

    it("has the right background", function () {
        expect(myEl.style.background).toEqual("rgb(204, 204, 204)");
    });
});

The SpecRunner.html file is fairly stock; the only difference is that I moved the script tags into the body to ensure the DOM completely loads before our tests run. You can open the file in a browser and see that all the tests pass just fine.

Let’s transition this project to PhantomJS. First, clone the phantom-jasmine project:

git clone git://github.com/jcarver989/phantom-jasmine.git

This project isn’t as organized as it could be, but there are two important parts you need from it:

  • the PhantomJS runner (which makes Jasmine use a PhantomJS DOM).
  • the Jasmine console reporter (which gives the console output).

Both of these files reside in the lib folder; copy them into jasmine-starter/lib. We now need to open our SpecRunner.html file and adjust the <script /> elements. Here’s what they should look like:

<script src="lib/jasmine-1.2.0/jasmine.js"></script>
<script src="lib/jasmine-1.2.0/jasmine-html.js"></script>
<script src="lib/console-runner.js"></script>
<script src="tests.js"></script>

<script>
    var console_reporter = new jasmine.ConsoleReporter()
    jasmine.getEnv().addReporter(new jasmine.HtmlReporter());
    jasmine.getEnv().addReporter(console_reporter);
    jasmine.getEnv().execute();
</script>

Notice that we have two reporters for our tests: an HTML reporter and a console reporter. This means SpecRunner.html and its tests can run in both the browser and the console. That’s handy. Unfortunately, we do need to have that console_reporter variable because it’s used inside the CoffeeScript file we’re about to run.

So, how do we go about actually running these tests on the console? Assuming you’re in the jasmine-starter folder on the terminal, here’s the command:

phantomjs lib/run\_jasmine\_test.coffee ./SpecRunner.html

We’re running the run\_jasmine\_test.coffee script with PhantomJS and passing our SpecRunner.html file as a parameter. You should see something like this:

Of course, if a test fails, you’ll see something like the following:

If you plan on using this often, it might be a good idea to move run\_jasmine\_test.coffee to another location (like ~/bin/run\_jasmine\_test.coffee) and create a terminal alias for the whole command. Here’s how you’d do that in a Bash shell:

alias phantom-jasmine='phantomjs /path/to/run\_jasmine\_test.coffee'  

Just throw that in your .bashrc or .bash_profile file. Now, you can just run:

phantom-jasmine SpecRunner.html

Now your Jasmine tests work just fine on the terminal via PhantomJS. You can see the final code in the jasmine-total folder in the download.


PhantomJS and Mocha

Thankfully, it’s much easier to integrate Mocha and PhantomJS with mocha-phantomjs. It’s super-easy to install if you have NPM installed (which you should):

npm install -g mocha-phantomjs

This command installs a mocha-phantomjs binary that we’ll use to run our tests.

In a previous tutorial, I showed you how to use Mocha in the terminal, but you’ll do things differently when using it to test DOM code. As with Jasmine, we’ll start with an HTML test reporter that can run in the browser. The beauty of this is we’ll be able to run that same file on the terminal for console test results with PhantomJS; just like we could with Jasmine.

So, let’s build a simple project. Create a project directory and move into it. We’ll start with a package.json file:

{
    "name": "project",
    "version": "0.0.1",
    "devDependencies": {
        "mocha": "*",
        "chai" : "*"
    }
}

Mocha is the test framework, and we’ll use Chai as our assertion library. We install these by running NPM.

We’ll call our test file test/tests.js, and here are its tests:

describe("DOM Tests", function () {
    var el = document.createElement("div");
    el.id = "myDiv";
    el.innerHTML = "Hi there!";
    el.style.background = "#ccc";
    document.body.appendChild(el);

    var myEl = document.getElementById('myDiv');
    it("is in the DOM", function () {
        expect(myEl).to.not.equal(null);
    });

    it("is a child of the body", function () {
        expect(myEl.parentElement).to.equal(document.body);
    });

    it("has the right text", function () {
        expect(myEl.innerHTML).to.equal("Hi there!");
    });

    it("has the right background", function () {
        expect(myEl.style.background).to.equal("rgb(204, 204, 204)");
    });
});

They’re very similar to the Jasmine tests, but the Chai assertion syntax is a bit different (so, don’t just copy your Jasmine tests).

The last piece of the puzzle is the TestRunner.html file:

<html>
    <head>
        <title> Tests </title>
        <link rel="stylesheet" href="./node_modules/mocha/mocha.css" />
    </head>
    <body>
        <div id="mocha"></div>
        <script src="./node_modules/mocha/mocha.js"></script>
        <script src="./node_modules/chai/chai.js"></script>
        <script>
            mocha.ui('bdd'); 
            mocha.reporter('html');
            var expect = chai.expect;
        </script>
        <script src="test/test.js"></script>
        <script>
            if (window.mochaPhantomJS) { mochaPhantomJS.run(); }
            else { mocha.run(); }
        </script>
    </body>
</html>

There are several important factors here. First, notice that this is complete enough to run in a browser; we have the CSS and JavaScript from the node modules that we installed. Then, notice the inline script tag. This determines if PhantomJS is loaded, and if so, runs the PhantomJS functionality. Otherwise, it sticks with raw Mocha functionality. You can try this out in the browser and see it work.

To run it in the console, simply run this:

mocha-phantomjs TestRunner.html 

Voila! Now you’re tests run in the console, and it’s all thanks to PhantomJS.


PhantomJS and Yeoman

I’ll bet you didn’t know that the popular Yeoman uses PhantomJS in its testing procedure, and it’s vritually seemless. Let’s look at a quick example. I’ll assume you have Yeoman all set up.

Create a new project directory, run yeoman init inside it, and answer ‘No’ to all the options. Open the test/index.html file, and you’ll find a script tag near the bottom with a comment telling you to replace it with your own specs. Completely ignore that good advice and put this inside the it block:

var el = document.createElement("div");
expect(el.tagName).to.equal("DIV");

Now, run yeoman test, and you’ll see that the test runs fine. Now, open test/index.html file in the browser. It works! Perfect!

Of course, there’s a lot more you can do with Yeoman, so check out the documentation for more.


Conclusion

Use the libraries that extend PhantomJS to make your testing simpler.

If you’re using PhantomJS on its own, there isn’t any reason to learn about PhantomJS itself; you can just know it exists and use the libraries that extend PhantomJS to make your testing simpler.

I hope this tutorial has encouraged you to look into PhantomJS. I recommend starting with the example files and documentation that PhantomJS offers; they’ll really open your eyes to what you can do with PhantomJS–everything from page automation to network sniffing.

So, can you think of a project that PhantomJS would improve? Let’s hear about it in the comments!

Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • http://www.facebook.com/onlydole Taylor James Dolezal

    conslole.log(“We can execute regular JS too:”, add(1, 2)); Should read “console”

  • Лёха

    cool article

  • http://twitter.com/Banford Nick Banford

    If you use Visual Studio and the ReSharper extension you can use Jasmine and PhantomJS quite seamlessly and execute tests with the ReSharper test runner. Great article!

  • kzelda

    can i integrate jQuery ?

    • andrew8088
      Author

      Yes, that should work fine, because PhantomJS has the DOM API that jQuery depends on.

  • GiN

    You forgot about one great thing – CasperJS.

    • andrew8088
      Author

      Thanks! Might be a good idea to have a followup tutorial on CasperJS.

      • Evan

        +1 CapserJS is fantastic, should be mentioned.

      • http://www.matthewsetter.com/ Matthew Setter

        I completely agree. It’s a really solid tool, so flexible and simple to learn.

  • http://twitter.com/founddrama founddrama

    Good piece. For folks that are interested in Jasmine, they might also want to consider Larry Myers’ jasmine-reporters project; it has a decent test runner (comparable to, but not as nice as the one phantom-jasmine project) but better reporters. Of particular interest: it has a JUnitXmlReporter which works wonders for CI environments like Hudson or Jenkins. (I blogged about that here and have some associated proof-of-concept code on Github.)

  • http://www.codeconquest.com/ Charles @ CodeConquest.com

    This is interesting as I’ve never bothered to use any kind of testing system before.

  • arif

    topic : return ture or false at the end of the function

    //scroll back to top btn

    $(‘.scrolltop’).click(function(){

    $(“html, body”).animate({ scrollTop: 0 }, 1000);

    return false;

    });

    this code is used to scroll top of a page slowly, i like it just one thing i dont understand why they use return false ? is it a good practice ? if yes then why ? i cut that but it still works without writing return false. i have seen this return true or false in many codes but i dont understand why they are used. will you please help me to know ?

    • Creatan

      Assuming that .scrolltop is a class on anchor-tag the “return false” will disable the normal anchor-tag behavior. The same thing as passing event to the function and calling event.preventDefault()

  • shamim

    Amazing articles and its awesome..
    Thanks to share.

  • hasan

    Thanks! I’ve been wondering how to do such things.

  • http://www.rebeccamurphey.com Rebecca Murphey

    Grunt.js is really worth a mention here too — it helps with automating all sorts of tasks — builds, testing, linting, and more — and has plugins for running QUnit, Mocha, and Jasmine tests using PhantomJS (and probably other testing frameworks too). If you use one of the testing plugins — I’m using grunt-mocha on my latest project — then the setup is ridiculously simple once you have PhantomJS installed.

    • pixelBender67

      love grunt!

  • jg

    thanks!
    Just to mention that on windows-7, phantomjs can be installed with PowerShell/chocolatey

    PS> chocolatey install phantomjs

  • http://www.facebook.com/christopherwalker1andonly Christopher James Walker

    In the first example (A Small Example) I think you misspelled console on line 5 for simple.js

  • pixelBender67

    great article, I was using just jasmine before ,mocha is similar and I like it!
    Good stuff!

  • http://www.icahbanjarmasin.com/ Icahbanjarmasin

    really I no undestand JavaScript please help me.

  • http://twitter.com/metaskills Ken Collins

    Thanks for mentioning the mocha-phantomjs project!!!

  • http://www.webdesign.org/ Julia Agnes

    cool tutorial! Thanks a lot

  • Eric

    CasperJS 4 President……seriusly the best…

  • peterherz

    If you’re using the new bleeding edge Yeoman branch called Yeoman-Express-Stack and then generate any of their component controllers.. the component may work out of the box with a manual tests but testing it with PhantomJS (default) is slightly trickier. This is because it tries to DI the $http object in their crud controller template, and that fails the PJS test. Luckily yearofMoo’s article on full-spectrum testing here (http://yearofmoo.com/2013/01/full-spectrum-testing-with-angularjs-and-testacular.html) can show you a similar setup that actually pasts the test.. so I’m working on a merge between express-stack and YOM’s organization to include it in the Yeoman controller/tests generator for Yeoman 1.0 coming soon.

  • Tiisetso

    I am on Windows 7 and trying mocha-phantomjs TestRunner.html gives me:

    events.js:72
    throw er; // Unhandled ‘error’ event
    ^
    Error: spawn ENOENT
    at errnoException (child_process.js:945:11)
    at Process.ChildProcess._handle.onexit (child_process.js:736:34)

    Please help

    • http://pdmlab.com/ Alexander Zeitler

      Got the same here – did you solve it?

  • http://twitter.com/atmd83 Andrew MarkhamDavies

    The first example

    conslole.log(“We can execute regular JS too:”, add(1, 2));

    should be changed to

    console.log(“We can execute regular JS too:”, add(1, 2));

  • R Franklin

    How did you learn to use PhantomJS? I have looked through the wiki and cannot find a list of modules or function names. I want to know what each function does and what parameters it accepts. Based on the quality and detail of this article, I’d say you are an expert. I’m looking to acquire similar skills. Thanks.

  • Asha12345

    When running PhantomJS script on Jenkins, following error is thrown:
    “_RegisterApplication(), FAILED TO establish the default connection to
    the WindowServer, _CGSDefaultConnection() is NULL”. Any idea how to
    resolve this?