I am unit testing my controller using mocha. My controller looks like:
AS.MyController = Ember.ObjectController.extend(Ember.Validations.Mixin, {
name: null,
description: null,
init: function () {
this._super();
this.get('store').find('something');
},
....
});
And my test begins like:
describe("MyControllerTest", function () {
//tried but didn't work
//delete AS.MyController.init;
var controller = AS.MyController.create();
.....
})
and the browser always throws error on "this.get('store')" call in init. I am not sure if I need to stub things out or there is a work around for it because my test case doesn't rely on store at all. In either case, I couldn't find much out there and would really appreciate any feedback.
Thanks, Dee
JSBIN : http://jsbin.com/aMASeq/3/
UPDATE :
There can be many ways to tackle this issue, but what I ended up doing is re-structuring the controller code a bit by putting all the function calls to store into separate actions and then in init I make calls to these action functions using this.send('actioName'). In my unit test, before instantiating the controller, I reopen the controller to modify these action functions(its easier to change action function than to change init function itself, when trying to change init I always got into some js error). Eg:
AS.MyController.reopen({actions: {setSomeActionThatUsesStore: function () {
//do something that doesn't involve using store
}}});
Controllers get access to the store from the container. You can create a mock container and instantiate the controller with it.
var mockContainer = new Ember.Container();
mockContainer.register('store:main', Ember.Object.extend({
find: function() { ... }
});
var controller = App.PostController.create({ container: mockContainer });
If you need access to the real store then you can just grab the controller from your App's container.
var controller = App.__container__.lookup('controller:post');
That will instantiate a PostController for you that has all of it's dependencies (such as store) wired together.
Related
I try to write my first unit test for a route.
project/files
actions: {
afterSave(savedFile){
// ... some code
let controller = this.controllerFor('project.files');
// ...
}
}
the test:
test('save file', function(assert) {
let route = this.subject();
console.log(route);
let project;
Ember.run(() => {
project = route.get('store').createRecord('project', {
id: '1',
name: 'test'
});
let afterSave = route.get('actions.afterSave');
afterSave(project);
});
assert.ok(true);
})
The problem that I am getting TypeError: Cannot read property 'controllerFor' of undefined.
It looks like this is undefined.
If you have a look at Testing Routes section from Ember Guides, you can see its suggestion is to separate the action and the function.
I can suggest it.
It uses send method of routes, such as: route.send('afterSave');
But if you only want to make run your code, call afterSave action from your test code such as: afterSave.bind(route)(project);. Ref: bind function (I don't suggest this. Also I don't suggest you to retrieve action such as: route.get('actions.afterSave'))
According to the blog-post for ember-data version 1.0.0-beta.16 the store can now be used as a service:
TweetComposerComponent = Ember.Component.extend({
store: Ember.inject.service()
});
However, I can't figure out how to do qunit unit tests on such a component. I've tried the following:
moduleForComponent('tweet-composer', {
needs: ['service:store']
});
and:
moduleForComponent('tweet-composer', {
needs: ['store:main']
});
And when I do the former I get an error Attempting to register an unknown factory: 'service:store' and if I do the latter then store is undefined.
Thoughts?
(I'm writing a ember-cli style app).
Update:
It seems there's an open issue for this in the ember-test-helpers repo.
While I'm waiting for this fix, I cooked up a helper that can work as a stop-gap measure (coffeescript):
`import TestModuleForComponent from 'ember-test-helpers/test-module-for-component'`
`import { createModule } from 'ember-qunit/qunit-module'`
# This assumes the last argument, the callbacks, is present, although it
# does support the description being an optional argument.
moduleForStoreComponent = ->
args = Array.prototype.slice.call arguments
callbacks = args[args.length-1]
# Wrap the original beforeEach callback in a modified version that
# also sets up the store for the test container.
originalSetup = callbacks.beforeEach
callbacks.beforeEach = ->
DS._setupContainer(#container)
originalSetup.call(#) if originalSetup
callbacks.store = ->
#container.lookup('store:main')
args.unshift TestModuleForComponent
createModule.apply #, args
`export default moduleForStoreComponent`
A unit test is a place where everything works perfectly except the code/component/unit that you are testing.
So, even the store should be assumed to be working perfectly (0 errors/bugs).
Something like this should work in your test:
moduleForComponent('tweet-composer', {
beforeEach: function() {
this.subject({
store: {/*empty object*/}
});
}
});
If parts of your tests depend on data retrieved from the store, you can do something like:
this.subject({
store: {
find: function() {
var mockedModel = Ember.Object.create({/*empty*/});
return mockedModel;
}
}
});
This is to preserve the status of "unit test", if you start including and registering other objects from your app you 'll be actually writing integration tests.
Note:
In general, looking up models directly in a component is an
anti-pattern, and you should prefer to pass in any model you need in
the template that included the component.
http://discuss.emberjs.com/t/should-ember-components-load-data/4218/2?u=givanse
I have a component that represent a map and after an action in my controller I want to call a method on the component to center the map. The code looks like this
App.PlacesController = Ember.Controller.extend({
actions : {
centerMap : function () {
// how to call from here to GoogleMapComponent.centerMap ??
}
}
});
App.GoogleMapComponent = Ember.Component.extend({
centerMap : function () {
}
});
template
{{google-map}}
<button {{action "centerMap"}}>Center Map</button>
I have found a workaround but I don't think this is the Ember way of doing this.
{{google-map viewName="mapView"}}
<button class="center-map">Center Map</button>
App.PlacesView = Ember.View.extend({
didInsertElement : function () {
this.$(".center-map").click(this.clickCenterMap.bind(this));
},
clickCenterMap : function () {
this.get("mapView").centerMap();
}
});
In Ember, views (Components are glorified views) know about their controller, but controllers do NOT know about views. This is by design (MVC) to keep things decoupled, and so you can have many views that are being "powered" by a single controller, and the controller is none the wiser. So when thinking about the relationship, changes can happen to a controller and a view will react to those changes. So, just to reiterate, you should never try to access a view/component from within a controller.
There are a few options I can think of when dealing with your example.
Make the button part of your component! Components are meant to handle user input, like button clicks, so you may want to consider making the button a part of the map component and handle clicks in the actions hash of your component. If this buttons is always going to accompany the map component, then I certainly recommend this approach.
You could have a boolean property on your controller like isCentered, and when the button is clicked it's set to true. In your component you can bind to that controller's property, and react whenever that property changes. It's a two-way binding so you can also change your locally bound property to false if the user moves the map, for example.
Controller:
...
isCentered: false,
actions: {
centerMap: {
this.set('isCentered', true);
}
}
...
Component:
...
isCenteredBinding: 'controller.isCentered',
onIsCenteredChange: function () {
//do your thing
}.observes('isCentered'),
...
Jeremy Green's solution can work if you mix in the Ember.Evented mixin into the controller (which adds the pub/sub trigger and on methods)
You can use on to have your component listen for an event from the controller, then you can use trigger in the controller to emit an event.
So in your component you might have something like this:
didInsertElement : function(){
this.get('controller').on('recenter', $.proxy(this.recenter, this));
},
recenter : function(){
this.get("mapView").centerMap()
}
And in your controller you could have :
actions : {
centerMap : function () {
this.trigger('recenter');
}
}
Bind a component property to the controller property in the template:
{{google-map componentProperty=controllerProperty}}
Then observe the component property in the component:
onChange: function () {
// Do your thing
}.observes('componentProperty')
Now every time controllerProperty is changed in the controller, onChange in the component will be called.
From this answer, second paragraph.
I think it's OK to have a reference in your controller to your component. It's true that your component encapsulates it's own behaviour, but public methods like reload etc. are perfectly fine.
My solution for this is to pass the current controller to the component and set a property on the controller within the component.
Example
template.hbs:
{{#component delegate=controller property="refComponent"}}
component.js:
init: function() {
this._super.apply(this, arguments);
if (this.get("delegate")) {
this.get('delegate').set(this.get("property") || "default", this);
}
}
Now in your controller you can simply get a reference to your component with this.get("refComponent").
Steffen
Inside of your component call:
var parentController = this.get('targetObject');
See: http://emberjs.com/api/classes/Ember.Component.html#property_targetObject
If I have directive like this
JS:
app.controller('MyController', function($scope) {
this.someMethod = function() {
};
});
app.directive('myDirective', function() {
return {
scope: true
link: function(scope, elem, attrs, controller) {
controller.someMethod();
}
controller: 'MyController',
}
});
I want to create a Jasmine spy to ensure that the link function called controller.someMethod, but this will not work:
Spec:
var elem = angular.element('<div my-directive></div>');
var scope = $rootScope.new();
$compile(elem)(scope);
var ctrl = elem.controller('myDirective');
spyOn(ctrl, 'someFunc').andCallThrough();
The spy is created too late, because the controller was instantiated and the link function called in the $compile statement.
What other ways are there for spying on something that happens in the link function? Is it possible to maybe instantiate the controller before hand and pass it into $compile?
From the AngularJS Developer Guide's page on Directives:
Best Practice: use controller when you want to expose an API to other directives. Otherwise use link.
I would suggest creating a service for whatever someMethod() does. Then you can mock and spy on that service. Otherwise you may have to look for some other sign that what you wanted to happen has actually happened.
I am trying to use observers to observe a change on my model after XHR. This is because the earlier approach of extending a fn and calling super is not allowed any more.
Running into this weird issue where my observer doesn't fire:
App = Ember.Application.create({
ready: function () {
console.log('Ember Application ready');
this.topCampaignsController = Ember.ArrayController.create({
content: null
});
App.TopCampaignsModel.create({
// Calling super is no longer allowed in object instances
//success: function () {
// this._super();
// App.topCampaignsController.set('content', this.get('data'));
//},
onDataChange: function () {
console.log('data property on the object changed');
App.topCampaignsController.set('content', this.get('data'));
}.observes('data')
});
}
});
App.TopCampaignsModel = Ember.Object.extend({
data: null,
// this will be actually called from an XHR request
success: function () {
this.set('data', [5,10]);
},
init: function () {
console.log('TopCampaignsModel created');
this.success();
console.log(this.get('data'));
}
});
Jsfiddle here: http://jsfiddle.net/gdXfN/26/
Not sure why the console doesn't log "data property on the object changed". Open to alternative approaches on how I can override the 'success' fn in my instance.
After this commit in December last year, it is no longer possible to set observers during object creation. This resulted in a huge performance win.
To set observers on create you need to use:
var Object = Object.createWithMixins({
changed: function() {
}.observes('data')
});
Here's a fiddle demonstrating this.
The API documentation should be updated accordingly, something I will do later on.
However, I don't advise you to do that, but instead set observers during object definition. The same result can be achieved: http://jsfiddle.net/teddyzeenny/gdXfN/32/
That said, there are two things you are doing that go against Ember concepts:
You should not create controller instances yourself, you should let Ember create them for you:
App.TopCampaignsController = Em.Controller.extend({ content: null });
When the App is initialized, Ember will generate the controller for you.
Models should not be aware of controller existence. Controllers should access models not the other way round.
Models and Controllers will interact together through routes.
For the last two points, you can watch the tutorial at http://emberjs.com/guides/ to see how the Application, Controllers, Models, and Routes should interact. Since you're not using
Ember Data, just ignore DS.Model and imagine an Ember.Object instead. The tutorial can give you a pretty good overview of how objects should interact.