ravishi ravishi - 2 months ago 21
AngularJS Question

Jasmine Unit Test with postMessage and addEventListener

I'm trying to unit test a situation with postMessage and addEventListener. The use case is that I use a separate window for user logins similar to the OAuth workflow and then use postMessage in the login window to notify the main window that the user has logged in. This is the listening code that would be in the main window:

$window.addEventListener("message", function(event) {
if (event.data.type === "authLogin") {
service.curUser = event.data.user;
utilities.safeApply($rootScope);
$window.postMessage({type: "authLoginSuccess"}, '*');
}
});


The utilities.safeApply function is defined as:

// Run $apply() if not already in digest phase.
utilitiesService.safeApply = function(scope, fn) {
return (scope.$$phase || scope.$root.$$phase) ? scope.$eval(fn) : scope.$apply(fn);
};


My unit test is designed to send a postMessage to simulate the login:

describe('auth login postMessage', function() {
var testUser = {handle: 'test'};
beforeEach(function(done) {
$window.postMessage({type: 'authLogin', user: testUser}, '*');
function onAuthLoginSuccess(event) {
$window.removeEventListener('message', onAuthLoginSuccess);
done();
}
$window.addEventListener("message", onAuthLoginSuccess);
});
it("should set the user object", function() {
expect(service.curUser).toEqual(testUser);
});
});


This is the result of running the unit test:

12 09 2015 14:10:02.952:INFO [launcher]: Starting browser Chrome
12 09 2015 14:10:05.527:INFO [Chrome 45.0.2454 (Mac OS X 10.10.5)]: Connected on socket 537CxfI4xPnR0yjLAAAA with id 12583721
Chrome 45.0.2454 (Mac OS X 10.10.5) ERROR
Uncaught Error: Unexpected request: GET security/loginModal.tpl.html
No more request expected
at http://localhost:8089/__test/Users/userX/esupport/code/proj/public/vendor/angular-mocks/angular-mocks.js:250
Chrome 45.0.2454 (Mac OS X 10.10.5): Executed 23 of 100 (skipped 60) ERROR (0.333 secs / 0.263 secs)


I'm at a loss of why it would try to load an HTML template. I've narrowed it down so that if I don't call the $scope.$apply() function, the unit test succeeds without error. However, I need that $apply() to update the view.

I've tried stubbing out the utilities.safeApply method in the unit test and also tried to set up an expectation for the HTML template GET request. These attempts look like:

describe('auth login postMessage', function() {
var testUser = {handle: 'test'};
beforeEach(function(done) {
$httpBackend.when('GET', 'security/loginModal.tpl.html').respond(''); // <-- NEW
spyOn(utilities, 'safeApply').and.callFake(angular.noop); // <-- NEW
window.postMessage({type: 'authLogin', user: testUser}, '*');
function onAuthLoginSuccess(event) {
window.removeEventListener('message', onAuthLoginSuccess);
done();
}
window.addEventListener("message", onAuthLoginSuccess);
});
it("should set the user object", function() {
expect(service.curUser).toEqual(testUser);
});
});


Both of these attempts do nothing. I still get the same error message. I've tried other debugging steps such as using spyOn for $location.path() so it returns a sample value like "/fake". In all other unit tests where I'm directly testing service methods and not using postMessage to trigger the service code, the stubbing works fine. However, in the addEventListener function, the $location.path() returns "" which points towards a theory that the addEventListener function is running in an entirely different instance than what the unit test has prepared. That would explain why the stubbed out functions aren't being used and why this other instance is trying to load a bogus template. This discussion also solidifies the theory.

So now the question is, how do I get it to work? i.e, how do I get the addEventListener function to run in the same instance where my stubbed functions are used and it doesn't make a request to the HTML template?

Answer

I'd just mock all the external parts and make sure the end result is what you expect.

Looking at your gist, you may need to mock some more services but this should be enough to test the "authLogin" message event.

describe('some test', function() {
    var $window, utilities, toaster, securityRetryQueue, service, listeners;

    beforeEach(function() {
        module('security.service', function($provide) {
            $provide.value('$window',
                $window = jasmine.createSpyObj('$window', ['addEventListener', 'postMessage']));
            $provide.value('utilities',
                utilities = jasmine.createSpyObj('utilities', ['safeApply']));
            $provide.value('toaster',
                toaster = jasmine.createSpyObj('toaster', ['pop']));
            $provide.value('securityRetryQueue',
                securityRetryQueue = jasmine.createSpyObj('securityRetryQueue', ['hasMore', 'retryReason']));

            // make sure you're not fetching actual data in a unit test
            securityRetryQueue.onItemAddedCallbacks = [];
            securityRetryQueue.hasMore.and.returnValue(false);

            $window.addEventListener.and.callFake(function(event, listener) {
                listeners[event] = listener;
            });
        });

        inject(function(security) {
            service = security;
        });
    });

    it('registers a "message" event listener', function() {
        expect($window.addEventListener).toHaveBeenCalledWith('message', listeners.message, false);
    });

    it('message event listener does stuff', inject(function($rootScope) {
        var event = {
            data: {
                type: 'authLogin',
                user: 'user'
            },
            stopPropagation: jasmine.createSpy('event.stopPropagation')
        };

        listeners.message(event);

        expect(service.curUser).toBe(event.data.user);
        expect(toaster.pop).toHaveBeenCalledWith('success', 'Successfully logged in.');
        expect(utilities.safeApply).toHaveBeenCalledWith($rootScope);
        expect($window.postMessage).toHaveBeenCalledWith({type: "authLoginSuccess"}, '*');
        expect(event.stopPropagation).toHaveBeenCalled();
    }));
});
Comments