Right way to unit test a component in EmberJS - unit-testing

I am trying to test a component in my Ember application unit tests, and until now all is good except that I am at a point where the assertions need its template to be rendered.
To do so normally one would call
var comp = App.SomeNamedComponent.create();
var comp.appendTo(App.rootElement);
But while this does create the base element of the component, it does not render its template. After a few research, I ended-up finding out that neither templateName nor template properties are set on the component. So I decided to set the templateName myself, but then it complains that A Component must have a parent view in order to yield..
I then decided to create another custom view in the test with a template using that component, but then I can't access the instance of the component...
I need to access the instance to make the assertions, and I need to have it's template rendered as some properties are calculated depending on the css of some elements in the template.

This is how I typically test a component when a container is not needed (specifically when the template and layout are provided to the component programmatically):
Ember.testing = true;
MyAwesomeComponent = Ember.Component.extend();
function createComponent(componentName, factory, options) {
if (typeof options.template === 'string') {
options.template = Ember.Handlebars.compile(options.template);
}
if (typeof options.layout === 'string') {
options.layout = Ember.Handlebars.compile(options.layout);
}
if (options.template && !options.layout) {
options.layout = options.template;
delete options.template;
}
var component = factory.create(options);
Ember.run(function(){
component.appendTo('#qunit-fixture');
});
return component;
}
module('component testing sample');
test('a component with template', function(){
var options = {layout: 'woot woot{{fullName}}'};
var component = createComponent('my-awesome', MyAwesomeComponent, options);
equal(component.$().text(), 'woot woot');
});
test('a component with custom options and a template', function(){
var options = {
fullName: 'Robert Jackson',
layout: '{{fullName}}'
};
var component = createComponent('my-awesome', MyAwesomeComponent, options);
equal(component.$().text(), 'Robert Jackson');
});
See an example JSBin.
If you need/want to be able to lookup the template you can use something like the following (which creates an isolated container):
Ember.testing = true;
MyAwesomeComponent = Ember.Component.extend();
function isolatedContainer() {
var container = new Ember.Container();
container.optionsForType('component', { singleton: false });
container.optionsForType('view', { singleton: false });
container.optionsForType('template', { instantiate: false });
container.optionsForType('helper', { instantiate: false });
return container;
}
function createComponent(componentName, factory, options) {
var fullName = 'component:' + componentName,
templateFullName = 'template:components/' + componentName;
container.register(fullName, factory);
if (container.has(templateFullName)) {
container.injection(fullName, 'layout', templateFullName);
}
var Component = container.lookupFactory(fullName),
component = Component.create(options);
Ember.run(function(){
component.appendTo('#qunit-fixture');
});
return component;
}
function registerTemplate(name, template){
if (typeof template !== 'function') {
template = Ember.Handlebars.compile(template);
}
container.register('template:' + name, template);
}
var container;
module('component testing sample', {
setup: function(){
container = isolatedContainer();
},
teardown: function(){
Ember.run(container, 'destroy');
}
});
test('a component with template', function(){
registerTemplate('components/my-awesome', 'woot woot{{fullName}}');
var component = createComponent('my-awesome', MyAwesomeComponent);
equal(component.$().text(), 'woot woot');
});
test('a component with custom options and a template', function(){
registerTemplate('components/my-awesome', '{{fullName}}');
var component = createComponent('my-awesome', MyAwesomeComponent, {fullName: 'Robert Jackson'});
equal(component.$().text(), 'Robert Jackson');
});
JSBin of the container version.

Related

Ember loading state not triggered on transitionTo

If I use a transitionTo on a route with a slow model hook, the loading.hbs state never gets triggered (I have loading.hbs files at all of the levels -- cluster, cluster.schedule and cluster.schedule.preview_grid). I tried renaming the one at cluster.schedule preview_grid-loading.hbs with no luck.
On the transitionTo, there is no model or model id passed in, just the route:
viewPreviewGrid: function() {
this.transitionTo('cluster.schedule.preview_grid');
},
I also have a loading action defined as follows:
loading(transition) {
var controller = this.controller;
if (!Ember.isNone(controller)) {
this.controller.reset();
}
transition.promise.finally(function() {
NProgress.done();
});
}
During the transitionTo call the page just stays on the previous route until the promises in the model hook resolve, and then it transitions to the other route. If I refresh the page, the loading state gets triggered just fine. Is this a known behaviour for transitionTo?
This is my model hook:
model: function (/*params*/) {
var socialProfile = this.modelFor('cluster.schedule').get('firstObject');
if (!socialProfile.get('isInstagram')){
throw new Error("Attempted to access preview with non-ig profile: " + socialProfile.get('id'));
}
var accessToken = socialProfile.get('token');
var self = this;
return Ember.RSVP.hash({
igPosts: new Ember.RSVP.Promise(function(resolve) {
self.getUsersRecentMedia(accessToken).then(function(response) {
var igPosts = Ember.A([]);
response.data.forEach(function(data) {
igPosts.pushObject(self.igPostFromResponse(data, socialProfile));
});
resolve(igPosts);
});
}),
posts: new Ember.RSVP.Promise(function(resolve) {
self.store.query('gram', { type: 'preview', social_profile_id: socialProfile.get('id'), limit: self.get('postLimit') }).then(function(grams) {
var filteredGrams = grams.filter(function(gram) {
return (gram.get('scheduledInFuture')) && (gram.belongsTo('socialProfile').id() === socialProfile.get('id')) && (gram.get('active'));
});
resolve(filteredGrams);
});
}),
igUser: new Ember.RSVP.Promise(function(resolve) {
self.getSelf(accessToken).then(function(response) {
resolve(self.igUserFromResponse(response.data, socialProfile));
});
})
});
},
You need to return true at the end of the loading() hook to tell Ember to go ahead and show the default loading route (loading.hbs).
loading(transition) {
var controller = this.controller;
if (!Ember.isNone(controller)) {
this.controller.reset();
}
transition.promise.finally(function() {
NProgress.done();
});
return true;
},

How to assert and test a ReactJS / Fluxible Component's state?

My app is a Fluxible / React application.
I have the following spec that attempts to test a LoginForm. Embedded components have been stubbed using rewire. I referenced http://fluxible.io/api/components.html#testing.
The first spec it("renders") passes. However, when I try to do more tests as shown in the commented code, the test fails.
I am unable to assert on LoginForm's state or trigger simulated events using TestUtils on the component. Are there any ways to do that?
import React from 'react/addons';;
import { createMockComponentContext } from 'fluxible/utils';
import createStore from 'fluxible/addons/createStore';
var rewire = require("rewire");
var rewireModule = require("../../helpers/rewire-module");
// stub inner components with LoginForm
// `rewire` instead of `require`
var LoginForm = rewire("../../../src/components/auth/login-form");
// Replace the required module with a stub component.
rewireModule(LoginForm, {
FormattedMessage: React.createClass({
render: function() { return <div />; }
}),
NavLink: React.createClass({
render: function() { return <div />; }
})
});
describe('LoginForm', function() {
var context;
var TestUtils;
var provideContext;
var connectToStores;
var MockIntlStore;
var MockAuthStore;
var noop = function(){};
var component;
beforeEach(function(){
MockIntlStore = createStore({
storeName: 'IntlStore',
getMessage: noop,
getState: function(){
return {}
}
});
MockAuthStore = createStore({
storeName: 'AuthStore'
});
context = createMockComponentContext({
stores: [MockIntlStore, MockAuthStore]
});
// React must be required after window is set
TestUtils = React.addons.TestUtils
provideContext = require('fluxible/addons/provideContext');
connectToStores = require('fluxible/addons/connectToStores');
// Wrap with context provider and store connector
LoginForm = provideContext(connectToStores(LoginForm, [MockIntlStore, MockAuthStore], function (stores) {
return {
};
}));
component = TestUtils.renderIntoDocument(
<LoginForm context={context} />
);
});
it("renders", function() {
var foundComponent = TestUtils.findRenderedDOMComponentWithClass(
component, 'login-form');
expect(foundComponent).toBeDefined();
});
// TODO fluxible wraps components so we cant reach the inner component to assert on state and trigger event handlers
// it("should have an initial state", function() {
// let initialState = {
// username: '',
// pass: ''
// }
// expect(component.state).toEqual(initialState);
// });
});
When you use provideContext and connectToStores, your component is wrapped. You have done it right to find the component using TestUtils. findRenderedDOMComponentWithClass, Simply use the foundComponent for test, that is what is being tested. i.e.
...
var foundComponent = TestUtils.findRenderedDOMComponentWithClass(
component, 'login-form');
expect(foundComponent.state).toEqual(initialState);
...
If you're still looking for a solution:
var tbg = React.createElement(x, { di: serviceLocator });
var renderer = React.addons.TestUtils.createRenderer();
var rtbg = renderer.render(tbg);
Then your method is here:
renderer._instance._instance.myMethod
Where myMethod is a function member of component x

Access view helper directly with Ember.Handlebars.helpers.view in ember 1.10.0

This code worked in ember 1.7.0:
var ViewTemplateHelper = Ember.Handlebars.makeBoundHelper(function(templateString, options) {
var dummy = Ember.View.extend({
classNames: ['view-template'],
template: Ember.Handlebars.compile(templateString)
});
var view = dummy.create();
if (options && options.hash) {
options.hash.content = template;
}
// Hack to prevent appendChild error
if (options.data.view._state === 'inDOM') {
options.data.view.rerender();
options.data.view.renderToBuffer();
}
return Ember.Handlebars.helpers.view.call(this, view, options); // undefined is not a function
});
export
default ViewTemplateHelper;
But now in ember 1.10.0 is gives the undefined is not a function error.
I tried to use Ember.Handlebars.helpers.view.helperFunction.call.
What do I miss?
The solution for this problem was not in a helper, but to use a component instead.
// components/content-element.js
import Ember from 'ember';
export
default Ember.Component.extend({
updateLayout: function() {
var store = this.container.lookup('store:main');
var projectId = this.get('project.id');
store.find('contentElement', {
key: this.get('key'),
project_id: projectId
}).then(function(contentElement) {
if (!Ember.isEmpty(contentElement.get('firstObject.value'))) {
var template = contentElement.get('firstObject.value');
var compiled = Ember.Handlebars.compile(template);
this.set('layout', compiled);
this.rerender();
}
}.bind(this));
}.observes('key').on('init')
});
We use the model contentElement for our templates. After setting the layout to the compiled Handlebars you have to run this.rerender();
For components you have to bind all to be used variables like this:
{{content-element key="name.of.element" project=project}}
In this case we use project in our dynamic template so we bound it. The key is used to get the right contentElement from the store.

Ember promise not resolved when I expect it to be

I have a custom component that expects data and not a promise, but I am unsure if they way that I am obtaining the data is the right way.
Is this the right way to do it?
component hbs
{{x-dropdown content=salutations valuePath="id" labelPath="description" action="selectSalutation"}}
Doesn't work
controller (this is the way I expect things to work
import Ember from 'ember';
export default Ember.Controller.extend({
bindSalutations: function() {
var self = this;
this.store.find('salutation').then(function(data) {
self.set('salutations', data);
});
}.on('init'),
components/x-dropdown.js
import Ember from 'ember';
export default Ember.Component.extend({
list: function() {
var content = this.get('content');
var valuePath = this.get('valuePath');
var labelPath = this.get('labelPath');
return content.map(function(item) {
return {
key: item[labelPath],
value: item[valuePath],
};
});
}.property('content'),
This works
controller
bindSalutations: function() {
var self = this;
this.store.find('salutation').then(function(data) {
self.set('salutations', data.get('content')); // pass the content instead of just the data
});
}.on('init'),
component
...
list: function() {
var content = this.get('content');
var valuePath = this.get('valuePath');
var labelPath = this.get('labelPath');
return content.map(function(item) {
return {
key: item._data[labelPath], // access through the _data attribute
value: item._data[valuePath],
};
});
}.property('content'),
Ember Data returns a Proxy Promise. This means you can use the promise as if it were a collection or model itself, as long as you aren't dependent on the property being completely populated when you use it. If you really want the promise resolved, you should probably be setting it up in the route.
If you want it on your controller, you can be lazy and do it like so:
Controller
salutations: function() {
this.store.find('salutation');
}.property(),
Component
...
list: function() {
var content = this.get('content'),
valuePath = this.get('valuePath'),
labelPath = this.get('labelPath');
return content.map(function(item) {
return {
key: item.get(labelPath),
value: item.get(valuePath),
};
});
}.property('content.[]'),
Template
{{x-dropdown content=salutations valuePath="id" labelPath="description" action="selectSalutation"}}
The real trick is to watch if the collection is changing. Hence you'll see I changed the property argument to content.[]

Ember - how to create and bind a Checkbox controller?

This question is linked to the answer given here.
Having a checkbox in a view
App.RoleCheckbox = Em.Checkbox.extend({
userRolesBinding: 'parentView.user.roles', // Points to the roles of the user
checked: function () {
var userRoles = this.get('userRoles');
return userRoles.contains(this.get('content'));
}.property('content', 'userRoles.#each'),
click: function (evt) {
//do something
var controller = this.get("controller");
controller.clicked(evt);
}
});
I would like that the click function calls the clicked function from the RoleCheckboxController:
App.RoleCheckboxController = Em.Controller.extend({
clicked: function(evt){
//Really do the thing
}
});
But this does not work. How could I fix this ?
JSFiddle: http://jsfiddle.net/3fMpD/
You can instantiate and associate the controller to the view using the correct naming conventions.
For example, this would associate the controller to the view:
// Instead of App.RoleCheckBoxController
App.ApplicationController = Ember.Controller.extend( /* ... */ );
App.ApplicationView = Ember.View.extend( /* .. */ );
JSFiddle: http://jsfiddle.net/YL5rQ/
#c4p is definitely right and the problem there is that your controller is not being created, and furthermore App.RoleCheckbox has no way of knowing it should use App.RoleCheckboxController as its controller.
I am not quite sure if this is the most Ember-y way of doing this but you can set the controller in the init (constructor function) of the Checkbox view, and then just make sure you send to the controller all the properties it needs to work with:
App.RoleCheckbox = Em.Checkbox.extend({
init: function(){
this._super();
this.set('controller', new App.RoleController());
},
userRolesBinding: 'parentView.user.roles',
checked: function () {
var userRoles = this.get('userRoles');
return userRoles.contains(this.get('content'));
}.property('content', 'userRoles.#each'),
click: function (evt) {
this.get('controller').send('clicked',this.checked, this.content);
}
});
And the controller's code (just changing the parameters used in the function);
App.RoleCheckboxController = Em.ObjectController.extend({
clicked: function(checked,role){
var userRoles = App.User.roles;
console.log("userRoles = ", userRoles);
console.log("role = ", role);
console.log("will be: ", !checked ? "removed" : "added");
if (checked) {
userRoles.pushObject(role);
} else {
userRoles.removeObject(role);
}
console.log("updated userRoles = ", userRoles);
}
});
Working fiddle here: http://jsfiddle.net/cfSwq/3/
Hope this helps!
Your App.RoleCheckboxController is never created. The way you have things set up there will only be an instance of ApplicationController.
You can move the logic back into the view's click event to have everything work:
App.RoleCheckbox = Em.Checkbox.extend({
userRolesBinding: 'parentView.user.roles',
checked: function () {
var userRoles = this.get('userRoles');
return userRoles.contains(this.get('content'));
}.property('content', 'userRoles.#each'),
click: function (evt) {
console.log("event triggered:", evt);
//var controller = this.get("controller");
//controller.clicked(evt);
var isPresent = this.get('checked'),
userRoles = this.get('userRoles'),
role = this.get('content');
console.log("userRoles = ", userRoles);
console.log("role = ", role);
console.log("will be: ", isPresent ? "removed" : "added");
if (!isPresent) {
userRoles.pushObject(role);
} else {
userRoles.removeObject(role);
}
}
});
Updated JSFiddle