dominator.open('#home')
         .click('a.redButton')
         .waitForPage('#boom')
         .destroy();

Synopsis

Dominator adds functional test assertions to QUnit.

Functional tests should be simple to write, easy to read, and adaptable to any JavaScript framework.

For us, functional tests are a smoke screen. When you need more, then it is time to abstract and unit test.

Quick Example


module('open');

test('should open first page', function() {
    dominator.open('#home')
             .waitForPage('#home')
             .destroy();
});

module('settings');

test('should save settings', function() {
    dominator.open('#settings')
             .click('#save')
             .waitForEvent('document', 'settingsChange');
             .destroy();
});

Downloading

Installing

Dominator


<!-- include dominator.js after qunit.js -->
<script type="text/javascript" src="qunit.js"></script>
<script type="text/javascript" src="dominator.js"></script>

Custom Dominators


<!-- include custom dominators after dominator.js -->
<script type="text/javascript" src="qunit.js"></script>
<script type="text/javascript" src="dominator.js"></script>
<script type="text/javascript" src="dominator.jqtouch.js"></script>

Getting Started

Download QUnit

Download QUnit from github.com/jquery/qunit.


Download Dominator.js

Go to the Downloading section.


File Structure

Feel free to keep your current application file structure. However, let's assume you are using something like this:



app/ ................... 
app/index.html ......... Web application
app/test/ .............. 
app/test/index.html .... Test runner
app/test/dominator/ .... Dominator.js
app/test/qunit/ ........ QUnit
app/test/tests/ ........ QUnit tests



Create a Test Page

In /app/test/index.html:


<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" type="text/css" href="qunit/qunit.css" />

  <!-- test framework (load qUnit first) -->
  <script type="text/javascript" src="qunit/qunit.js"></script>
  <script type="text/javascript" src="dominator/dominator.js"></script>

  <!-- load application to test -->
  <script type="text/javascript">
    dominator.load({
        src:         '../index.html',
        id:          'app',
        name:        'app',
        width:       '320',
        height:      '480',
        scrolling:   'auto',
        frameborder: '0'
    });
  </script>

  <!-- tests -->
  <script type="text/javascript" src="tests/home.test.js"></script>
  <script type="text/javascript" src="tests/settings.test.js"></script>

  <title>App Tests</title>
</head>
<body>
  <h1 id="qunit-header">App Tests</h1>
  <h2 id="qunit-banner"></h2>
  <div id="qunit-testrunner-toolbar"></div>
  <h2 id="qunit-userAgent"></h2>
  <ol id="qunit-tests"></ol>
  <div id="qunit-fixture">
    <!-- populated dynamically -->
  </div>
</body>
</html>


Write Tests

In /app/test/tests/home.test.js:


module('open');

test('should open home page', function() {
    dominator.open('#home')
             .waitForPage('#home')
             .destroy();
});

test('should have a logo', function() {
    dominator.open('#home')
             .waitForVisible('#home .logo')
             .destroy();
});

In /app/test/tests/settings.test.js:


module('settings');

test('should save settings', function() {
    dominator.open('#settings')
             .click('#save')
             .waitForEvent('document', 'settingsChange');
             .destroy();
});


Run Tests

Dominator assertions are chainable and executed with destroy();

Open /app/test/index.html


Manually Loading the App to Test (optional)

If the default dominator.load implementaton is not doing it for you, then you can override it or not use it.

Just remember that Dominator disables QUnit's auto-run, so that you can wait for your application to load.


// After loading the test and application
// you can safely start QUnit with:
QUnit.start();

You could override the application loader like this:


dominator.fn.load = function(some, custom, params) {

    // do whatever you need to load the application

    dominator.config.window = myApplicationWindow;

    QUnit.start();
};

Reference

Application Loader

An optional helper function to load the test application.


//
// Convenience function to load an application into an <iframe />
//
// Can be called before or after adding the QUnit tests.
//
// - Sets dominator.config.window
// - Calls QUnit.start(); after the application has loaded
//
// @param options {Object} attributes to add to the <iframe />
//
dominator.load({
    src:         '../index.html',
    id:          'app',
    name:        'app',
    width:       '320',
    height:      '480',
    scrolling:   'auto',
    frameborder: '0'
});

Assertions

Here are the cold, mechanical limbs of the dominator:


dominator.open('#home')

dominator.click('#home a.button')

dominator.waitForVisible('#home .notification')

dominator.waitForNotVisible('#home .error')

dominator.waitForEvent('#home a.button', 'touchend')

dominator.waitForPage('#settings')

About Chaining

Assertions are queued and executed with destroy();:


// Does nothing, but tries to run all queued assertions
dominator.destroy();

// Asserts that home page opened
dominator.open('#home')
         .destroy();

// Asserts that:
//   - home page opened
//   - a button inside the home page was clicked
dominator.open('#home')
         .click('#home a.button')
         .destroy();

// Same as above
dominator.open('#home');
dominator.click('#home a.button');
dominator.destroy();

Customizing

Every application is structured different.

This is why Dominator was designed for customization.

Feel free to override any existing assertion and add your own.

Extend


//
// Add and Override Assertions
//
// Add your arguments that you need, such as `id`.
// The final argument must be a callback function.
// Call the callback when your assertion is complete.
//
// This example adds `dominator.tap("body a.button");`
// and uses the XUI JavaScript library.
//
dominator.extend('tap', function(id, callback) {
    this.window.x$(id).trigger('mousedown');
    callback();
});

Window


//
// Window Handle of Test Application
//
// While testing, you will need a handle to the
// application window.
//
// The window value is set before running QUnit.start() with:
//    dominator.config.window = window;
//
dominator.extend('tap', function(id, callback) {
    this.window.x$(id).trigger('tap');
    callback();
});

Expect


//
// Expect an Assertion Count
//
// Tells QUnit how many assertions should fire.
//
// Typically, expect is used within extend.
//
// Expect will auto-increment the existing
// expectation count, so there is no need to worry
// about clobbering a preset value.
//
dominator.extend('tap', function(id, callback) {
    // 'tap' should fire on positive assertion
    this.expect(1);

    // 'tap' requires at least one tappable element
    if (x$(id).length > 0) ok(true, 'found at least one ' + id);

    x$(id).trigger('mousedown');
    callback();
});

Config

The Dominator configuration is stored in dominator.config:


// window {Object}
//
// The window handle to the test application.
// This must be set before calling QUnit.start();
//
// This value is used by all Dominator assertions.
//
// See the Reference section for more detail.
//
dominator.config.window = DOMElement;

// Feel free to pass any property or function
// that you extensions may need
//
// For example:
//
dominator.config.postResults = function(value) {
    // XHR POST results to a server
};

Fn

Extend the dominator prototype:


// Override Application Loader
//   dominator.load({});
//
dominator.fn.load = function(options) {
    // implementation
};

// Create Something New
//   dominator.beep();
//
dominator.fn.beep = function() {
    console.log('BOOM!!!');
};

Community

Google Groups

Please use the PhoneGap Google Group.

IRC

Jump onto #phonegap @ irc.free.node and look for mwbrooks

Direct Message

Contact Michael Brooks through a GitHub message.

Contribute

Improvements

Help me out by using the GitHub Issue Tracker to report a bug and discuss changes.


Pull Requests

I appreciate pull requests!

I appreciate them more when on topic branch, instead of the master branch.


Everything else

Here are a few ideas:

Acknowledgement

License

The MIT License

Fork me on GitHub