How do I call triggerHandler with a specific fake 'event' parameter? - angularjs

I'm trying to test that on a link click 'preventDefault' is called. However, I have difficulties to replace a real 'event' object with one I can spy on:
Here is how I trigger the click event:
var e = jasmine.createSpyObj('e', [ 'preventDefault' ]);
$element.triggerHandler('click', [e]);
However, when the directive code is being run, the event element is not replaced with a fake one:
$element.on('click', function(event) {
console.log(event);
}
I tried different ways of adding a second parameter to triggerHandler - as an array, as an object, just some line, etc. Neither worked.. It is also not that many examples of triggerHandler together with additional parameters, so I feel a little bit lost...
Thanks in advance!

We also ran into this issue when we wanted to pass an event for testing that had a specific value for the which property.
As c0bra pointed out, the triggerHandler function creates its own dummy event object which it then passes on to the event handler. However, you can take advantage of the fact that the triggerHandler function will extend the dummy event with an event object passed in if there is a valued type property on the event:
https://github.com/angular/angular.js/blob/v1.6.5/src/jqLite.js#L1043
triggerHandler: function(element, event, extraParameters) {
...
// If a custom event was provided then extend our dummy event with it
if (event.type) {
dummyEvent = extend(dummyEvent, event);
}
...
}
After inspecting the Angular code, we found that when you call element.triggerHandler from your test, it will first call an anonymous function which then calls Angular's triggerHandler function. When Angular's triggerHandler function is called, it is passed the element as the first argument, then the first argument that you passed to triggerHandler in your test as the second argument, then your second argument that you passed to triggerHandler as the third etc. https://github.com/angular/angular.js/blob/v1.6.5/src/jqLite.js#L1068
What that then means is you can pass an object to triggerHandler in your test as the first argument, and that object's properties will be appended to the dummy event object (or overwritten if there was already a property on the dummy object with the same name), thus allowing you to pass specific values and add spies for testing.
Note that the event type will need to be passed to triggerHandler as the type property on your object so as to satisfy the if condition mentioned above. As an example, this is how we used triggerHandler in our test:
element.triggerHandler({
type: 'keydown',
which: 13
});

The docs say that triggerHandler() passes a dummy object to the handler: http://docs.angularjs.org/api/ng/function/angular.element
If you check the source, you can see that triggerHandler() creates its own event object, and then passes your second argument as the event data, not the actual event object:
https://github.com/angular/angular.js/blob/master/src/jqLite.js#L882
Relevant code:
var event = [{
preventDefault: noop,
stopPropagation: noop
}];
forEach(eventFns, function(fn) {
fn.apply(element, event.concat(eventData));
});
I've used jQuery's internal event simulator for creating my own events. That may work for you: http://wingkaiwan.com/2012/09/23/triggering-mouse-events-with-jquery-simulate-when-testing-in-javascript/

Related

Why angularjs throw 'Illegal invocation' when using promise construction?

I invoke some promise function:
return $http.post("anyCtrl").then(location.reload);
After that I have thrown exception in browser console 'Illegal invocation' in angular.
If I invoke:
return $http.post("anyCtrl").then(function(){location.reload()});
Everything is good.
I expected that all of my code snippets should be working.
Passing location.reload as an argument works more or less the same as reassigning it. If you reassign an object's method and it's not bound, that object's this will become the object that it's assigned to. For example:
const notlocation = {};
notlocation.reload = location.reload();
notlocation.reload(); // illegal invocation
You need to invoke reload from the location object. There are a couple of ways you can do this. One is to have the parentheses with the method call explicitly as you've done:
$http.post("anyCtrl").then(() => location.reload());
Another is to use the .bind and bind it to the object you want to invoke the method:
$http.post("anyCtrl").then(location.reload.bind(location));

triggering $onChanges for updated one way binding

I'm really happy with the "new" $onChanges method you can implement in a component's controller. However it only seems to be triggered when the bound variable is overwritten from outside my component, not (for instance) when an item is added to an existing array
It this intended behaviour or a bug? Is there another way of listening to updates to my input bindings, besides doing a $scope.$watch on it?
I'm using Angular 1.5.3
First TL;DR
For an array that is bounded via one-way binding, a watch expression is added that does not check for object equality but uses reference checking. This means that adding an element to the array will never fire the '$onChanges' method, since the watcher will never be 'dirty'.
I've created a plnkr that demonstrates this:
http://plnkr.co/edit/25pdLE?p=preview
Click the 'add vegetable in outer' and 'change array reference in outer' and look at the 'Number of $onChanges invocation'. It will only change with the latter button.
Complete explanation
To fully grasp what is going on, we should check the angular code base. When a '<' binding is found, the following code is used to set up a watch expression.
case '<':
if (!hasOwnProperty.call(attrs, attrName)) {
if (optional) break;
attrs[attrName] = void 0;
}
if (optional && !attrs[attrName]) break;
parentGet = $parse(attrs[attrName]);
destination[scopeName] = parentGet(scope);
// IMPORTANT PART //
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newParentValue) {
var oldValue = destination[scopeName];
recordChanges(scopeName, newParentValue, oldValue);
destination[scopeName] = newParentValue;
}, parentGet.literal);
// ------------- //
removeWatchCollection.push(removeWatch);
break;
The important part here is how the 'scope.$watch' expression is set up. The only parameters passed are the parsed expression and the listener function. The listener function is fired once the '$watch' is found dirty in the digest cycle. If it is fired, the listener will execute the 'recordChanges' method. This records an '$onChanges' callback task that will be executed in the '$postDigest' phase and notify all components that are listening for the '$onChanges' lifecycle hook to tell them if the value has changed.
What's important to keep in mind here, if the '$watcher' is never dirty, the '$onChanges' callback is not triggered. But even more importantly, by the way the '$watch' expression is created, it will NEVER be dirty, UNLESS the reference changes. If you wanted to check for equality between objects instead of reference, you should pass an extra third parameter that asks for this:
$watch: function(watchExp, listener, objectEquality, prettyPrintExpression)
As this is not the case here with the way the one way binding is set up, it will ALWAYS check for reference.
This means, if you add an element to an array, the reference is not changed. Meaning the '$watcher' will never be dirty, meaning the '$onChanges' method will not be called for changes to the array.
To demonstrate this, I've created a plnkr:
http://plnkr.co/edit/25pdLE?p=preview
It contains two components, outer and inner.
Outer has primitive string value that can be changed through an input box and an array that can be extended by adding an element or have its reference changed.
Inner has two one-way bounded variables, the value and the array. It listens for all changes.
this.$onChanges = setType;
function setType() {
console.log("called");
vm.callCounter++;
}
If you type into the input field, the '$onChanges' callback is fired every time. This is logical and expected, since a string is primitive so it cannot be compared by reference, meaning the '$watcher' will be dirty, and the '$onChanges' lifecycle hook fired.
If you click the 'Add vegetable in outer', it will execute the following code:
this.changeValueArray = function() {
vm.valueArray.push("tomato");
};
Here we just add a value to the existing bounded array. We're working by reference here, so the '$watcher' is not fired and there is no callback. You will not see the counter increment or the 'called' statement in your console.
Note: If you click the 'Add something to the array' inside the inner component, the array in outer component also changes. This is logical, since we are updating the exact same array by reference. So even though it is a one-way binding, the array can be updated from inside the inner component.
If you change the reference in the outer component by clicking 'Change array reference in outer', the '$onChanges' callback is fired as expected.
As to answer your question: Is this intended behaviour or a bug? I guess this is intended behaviour. Otherwise they would have given you the option to define your '<' binding in a way that it would check for object equality. You can always create an issue on github and just ask the question if you'd like.

Is a parameter passed to a view defined only inside a initialize function?

var plistview = new CollectionListView(productcollection);
in the above line i am passing a collection to a view.
in the CollectionListView i am trying to write a function that makes use of this collection passed. But the collection is defined only in the initialize function and we need to call the other function through the initialize(). But i want that function to be fired onClick by defining event.
categoryList: function(collection){
var length = collection.length;
alert(length);
},
the above function returns length if collection is passed to it from initialize. but
returns undefined when called independently from event
events : {
'click #category' : 'categoryList'
},
I want the value passed to be defined through out the view even if its fired independently.
Please shed some light.

find which event is triggred in backbone js event callback function

obj.on('evt_A evt_B evt_c', function(eventData){
console.log("Is it possible here to find which event is triggered. As this callback is registered for three events. This callback is like a central callback for all the events on this object.")
})
obj.trigger('evt_A evt_B evt_c', [{eventDataForevt_A}, {eventDataForevt_B, {eventDataForevt_C}}])
There is one way of doing it, which is having a property in eventDataForevt_<A||B||C> which says the name of the event. But is it possible to do this without modifying the eventDataForevt_<A||B||C> ?
Check out this fiddle: http://jsbin.com/ucevov/4/edit
It doesn't seem possible to detect the event name from your obj.on - note that evt_c in this._events is true even when evt_c is not called.
However there is a special "all" event which passes in the name of the calling event as the first parameter, so
obj.on("all", function(eventName, eventData) {
console.log(eventName);
});
should output the name of the event that was called.

In backbone "why the model created", what is the use of it? - below is my code

I took a small code from backbone home site, and consoled the function, in this example, sidebar.on('change:color'), takes the function. but it requires two parameter, one is abiously we need that is 'color', and we defining the element inside the function, still why we giving another parameter as 'model' here, what that parameter does?
if i remove that parameter send only the color, the function doesn't work at all... any one help me to understand this?
sample function here:
var Sidebar = Backbone.Model.extend({
promptColor : function(){
var cssColor = prompt('Please enter a css color');
this.set({color:cssColor});
}
});
window.sidebar = new Sidebar;
sidebar.on('change:color',function(model,color){ // what model parameter do here?
console.log(model);
$('#sidebar').css({
background:color
})
})
sidebar.set({color:'green'});
sidebar.promptColor();
when i console the model i got this:
d
_callbacks: Object
_changing: false
_escapedAttributes: Object
_pending: Object
_previousAttributes: Object
_silent: Object
attributes: Object
changed: Object
cid: "c1"
__proto__: x
It is possible that you want to know which model was affected.
Consider a case where you are listening to an event on a collection instead. Which model's color value was modified? The model parameter tells you this.
Also, consider a case where the same handler is listening to "change:color" on multiple models. Again, you might want to know which model sent the event.
Just like in other event-driven environments, the "sender" is always passed along with the event data. model, in this case, is the sender.
Backbone is a Javascript MVC framework. (Unlike standard MVC, Backbone doesn't have controllers, instead it has collections).
The model you are receiving is a standard model from the MVC paradigm. Model's are the underlying data structures that hold the data that the user is working with.
When you do
sidebar.on('change:color', function(model, color) {
// some code here
});
you are attaching an event handler to the sidebar model. Specifically, you are saying that when the color attribute on this model changes, call the function. Since this event can and will trigger at a later point in time, Backbone passes the event handler function two arguments: the first is the model on which the event fired, and the second is the attribute that changed.
The arguments are passed in a specific order, that is model is the first argument, and the changed attribute is the second. Thus if you omit the model argument from your event handler function, the passed in model gets assigned to color, and color doesn't get assigned to any argument.
Recommended reading:
More about MVC and models
More about backbone models

Resources