Angular 8 Jasmine/Karma spyOn behavior questions - angularjs

I have a directive with custom debounce decorators. The directive is a fairly simple that listen on "scroll" event.
export class MyScrollDirective {
#Output() scrolled = new EventEmitter<any>();
#HostListener('scroll', ['$event'])
//debouce is added to prevent multiple call when scroll reach the end
//in the if statement below
#myDebounce(300)
onScroll(event) {
this.scrolled.emit(null);
}
}
In my test, I have an assumption that this.scrolled.emit() will be called whenever onScroll is called. Obviously this is not the case.
With the debounce, it looks like onScroll can be called multiple times from spyOn while this.scrolled only emit once. For example, if I trigger scroll events 2 times in my test within 300ms intervals, the onScroll is called 2 time while the this.scrolled has only 1 emit. The 1 emit is of course desired behavior, but how come onScroll is called twice. When the app is actually running in browser - instead of tests - the onScroll is actually called only once.
Why is this?!
The test detail can be view at StackBliz.io

This is a very interesting question!
In order to get a better understanding of how things work, I'd recommend opening this forked StackBlitz and place these breakpoints:
jasmine.js
calls.push(context); - line 2162
{ - line 5972
myDebounceDecorator.ts
descriptor.value - line 16
var params = []; - line 18
dummy.component.spec.ts
vsCss.triggerEventHandler(...) - line 36
vsCss.triggerEventHandler(...) - line 40
Note: I've used Firefox Dev Tools
This is what happens after you refresh the app:
descriptor.value; is reached; this means that the spy will be applied on the newly created function:
descriptor.value = function () { /* this will be spied */ }
vsCss.triggerEventHandle (line 36) is reached
var callData = { is reached, because of the spy.
calls.push(context); is reached, which means the spy(onScroll) was called
var params = []; is reached, because the spy was defined with callThrough(), which means that the original implementation will be used
vsCss.triggerEventHandler (line 40) is reached
var callData = { is reached, because of the spy
calls.push(context); is reached again; if we were to hover over calls, we'd see that it already has an element(from the previous vsCss.triggerEventHandle), hence the calls' length will be 2
var params = []; - the original implementation is used
var callData = { is reached; this time, it's the spy what we used on the Subject(i.e EventEmitter); we can be sure of this by hovering over this from object: this
calls.push(context); - calls belongs to the Subject's spied method
var callData = { is reached; this time, the spy belongs to onDummy method
calls.push(context); - onDummy's spy

Related

How can I test a method from third party library?

I am using a 3rd party library for logging some events, setup in my component:
In component componentDidMount:
this.evenStream = new thirdPartyLib();
this.evenStream.setArgs(args);
this.evenStream.addQueue('the'); // should be 'the-spell' for misspell page
const event1 = new Event1();
const event2 = new Event2();
this.evenStream.addQueue(event1); // adds events in sequence in queue
this.evenStream.addQueue(event2); // adds events in sequence in queue
this.evenStream.reportEvents(); // reports events previously added to queue, once reported the queue is cleared
In test:
this.wrapper = mount(<component />); // looks good
this.evenStreamInstance = this.wrapper.instance().evenStream; //returns properties of evenStream
this.eventStub = sinon.stub(this.evenStreamInstance, 'reportEvents');
expect(this.wrapper).to.be.ok; // looks good
expect(this.evenStreamInstance).to.exist; // looks good
expect(this.eventStub).to.have.been.called; // returns error
AssertionError: expected reportEvents to have been called at least once, but it was never called
I am sure the events are being logged correctly, I've checked the stream and all looks good, am not sure how to test it. Ideally I would like to test what all events are added to addQueue and then get reported but I am not able to capture that information.
Any help is appreciated. Thanks.
Update
I tried again as per this post with stub, but still getting same error
const eventStub = sinon.stub(new thirdPartyLib());
this.wrapper = mount(<component evenStream={eventStub} />); //looks good
this.evenStreamInstance = this.wrapper.instance().evenStream; //returns properties of evenStream
this.evenStreamInstance.reportEvents = sinon.spy(this.evenStreamInstance.reportEvents);
expect(this.wrapper).to.be.ok; // looks good
expect(this.evenStreamInstance).to.exist; //looks good
this.wrapper.update();
expect(this.evenStreamInstance.reportEvents).to.have.been.calledOnce; // gives error
AssertionError: expected reportEvents to have been called exactly once, but it was called 0 times

$scope variable is undefined when it is set inside a function

I have the following example code in my learning app. The service does his job and pulls some data out of a page with json code generated by php, so far so good.
service:
(function() {
'use strict';
angular
.module('app.data')
.service('DashboardService', DashboardService);
DashboardService.$inject = ['$http'];
function DashboardService($http) {
this.getFormules = getFormules;
////////////////
function getFormules(onReady, onError) {
var formJson = 'server/php/get-formules.php',
formURL = formJson + '?v=' + (new Date().getTime()); // Disables cash
onError = onError || function() { alert('Failure loading menu'); };
$http
.get(formURL)
.then(onReady, onError);
}
}
})();
Then i call the getFormules function in my controller and put all the data inside my $scope.formuleItems and test if everything succeeded and 'o no'... $scope.formuleItems = undefined! - Strange because my view is showing data?
part of the controller:
dataLoader.getFormules(function (items){
$scope.formuleItems = items.data;
});
console.log('+++++++++++++++++', $scope.formuleItems); // gives undefined
The first thing i did was search around on stackoverflow to look if someone else had the same issue, and there was: Undefined variable inside controller function.
I know there are some walkarounds for this, i've done my own research, but something tells me that this (see example below) isn't the best way to solve this problem.
solution one: put $watch inside of the controller
$scope.$watch('formuleItems', function(checkValue) {
if (checkValue !== undefined) {
//put the code in here!
}
}
or even:
if($scope.formuleItems != null) {}
The rest of the controller is relying on $scope.formuleItems. Do i really have to put everything into that $watch or if? Can i fix this with a promise? I never did that before so some help would be appreciated.
The code in your callback
function (items){
$scope.formuleItems = items.data;
}
is evaluated asynchronously. That means you first fire the request, then javascript keeps on executing your lines of code, hence performs
console.log('+++++++++++++++++', $scope.formuleItems); // gives undefined
At this point the callback was not invoked yet, because this takes some time and can happen at any point. The execution is not stopped for this.
Therefore the value of $scope.formuleItems is still undefined, of course.
After that - at some not defined time in the future (probably a few milliseconds later) the callback will be invoked and the value of $scope.formuleItems will be changed. You have to log the value INSIDE of your callback-function.
You urgently have to understand this concept if you want to succeed in JavaScript, because this happens over and over again :)

Protractor - Page Object is not updating when the DOM elements are changed

I am testing a SPA built with angular.js and im using Page Objects pattern to write my tests. In the app we have a number of lists that will be updated. For example there is a list of attachments that will update when ever attachments are added/removed. To add an attachment we have a modal window and when we upload a file and click ok. The file uploads and the lists update.
I have written 2 page objects, one for the upload modal window and the other one for the preview of the attachment list. In my tests i first get the current count of the attachments then i click on a button to activate the modal window and attach the file. Then i take another count of the attachments in the preview page and compare it to be incremented by 1. But the tests fails. The page object is not updated and it still shows attachment count as 2.
Test
it('Should attach a file when a file is selected and OK button is pressed.', function () {
var currentFileCount = viewMeetingTabPage.getMeetingAttachmentCount();
viewMeetingPage.clickAddAttachmentButton();
addAttchmentPage.attachFile();
addAttchmentPage.clickConfimAttachFileButton();
currentFileCount.then(function (curCount) {
viewMeetingTabPage.getMeetingAttachmentCount().then(function (newCount) {
expect(newCount).toBe(curCount + 1);
//expect(viewMeetingTabPage.getMeetingAttachmentName()).toBe('test-file.pdf');
});
});
});
ViewMeetingTabPage
this.getMeetingAttchments = function () {
return element.all(by.repeater('attachment in meeting.AttachmentViewModelList track by $index'));
};
this.getMeetingAttachmentCount = function () {
return this.getMeetingAttchments().count();
};
What i need to have is to somehow update the page object after i upload the file. How can i do that.
This is how the control-flow works. Executing the code for the test queues a bunch of promises. They get resolved in the order they were added to the flow while each of them waits for the previous to finish.
it('Should attach a file when a file is selected and OK button is pressed.', function () {
# Queues the count promise
var currentFileCount = viewMeetingTabPage.getMeetingAttachmentCount();
# Queues some other promises that would start
# to get executed once the one above finishes
viewMeetingPage.clickAddAttachmentButton();
addAttchmentPage.attachFile();
addAttchmentPage.clickConfimAttachFileButton();
# This piece of code branches-off the control-flow
# and gets executed immediately after currentFileCount is resolved
# i.e. before the clickAddAttachmentButton
currentFileCount.then(function (curCount) {
# That's why newCount equals curCount,
# they are counting the same number of elements
# since nothing changed in the meantime
viewMeetingTabPage.getMeetingAttachmentCount().then(function (newCount) {
expect(newCount).toBe(curCount + 1);
//expect(viewMeetingTabPage.getMeetingAttachmentName()).toBe('test-file.pdf');
});
});
});
currentFileCount could be considered a setup phase for the test so you can extract it to a beforeEach block:
var initialFileCount;
beforeEach(function() {
viewMeetingTabPage.getMeetingAttachmentCount().then(function(count) {
initialFileCount = count;
});
});
it('Should attach a file when a file is selected and OK button is pressed.', function () {
viewMeetingPage.clickAddAttachmentButton();
addAttchmentPage.attachFile();
addAttchmentPage.clickConfimAttachFileButton();
expect(viewMeetingTabPage.getMeetingAttachmentCount()).toBe(initialFileCount + 1);
});
Since protractor patches jasmine to wait between the test-blocks for the control-flow to empty, this would probably work.
Keep in mind expect is also patched to handle promises so you don't need to place it in a then.
UPDATE:
Actually, you shouldn't need the beforeEach above, it is supposed to work like that too:
var initialFileCount;
it('Should attach a file when a file is selected and OK button is pressed.', function () {
viewMeetingTabPage.getMeetingAttachmentCount().then(function(count) {
initialFileCount = count;
});
viewMeetingPage.clickAddAttachmentButton();
addAttchmentPage.attachFile();
addAttchmentPage.clickConfimAttachFileButton();
expect(viewMeetingTabPage.getMeetingAttachmentCount()).toBe(initialFileCount + 1);
});
It's called framing in the WebDriverJS User’s Guide.

Test Marionette vent.on Listener

I have a Layout that has a vent.on call in it that gets setup on model initiation.
initialize: () ->
App.vent.on("deciseconds:updated", this.updateDVR)
updateDVR: () ->
console.log 'this updateDVR is called'
I want to make sure this.updateDVR is hooked up correctly in my app. In my tests I have this:
beforeEach ->
this.showDvr = new Arc.DvrApp.Show.DvrView()
spyOn(this.showDvr, 'updateDVR')
it "calls updateDvr when new data comes in", ->
Arc.vent.trigger("deciseconds:updated")
expect(this.showDvr.updateDVR).toHaveBeenCalled()
This spec is failing, but when I check my log I am seeing this updateDVR is called, the line I was logging in the updateDVR function. So I know the function is being called.
It I directly call updateDVR, the spec passes:
it "calls updateDvr when new data comes in", ->
this.showDVR.updateDVR()
expect(this.showDvr.updateDVR).toHaveBeenCalled()
I thought the vent might be treated as an asynchronous function, so I tried waiting a few seconds before the expect clause to see if that would work, but it didn't:
it "calls updateDvr when new data comes in", ->
Arc.vent.trigger("deciseconds:updated")
setTimeout(myCheck, 3000)
myCheck = () ->
expect(this.showDvr.updateDVR).toHaveBeenCalled()
The call to App.vent.on in your initialize function passes a reference to the view instance's this.updateDVR function -- and that happens right before you spyOn(this.showDvr, ...) in the test's beforeEach. So when you trigger the event, the trigger calls that retained reference to the actual updateDVR function, not the spy.
You should be able to fix it by passing a callback function to the App.vent.on call, like so (sorry for the javascript, I'm not a coffeescripter!):
initialize: function () {
var that = this;
// hold onto the callback so we can unregister it in App.vent.off, too
this.onUpdateCb = function() { that.updateDVR() };
App.vent.on("deciseconds:updated", this.onUpdateCb );
// if this is a view, stop listening for App.vent when the view closes
this.listenTo(this, 'close', function() { App.vent.off("deciseconds:updated", this.onUpdateCb ) } );
}
That will make the event handler look up the symbol named "updateDVR" at the time the event is triggered, & it will call your spy as your test expects.
Edit: Updated to retain this.onUpdateCb so we can unregister the listener on close.

Backbone, Marionette, Jasmine: How to test jQuery deferred event

I'm very new to Jasmine and Marionette and looking for some help on how to test and even just the proper way to think about testing my application. Any pointers are welcome.
I have a Marionette Controller that I use to fetch my model, instantiate my views and render them. I use a method found at the bottom of this page so that the model is fetched before the view is rendered: https://github.com/marionettejs/backbone.marionette/blob/master/upgradeGuide.md#marionetteasync-is-no-longer-supported.
My controller method to fetch the model and display the view looks like so:
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
$.when(promise).then(_.bind(this.showContentView, this));
},
As you can see, it calls the showContentView after the model is fetched. That method is here:
showContentView: function(model){
App.views.Body = new bodyView({
model: App.models.Case
});
App.views.Body.on('case:update', this.submitCase, this);
// this.layout is defined in the controller's initialize function
this.layout.content.show(App.views.Body);
},
What is the proper way to test this functionality? I'd like to test the calling of the showContentView function after the completion of the promise. How should I break up the specs for this?
Thanks.
First, spy on your showContentView method and assert it has been called:
it('showCaseById', function (done) {
var controller = new Controller();
spyOn(controller, 'showContentView');
controller.showCaseById('foo');
expect(controller.showContentView).toHaveBeenCalledWith(jasmine.any(caseModel));
});
Secondly, I would recommend you stub out the call to fetch() so you don't hit the network, but it's starting to get a bit hairy now:
function caseModel() {
this.fetch = function () {
// return a promise here that resolves to a known value, e.g. 'blah'
};
}
Now, you can have a slightly stronger assertion, but this is a bit shonky because you're fiddling around with internals of your dependencies:
expect(controller.showContentView).toHaveBeenCalledWith('blah');
By overriding caseModel, when your controller method goes to create one, it gets your new version instead of the old one, and you can control the implementation of the new one just for this test.
There are ways to make this code more testable, but as it seems you're just starting out with testing I won't go into it all. You'll certainly find out those things for yourself as you do more testing.
First, it's important to understand that _.bind(fn, context) doesn't actually call fn. Instead, it returns a function that when called will call fn(). The context defines the object that fn will use internally as this.
It's not necessary but you could write showCaseById as :
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
var fn = _.bind(this.showContentView, this);
$.when(promise).then(fn);
},
As I say, that is unnecessary but now you understand that _.bind() returns a function and that $.when(promise).then(...) accepts a function as its (first) argument.
To answer the actual question, you can confirm that the App.models.Case.fetch() promise has been fulfilled by adding a further $.when(promise).then(...) statement, with a test function of your own choosing.
showCaseById: function(id){
App.models.Case = new caseModel({ id: id });
var promise = App.models.Case.fetch();
$.when(promise).then(_.bind(this.showContentView, this));
// start: test
$.when(promise).then(function() {
console.log("caseModel " + id + " is ready");//or alert() if preferred
});
// fin: test
},
The second $.when(promise).then(...) will not interfere with the first; rather, the two will execute sequentially. The console.log() satatement will provide reliable confirmation that the this.showContentView has been called successfully and that initial rendering should have happened.
If nothing is rendered at this point or subsequently, then you must suspect that this.showContentView needs to be debugged.

Resources