HTML5 Zone is brought to you in partnership with:

Leigh has been in the technology industry for over 15 years, writing marketing and technical documentation for Sun Microsystems, Wells Fargo, and more. She currently works at New Relic as a Marketing Manager. Leigh is a DZone MVB and is not an employee of DZone and has posted 106 posts at DZone. You can read more from them at their website. View Full User Profile

Simpler UI Testing with CasperJS – Part 2

07.30.2013
| 6216 views |
  • submit to reddit

Matthew Setter is a professional technical writer and passionate web application developer. He’s also the founder of Malt Blue, the community for PHP web application development professionals and PHP Cloud Development Casts – learn Cloud Development through the lens of PHP. You can connect with him on TwitterFacebookLinkedIn or Google+ anytime.

Summary
In the first part of this series, I introduced the wonderful technology called CasperJS, a companion project to PhantomJS (a headless WebKit port with an impressive JavaScript API). We reviewed the key features in CasperJS and learned how to use it to test websites by using a section of the New Relic site as an example. Then we used resurectio (an extension for Google Chrome) to automate the setup process of our tests.

In the second part of the series, we’ll cover some of the more interesting aspects of CasperJS testing. We’ll look at different testing areas, including the following fun topics:

* HTTP authentication
* Mouse events
* Submitting forms
* Hooking into events
* Remote requests

I hope that you’ll end up with a solid grasp of how to perform comprehensive testing for your site or application.

HTTP Authentication
Let’s start with something simple, shall we? Let’s say you need to test a page or a set of pages, but you need to authenticate before you can get access to them. Happily, this is quite easy to get around.

In the script below, you can see an example of how to step through the authentication process and its output.

var casper = require('casper').create({
    verbose: false,
    logLevel: 'debug'
});
 
casper.start();
 
casper.setHttpAuth('funkyUser', 'superS3curePassw0rd');
 
casper.thenOpen('http://dev-server/protected/index.html', function() {
    this.echo("I'm in. Bazinga.");
})
 
casper.run(function() {
    this.exit();
});

Let’s walk through this script. First, it performs the usual CasperJS object setup. Then it uses the ‘setHttpAuth’ method to load the authentication credentials into the CasperJS object. These are the credentials you will use when you see the authentication request.

Next, it opens up a URL that requires HTTP authentication. It should echo ‘I’m in’ when it’s done. In this case, I used an authentication protected page that I created on my local development environment with ‘htpasswd’ and related tools.

If you aren’t sure how to set this up, look at the ‘Further Reading’ section for some great links.

Mouse Events
The example below simulates landing on Google, entering a search query and clicking the Submit button.

After CasperJS and PhantomJS load the requested page internally, we click the first link, which is identified by a CSS selector: h3.r a.

// include previous CasperJS initialization
 
casper.thenEvaluate(function(term) {
    document.querySelector('input[name="q"]').setAttribute('value', term);
    document.querySelector('form[name="f"]').submit();
}, { term: 'CasperJS' });
 
casper.then(function() {
    // Click on 1st result link
    this.click('h3.r a');
});
 
casper.then(function() {
    console.log('clicked ok, new location is ' + this.getCurrentUrl());
});
 
casper.run();

If you aren’t familiar with CSS selectors – or you want a simple way to build them before running them in your code – install a copy of the CSS Selector Tester for Google Chrome.

The screenshot, below, shows the output of the above code.

using-css-selectors-550px

Running the code in this example will display output similar to:

matthewsetter$ casperjs mouseclick.js
clicked ok, new location is http://casperjs.org/

Submitting Forms
Simulating mouse events is OK, but there are easier ways to do it with CasperJS. In the following example, load Google as we did before.

// include previous CasperJS initialization
 
function getLinks() {
    var links = document.querySelectorAll('h3.r a');
    return Array.prototype.map.call(links, function(e) {
        return e.getAttribute('href')
    });
}

Then we call the ‘fill’ function, identifying the form with a CSS selector: form[action="/search"].

By passing a JSON object in as the second parameter, we can specify the information we need in the form. The key is the name of the form field, the value is the value that will populate the field (in this case, ‘new relic’).

casper.start('http://google.com/', function() {
    // search for 'casperjs' from google form
    this.fill('form[action="/search"]', { q: 'new relic' }, true);
});

With the page loaded and the form filled out, the next step is to process a callback function, ‘getLinks’. This parses the loaded page and filters out everything but the H3 tags.

casper.then(function() {
    // aggregate results for the 'casperjs' search
    links = this.evaluate(getLinks);
});

After retrieving the information, the ‘href’ attribute is extracted by calling the ‘getAttribute’ method. So now we produce a list of all the links on the first page of Google results for our query: new relic.

casper.run(function() {
    // echo results in some pretty fashion
    this.echo(links.length + ' links found:');
    this.echo(' - ' + links.join('\n - ')).exit();
});

Let’s finish up by printing out the list of links. Running the script, above, will display output similar to the following:

matthewsetter$ casperjs submitform.js
    matthewsetter$ casperjs submitform.js
    24 links found:
     - /url?q=http://newrelic.com/&sa=U&ei=ewbwUIzYK4eZhQfN-oCoCQ&ved=0CB4QFjAA&usg=AFQjCNFgMGEPhua0ZV8FE7WCcLgHylm5yw
     - /url?q=http://newrelic.com/aws&sa=U&ei=ewbwUIzYK4eZhQfN-oCoCQ&ved=0CCEQqwMoADAA&usg=AFQjCNFsRHSDxZ59z0gyb1OtDqcRw50kaw
     - /url?q=http://newrelic.com/about&sa=U&ei=ewbwUIzYK4eZhQfN-oCoCQ&ved=0CCcQqwMoAzAA&usg=AFQjCNE58D8CoomimZkyUATnaRmxC4flnQ
     - /url?q=http://newrelic.com/pricing&sa=U&ei=ewbwUIzYK4eZhQfN-oCoCQ&ved=0CCMQqwMoATAA&usg=AFQjCNEJsTqH0aR7Aem5O-dIHbyVjp94Hw
     - /url?q=http://newrelic.com/product/why-new-relic&sa=U&ei=ewbwUIzYK4eZhQfN-oCoCQ&ved=0CCkQqwMoBDAA&usg=AFQjCNHlDED_Rqy4vlbpfvQYagSiQ1SXWQ
     - /url?q=http://newrelic.com/about/pdx-jobs&sa=U&ei=ewbwUIzYK4eZhQfN-oCoCQ&ved=0CCUQqwMoAjAA&usg=AFQjCNGVDzMCrDJ2GQyQD217RsGMk4J9RQ
     - /url?q=http://blog.newrelic.com/&sa=U&ei=ewbwUIzYK4eZhQfN-oCoCQ&ved=0CCsQqwMoBTAA&usg=AFQjCNGD2E_u2PGeBpKpMKDYnmPQ5ypJyg
     - /url?q=https://twitter.com/newrelic&sa=U&ei=ewbwUIzYK4eZhQfN-oCoCQ&ved=0CDsQFjAH&usg=AFQjCNG09UqhthCDZ_fexAlSFdr0my152Q
     - /url?q=http://en.wikipedia.org/wiki/New_Relic&sa=U&ei=ewbwUIzYK4eZhQfN-oCoCQ&ved=0CD0QFjAI&usg=AFQjCNFvvsPXjviNIrKOAWVgwDb65yMLMw
     - /url?q=https://addons.heroku.com/newrelic&sa=U&ei=ewbwUIzYK4eZhQfN-oCoCQ&ved=0CD8QFjAJ&usg=AFQjCNHx-pELQH6ljWVYzK9r-M-q4fL7uA
     - /url?q=http://www.crunchbase.com/company/new-relic&sa=U&ei=ewbwUIzYK4eZhQfN-oCoCQ&ved=0CEEQFjAK&usg=AFQjCNG6DqaFKDh1q0Fhf1867R4G5wz_2A
     - /url?q=https://github.com/newrelic&sa=U&ei=ewbwUIzYK4eZhQfN-oCoCQ&ved=0CEcQFjAL&usg=AFQjCNE3CoqebYwkIelsedyagcnIkwdXlQ
     - /url?q=http://techcrunch.com/2012/11/05/ca-files-lawsuit-against-new-relic-seeking-injunction-claims-patent-infringement/&sa=U&ei=ewbwUIzYK4eZhQfN-oCoCQ&ved=0CEkQFjAM&usg=AFQjCNGCb74Ip7xQfcq-ppR1GGnDyyI64w

Hooking into Events
Events are an aspect of programming I love more and more because I was inspired by ‘The Art of UNIX Programming,’ by Eric S. Raymond. In my opinion, event-driven programming follows some of the key rules in this book, including:

* The rule of composition: Design programs to connect to other programs
* The rule of modularity: Write simple parts connected by clean interfaces

Through events we can link up different modules or components of applications without actually coupling them together. To get a more in-depth understanding of how it works, check out this post I wrote. It covers how events work in the context of version 2 of the Zend Framework.

What you see above is the CasperJS ‘on’ function, which implements the fluent interface and allows the calls to be chained. This makes it easier to write too, don’t you agree?

/**
    /**
     * Set up a set of filters on CasperJS events
     */
    casper.on('run.complete', function() {
            this.echo('Job run completed');
        }).on('run.start', function() {
            this.echo('Job run starting');
        }).on('http.status.200', function(resource) {
            casper.echo('Successfully retrieved: ' + resource.url + ' with status code 200');
        }).on('exit', function(status) {
            casper.echo('Casper exited with status: ' + status);
        }).on('open', function(location, settings) {
            casper.echo('Opened: ' + location + " with method: " + settings.method + " and data payload: " + settings.data);
        });
 
    casper.start();

The ‘on’ function is a good name when you think of some pseudo code like the following:

‘On the run being complete (run.complete) execute the following code’

CasperJS has a series of events, about 49, that you can list (or create filters on). You can see I’ve used five in the above example:

* run.complete: After the test run is finished
* run.start: When the test run starts
* http.status: When an http status is received with a given code
* exit: When the CasperJS process exits
* open: When a resource or link is opened

I’ve done nothing special in my example. I just echo’d the fact the event had been caught.

Making Remote Requests
In this example, I’m expanding on the previous one and making it more meaty. I’ll be querying the Joind.in API for their list of upcoming events.

If you look at the API URL, you’ll see output similar to the following:

{
        "events":[
        {
            "name":"phplx meetup - January 2013",
            "start_date":"2013-01-10T00:00:00+00:00",
            "end_date":"2013-01-10T23:59:59+00:00",
            "description":"The second phplx meetup will take place at Rua da Prata, 80 in Startup Lisboa on January 10, 2013 with registrations starting at 19:00.",
            "href":"http:\/\/phplx.net",
            "attendee_count":4,
            "attending":false,
            "event_comments_count":0,
            "icon":"phplx-logo-bg-90x901.png",
            "tags":
            [
                "php","meetup","phplx"
            ],
            "uri":"http:\/\/api.joind.in\/v2.1\/events\/1174",
            "verbose_uri":"http:\/\/api.joind.in\/v2.1\/events\/1174?verbose=yes",
            "comments_uri":"http:\/\/api.joind.in\/v2.1\/events\/1174\/comments",
            "talks_uri":"http:\/\/api.joind.in\/v2.1\/events\/1174\/talks",
            "website_uri":"http:\/\/joind.in\/event\/view\/1174"
        }]
    }

I’ve set a variable, ‘wsurl’, to the 2.1 version of the joind.in api. You’ll see the CasperJS initialization is slightly different this time. It incorporates the ‘httpStatusHandlers’ configuration option.

Sometimes the API may be down or you could hit it during a particularly busy time, so you shouldn’t expect it to work every time. Therefore, I’ve prepared for the possibility of a 404 or a 500 status in the response header.

var data, events, wsurl = 'http://api.joind.in/v2.1/events?filter=upcoming';
 
var casper = require('casper').create({
    httpStatusHandlers: {
        404: function(self, resource) {
            this.echo(
              "Resource at " + resource.url + " not found (404)",
              "COMMENT"
            );
        }
        500: function(self, resource) {
            this.echo(
              "Resource at " + resource.url + " server error (500)",
              "COMMENT"
            );
        }
    },
    verbose: false,
    logLevel: 'debug'
});

Then include the filters from the previous example, which should show some meaningful information this time.

casper.on('run.complete', function() {
    this.echo('Job run completed');
}).on('run.start', function() {
    this.echo('Job run starting');
}).on('http.status.200', function(resource) {
    casper.echo(
      'Successfully retrieved: ' + resource.url +
        ' with status code 200');
}).on('exit', function(status) {
    casper.echo('Casper exited with status: ' + status);
}).on('open', function(location, settings) {
    casper.echo(
      'Opened: ' + location + " with method: " +
        settings.method + " and data payload: " + settings.data);
});

Next, chain together the ‘start’ and ‘then’ method calls to make it simpler to specify more options to the open call on the URL. Because the joind.in API is RESTful, I want to do my best to interact with it the right way.

By passing a JSON object as the second parameter, I can specify the method is a GET request and that I’m expecting a JSON payload as the response.

casper.start().then(function() {
    this.open(wsurl, {
        method: 'get',
        headers: {
            'Accept': 'application/json'
        }
    });
});

Assuming an HTTP 200 status code, I then retrieve the list of events with the ‘getPageContent’ method and convert it to a JSON object with JSON.parse.

In this example, I just want to see some information about upcoming events. I call the CasperJS ‘each’ method to iterate over the list. The ‘each’ method takes two parameters, an array and a function or callback. The ‘events’ array of our initialized events object will be the array in parameter one.

I’ve supplied a basic anonymous function as parameter two. It takes two parameters. The first is CasperJS itself and the second is the current element in the iteration.

casper.then(function() {
    events = JSON.parse(this.getPageContent());
    this.echo('Upcoming Events');
    this.each(events.events, function(casper, term) {
        startDate = new Date(term.start_date);
        endDate = new Date(term.end_date);
        this.echo(
          term.name + ". Starts: " + startDate.toLocaleDateString() +
              ". Ends: " +  endDate.toLocaleDateString());
    });
});

To make the date information prettier (and simpler to read), I’ve initialized two JavaScript Date Objects with the ‘start_date’ and ‘end_date’ values from the current element.

From all the returned information, I only want three things:

1. The name of the event
2. The start date
3. The end date

Then I created a string composed of the event name and the localized start and end dates. This test – like all others – is run by calling the ‘run’ method. To make it a bit more interesting, I’ve passed -1 to the ‘exit’ method.

casper.run(function() {
    this.exit(-1);
});

The result of all this producing the following events list (at the time of this writing):

matts-mac:phantomjs matthewsetter$ casperjs casperajaxrequest.js
Job run starting
Opened: http://api.joind.in/v2.1/events?filter=upcoming with method: get and data payload: undefined
Successfully retrieved: http://api.joind.in/v2.1/events?filter=upcoming with status code 200
Upcoming Events
phplx meetup - January 2013. Starts: 10/01/2013. Ends: 11/01/2013
Seattle PHP Meetup Group January Meetup. Starts: 10/01/2013. Ends: 11/01/2013
PHPUGFFM I/2013. Starts: 17/01/2013. Ends: 17/01/2013
PHPBenelux Conference 2013. Starts: 25/01/2013. Ends: 26/01/2013
Job run completed
Casper exited with status: -1

There are far more entries in the list, but I want to keep it simple. Are you attending any of these events? Are you going to use CasperJS?

I’m not a core contributor to CasperJS. I’m learning, just like you. But I see so much potential in adding CasperJS to my arsenal of testing tools. Do you see the benefits of using it as well?

What’s your experience with using it? And can you see a place for it in to your development workflow? Share your experience and ideas in the comments below so we can all learn more about this great tool.

Further reading

* HTPASSWD GENERATOR – CREATE HTPASSWD
* Password Protection with .htaccess & .htpasswd



Published at DZone with permission of Leigh Shevchik, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)