Chapter 5. Manipulating the Zombie Browser

Now that we have our to-do HTTP application and understand how the Mocha testing framework works, we are ready to start creating tests using Zombie.js.

As covered before, Zombie.js allows you to create a simulated browser environment and manipulate it. These manipulations are the usual things users do with browsers, such as visiting a URL, clicking on links, filling and submitting forms, and others.

This chapter covers the following:

This chapter shows you how you can setup a Zombie.js browser that interacts with your web application.

Visiting a URL: First, we are going to pick up our application tests from where we left off. The whole app concerns users, but in this part, we're mainly going to focus on functionality that the Users routes touch—rendering a signup form and actually creating a user record in the database.

As mentioned, we left off with this single test file:

var assert  = require('assert'),
    Browser = require('zombie'),
    app     = require('../app')
    ;


describe('Users', function() {

  before(function(done) {
    app.start(3000, done);
  });

  after(function(done) {
    app.server.close(done);
  });

  describe('Signup Form', function() {
    
    it('should load the signup form', function(done) {
      var browser = new Browser();
      browser.visit("http://localhost:3000/users/new", function() {
        assert.ok(browser.success, 'page loaded');
        done();
      });
    });

  });
});

This test simply loaded the user signup form and tested whether the browser considered it a success. Let's go through this test to fully understand what is going on.

First we create a new browser by instantiating a new browser object:

var browser = new Browser();

This creates a Zombie.js browser, which represents an independent browser process that has the main job of maintaining state across requests: the URL history, the cookies, and the local storage.

A browser also has a main window, and you can load a URL in it by using browser.visit(), like this:

browser.visit("http://localhost:3000/users/new");

This makes the browser perform an HTTP GET request to load the HTML page from that URL. Since Node.js and Zombie.js do asynchronous I/O processing, this only makes Zombie.js to start loading the page. Then Zombie.js tries to fetch the URL, parse the HTML document, and resolve all the dependencies by loading the referenced JavaScript files.

Once all that is done, we can be notified by passing a callback function to the browser.wait() method, like this:

browser.visit("http://localhost:3000/users/new");
browser.wait(function() {
  console.log('browser page loaded');
});

Instead of using the browser.wait function, we pass a callback directly into the browser.visit() call, like this:

browser.visit("http://localhost:3000/users/new",
  function(err, browser) {
    if (err) throw err;
    assert.ok(browser.success, 'page loaded');
    done();
  }
);

Here you pass in a callback function that gets invoked once there is an error or the browser is ready. If an error occurs, it is returned as the first argument—we check whether an error exists and throw it if it does, so that the test fails.

The second argument containing the browser object, which is the same as the browser object we already had. This means that we can omit the second argument altogether and work with the previous browser reference, like this:

browser.visit("http://localhost:3000/users/new",
  function(err) {
    if (err) throw err;
    assert.ok(browser.success, 'page loaded');
    done();
  }
);

If it's the same browser object, you may ask why that object is even passed. It's there to support this form of invocation:

var Browser = require('zombie');

Browser.visit(("http://localhost:3000/users/new",
  function(err, browser) {
    if (err) throw err;
    assert.ok(browser.success, 'page loaded');
    done();
  }
);

Note that here we're using the capitalized pseudo-class Browser object; we're not instantiating browser. Instead, we're leaving that up to the Browser module to do it and to pass it on to us as the second argument of our callback function.

When we ask the browser to visit a URL, it calls us back when it's finished, but as web developers know, it's tricky to know exactly when a page load can be considered fully finished

A browser object has its own event loop that handles asynchronous events, such as loading resources, events, timeouts, and intervals. After a page is loaded and parsed, all the dependencies are loaded and parsed asynchronously—just like in real browsers—using this event loop.

Some of these dependencies may contain JavaScript files that will be loaded, parsed, and evaluated. Furthermore, the HTML document may contain some additional inline scripts that will be executed. If any of these scripts have a callback waiting for the document to be ready, these callbacks will be executed before your browser.visit() callback fires your test callback. This means that if, for instance, you have jQuery code that gets fired when the document is ready, it will run before your callback. The same can be said for any subsequent AJAX callbacks.

To see this in action, try adding the following code immediately before the closing </body> tag in the templates/layout.html file:

Then change the test code in test/users.js so that it logs when the visit callback gets fired:

To analyze this, we are going to run our tests in debug mode. In this mode, Zombie.js outputs some useful information, which includes the HTTP request activity that the browser is carrying out. To enable this mode, set the DEBUG environment variable like this:

You should now get the following debug output:

As you can see, the LOADED NEW string is printed before the VISIT IS DONE string, which means that the browser performed and finished the AJAX request before your visit callback fired. You may wish to return to the code now and remove this extra console logging.

You can also pass in some options to the browser, to modify some of the actions and conditions regarding how it loads the page. These options come in the form of an object that you pass as an argument to the Browser.visit() call, right before the callback, like this:

Here are the most useful options that we will discuss in detail: