I’ve been playing around with AngularJS for the last couple of weeks during my spare time while being on paternity leave (read: late at night). I really like the framework and last night I came across Protractor, the end to end testing framework, and for the first time I think I saw some light in the the UI-testing-tunnel and thought I should share some ideas in a couple of posts. First I’ll start with how to use Protractor with CoffeScript and how to get a nice fluent syntax in your testing scenarios (thanks to cyranix for getting me inspired).

Using Protractor with CoffeeScript

I like using CoffeScript (even though I’m kind of verbose when I use it compared to most as I like keeping the parentheses) and thought it would be great if I could write my Page Objects as CoffeScript classes and chain the functions giving me a fluent syntax like:

startPage.
    .clickLogin()
            .setUserName('[email protected]')
            .setPassword('donttellanyone')
            .submit()
    browser.waitForAngular()
    
    expect(browser.getLocationAbsUrl()).toMatch('/app/#!/dashboard')

So, how do we do this? First we need to register CoffeScript in the protractor configuration to be able to use it in our scenarios:

protractor-conf.js

exports.config = {
    allScriptsTimeout: 11000,

    specs: [
        'e2e/*.scenarios.coffee'
    ],

    capabilities: {
        'browserName': 'chrome'
    },

    baseUrl: 'http://localhost:10000/app',

    framework: 'jasmine',

    jasmineNodeOpts: {
        defaultTimeoutInterval: 30000
    }
};

Note: If you don’t have CoffeScript installed grab it with:

npm install coffee-script

Next we need to create our Page Objects and we will start with the start page:

start_page.coffee

LoginPage = require('./login_page')

class StartPage
    constructor: ->
        @loginLink = element(By.id('loginLink'))

    get: ->
        browser.get('app/#!/')
        return @

    clickLogin: ->
        @loginLink.click()
        browser.waitForAngular()
        return new LoginPage()

module.exports = StartPage

We grab the login link element in the constructor (I’ve just set the id in the html view so that it is easy to get hold of in the tests and making the test a bit more robust if I decide to move the login link). When someone then clicks login we click the link and wait for angular to complete the request and then we return the Page Object for the login page.

The Page Object for the login page looks similar in structure but have a username and password field that the user can fill out (Here I use the By.model to get hold of the input fields for the username and password. Also note that I use return @ to return this in the functions to make it chainable):

login_page.coffee

LoginPage = require('./login_page')

class LoginPage
    constructor: ->
        @username = element(By.model('user.name'))
        @password = element(By.model('user.password'))
        @loginButton = element(By.id('loginButton'))
        @errorMessage = element(By.id('loginErrorMessage'))

    get: ->
        browser.get('app/#!/auth/login')
        return @

    getErrorMessage: ->
        return @errorMessage.getText()

    setUserName: (text) ->
        @username.sendKeys(text)
        return @

    clearUserName: ->
        @username.clear()
        return @

    setPassword: (text) ->
        @password.sendKeys(text)
        return @

    clearPassword: ->
        @password.clear()
        return @

    submit: ->
        @loginButton.click()

module.exports = LoginPage

Now we can simply require the start page Page Object in our scenarios to get the fluent syntax in our testing scenario:

login.scenarios.coffee

StartPage = require('../PageObjects/start_page')

describe('<e2e>', () ->
    loginPage = null

    beforeEach(() ->
        startPage = new StartPage()
        startPage.get()
        loginPage = startPage.clickLogin()
    )

    describe('login', () ->
        it('should login and redirect to the dashboard with valid user credentials', () ->
            loginPage
                .setUserName('[email protected]')
                .setPassword('donttellanyone')
                .submit()
            browser.waitForAngular()
            
            expect(browser.getLocationAbsUrl()).toMatch('/app/#!/dashboard')
        )

...more tests
}

This way we get a nice structure and a pretty neat fluent syntax in our testing scenarios. Next up is how to bootstrap your server/database before each test scenario so that you can make integration test scenarios from a consistent state every time. I’ll get back to that in a future post.