A functional testing framework that allows for control of multiple "windows"
JavaScript HTML CSS

README.md

Sparrow functional testing

A functional website testing framework based on jasmine and JQuery. Sparrow is designed to solve the problem of async testing inherent in website testing by making async testing easier.

Sparrow:

  • Makes async testing easier
  • Allows testing multiple pages simultaneously so complex interactions can be tested
  • Runs tests in a browser and headless
  • Allows using the IntelliJ/Webstorm debugger with both test and page code
  • Uses JQuery and Jasmine to reduce the learning curve
  • Makes it easier to create independent tests that are not dependent on each other

Quick Start

1) Download the latest sparrow

2) cd to installation directory

3) type 'npm install' (requires node)

4) type 'grunt headed' to create specRunner.html (requires grunt)
NOTE: Make sure you run this command after creating new spec/helper files and after upgrading Sparrow. You can also run grunt watch to build specRunner.html automatically when spec/helper files change.

5) open runner.html in a browser (tested with newest Chrome and FF)
(NOTE: FF seems to allow running from the local filesystem. Chrome requires running it from a web server)

6) Look at the files in /specs to see some of the possibilities

Running a single test or a group of tests

To run a single test or a group of tests, simply add ?spec=some%20it%20or%20description to the end of the url the same way you would running Jamsine directly.

example:

to run 'should do something' use ?spec=should%20do%20something at the end of the url.

Running tests in headless mode

To run tests in headless/CI mode. type 'grunt' or 'grunt headless'

If you need to run the tests from a server with dynamic content, simply add the host address in Gruntfile.js. It is best to run the tests from the source directory the server is pointed to or a symlink.

Sparrow options

sparrow.WAIT_TIME

The time to wait for the waitFor, waitUntil, waitWhile*

This can be set in a helper to make it a global setting for all tests

Basics

There are two ways to use the sparrow functions. You can call them directly and provide a callback for the async functions, or you can do things the easier way and use the sparrow async() monad.

Callback example

    describe('block of tests', function() {
        it('should do something', function(done) {
            createTestWindow('win');

             // open the page
            $win.open('http://example.com', function() {
            $win.click('#some-link');

                // fill the form in the popup
                $win.waitForSelector('#some-popup', function() {
                    $win.fill('#some-form', {name:'me', ...};
                    $win.click('#sumit-button');

                    // Do something with the resulting page
                    $win.waitforText('new page loaded', function() {
                        $win.click('#another-link');
                        $win.waitWhileVisible('#some-modal', function() {
                            myAsyncFunction(function() {

                                // Test that we see 'finished'
                                expect($win.find('#result').html()).toBe('finished');
                                done();
                            });
                        });
                    });
                });
            });
        });
    });

Async monad example (much cleaner and easier to refactor)

    describe('block of tests', function() {
        it('should do something', function(done) {
            createTestWindow('win');
            $win.async(done)

                // open the page
                .open('http://example.com')
                .click('#some-link')

                // fill the form in the popup
                .waitForSelector('#some-popup')
                .fill('#some-form', {name: 'me', ...}
                .click('#submit-button')

                // Do something with the resulting page
                .waitForText('new page loaded')
                .click('#another-link')
                .waitWhileVisible('#some-modal')
                .fn(myAsyncFunction)

                // Test that we see 'finished'
                .syncFn(function() {
                    expect($win.find('#result').html()).toBe('finished')
                })

                .run();

        });

        function myAsyncFunction(done) {
            // do something async
            done();
        }

    });

Sparrow is based on jasmine so tests are written in the same style. See jasmine docs for more possibilities.

Using with Meteor

Unpack sparrow someplace in the /public directory. I put it in /public/sparrow. Sparrow tests will then be available at http://localhost:3000/sparrow/runner.html.

NOTE: I have found that I must clear the cache in my browser to get updates after changing specs.

Documentation


NOTE: If you are using the async monad, ignore the doneCB argument on the following, this is handled for you.

createTestWindow('name')

Opens a test window tab and creates $name variable in the test scope.

    it('should do something', function() {
        createTestWindow('aTestWindow');
        ... creates $aTestWindow
    });

.async(doneCB)

Creates an async monad to make async functions easier

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            .click('a#link')
            .waitForText('something on new page')
            .run()
    });

.run()

Starts a previously created async monad

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            // other instructions here
            .run();
    });

.open(url, doneCB)

Opens a url in a window tab.

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            .open('http://google.com'
            // other instructions here
            .run()
    });

.show()

Show the tab for this window

    it('should do something', function(done) {
        createTestWindow('win');
        createTestWindow('another');
        $win.async(done)
            .show() // This will show the $win window (tab)
            .run()

        $another.async(done)
            .show()    // This will show the $another window (tab)
            .run()
    });

.waitUntilTrue(testFn, doneCB)

Wait until the test function returns true

    it('should do something', function(done) {
        createTestWindow('win');
         $win.async(done)
            .click('#something')
            .waitUntilTrue(somethingHappening)
    });

    function somethingHappening() {
        return $j('#xxxx').html() === 'ready'
    }

waitUntilTrue can also takes an async function containing a "done" final argument.
The passed async function will be called until it calls done() with a truthy value or sparrow.WAIT_TIME is reached.

        it('should do something', function(done) {
            var count = 0;
            $waitFor.async(done)
                .waitUntilTrue(asyncFunc)
                .run();

            function asyncFunc(done) {
                setTimeout(function() {
                    ++count === 2 ? done(1) : done(0);
                },1);
            }
        })

    });

.waitForText(text, doneCB)

Wait until the text is visible on the webpage

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            .click('a')
            .waitForText('some text on the page')
            .run()
    });

.waitForSelector(selector, doneCB)

Wait until the selected element is in the dom

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            .click('a')
            .waitForSelector('a#my-link')
            .run()
    });

.waitUntilVisible(selector, doneCB)

Wait until the selected element is visible

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            .click('a')
            .waitUntilVisible('#myAsyncModal')
            .run()
    });

.waitWhileVisible(selector, doneCB)

Wait while the selected element is visible

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            .click('a')
            .waitWhileVisible('#loadingMessage')
            .run()
    });

.wait(ms, doneCB)

Wait for some period of milliseconds

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            .click('a')
            .wait(2000)
            .click('#something else')
            .run()
    });

.click(selector)

Click on some selected element

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            .click('a')
            .wait(2000)
            .click('#something else')
            .run()
    });

.log(message)

Send a log message to the console.

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            .click('a')
            .log('I clicked it')
            .run()
    });

.fill(selector, formData)

Fill in the selected form

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            .fill('#my-form', {name: 'Scott', email:'me@mine.com'})
            .click('#submit')
            .run()
    });

.close()

Close an open test window (tab)

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            .fill('#my-form', {name: 'Scott', email:'me@mine.com'})
            .click('#submit')
            .close()
            .run()
    });

.fn(someFunction)

Call an async function

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            .click('a')
            .fn(myAsyncFunction)
            .run()
    });

    function myAsyncFunction(done) {
        // do something
        done();
    }

.syncFn(someFunction)

Call a function. Currently this is the best way to add expects into your test code.

    it('should do something', function(done) {
        createTestWindow('win');
        $win.async(done)
            .click('a')
            .syncFn(function() {
                expect($win.find('#something').html()).toBe('my content');
            })
            .run()
    });

sparrow.extend(obj)

Add functions to sparrow. The first argument will be the test window variable. For example, if you created a test window called "myWin", then called $myWin.write(). winVar would be $myWin.

NOTE: To add an async function, the final argument must be named 'done'

        sparrow.extend({
            write: function(winVar, selector, content) {
                winVar.find(selector).append(content);
            },
            waitForTesting: function(winVar, done) {
                winVar.waitForText('testing', done);
            }
        });
`
####<a name="httpPost">.http.post(url, data, success, done)

```javascript
        $tests.async(done)
            .fn(function(done) {
                $tests.http.post('/some/url', {some:'data'}, _.partial(checkReturn, done));
            })
            .run();

        function checkReturn(done, ret) {
            expect(ret).toBe('<some> <html>');
            done();
        }

Troubleshooting

Accessing the window object in a tab

You can use the $window variable to access the window object inside of a tab from the debugging console.
For example, if your tab is $page, then $page.$window will give you the window variable within that tab. From there you can access any global variables.

Debugging in the middle of an async monad chain

The easiest way to debug in the middle of the async chain is to use syncFn. Add a temporary .syncFn() call and put your debug code inside of the passed function. The same technique works for setting breakpoints.

    $win.async(done)
    .click('#something')
    .waitFor('#something-else')
    .syncFn(function() {
        console.log($win.find('#some-thing').html())
    })
    .run()

Feedback

If you find Sparrow useful, please let me know. If you have any questions or concerns, please feel free to let me know at scott@bulldoginfo.com.

I created Sparrow because none of the other testing frameworks I found had the features I was looking for. I have found this framework very useful for testing production websites.

If you are looking for someone to setup functional testing for your web application or website, contact me.