Email iconarrow-down-circleGroup 8Path 3arrow-rightGroup 4Combined Shapearrow-rightGroup 4Combined ShapeUntitled 2Untitled 2Path 3ozFill 166crosscupcake-icondribbble iconGroupPage 1GitHamburgerPage 1Page 1LinkedInOval 1Page 1Email iconphone iconPodcast ctaPodcast ctaPodcastpushpinblog icon copy 2 + Bitmap Copy 2Fill 1medal copy 3Group 7twitter icontwitter iconPage 1

Now we’re getting somewhere and hopefully we’ve got most of the boring set-up and configuration out of the way. From here on we should be smooth sailing!

Carrying on in BDD fashion, let’s create our feature file first user-views-episodes-for-a-podcast.feature:

Feature: User views episodes for a podcast

  As a user of the podcast application
  I want to see the episodes for a particular podcast
  In order to select an episode that I want to listen to

  Scenario: User clicks through to the podcast view from search results
    Given that I am on the podcast search page
    When I search for the podcast "JavaScript"
    And I click to view episodes for the podcast "JavaScript Jabber"
    Then I should be on the episodes page for the podcast with feed "http://feeds.feedwrench.com/JavaScriptJabber.rss"

  Scenario: User views the episodes for a podcast
    When I am on the episodes view for the podcast with feed "http://feeds.feedwrench.com/JavaScriptJabber.rss"
    Then I should see the following podcast episodes:
      | Title                                                                | Date                            | Duration |
      | 018 AiA Style Guides                                                 | Thu, 27 Nov 2014 07:00:00 -0700 | 35:31    |
      | 017 AiA AtScript with Miško Hevery                                   | Thu, 20 Nov 2014 07:00:00 -0700 | 31:58    |
      | 016 AiA NG 1.3 and 2.0 with Brad Green, Igor Minar, and Miško Hevery | Thu, 13 Nov 2014 07:00:00 -0700 | 54:31    |

If we run with grunt e2e again now we should get our new empty step definitions output in our terminal, as before. Let’s get these in to our new step definitions file step_definitions/user-views-episodes-fora-podcast.js:

function UserViewsEpisodesForAPodcast() {

    'use strict';
    this.World = require('../support/world.js').World;
    var searchPage = require('../page_object/search-page');

    this.When(/^I click to view episodes for the podcast "([^"]*)"$/, function (arg1, callback) {
        // Write code here that turns the phrase above into concrete actions
        callback.pending();
    });

    this.Then(/^I should be on the episodes page for the podcast with feed "([^"]*)"$/, function (arg1, callback) {
        // Write code here that turns the phrase above into concrete actions
        callback.pending();
    });

    this.When(/^that I am on the episodes view for the podcast with feed "([^"]*)"$/, function (arg1, callback) {
        // Write code here that turns the phrase above into concrete actions
        callback.pending();
    });

    this.Then(/^I should see the following podcast episodes:$/, function (table, callback) {
        // Write code here that turns the phrase above into concrete actions
        callback.pending();
    });

    this.When(/^I am on the episodes view for the podcast with feed "([^"]*)"$/, function (arg1, callback) {
        // Write code here that turns the phrase above into concrete actions
        callback.pending();
    });

}

module.exports = UserViewsEpisodesForAPodcast;

Our first step in the this feature is I click to view episodes for the podcast “JavaScript Jabber”. We can start implementing this step right away.

Let’s add a new action to our search-page.js page object:

viewEpisodes: function (podcastName) {
    return element(by.cssContainingText('h4.media-heading', podcastName))
        .element(by.xpath('..'))
        .element(by.cssContainingText('a[ui-sref="podcast({ feed: podcast.feedUrl })"]', 'View episodes'))
        .click();
}

This action will look for the heading with the podcast name we specify, then eventually find and click the new button we’ve created. Now update your first step:

this.When(/^I click to view episodes for the podcast "([^"]*)"$/, function (podcastTitle, callback) {
    searchPage.viewEpisodes(podcastTitle).then(callback);
});

So protractor is now clicking on the link we’ve created to view all episodes (this link was already present in the template we created previously). Now we’ll write the next step to check if we are now on the correct page (which we obviously know will fail, as we do not have a state for this page yet. Remember always have a failing test before code can be written!). For this test we won’t be interacting with the page or manually visiting a new page, so we don’t need to create a new page object (yet).

Let’s add the next assertion:

this.Then(/^I should be on the episodes page for the podcast with feed "([^"]*)"$/, function (feed, callback) {
    browser.getLocationAbsUrl().then(function (url) {
        expect(decodeURIComponent(url)).to.equal('http://127.0.0.1:9003/#/podcast?feed=' + feed);
        callback();
    });
});

Which when run should give us the expected output of AssertionError: expected ‘http://127.0.0.1:9003/#/search?term=JavaScript’ to equal ‘http://127.0.0.1:9003/#/podcast?feed=http://feeds.feedwrench.com/JavaScriptJabber.rss’

This kind of response in your terminal should start becoming music to your ears because it is this that is essentially permission for you to go inside to start writing some unit tests. We can’t get this test to pass without writing some code, and we can’t write code without writing a unit test to cover it! So lets get started.

test/spec/state.js

Lets get back to our state.js tests and define a nice simple new test:

    it('should have an episodes state', function () {
        expect($state.href('podcast')).to.equal('#/podcast');
        $state.go('podcast', { feed: 'http://feeds.com/feed.rss' });
        $rootScope.$digest();
        expect($state.current.name).to.equal('podcast');
        expect($stateParams.feed).to.equal('http://feeds.com/feed.rss');
    });

Note you’ll have to inject $stateParams in to your test for this to work as this is the first time we’ll make assertions on the query parameters in the state.

As before, we’ve done the bare minimum here. We know this state will need to have results resolved to it as with the search state, but we only want to get to the next step of our E2E test for now.

Add the following state to our state.js file:

$stateProvider.state('podcast', {
    url: '/podcast?feed',
    templateUrl: 'views/podcast.html'
});

And add a blank views/podcast.html file so we can get the test to pass:

Podcast view!

Restart the tests here so we can get the $templateCache to kick in and..

Chrome 38.0.2125 (Mac OS X 10.8.3): Executed 9 of 9 SUCCESS (0.198 secs / 0.033 secs)

Great! Now we should be able to get the next step of our E2E test to pass:

  Scenario: User clicks through to the podcast view from search results                                                # features/user-views-episodes-for-a-podcast.feature:7
    Given that I am on the podcast search page                                                                         # features/user-views-episodes-for-a-podcast.feature:8
    When I search for the podcast "JavaScript"                                                                         # features/user-views-episodes-for-a-podcast.feature:9
    And I click to view episodes for the podcast "JavaScript Jabber"                                                   # features/user-views-episodes-for-a-podcast.feature:10
    Then I should be on the episodes page for the podcast with feed "http://feeds.feedwrench.com/JavaScriptJabber.rss" # features/user-views-episodes-for-a-podcast.feature:11

And it magically does. Let’s create the next step of our E2E tests now that we’ve created the next state. We’ll create a new page object for this one; episodes-page.js.

var EpisodesPage = {

    visit: function (feed) {
        return browser.get('#/podcast?feed='+feed);
    }

};

module.exports = EpisodesPage;

and update the step definitions:

this.When(/^I am on the episodes view for the podcast with feed "([^"]*)"$/, function (feed, callback) {
    episodePage.visit(feed).then(callback);
});

So now we can go to the episodes view we’ll need to go inside to start grabbing the real episodes. How exciting.

We’ll be using the Google Feed API to retrieve a particular podcasts’ RSS feeds, as with the ItunesAPI we’ll be creating a new configured Restangular instance.

Start with creating the files ‘yo angular:service google-feed-api’ and removing the stock test that has been generated.

Now let’s start our spec with some sensible basics that we can grab from our existing test:

'use strict';

describe('Service: GoogleFeedApi', function () {

    // load the service's module
    beforeEach(module('uvdAngularPodcastTutorialApp'));

    // instantiate service
    var Restangular, callback, configurer;

    beforeEach(inject(function (_Restangular_) {

        Restangular = _Restangular_;

        configurer = {
            setJsonp: sinon.spy(),
            setDefaultRequestParams: sinon.spy(),
            setBaseUrl: sinon.spy(),
            setResponseInterceptor: sinon.spy()
        };

        sinon.stub(Restangular, 'withConfig', function (_callback_) {
            callback = _callback_;
            return true;
        });

    }));

    describe('Basic configuration', function () {

        it('should return Restangular with config', inject(function (GoogleFeedApi) {
            expect(Restangular.withConfig).to.have.been.called;
        }));

        it('should set Jsonp to true', inject(function (GoogleFeedApi) {
            callback(configurer);
            expect(configurer.setJsonp).to.have.been.calledWithExactly(true);
        }));

        it('should set the default request params', inject(function (GoogleFeedApi) {
            callback(configurer);
            expect(configurer.setDefaultRequestParams).to.have.been.calledWithExactly('jsonp', { callback: 'JSON_CALLBACK', v: '1.0', output: 'xml', num: -1 });
        }));

        it('should set the base url to that specified in a constant', inject(function (GoogleFeedApi) {
            callback(configurer);
            expect(configurer.setBaseUrl).to.have.been.calledWithExactly('//ajax.googleapis.com/ajax/services/feed/');
        }));

    });

});

Now let’s update our Gruntfile. Add the google feed api base_url to our ngConstant configuration section:

        serve: {
            constants: {
                ITUNES_BASE_URL: 'https://itunes.apple.com/',
                GOOGLE_FEED_BASE_URL: '//ajax.googleapis.com/ajax/services/feed/'
            }
        },
        e2e: {
            constants: {
                ITUNES_BASE_URL: 'http://localhost:3000/',
                GOOGLE_FEED_BASE_URL: 'http://localhost:3000/'
            }
        }

And now start creating the actual Restangular instance:

'use strict';
/**
 * GoogleFeedApi
 *
 * @param Restangular
 * @returns {*}
 * @constructor
 */
function GoogleFeedApi (Restangular, GOOGLE_FEED_BASE_URL) {

    /**
     * Configurer the Restangular instance
     *
     * @param Configurer
     * @constructor
     */
    function GoogleFeedApiConfigurer (Configurer) {
        Configurer.setJsonp(true);
        Configurer.setDefaultRequestParams('jsonp', { callback: 'JSON_CALLBACK', v: '1.0', output: 'xml', num: -1 });
        Configurer.setBaseUrl(GOOGLE_FEED_BASE_URL);
    }

    return Restangular.withConfig(GoogleFeedApiConfigurer);
}

/**
 * @ngdoc service
 * @name uvdAngularPodcastTutorialApp.GoogleFeedApi
 * @description
 * # GoogleFeedApi
 * Factory in the uvdAngularPodcastTutorialApp.
 */
angular.module('uvdAngularPodcastTutorialApp')
    .factory('GoogleFeedApi', GoogleFeedApi);

We should now have a passing first test. RSS feeds are XML, therefore we need to somehow parse this XML into JSON for us to use. (Google Feed API can attempt to do this automatically for you but unfortunately for most podcast RSS feeds this won’t be enough, we’ll have to do a bit of our own). Fortunately, our unit tests can tell us when we’ve got it working.

Let’s get a real GoogleFeedAPI response to use as a base for our unit tests. Grab the JSON from the following: Adventures In Angular google feed RSS response

Save this file to test/spec/fixtures/googleFeedResponse.json.

Install karma-json-fixtures-preprocessor karma-json-fixtures-preprocessor --save-dev and include it in your karma.conf.js preprocessors list:

...
'test/spec/fixtures/*.json': ['json_fixtures']
...

And add to the files array:

...
'test/spec/fixtures/*.json'

And enable it in the plugins list:

'karma-json-fixtures-preprocessor'

And add the following configuration to karma.conf.js so we can easily grab the fixtures in our tests:

    jsonFixturesPreprocessor: {
        variableName: 'jsonFixtures'
    }

At this point we should be configured test-wise to start writing some unit tests.

We’re going to get some help with parsing the XML feed from a library called angular-x2js, which is just a wrapper around the x2js library. We can install this with bower, so let’s do that now. bower install angular-xml --save

Add it to our app.js:

angular
  .module('uvdAngularPodcastTutorialApp', [
        'config',
        'ui.router',
        'restangular',
        'xml'
    ]);

And include it in our karma.conf.js with our other included bower components:

...
            'bower_components/x2js/xml2json.js',
            'bower_components/angular-xml/angular-xml.js',
...

Restart your test runner so that karma picks up on your new dependencies.

We can now create our test. This test works by executing our response interceptor function with the fixture, and checking the resulting outcome (it looks more complicated then it is!)

'use strict';

describe('Service: GoogleFeedApi', function () {

    // load the service's module
    beforeEach(module('uvdAngularPodcastTutorialApp'));

    // instantiate service
    var Restangular, callback, configurer, $http;

    beforeEach(inject(function (_Restangular_, _$http_) {

        Restangular = _Restangular_;
        $http = _$http_;

        configurer = {
            setJsonp: sinon.spy(),
            setDefaultRequestParams: sinon.spy(),
            setBaseUrl: sinon.spy(),
            setResponseInterceptor: sinon.spy()
        };

        sinon.stub(Restangular, 'withConfig', function (_callback_) {
            callback = _callback_;
            return true;
        });

    }));

    describe('Basic configuration', function () {

        it('should return Restangular with config', inject(function (GoogleFeedApi) {
            expect(Restangular.withConfig).to.have.been.called;
        }));

        it('should set Jsonp to true', inject(function (GoogleFeedApi) {
            callback(configurer);
            expect(configurer.setJsonp).to.have.been.calledWithExactly(true);
        }));

        it('should set the default request params', inject(function (GoogleFeedApi) {
            callback(configurer);
            expect(configurer.setDefaultRequestParams).to.have.been.calledWithExactly('jsonp', {
                callback: 'JSON_CALLBACK',
                v: '1.0',
                output: 'xml',
                num: -1
            });
        }));

        it('should set the base url to that specified in a constant', inject(function (GoogleFeedApi) {
            callback(configurer);
            expect(configurer.setBaseUrl).to.have.been.calledWithExactly('//ajax.googleapis.com/ajax/services/feed/');
        }));

    });

    describe('Parsing XML', function () {

        it('should parse a googleFeedApi XML response', inject(function (GoogleFeedApi) {

            var interceptor,
                parsedResponse;

            callback(configurer);
            expect(configurer.setResponseInterceptor).to.have.been.calledOnce;
            //Get the callback we have added as a response interceptor
            interceptor = configurer.setResponseInterceptor.getCall(0).args[0];

            parsedResponse = interceptor(jsonFixtures['test/spec/fixtures/googleFeedResponse'], 'getList', 'load', '//ajax.googleapis.com/ajax/services/feed/?q=test', {}, {});

            expect(parsedResponse).to.deep.equal([
                {title: '018 AiA Style Guides', date: 'Thu, 27 Nov 2014 07:00:00 -0700', url: 'http://devchat.cachefly.net/angular/AiA018StyleGuides.mp3', duration: '35:31'},
                {title: '017 AiA AtScript with Miško Hevery', date: 'Thu, 20 Nov 2014 07:00:00 -0700', url: 'http://devchat.cachefly.net/angular/AiA017AtScript.mp3', duration: '31:58'},
                {title: '016 AiA NG 1.3 and 2.0 with Brad Green, Igor Minar, and Miško Hevery', date: 'Thu, 13 Nov 2014 07:00:00 -0700', url: 'http://devchat.cachefly.net/angular/AiA016NG13&20.mp3', duration: '54:31'},
                {title: '015 AiA Angular and Kendo UI with Jesse Liberty', date: 'Thu, 06 Nov 2014 06:00:00 -0700', url: 'http://media.devchat.tv/adventures-in-angular/AiA015KendoUI.mp3', duration: '34:11'},
                {title: '014 AiA Using ES6 with Angular with Scott Allen', date: 'Thu, 30 Oct 2014 07:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA014ES6.mp3?rss=true', duration: '38:25'},
                {title: '013 AiA Modern Web and Open Source with Scott Hanselman', date: 'Thu, 23 Oct 2014 07:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA013ScottHanselman.mp3?rss=true', duration: '38:33'},
                {title: '012 AiA Directives', date: 'Thu, 16 Oct 2014 08:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA012Directives.mp3?rss=true', duration: '32:25'},
                {title: '011 AiA Angular Fire with David East and Kato Wulf', date: 'Thu, 09 Oct 2014 07:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA011AngularFire.mp3?rss=true', duration: '34:56'},
                {title: '010 AiA Preferred Backends', date: 'Thu, 02 Oct 2014 07:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA010Backends.mp3?rss=true', duration: '34:13'},
                {title: '009 AiA ng 2.0 with Rob Eisenberg', date: 'Thu, 25 Sep 2014 07:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA009NG2.mp3?rss=true', duration: '54:38'},
                {title: '008 AiA Angular & WebGL with Sean Griffin', date: 'Thu, 18 Sep 2014 07:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA008WebGL.mp3?rss=true', duration: '27:40'},
                {title: '007 AiA HabitRPG', date: 'Thu, 11 Sep 2014 07:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA007HabitRPG.mp3?rss=true', duration: '37:11'},
                {title: '006 AiA Build Processes', date: 'Thu, 04 Sep 2014 07:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA006BuildPipelines.mp3?rss=true', duration: '38:19'},
                {title: '005 AiA Teaching Angular', date: 'Thu, 28 Aug 2014 07:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA005Teaching.mp3?rss=true', duration: '37:33'},
                {title: '004 AiA Resources & Learning Angular', date: 'Thu, 21 Aug 2014 07:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA004Learning.mp3?rss=true', duration: '24:01'},
                {title: '003 AiA GDEs', date: 'Thu, 14 Aug 2014 07:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA003GDEs.mp3?rss=true', duration: '33:30'},
                {title: '002 AiA Angular Meetups with Matt Zabriskie and Sharon DiOrio', date: 'Thu, 07 Aug 2014 07:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA002AngularMeetups.mp3?rss=true', duration: '34:37'},
                {title: '001 AiA The Birth of Angular', date: 'Thu, 31 Jul 2014 07:00:00 -0600', url: 'http://media.devchat.tv/adventures-in-angular/AiA001BirthofAngular.mp3?rss=true', duration: '47:55'}
            ]);
        }));
    });

});

And here’s the application code to satisfy this:

'use strict';
/**
 * GoogleFeedApi
 *
 * @param Restangular
 * @param GOOGLE_FEED_BASE_URL
 * @returns {*}
 * @constructor
 */
function GoogleFeedApi (Restangular, GOOGLE_FEED_BASE_URL) {

    /**
     * Configurer the Restangular instance
     *
     * @param Configurer
     * @constructor
     */
    function GoogleFeedApiConfigurer (Configurer) {

        var x2js = new X2JS();

        function googleFeedRssParser (response) {

            var parsedJson = x2js.xml_str2json(response.responseData.xmlString);

            return _.map(parsedJson.rss.channel.item, function (episodeData) {
                return {
                    title: episodeData.title,
                    date: episodeData.date.__text,
                    url: episodeData.enclosure._url,
                    duration: episodeData.duration.__text
                }
            });

        }

        Configurer.setJsonp(true);
        Configurer.setDefaultRequestParams('jsonp', { callback: 'JSON_CALLBACK', v: '1.0', output: 'xml', num: -1 });
        Configurer.setBaseUrl(GOOGLE_FEED_BASE_URL);
        Configurer.setResponseInterceptor(googleFeedRssParser);
    }

    return Restangular.withConfig(GoogleFeedApiConfigurer);
}

/**
 * @ngdoc service
 * @name uvdAngularPodcastTutorialApp.GoogleFeedApi
 * @description
 * # GoogleFeedApi
 * Factory in the uvdAngularPodcastTutorialApp.
 */
angular.module('uvdAngularPodcastTutorialApp')
    .factory('GoogleFeedApi', GoogleFeedApi);

The response interceptor uses the x2js library to parse the xml, we just then take the properties that we need from it. Underscores denote xml attributes.
If you get the error message ‘jsonFixtures is not defined’ then you probably haven’t got a fixtures file in the correct directory.

We’re now ready to create the episodes controller. Bootstrap the controller as always with ‘yo angular:controller episodes’. Our episode controller is very simple (as all of our controllers should be), so the unit test is pretty straight forward:

'use strict';

describe('Controller: EpisodesCtrl', function () {

    // load the controller's module
    beforeEach(module('uvdAngularPodcastTutorialApp'));

    var EpisodesCtrl,
        scope,
        episodes =  [
            {
                title: '018 AiA Style Guides',
                date: 'Thu, 27 Nov 2014 07:00:00 -0700',
                src: 'http://devchat.cachefly.net/angular/AiA018StyleGuides.mp3',
                duration: '35:31'
            },
            {
                title: '017 AiA AtScript with Miško Hevery',
                date: 'Thu, 20 Nov 2014 07:00:00 -0700',
                src: 'http://devchat.cachefly.net/angular/AiA017AtScript.mp3',
                duration: '31:58'
            }
        ];

    // Initialize the controller and a mock scope
    beforeEach(inject(function ($controller, $rootScope) {
        scope = $rootScope.$new();
        EpisodesCtrl = $controller('EpisodesCtrl as episodesCtrl', {
            $scope: scope,
            episodes: episodes
        });
    }));

    it('should assign episodes to scope', function () {
        expect(scope.episodesCtrl.episodes).to.deep.equal(episodes);
    })

});

And the application code:

'use strict';

/**
 * EpisodesCtrl
 *
 * @param episodes
 * @constructor
 */
function EpisodesCtrl(episodes) {
    this.episodes = episodes;
}

/**
 * @ngdoc function
 * @name uvdAngularPodcastTutorialApp.controller:EpisodesCtrl
 * @description
 * # EpisodesCtrl
 * Controller of the uvdAngularPodcastTutorialApp
 */
angular.module('uvdAngularPodcastTutorialApp')
  .controller('EpisodesCtrl', EpisodesCtrl);

We now need to write a test for the resolve function; the function that’s called to get the episodes that we can inject in to the state. Update the state.js tests so that we test the resolve is called (note we don’t really care how the GoogleFeedApi works in this test, because that functionality is covered in other unit tests, we just want to make sure it is called.)

'use strict';

describe('State', function () {

    var $rootScope,
        $state,
        ItunesApi,
        GoogleFeedApi,
        searchStub,
        $injector,
        $stateParams,
        dummyResults;

    beforeEach(function () {

        module('uvdAngularPodcastTutorialApp');

        inject(function (_$rootScope_, _$state_, _ItunesApi_, _$injector_, _$stateParams_, _GoogleFeedApi_) {
            $rootScope = _$rootScope_;
            $state = _$state_;
            ItunesApi = _ItunesApi_;
            GoogleFeedApi = _GoogleFeedApi_;
            $injector = _$injector_;
            $stateParams = _$stateParams_;
        });

        dummyResults = [
            { name: 'Dummy result 1' },
            { name: 'Dummy result 2' }
        ];

        searchStub = {
            getList: function () {}
        };

        sinon.stub(ItunesApi, 'all', function () {
            return searchStub;
        });

        sinon.stub(searchStub, 'getList', function () {
            return dummyResults;
        });

        sinon.stub(GoogleFeedApi, 'all', function () {
            return {
                getList: function () {
                    return [
                        { title: 'episode' }
                    ];
                }
            }
        });
    });

    afterEach(function () {
        ItunesApi.all.restore();
        searchStub.getList.restore();
    });

    it('should have a search state', function () {
        expect($state.href('search')).to.equal('#/search');
        $state.go('search', { term: 'Angular Podcast' });
        $rootScope.$digest();
        expect($state.current.name).to.equal('search');
        expect(ItunesApi.all).to.have.been.calledOnce.and.calledWithExactly('search');
        expect(searchStub.getList).to.have.been.calledOnce.and.calledWithExactly({ term: 'Angular Podcast' });
        expect($injector.invoke($state.current.resolve.results)).to.equal(dummyResults);
        expect($stateParams.term).to.equal('Angular Podcast');
    });

    it('should have an episodes state', function () {
        expect($state.href('podcast')).to.equal('#/podcast');
        $state.go('podcast', { feed: 'http://feeds.com/feed.rss' });
        $rootScope.$digest();
        expect($state.current.name).to.equal('podcast');
        expect($stateParams.feed).to.equal('http://feeds.com/feed.rss');
        expect($injector.invoke($state.current.resolve.episodes)).to.deep.equal([
            { title: 'episode' }
        ]);
    });
});

And now we can update the state:

$stateProvider.state('podcast', {
    url: '/podcast?feed',
    templateUrl: 'views/podcast.html',
    controller: 'EpisodesCtrl',
    controllerAs: 'episodesCtrl',
    resolve: EpisodesCtrl.resolve
});

And update the EpisodesCtrl with the resolve to make this work:

/**
 * Get the episodes
 *
 * @type {{episodes: episodes}}
 */
EpisodesCtrl.resolve = {
    episodes: function ($stateParams, GoogleFeedApi) {
        return GoogleFeedApi.all('load').getList({ q: $stateParams.feed });
    }
};

Excellent! Our application is getting close!

Let’s update the template.

<div id="search-error-message" ng-if="episodesCtrl.episodes.length === 0">
    No search results
</div>

<table class="table table-striped">

    <thead>
        <tr>
            <th>Title</th>
            <th>Date</th>
            <th>Duration</th>
        </tr>
    </thead>

    <tbody>
        <tr ng-repeat="episode in episodesCtrl.episodes">
            <td>{{ episode.title }}</td>
            <td>{{ episode.date }}</td>
            <td>{{ episode.duration }}</td>
        </tr>
    </tbody>

</table>

Now we should have a working episodes page! We’re ready to finish off our E2E tests.

Create another robohydra file for the mock google feed api in robohydra/plugins/google-feed-api/index.js. This one will load our fixture that we previously created from the filesystem

var RoboHydraHead = require("robohydra").heads.RoboHydraHead,
    fs = require('fs');

exports.getBodyParts = function () {
    return {
        heads: [
            new RoboHydraHead({
                path: '/load',
                handler: function (req, res) {

                    fs.readFile('test/spec/fixtures/googleFeedResponse.json', "utf8", function(error, data) {
                        var response = req.queryParams.callback + '(' + JSON.stringify(data) + ');';
                        res.send(response);
                    });

                }
            })
        ]
    };
};

Add the additional api to e2e-apis.json file in the root of the project:

{
  "plugins": [
    { "name": "itunes-api" },
    { "name": "google-feed-api" }
  ]
}

and restart robohydra. Our first scenario should now pass. To finish the next one we need to get a list of the episodes that are currently on display. Let’s add a function to our episodes-page.js page object:

episodes: by.repeater('episode in episodesCtrl.episodes'),

getEpisodes: function () {
    return getRepeaterColumns(this.episodes, ['title', 'date', 'duration']);
}

and add the step definition to user-views-episodes-for-a-podcast.js:

this.When(/^I am on the episodes view for the podcast with feed "([^"]*)"$/, function (feed, callback) {
    episodePage.visit(feed).then(callback);
});

this.Then(/^I should see the following podcast episodes:$/, function (table, callback) {
    expect(episodePage.getEpisodes()).to.eventually.deep.equal(table.rows()).notify(callback);
});

And now all of our tests should be passing!

The final part of this tutorial series, allowing the user to listen to the episodes of a podcast, is coming soon.

Share: