Skip to content

Instantly share code, notes, and snippets.

@oliverbarnes
Last active August 29, 2015 14:04
Show Gist options
  • Save oliverbarnes/3bbad90bac87ff2e8014 to your computer and use it in GitHub Desktop.
Save oliverbarnes/3bbad90bac87ff2e8014 to your computer and use it in GitHub Desktop.
Draft for tutorial on bdd-ing an ember-cli feature

Participate is being worked on by a great team in Helsinki as part of this year's Rails Girls Summer of Code!

The front-end application is being built with Emberjs and the brand-spanking-new Ember CLI, and we're all getting a crash course on both.

This tutorial walks through building a simplistic feature in Ember following Behavior-Driven Development as much as possible.

It's meant as a sample workflow for the RGSOC team, new to BDD itself, and as a possibly useful example to others grokking bdd-ing with Ember, like myself. It comes from my attempts to port my usual workflow in Ruby.

In this first installment, I'll create a very simple Initiative with just a title and a description.

I start with an acceptance test that drives the interface, describing user interactions and expectations first, then implement these by letting test errors be the guide.

The next installment will deal with simple validation.

As initiatives get incremented - they will have an author and belong to an issue, and have different phases, among other things - I hope to post new ones describing the process.

First step: start an acceptance test (TODO: update this)

Create a file named posting-an-initiative-test.js under tests/acceptance.

Setup

import startApp from 'participate-frontend/tests/helpers/start-app';
import Resolver from 'participate-frontend/tests/helpers/resolver';

var App;

suite('Posting an initiative', {
  setup: function(){
    App = startApp();
  },
  teardown: function() {
    Ember.run(App, 'destroy');
  }
});

(I'm using Mocha/Chai on top of Qunit, following the steps here. Don't yet have the describe() blocks going)

Let's add a test for a link to create a new initiative:

test('Successfully', function(){
  visit('/').then(function() {
    click( $("a:contains('Start a new initiative')") ).then(function() {
      expect(currentPath()).to.equal('/initiatives/new');
    });
  });
});

And run the tests:

➜  participate-frontend git:(simple-new-initiative) ✗ ember test
version: 0.0.37
Built project successfully. Stored in "/Users/work/Projects/participate-frontend/tmp/class-tests_dist-Bv3r6aYr.tmp".
not ok 1 PhantomJS 1.9 - Posting an initiative Successfully
    ---
        message: >
            Error: Element [object Object] not found.

The key here is the message output. The opaque error means Jquery hasn't found the link.

And understandably so, since it doesn't exist yet.

Link to new initiative

Let's implement it by adding it to application.emblem, under app/templates.

h2 Participate App

#menu
  = link-to 'initiatives.new' | Start a new initiative

=outlet

(In case you're wondering, Emblem is an alternative to Handlebars, similar to Ruby's Slim templating engine)

Run ember test again and you'll get a new message:

message: >
    Assertion Failed: The attempt to link-to route 'initiatives.new' failed. The router did not find 'initiatives.new' in its possible routes: 'loading', 'error', 'index', 'application'

Route

Let's add a route to a new initiative resource

Router.map(function() {
  this.resource('initiatives', function() {
    this.route('new');
  });
});

Tests pass

1..2
# tests 2
# pass  2
# fail  0

# ok

Adding the template and form for the new initiative

Add a this line to the spec:

fillIn('div.title input', 'Public health clinic');

So it now looks like this

test('Successfully', function(){
  visit('/').then(function() {
    click( $("a:contains('Start a new initiative')") ).then(function() {
      expect(currentPath()).to.equal('/initiatives/new');
      fillIn('div.initiative div.title input', 'Public health clinic')
    });
  });
});

test output

message: >
    Error: Element div.initiative div.title input not found.

Let's create a template with a form

Create a directory initiatives under app/templates, and then add a file called new.emblem.

Paste the following in it:

form-for model
  = input title

(I'm using EasyForm here. It's not an ember-cli add-on yet, but one can get it working with it pretty easily following the steps on this thread)

Run the tests again, and they should pass.

Let's add the remainder of the form-filling steps in our test:

visit('/').then(function() {
  click( $("a:contains('Start a new initiative')") ).then(function() {
    expect(currentURL()).to.equal('/initiatives/new');
    fillIn('div.title input', 'Public health clinic');
    fillIn('div.descrition textare', 'Allocate compensation money to create a local public health clinic');
    click('form input[type=submit]');
  });

test output

message: >
    Error: Element div.descrition input not found.

Add the next input and the save button

form-for controller
  = input title
  = input description
  = submit

The tests should now pass again.

Of course, submitting the form doesn't do anything yet :)

Submitting the form

So what would a user expect to see after submitting the form. Likely she'll:

  • Want to see a confirmation message
  • Expect to see the url change
  • And to see the new initiative's content so she can be sure it went through correctly

(I'm leaving out failure scenarios, like validation errors, for now. I'll deal with it in future sequels to this first tute.)

click('form input[type=submit]').then(function() {
  expect(currentPath()).to.equal('products.show');
  expect(find('.title').text()).to.equal('Public health clinic');
  expect(find('.description').text()).to.equal('Allocate compensation money to create a local public health clinic');
  expect(find('.flash-message').text()).to.equal('Initiative created successfully');
});

AssertionError: expected 'initiatives.new' to equal 'products.show'

message: >
    AssertionError: expected 'initiatives.new' to equal 'initiatives.show'

To get to the next error, we'll need to take a few steps, inherent to how Ember wires routes and data. The errors I got when going through each of the steps weren't very informative, and I got things working by trial & error & lot of googling and asking things on #emberjs. So I'm pragmatically breaking tdd here and just wiring enough to get to a useful error.

(For more info on what these steps are about, read the Ember guides on routing and controllers, and this thread on Discuss, which clarified things a lot for me. Ember's architecture is still a moving target.)

First, let's add this route handler for /initiatives/new (app/routes/initiatives/new.js)

import Ember from 'ember';

var InitiativesNewRoute = Ember.Route.extend({
  model: function() {
    return this.store.createRecord('initiative');
  },

  actions: {
    submit: function() {
      this.transitionTo('initiatives.show');
    }
  }
});

export default InitiativesNewRoute;

And this model definition (app/models/initiative.js) to go with it:

var Initiative = DS.Model.extend({
  title: DS.attr('string'),
  description: DS.attr('string')
});

export default Initiative;

Next, update the router (app/router.js) to include a path to /initiatives/show:

Router.map(function() {
  this.resource('initiatives', function() {
    this.route('new');
    this.route('show');
  });
});

And add the corresponding template (app/templates/initiatives/show.emblem). It can be empty for now.

Run the tests and we'll get

AssertionError: expected '' to equal 'Public health clinic'

Which means that we got the route transition to work, and are now looking at this error test:

expect(find('.title').text()).to.equal('Public health clinic');

So we made some progress. So far the user can:

  • navigate to our app's homepage
  • click on the link for a new initiative
  • fill in a form with the title and description for it
  • submit it
  • get redirected to the created initiative page.

But there's no initiative created yet. Let's tackle this next:

Handling the form submission

Here I'm also going to wire a few things up to get to the next test error.

Let's update InitiativesNewRoute to handle the submitted params, and then transition to /initiatives/show/:initiative_id

var InitiativesNewRoute = Ember.Route.extend({
  model: function(params) {
    return this.store.createRecord('initiative', params);
  },

  actions: {
    submit: function() {
      var _this = this;
      var initiative = this.get('controller.model');
      initiative.save().then(function(model) {
        _this.transitionTo('initiatives.show', model.get('id'));
      });
    }
  }
});

Update the router to accept the :initiative_id segment:

this.resource('initiatives', function() {
  this.route('new');
  this.route('show', {path: '/:initiative_id'});
});

Create a InitiativesShowRoute (app/routes/initiatives/show.js) to fetch the initiative model:

import Ember from 'ember';

var InitiativesShowRoute = Ember.Route.extend({
  model: function(params) {
    return this.store.find('initiative', params.initiative_id);
  }
});

export default InitiativesShowRoute;

And a new template (app/templates/initiatives/show.emblem)

.title
  h2
    model.title

.description
  p
    model.description

Running the tests now gives us:

AssertionError: expected '' to equal 'Initiative created successfully'

Which means that both of these pass:

expect(find('.title').text()).to.equal('Public health clinic');
expect(find('.description').text()).to.equal('Allocate compensation money to create a local public health clinic');

And we're at the last expectation:

expect(find('.flash-message').text()).to.equal('Initiative created successfully');

To be continued...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment