How to unit test views in ember.js? - javascript

We are in the process of learning Ember.js. We do all our development TDD, and want Ember.js to be no exception. We have experience building Backbone.js apps test-driven, so we are familiar with testing front-end code using Jasmine or Mocha/Chai.
When figuring out how to test views, we ran into a problem when the template for the view uses has a #linkTo statement. Unfortunately we are unable to find good test examples and practices. This gist is our quest to get answers how to decently unit-test ember applications.
When looking at the test for linkTo in Ember.js source code, we noticed it contains a full wiring of an ember app to support #linkTo. Does this mean we cannot stub this behaviour when testing a template?
How do you create tests for ember views using template renders?
Here is a gist with our test and a template that will make the test pass, and a template that will make it fail.
view_spec.js.coffee
# This test is made with Mocha / Chai,
# With the chai-jquery and chai-changes extensions
describe 'TodoItemsView', ->
beforeEach ->
testSerializer = DS.JSONSerializer.create
primaryKey: -> 'id'
TestAdapter = DS.Adapter.extend
serializer: testSerializer
TestStore = DS.Store.extend
revision: 11
adapter: TestAdapter.create()
TodoItem = DS.Model.extend
title: DS.attr('string')
store = TestStore.create()
#todoItem = store.createRecord TodoItem
title: 'Do something'
#controller = Em.ArrayController.create
content: []
#view = Em.View.create
templateName: 'working_template'
controller: #controller
#controller.pushObject #todoItem
afterEach ->
#view.destroy()
#controller.destroy()
#todoItem.destroy()
describe 'amount of todos', ->
beforeEach ->
# $('#konacha') is a div that gets cleaned between each test
Em.run => #view.appendTo '#konacha'
it 'is shown', ->
$('#konacha .todos-count').should.have.text '1 things to do'
it 'is livebound', ->
expect(=> $('#konacha .todos-count').text()).to.change.from('1 things to do').to('2 things to do').when =>
Em.run =>
extraTodoItem = store.createRecord TodoItem,
title: 'Moar todo'
#controller.pushObject extraTodoItem
broken_template.handlebars
<div class="todos-count"><span class="todos">{{length}}</span> things to do</div>
{{#linkTo "index"}}Home{{/linkTo}}
working_template.handlebars
<div class="todos-count"><span class="todos">{{length}}</span> things to do</div>

Our solution has been to essentially load the whole application, but isolate our test subjects as much as possible. For example,
describe('FooView', function() {
beforeEach(function() {
this.foo = Ember.Object.create();
this.subject = App.FooView.create({ foo: this.foo });
this.subject.append();
});
afterEach(function() {
this.subject && this.subject.remove();
});
it("renders the foo's favoriteFood", function() {
this.foo.set('favoriteFood', 'ramen');
Em.run.sync();
expect( this.subject.$().text() ).toMatch( /ramen/ );
});
});
That is, the router and other globals are available, so it's not complete isolation, but we can easily send in doubles for things closer to the object under test.
If you really want to isolate the router, the linkTo helper looks it up as controller.router, so you could do
this.router = {
generate: jasmine.createSpy(...)
};
this.subject = App.FooView.create({
controller: { router: this.router },
foo: this.foo
});

One way you can handle this is to create a stub for the linkTo helper and then use it in a before block. That will bypass all the extra requirements of the real linkTo (e.g. routing) and let you focus on the contents of the view. Here's how I'm doing it:
// Test helpers
TEST.stubLinkToHelper = function() {
if (!TEST.originalLinkToHelper) {
TEST.originalLinkToHelper = Ember.Handlebars.helpers['link-to'];
}
Ember.Handlebars.helpers['link-to'] = function(route) {
var options = [].slice.call(arguments, -1)[0];
return Ember.Handlebars.helpers.view.call(this, Em.View.extend({
tagName: 'a',
attributeBindings: ['href'],
href: route
}), options);
};
};
TEST.restoreLinkToHelper = function() {
Ember.Handlebars.helpers['link-to'] = TEST.originalLinkToHelper;
TEST.originalLinkToHelper = null;
};
// Foo test
describe('FooView', function() {
before(function() {
TEST.stubLinkToHelper();
});
after(function() {
TEST.restoreLinkToHelper();
});
it('renders the favoriteFood', function() {
var view = App.FooView.create({
context: {
foo: {
favoriteFood: 'ramen'
}
}
});
Em.run(function() {
view.createElement();
});
expect(view.$().text()).to.contain('ramen');
});
});

Related

How to test if a backbone view listens to specific event correctly

I'm trying to test if a Backbone.js view is correctly listening for a specific event triggered by the router. The initialize method on the view I'm testing looks something like this:
initialize: function(options) {
this.router = options.router; // pass router obj through args
this.listenTo(this.router, 'login_manager:show', this.buildLoginPage); // not shown on this snippet, but defined later
}
When a route is matched on my router, I do the following:
showLogin: function() {
this.trigger('login_manager:show');
}
This code works as expected on the browser, but the test I did for it doesn't pass. Here's the test I'm trying to do:
beforeEach(function() {
this.router = new Backbone.Router();
this.loginManager = new LoginManager({
router: this.router
});
});
afterEach(function() {
this.loginManager = null;
this.router = null;
});
it('listens to correct event', sinon.test(function() {
var spy = sinon.spy(this.loginManager, 'buildLoginPage');
this.loginManager.router.trigger('login_manager:show');
expect(spy.called).to.be.true;
}));
I haven't been able to get this test to pass, so I was wondering if anyone could help me out?
Thanks,
Diego.
Maybe this could help
it('listens to correct event', () => {
var stub = sinon.stub();
Backbone.Events.listenTo(this.loginManager.router, 'login_manager:show', stub);
this.loginManager.router.trigger('login_manager:show');
stub.called.should.be.true;
});

Extending a base class in an Angular service

I have a base class that I would like to extend in a service to help get data in to the angular scope. I have searched around the net for a solution, but have not found one that I like. I have a base class that is used to access the File systems of devices
the class structure:
var cOfflineStorageBase = Class.extend({
init: function(){
},
CreateFolderDir: function(){
},
DeleteAll: function(){
},
DeleteDirectories: function(){
},
DeleteItem: function(){
},
GetFiles: function(){
},
FileExists: function(){
},
GetPath: function(){
},
GetObject: function(){
},
SaveObject: function(){
},
});
I would like to be able to extend this class in several different angular services (ie offlineCart, offlineCustomLists, ect...) where each service would be able to use the storage base to store the various different data types. I am looking for the best, most appropriate way to do this in angular. In vanilla JavaScript one would just do something like this:
var newClass = cOfflineStorageBase.extend({
//add new stuff here
});
but I want to do this same thing the angular way.
The approach I have been considering are to use the angular.extend functionality, but I am not sure this is appropriate or would something like this be a more appropriate approach:
app.factory('someChild', ['$http' , 'cOfflineStorageBase',
function($http, cOfflineStorageBase){
var SomeClass = cOfflineStorageBase.extend({
init: function(){
this._super.init()
},
//Add more stuff here
});
return SomeClass;
}]);
I would like some advice if theses approaches are correct or if there might be another that is better for what I am wanting to accomplish. I would also like or rather need to use promises in much of this code as it would be async.
I pulled off this trick recently.
I will start by defining a plain JavaScript constructor. This does not need to be an angular service. What I do is that, later, the extending constructors can pass any necessary injections by parameter. So, this will be the base "class" of my angular services. This is where I would expose anything I want all angular services to inherit.
function ParentService($http) {
this.$http = $http;
}
ParentService.prototype.foo = function () {
alert("Hello World");
};
Then I will proceed to define a child constructor using prototypal inheritance. This constructor will indeed be an angular service (you can tell by my use of $inject at the end).
function ChildService($http) {
Parent.call(this, $http);
}
ChildService.prototype = new ParentService();
ChildService.prototype.baz = function() {
return this.$http.get('/sample/rest/call');
}
ChildService.$inject = ['$http'];
Then I will proceed to register the services à la carte in the corresponding angular modules:
var app = angular.module('SampleApp', []);
app.service('child', ChildService);
Finally, in my controller I will simply inject my service, which will be an instance of my ChildService constructor, which in turn extends my ParentService constructor:
app.controller('MainCtrl', ['$scope', 'child', function ($scope, child) {
child.foo(); //alert("Hello World")
var promise = child.bar();
}]);
You can see a JSFiddle here
Also there is an interesting video in Youtube from ngConf called Writing A Massive Angular App which covers some of these topics and a few other ideas on code reusability with angular.
This question was asked, and answered, 18 months ago. I recently went through the same issue on a project. I wanted to have a base Model defined that I could use to build factories off of. Angular has a very simple Provider to assist with this called the Value Provider, which Angular implements using the Value Recipe.
I'm not sure what version of Angular you may have been using at the time, but this dates back (AFAIK) to version 1.3.0. (As of this writing, current stable is 1.4.8)
I'm also using John Resig's Simple Inheritance Script.
http://ejohn.org/blog/simple-javascript-inheritance/
Here's a snippet of my code (with most of the application specific logic removed).
var MyApp = angular.module( 'MyApp',['ngResource','ngAnimate','ngSanitize'] );
/* ==================================================================================== */
// - Base Model Class -------------------------------------------------------------
/* ==================================================================================== */
MyApp
/**
* BaseModel - Value Provider
*
*/
.value( 'BaseModel',Class.extend({
attribs: {},
init: function(){
var self = this;
_active = true;
_new = true;
_origs = {};
_loadByObject = function( obj ){ ... }
},
get: function( key ){ ... },
set: function( key,val ){ ... },
isNew: function(){ ... },
keep: function(){ ... },
remove: function(){ ... },
load: function( obj ){ ... }
verify: function(){ ... },
save: function(){ ... },
}))
.factory( 'UserFactory',
[ '$http', '$q', 'BaseModel',
function( $http, $q, BaseModel ){
var UserFactory = BaseModel.extend({
init: function(){
this._super( false );
_fields = [
'first', 'last', 'email',
'phone', 'password', 'role'
];
_permitted = [
'first', 'last', 'email',
'phone', 'password', 'role'
];
_required = [
'first', 'last', 'email', 'role'
];
_resource = "users";
_api = "users";
}
});
return UserFactory;
}])
I'd love to hear anyone's feedback, too.
Here's the Angular
Docs:
https://code.angularjs.org/1.3.0/docs/guide/providers

Change page view with Backbone

I've implemented "change page" in my one page application with Backbone.js. However, I'm not sure if my Router should contain so much business logic. Should I consider go with Marionette.js to implement such functionality and make my Router thin? Should I worry about destroying Backbone models and views attached to "previous" active page/view when I change it (in order to avoid memory leaks) or it's enough to empty html attached to those models/views.
Here is my Router:
App.Router = Backbone.Router.extend({
routes: {
'users(/:user_id)' : 'users',
'dashboard' : 'dashboard'
},
dashboard: function() {
App.ActiveView.destroy_view();
App.ActiveViewModel.destroy();
App.ActiveViewModel = new App.Models.Dashboard;
App.ActiveViewModel.fetch().then(function(){
App.ActiveView = new App.Views.Dash({model: App.ActiveViewModel });
App.ActiveView.render();
});
},
users: function(user_id) {
App.ActiveView.destroy_view();
App.ActiveViewModel.destroy();
App.ActiveViewModel = new App.Models.User;
App.ActiveViewModel.fetch().then(function() {
App.ActiveView = new App.Views.UsersView({model: App.ActiveViewModel});
App.ActiveView.render();
});
}
});
Another approach:
Create an AbstractView
Having an AbstractView declared and then extending your other application specific View's from AbstractView has many advantages. You always have a View where you can put all the common functionalities.
App.AbstractView = Backbone.View.extend({
render : function() {
App.ActiveView && App.ActiveView.destroy_view();
// Instead of destroying model this way you can destroy
// it in the way mentioned in below destroy_view method.
// Set current view as ActiveView
App.ActiveView = this;
this.renderView && this.renderView.apply(this, arguments);
},
// You can declare destroy_view() here
// so that you don't have to add it in every view.
destroy_view : function() {
// do destroying stuff here
this.model.destroy();
}
});
Your App.Views.UsersView should extend from AbstractView and have renderView in place of render because AbstractView's render will make a call to renderView. From the Router you can call render the same way App.ActiveView.render();
App.Views.UsersView = AbstractView.extend({
renderView : function() {
}
// rest of the view stuff
});
App.Views.Dash = AbstractView.extend({
renderView : function() {
}
// rest of the view stuff
});
Router code would then change to :
dashboard: function() {
App.ActiveViewModel = new App.Models.Dashboard;
App.ActiveViewModel.fetch().then(function(){
new App.Views.Dash({model: App.ActiveViewModel }).render();
});
}

Injecting session into a model

I'd like to be able to inject my Session singleton into my Ember models. The use case I'm trying to support is having computed properties on the model that react to the user's profile (a property on the Session object).
App = window.App = Ember.Application.create({
ready: function() {
console.log('App ready');
this.register('session:current', App.Session, {singleton: true});
this.inject('session:current','store','store:main');
this.inject('controller','session','session:current');
this.inject('model','session','session:current');
}
});
The injection works fine into the controller but I'm having trouble with getting it to the model. Is there any limitation here? Any special techniques?
-------- Additional Context ---------
Here's an example of what I'd like to be able to do in my model definition:
App.Product = DS.Model.extend({
name: DS.attr("string"),
company: DS.attr("string"),
categories: DS.attr("raw"),
description: DS.attr("string"),
isConfigured: function() {
return this.session.currentUser.configuredProducts.contains(this.get('id'));
}.property('id')
});
By default injections in models don't work. To do so you need to set the flag Ember.MODEL_FACTORY_INJECTIONS = true:
Ember.MODEL_FACTORY_INJECTIONS = true;
App = window.App = Ember.Application.create({
ready: function() {
console.log('App ready');
this.register('session:current', App.Session, {singleton: true});
this.inject('session:current','store','store:main');
this.inject('controller','session','session:current');
this.inject('model','session','session:current');
}
});
The downside of this, is that it create some break changes:
If you have App.Product.FIXTURES = [...] you neeed to use App.Product.reopenClass({ FIXTURES: [...] });
productRecord.constructor === App.Product will evaluate to false. To solve this you can use App.Product.detect(productRecord.constructor).

Marionette Layout switching strategy

I have the following situation:
app.js: Singleton Marionette.Application() where I define a nav, a footer, and a main region. In the initializer I construct Marionette.Contoller's and attach them to the app's this.controller object for later control. I might not construct all the Controller's here, just the ones I want to Eagerly Load. Some are Lazy Loaded later. I also instantiate a Backbone.Router here, and pass in a reference to my app object:
var theApp = new TP.Application();
theApp.addRegions(
{
navRegion: "#navigation",
mainRegion: "#main",
footerRegoin: "#footer"
});
theApp.addInitializer(function()
{
// Set up controllers container and eagerly load all the required Controllers.
this.controllers = {};
this.controllers.navigationController = new NavigationController({ app: this });
this.controllers.loginController = new LoginController({ app: this });
this.controllers.calendarController = new CalendarController({ app: this });
this.router = new Router({ app: this });
});
**Controller.js: this is a general use controller that handles view & model intsantiation and eventing. Each Controller owns its own Marionette.Layout, to be filled into the App.mainRegion. Each Controller binds to the layout's "show" event to fill in the layout's regions with custom views. Each Controller offers a getLayout() interface that returns the controller's associated layout.
Marionette.Controller.extend(
{
getLayout: function() { return this.layout; },
initialize: function()
{
this.views.myView = new MyView();
...
this.layout.on("show", this.show, this);
...
},
show: function()
{
this.layout.myViewRegion.show(myView);
}
});
router.js: the router uses the app singleton to load a Controller's layout into the App's main region:
...
routes:
{
"home": "home",
"login": "login",
"calendar": "calendar",
"": "calendar"
},
home: function ()
{
var lazyloadedController = new LazyLoadController();
this.theApp.mainRegion.show(lazyLoadController.getLayout());
},
login: function (origin)
{
this.theApp.mainRegion.show(this.theApp.controllers.loginController.layout);
}
As it is, everything works fine except for reloading the same layout / controller twice. What happens is that the DOM events defined in the LoginView do not re-bind on second show. Which is easily solved by moving the LoginView initialization code into the "show" event handler for that Controller:
LoginController = Marionette.Controller.extend(
{
...
show: function()
{
if (this.views.loginView)
delete this.views.loginView.close();
this.views.loginView = new LoginView({ model: this.theApp.session });
this.views.loginView.on("login:success", function()
{
});
this.layout.mainRegion.show(this.views.loginView);
}
Now everything works fine, but it undoes part of the reason I created Controller's to begin with: I want them to own a View and its Models, create them once, and not have to destroy & recreate them every time I switch layouts.
Am I missing something? Is this not how I should be using Layouts? Isn't the whole point of Layouts and Regions that I can switch in & out Views at will?
Obviously I wouldn't jump back to LoginController/Layout often, but what about between a HomeController/Layout, CalendarController/Layout, SummaryController/Layout, etc... in a single page application I might switch between those 'top-level' layouts rather often and I would want the view to stay cached in the background.
I think your problem is that you don't maintain a single instance of the controller. The recommended way to handle routing/controllers (based on Brian Mann's videos) is like this
App.module('Routes', function (Routes, App, Backbone, Marionette, $, _) {
// straight out of the book...
var Router = Marionette.AppRouter.extend({
appRoutes: {
"home": "home",
"login": "login",
"calendar": "calendar"
}
});
var API = {
home: function () {
App.Controller.go_home();
},
login: function () {
App.Controller.go_login();
},
calendar: function () {
App.Controller.go_calendar();
}
};
App.addInitializer(function (options) {
var router = new Router({controller: API});
});
});
... and the controller:
App.module("Controller", function (Controller, App, Backbone, Marionette, $, _) {
App.Controller = {
go_home: function () {
var layout = new App.Views.Main();
layout.on('show', function () {
// attach views to subregions here...
var news = new App.Views.News();
layout.newsRegion.show(news);
});
App.mainRegion.show(layout);
},
go_login: function () {
....
},
go_calendar: function () {
....
}
};
});
I suspect your problem is the lazy-loaded controller...

Categories