I am trying to unit test my first backbone.js application using sinon.js and jasmine.js.
In this particular test case, I used the sinon.js fakeServer method to return a dummy response with the following structure.
beforeEach( function(){
this.fixtures = {
Tasks:{
valid:{
"tasks":[
{
id: 4,
name:'Need to complete tests',
status: 0
},
{
id: 2,
name:'Need to complete tests',
status: 1
},
{
id: 3,
name:'Need to complete tests',
status: 2,
}
]
}
}
};
});
So when I actually call the fetch call in the below test case, it returns the 3 models correctly. In the parse method of the collection, I tried to remove the root 'tasks' key and just return the array of objects alone, which was mentioned in the backbone.js documentation. But when I do this, no models are getting added to the collection and the collection.length returns 0.
describe("it should make the correct request", function(){
beforeEach( function(){
this.server = sinon.fakeServer.create();
this.tasks = new T.Tasks();
this.server.respondWith('GET','/tasks', this.validResponse( this.fixtures.Tasks.valid) );
});
it("should add the models to the tasks collections", function(){
this.tasks.fetch();
this.server.respond();
expect( this.tasks.length ).toEqual( this.fixtures.Tasks.valid.tasks.length );
});
afterEach(function() {
this.server.restore();
});
});
Task Collection
T.Tasks = Backbone.Collection.extend({
model: T.Task,
url:"/tasks",
parse: function( resp, xhr ){
return resp["tasks"];
}
});
Can you please tell me what am I doing wrong here?
The problem with my code was in the validate method of the model and not the parse method of the collection. I was testing for the attributes even when they dont exist. The object that is sent to validate will not have all the attributes every time. For example, in a task model with id,title and status, where status is set as 0 by default, if I create a model like
var t = new Task({'title':'task title'});
t.save();
here, the validate method will only get {'title':'task title'} as a parameter to the validate method.
So it is important to add those conditions too in the validate method and when I added conditions to check for the presence of the particular attribute and also when it is not null or undefined, my tests started passing.
Related
I have a "box" route/controller as below;
export default Ember.Controller.extend({
initialized: false,
type: 'P',
status: 'done',
layouts: null,
toggleFltr: null,
gridVals: Ember.computed.alias('model.gridParas'),
gridParas: Ember.computed('myServerPars', function() {
this.set('gridVals.serverParas', this.get('myServerPars'));
this.filterCols();
if (!this.get('initialized')) {
this.toggleProperty('initialized');
} else {
Ember.run.scheduleOnce('afterRender', this, this.refreshBox);
}
return this.get('gridVals');
}),
filterCols: function()
{
this.set('gridVals.layout', this.get('layouts')[this.get('type')]);
},
myServerPars: function() {
// Code to set serverParas
return serverParas;
}.property('type', 'status', 'toggleFltr'),
refreshBox: function(){
// Code to trigger refresh grid
}
});
My route looks like;
export default Ember.Route.extend({
selectedRows: '',
selectedCount: 0,
rawResponse: {},
model: function() {
var compObj = {};
compObj.gridParas = this.get('gridParas');
return compObj;
},
activate: function() {
var self = this;
self.layouts = {};
var someData = {attr1:"I"};
var promise = this.doPost(someData, '/myService1', false); // Sync request (Is there some way I can make this work using "async")
promise.then(function(response) {
// Code to use response & set self.layouts
self.controllerFor(self.routeName).set('layouts', self.layouts);
});
},
gridParas: function() {
var self = this;
var returnObj = {};
returnObj.url = '/myService2';
returnObj.beforeLoadComplete = function(records) {
// Code to use response & set records
return records;
};
return returnObj;
}.property(),
actions: {
}
});
My template looks like
{{my-grid params=this.gridParas elementId='myGrid'}}
My doPost method looks like below;
doPost: function(postData, requestUrl, isAsync){
requestUrl = this.getURL(requestUrl);
isAsync = (isAsync == undefined) ? true : isAsync;
var promise = new Ember.RSVP.Promise(function(resolve, reject) {
return $.ajax({
// settings
}).success(resolve).error(reject);
});
return promise;
}
Given the above setup, I wanted to understand the flow/sequence of execution (i.e. for the different hooks).
I was trying to debug and it kept hopping from one class to another.
Also, 2 specific questions;
I was expecting the "activate" hook to be fired initially, but found out that is not the case. It first executes the "gridParas" hook
i.e. before the "activate" hook. Is it because of "gridParas"
specified in the template ?
When I do this.doPost() for /myService1, it has to be a "sync" request, else the flow of execution changes and I get an error.
Actually I want the code inside filterCols() controller i.e.
this.set('gridVals.layout', this.get('layouts')[this.get('type')]) to
be executed only after the response has been received from
/myService1. However, as of now, I have to use a "sync" request to do
that, otherwise with "async", the execution moves to filterCols() and
since I do not have the response yet, it throws an error.
Just to add, I am using Ember v 2.0
activate() on the route is triggered after the beforeModel, model and afterModel hooks... because those 3 hooks are considered the "validation phase" (which determines if the route will resolve at all). To be clear, this route hook has nothing to do with using gridParas in your template... it has everything to do with callling get('gridParas') within your model hook.
It is not clear to me where doPost() is connected to the rest of your code... however because it is returning a promise object you can tack on a then() which will allow you to essentially wait for the promise response and then use it in the rest of your code.
Simple Example:
this.doPost().then((theResponse) => {
this.doSomethingWith(theResponse);
});
If you can simplify your question to be more clear and concise, i may be able to provide more info
Generally at this level you should explain what you want to archive, and not just ask how it works, because I think you fight a lot against the framework!
But I take this out of your comment.
First, you don't need your doPost method! jQuerys $.ajax returns a thenable, that can be resolved to a Promise with Ember.RSVP.resolve!
Next: If you want to fetch data before actually rendering anything you should do this in the model hook!
I'm not sure if you want to fetch /service1, and then with the response you build a request to /service2, or if you can fetch both services independently and then show your data (your grid?) with the data of both services. So here are both ways:
If you can fetch both services independently do this in your routes model hook:
return Ember.RSVP.hash({
service1: Ember.RSVP.resolve($.ajax(/*your request to /service1 with all data and params, may use query-params!*/).then(data => {
return data; // extract the data you need, may transform the response, etc.
},
service2: Ember.RSVP.resolve($.ajax(/*your request to /service2 with all data and params, may use query-params!*/).then(data => {
return data; // extract the data you need, may transform the response, etc.
},
});
If you need the response of /service1 to fetch /service2 just do this in your model hook:
return Ember.RSVP.resolve($.ajax(/*/service1*/)).then(service1 => {
return Ember.RSVP.resolve($.ajax(/*/service2*/)).then(service2 => {
return {
service1,
service2
}; // this object will then be available as `model` on your controller
});
});
If this does not help you (and I really think this should fix your problems) please describe your Problem.
I'm trying to make a publishment statement to publish
ONLY the author(OP)'s profile avatar. I am thinking of grabbing the _id of the page. And from that page, I will grab the userId which is the author's _id and try to show the profile.
However, I have been very unsuccessful, and currently, I am using the following. Publishing EVERY user's profile avatar.
Publications.js
//Need to filter this to show only OP.
Meteor.publish("userPostAvatar", function() {
return Meteor.users.find( {} ,
{
fields: {'profile.avatar': 1}
})
});
Meteor.publish('singlePost', function(id) {
check(id, String);
return Posts.find(id);
});
Router.js
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return [
Meteor.subscribe('singlePost', this.params._id),
Meteor.subscribe('userStatus'),
Meteor.subscribe('userPostAvatar')
];
},
data: function() {
return Posts.findOne({_id:this.params._id});
}
});
You can do a simple join in the userPostAvatar publish function like this:
Meteor.publish('userPostAvatar', function(postId) {
check(postId, String);
var post = Posts.findOne(postId);
return Meteor.users.find(post.authorId, {fields: {profile: 1}});
});
This assumes posts have an authorId field - adjust as needed for your use case. Note three important things:
You will need to subscribe with this.params._id just as you did for singlePost.
The join is non-reactive. If the author changes, the avatar will not be republished. Given the general nature of posts I assume this isn't a problem.
I didn't publish the nested field profile.avatar on purpose because doing so can cause weird behavior on the client. See this question for more details.
I believe you can achieve this within the iron:router data context, by finding the post, associated author (whatever the field is), and then the subsequent user avatar. You can return an object to the iron:router data context. Then you can access post and avatar in the template as variables (so you might need to adjust the template output a little).
Publications.js
Meteor.publish("userPostAvatar", function() {
return Meteor.users.findOne( {} ,
{
fields: {'profile.avatar': 1}
})
});
Meteor.publish('singlePost', function(id) {
check(id, String);
return Posts.find(id);
});
Router.js
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return [
Meteor.subscribe('singlePost', this.params._id),
Meteor.subscribe('userStatus'),
Meteor.subscribe('userPostAvatar')
];
},
data: function() {
var post = Posts.findOne({_id: this.params._id});
var avatar = Users.findOne(post.authorId).profile.avatar;
return {
post: post,
avatar: avatar
};
}
});
Two problems with this method are that you could achieve the same thing with template helpers, and the user publication hasn't been limited to one user (I'm unsure how to do this unless we know the authorId within the waitOn, although maybe you could try moving the logic to there instead of the data context as my example shows).
I have a game that I know works properly with an ID per the code on the client side. For example if I were to use the below with {{game._id}} it works properly:
Template.gamePage.game = function() {
return GameCollection.findOne({current: true});
};
However, I am trying to gain access to the publications of 'submissions; only for the specific game ID. Console log below returns undefined.
router.js
this.route('gamePage', {
path: '/games/:_id?',
waitOn: function() {
console.log(this.params._id);
return [
Meteor.subscribe('randomQuestions', Random.id()),
Meteor.subscribe('submissions', this.params._id)
];
}
});
I suspect that params._id pulls from games/:_id, however, I would like it so that that it remains games/:_id? so that I do not have an unnecessary long address.
Any thoughts on why I am getting undefined for params._id
I think you have one button for to access a game, for example...
Tracker.autorun(function () {
Session.set('gameCurrent');
});
Template.gamePage.helpers({
allGames: function(){
return GameCollection.find({});
},
getCurrentGame:function(){
return Session.get('gameCurrent');
}
})
// with this action you access a the route with the id specified
Template.gamePage.events({
'click button#game' : function(event,template){
Session.set('gameCurrent',this._id);
Router.go('editEmail',{_id:this._id})
}
})
Remember that Session only works in the client.
In my express app, when the DELETE method below is called, the GET method is immediately called after and it's giving me an error in my angular code that says it is expected an object but got an array.
Why is my GET method being called when i'm explicitly doing res.send(204); in my DELETE method and how can I fix this?
Server console:
DELETE /notes/5357ff1d91340db03d000001 204 4ms
GET /notes 200 2ms - 2b
Express Note route
exports.get = function (db) {
return function (req, res) {
var collection = db.get('notes');
collection.find({}, {}, function (e, docs) {
res.send(docs);
});
};
};
exports.delete = function(db) {
return function(req, res) {
var note_id = req.params.id;
var collection = db.get('notes');
collection.remove(
{ _id: note_id },
function(err, doc) {
// If it failed, return error
if (err) {
res.send("There was a problem deleting that note from the database.");
} else {
console.log('were in delete success');
res.send(204);
}
}
);
}
}
app.js
var note = require('./routes/note.js');
app.get('/notes', note.get(db));
app.post('/notes', note.create(db));
app.put('/notes/:id', note.update(db));
app.delete('/notes/:id', note.delete(db));
angularjs controller
$scope.delete = function(note_id) {
var note = noteService.get();
note.$delete({id: note_id});
}
angularjs noteService
angular.module('express_example').factory('noteService',function($resource, SETTINGS) {
return $resource(SETTINGS.base + '/notes/:id', { id: '#id' },
{
//query: { method: 'GET', isArray: true },
//create: { method: 'POST', isArray: true },
update: { method: 'PUT' }
//delete: { method: 'DELETE', isArray: true }
});
});
** UPDATE **
To help paint the picture, here's the angular error i'm getting:
Error: [$resource:badcfg] Error in resource configuration. Expected response to contain an object but got an array http://errors.angularjs.org/1.2.16/$resource/badcfg?p0=object&p1=array
I'm assuming that i'm getting this error because my delete method is calling my get method (somehow) and the get method returns the entire collection.
Server side
You're removing an element from a collection in your delete function. This is done asynchronously and calling your callback when it's finished.
During this time, other requests are executed, this is why your GET request is executed before your DELETE request is finished.
The same happens in your get function, you're trying to find an element from a collection and this function is too asynchronous.
But this is server side only and it is fine, it should work this way, your problem is located client side.
Client side
If you want to delete your note after you got it, you will have to use a callback function in your angular controller which will be called only when you got your note (if you need help on that, show us your noteService angular code).
This is some basic javascript understanding problem, actions are often made asynchronously and you need callbacks to have an execution chain.
Maybe try doing something like this:
$scope.delete = function(note_id) {
var note = noteService.get({ id: note_id }, function()
{
note.$delete();
});
}
Your code doesn't make sense though, why is there a get in the $scope.delete? Why not do as simply as following:
$scope.delete = function(note_id) {
noteService.delete({ id: note_id });
}
Error
I think you get this error because of what your server sends in your exports.delete function. You're sending a string or no content at all when angular expects an object (a REST API never sends strings). You should send something like that:
res.send({
results: [],
errors: [
"Your error"
]
});
I'm writing a few tests for an Angular application, these are my first stab at unit tests for Angular using Jasmine. I'm having trouble structuring the test to cater for the various scenarios inside the function (namely the if statement and callbacks).
Here's my $scope function, which takes an Object as an argument, and if that object has an id, then it updates the object (as it'll already exist), otherwise it creates a new report and pushes to the backend using the CRUD service.
$scope.saveReport = function (report) {
if (report.id) {
CRUD.update(report, function (data) {
Notify.success($scope, 'Report updated!');
});
} else {
CRUD.create(report, function (data) {
$scope.report = data;
Notify.success($scope, 'Report successfully created!');
});
}
};
My test so far passes in a fake Object with an id so it'll trigger the CRUD.update method, which I then check is called.
describe('$scope.saveReport', function () {
var reports, testReport;
beforeEach(function () {
testReport = {
"id": "123456789",
"name": "test"
};
spyOn(CRUD, 'update');
$scope.saveReport(testReport);
});
it('should call CRUD factory and update', function () {
expect(CRUD.update).toHaveBeenCalledWith(testReport, jasmine.any(Function));
});
});
I understand Jasmine doesn't allow multiple spies, but I want to be able to somehow test for the if condition, and run a mock test for when the Object doesn't pass in an Object too:
describe('$scope.saveReport', function () {
var reports, testReport;
beforeEach(function () {
testReport = {
"id": "123456789",
"name": "test"
};
testReportNoId = {
"name": "test"
};
spyOn(CRUD, 'update');
spyOn(CRUD, 'create'); // TEST FOR CREATE (NoId)
spyOn(Notify, 'success');
$scope.saveReport(testReport);
$scope.saveReport(testReportNoId); // TEST FOR NO ID
});
it('should call CRUD factory and update', function () {
expect(CRUD.update).toHaveBeenCalledWith(testReport, jasmine.any(Function));
// UNSURE ON THIS PART TOO
});
});
I've read things about using the .andCallFake() method, but I could not see how this could work with my setup. Any help really appreciated.
It seems that you should decide on what you need to test first. If you want to test simply that update is called when id exists or create is called when it does not then you should just structure the it function with those conditions. The before each is the wrong place for some of those things.
it('should call CRUD factory and update', function () {
spyOn(CRUD, 'update');
$scope.saveReport(testReport);
expect(CRUD.update).toHaveBeenCalledWith(testReport, jasmine.any(Function));
});
it('should call CRUD create', function() {
spyOn(CRUD, 'create');
$scope.saveReport(testReportNoId); // TEST FOR NO ID
expect(CRUD.create).toHaveBeenCalledWith(testReport, jasmine.any(Function));
});
Only put things in the before each that you actually should do before each test.
Hope this helped!