Asynchronous Mocha Testing with Node and Backbone - backbone.js

I'm using beforeEach and afterEach hooks to instantiate a view with a model and a user for each it block in the test. But in the beforeEach hook of the first it block user.attributes is empty. In the second one, user.attributes has an attribute even though it was instantiated with an empty user. Why is the second user instantiated with an attribute?
describe("TestView", ->
beforeEach((done)->
#view = new TestView({model: new SomeModel(), user: new User()})
console.log(#view.user)
#view.render(done)
)
afterEach(->
#view?.remove()
)
it("should initialize and render without errors", ->
expect(#view).to.be.ok
)
it("should get the test class after being clicked", (done)->
expect($('.some-class').hasClass('test')).to.be.equal(false)
#view.$el.click()
setTimeout(->
expect($('.some-class').hasClass('test')).to.be.equal(true)
done()
, 100) # give it 100ms to let the event handlers do their things
)
)

console.log() gives you an inspector for an object in the browser's Javascript Console, but the inspector is not immediately populated with all current data. By the time you can expand the user object in the console, the click has already happened.
You could get a more static snapshot to inspect by saying
console.log(user.clone())
or even
console.log(JSON.parse(JSON.stringify(user))
or you could just log the specific property you are interested in:
console.log(user.attribute)

Related

How do you test a Backbone Marionnette method inside of onShow in Jasmine?

I'm trying to test a view in Backbone Marionette but the onShow() never gets called so I can't test a method that is being called in that method.
views/test.coffee
onShow: () ->
debugger # this never happens when I run the Jasmine tests
alert "HI"
spec/javascripts/views/test_spec.coffee
describe 'a test', ->
beforeEach ->
#view = new window.TestView
#view.render()
it "does something", ->
# not important
onShow() generally only gets called when you show it inside a region. There a two options you have for testing.
1) call onShow manually after render:
#view.render();
#view.onShow();
2) show the view inside a region:
You can just make a new region inside your test file, just add a detached DOM element if you don't need to use the DOM, otherwise you can just make an element and put it in the DOM.
Sidenote*** I don't know CoffeeScript, so the following might not be syntactically correct!
beforeEach ->
#view = new window.TestView
#testRegion = new Backbone.Marionette.Region({el: document.createElement('div')})
#testRegion.show(#view)
You can also trigger onShow. Sorry no CoffeeScript.
view.triggerMethod("show");

How come Angular doesn't update with scope here?

I'm pretty new to Angular and I'm using firebase as my backend. I was hoping someone could debug this issue. When I first go to my page www.mywebsite.com/#defaultHash the data doesn't load into the DOM, it does after visiting another hash link and coming back though.
My controller is like this:
/* initialize data */
var fb = new Firebase('https://asdf.firebaseio.com/');
/* set data to automatically update on change */
fb.on('value', function(snapshot) {
var data = snapshot.val();
$scope.propertyConfiguration = data.products;
console.log($scope.propertyConfiguration);
console.log("Data retrieved");
});
/* save data on button submit */
$scope.saveConfigs = function(){
var setFBref = new Firebase('https://asdf.firebaseio.com/products');
setFBref.update($scope.propertyConfiguration);
console.log("configurations saved!");
};
I have 3 hash routes say "Shared", "Registration", and "Home" with otherwise.redirectTo set to "Shared".(They all use this controller) Here's the error that occurs: (all "links" are href="#hashWhereever")
1) Go to website.com/#Shared or just refresh. Console logs $scope.propertyConfiguration and "Data Retrieved". DOM shows nothing.
2) Click to website.com/#Registration, console logs $scope data properly, DOM is loaded correctly.
3) Click back to website.com/#Shared, console logs $scope data properly yet this time DOM loads correctly.
4) Refresh currently correctly loaded website.com/#Shared. DOM elements disappear.
Since $scope.data is correct in all the cases here, shouldn't Angular make sure the DOM reflects the model properly? Why is it that the DOM loads correctly only when I am clicking to the page from another link.
I can "fix" it by adding window.location.hash = "Shared" but it throws a huge amount of errors in the console.
FIXED:(sorta)
The function $scope.$apply() forces the view to sync with the model. I'd answer this question myself and close it but I'm still wondering why the view doesn't load correctly when I correctly assign a value to $scope. If Angular's "dirty checking" checks whenever there is a possibility the model has changed, doesn't assigning a value to $scope overqualify?
Angular has no way to know you've assigned a value to $scope.variable. There's no magic here. When you run a directive (ng-click/ng-submit) or Angular internal functions, they all call $apply() and trigger a digest (a check of the dirty flags and update routine).
A possibly safer approach than $apply would be to use $timeout. Currently, if you call a write op in Firebase, it could synchronously trigger an event listener (child_added, child_changed, value, etc). This could cause you to call $apply while still within a $apply scope. If you do this, an Error is thrown. $timeout bypasses this.
See this SO Question for a bit more on the topic of digest and $timeout.
This doc in the Angular Developer Guide covers how compile works; very great background read for any serious Angular dev.
Also, you can save yourself a good deal of energy by using the official Firebase bindings for Angular, which already take all of these implementation details into account.
Vaguely Related Note: In the not-too-distant future, Angular will be able to take advantage of Object.observe magic to handle these updates.

Backbone js, showing undefined in console

Im writing a backbone application where the User model has to be fetched with the data of the logged in user. In my template i need to display user information(Username, Name etc). After compiling template error message is been logged in console "Uncaught ReferenceError: username is not defined". The Ajax request is also completed successfully. What might be the problem?
Im linking my js files.
Link to js files
Even though your AJAX request succeeds, it most likely hasn't succeeded yet by the time you execute the underscore template. You call render immediately after fetch, which is an asynchronous operation. When this line of code executes, model.toJSON() call most likely returns an empty object.
You should move the fetch call to your view's initialize function like this:
initialize: function() {
_.bindAll(this);
this.model.fetch({success:this.render});
}
This will postpone the rendering until the model is successfully fetched.

Backbone.js event handler triggers multiple times

I have a View called Form that renders either a form to edit a list, or the list itself, depending on what argument is passed to render. I've added event handlers so that the show/edit mode can be toggled. I've taken this out from the code below to keep it simple, but this just gives a bit of context to what the View does in context.
I can instantiate this Form view as a child in another view that requires a form, or the list to be rendered, which I've done in the New view, where it would be rendered as a form.
When I need to save, I call the form:save event, which triggers a routine in the Form view that saves the form, I've just made it call a console.log here to show it works. In my code, I call form:save through an $('a#submit').click binding which binds to navigation buttons that are inserted by an ApplicationView (but I don't think that matters for the purposes of this question.)
Lets say I navigate away from the New view, and I go back to it a number of times. When I hit save, the method runs the number of times I have instantiated and rendered a new Form view.
So far:
I've tried doing unbind() and remove() in a close method on the Form view from the New view with no luck.
I think I may have problems with scoping, but I'm unsure.
I know this isn't related to my navigation bindings.
I think this may be to do with zombie views.
Any pointers to make it only run once?
App.Views.New = Support.CompositeView.extend
initialize: (options) ->
_.bindAll this, 'render'
#model = new App.Models.Item()
render: ->
self = this
form = new App.Views.Form model: #model, collection: #collection
#$el.append form.render().el
setTimeout (->
$('a#submit').click (e) ->
e.preventDefault()
App.eventHandler.trigger 'form:save'
), 0
this
App.Views.Form = Support.CompositeView.extend
initialize: ->
_.bindAll this, 'render', 'save'
App.eventHandler.on 'form:save', #save
render: ->
self = this
# RENDER TEMPLATE HERE
this
save: ->
console.log 'form saved'
I believe your issue is that you are creating a new view each time you want to render the form, but you aren't getting rid of your old view. What you can do is either destroy your old view, or keep a reference to it and instead of creating a new view each time, just pass in the model to the existing view and refresh/rerender the display

testing backbone.js view events with jasmine

I'm trying to implement view tests for a Coffeescript implementation of the ubiquitous backbone.js 'todo' example (see github.com/rsim/backbone_coffeescript_demo.)
My jasmine tests of the above demo work pretty well, except for view events. I expect I am stuck on one or both of the following i) I do not understand the event binding in the view code, ii) I do not understand how to properly set up the Jasmine test of the view code events.
Here is an example of the 'edit' event...
class TodoApp.TodoView extends Backbone.View
tagName: "li"
template: TodoApp.template '#item-template'
events:
"dblclick div.todo-content" : "edit"
...
initialize: ->
_.bindAll this, 'render', 'close'
#model.bind 'change', #render
#model.bind 'destroy', => #remove()
render: ->
$(#el).html #template #model.toJSON()
#setContent()
this
edit: ->
$(#el).addClass "editing"
#input.focus()
...
...now here's a test of whether focus was gained upon double clicking:
describe "edit state", ->
li = null
beforeEach ->
setFixtures('<ul id="todo-list"></ul>')
model = new Backbone.Model id: 1, content: todoValue, done: false
view = new TodoApp.TodoView model: model, template: readFixtures("_item_template.html")
$("ul#todo-list").append(view.render().el)
li = $('ul#todo-list li:first')
target = li.find('div.todo-content')
expect(target).toExist()
target.trigger('dblclick') # here's the event!
it "input takes focus", ->
expect(li.find('.todo-input').is(':focus')).toBe(true)
The expectation on neither i) the spy nor ii) the focus is met.
Is there a peculiarity to testing backbone.js event code about which I should be aware in Jasmine?
you're spying on the view's edit method. this replaces the method with a spy object, which means the actual edit method won't get called. therefore, you're #input.focus will never fire.
since you want the test to actually call your edit method, i would remove the spy for it.
side note: don't call expect methods in your beforeEach. if you truly need to set an expectation on those, create an it block for them.
I'm not great with coffescript so I might be missing something but where are you setting up your spy?
In order to test event calling you may need to refresh the view's events once you've set up the spy.
spyOn(view, 'edit');
view.delegateEvents();
target.trigger('dblclick');
it("should call edit when target is double clicked", function() {
expect(view.edit).toHaveBeenCalled()
});
The issue with this is that Backbone.View events object is using event delegation. For the events to be able to be called work the element has to be part of DOM, you can do this by doing something like $('body').append(someView.el) in your beforeEach. Personally, I try not to test if Backbone is correctly setting the events and triggering clicks manually, is more practical for unit tests to call the callback handlers directly avoiding the DOM completely which can slow down your tests a lot.
For :focus is the same problem, there has to be an element in the DOM so that jQuery can tell if an element is focused. In this case it's better to set some state as part of your component and not checking for state via querying the DOM, e.g.: someView.hasFocus === true. Alternatively you can spy on the elements focus implementation and check if it was called.
I did not write my test in coffeescript, but I did have the same problem, so I hope you will forgive me for answering in javadcript. I ended up breaking down your test into two different tests. First, I tested if calling the view's edit function set the focus on the input box. After that, I tested whether the edit was called when the label was double-clicked, and have not yet gotten that test to pass. But here's how I tested if the edit function worked.
describe ("A todo item view", function() {
var my_model;
var todo_view;
beforeEach(function() {
my_model = new Todo({content:"todo value", done:false});
todo_view = new TodoView({model:my_model});
});
it("should set the focus on the input box when the edit function is called", function(){
$('body').append( todo_view.$el ); //append the view to Specrunner.html
todo_view.edit(); //call the view's edit function
var focus= document.activeElement; //finds what element on the page has focus
expect(focus).toBe('.todo-input'); //jasmine-jquery matcher checks if focused element has the .todo-input class
});
Something that might be causing problems is that your model And your view are declared inside beforeEach. Declaring them inside beforeEach means they only exist inside beforeEach's scope, and no longer exist when you run your it.
Also, does setFixtures do what you think it does? The focus cannot be set on an element that is not part of the DOM tree, so I appended the view's el to the body of the jasmine spec itself. (I'm using the HTML specrunner, not the command-line version) That makes it part of the dom tree and therefore allows it to have focus, and also makes whether it has focus testable.

Resources