/* global describe global it global beforeEach global angular global jasmine global inject global expect global spyOn */
'use strict';
var rootScope, toaster, $compile;
describe('toasterContainer', function () {
beforeEach(function () {
module('toaster');
// inject the toaster service
inject(function (_toaster_, _$rootScope_, _$compile_) {
toaster = _toaster_;
rootScope = _$rootScope_;
$compile = _$compile_;
});
});
it('should pop a toast via individual parameters', function () {
var container = compileContainer();
var scope = container.scope();
toaster.pop('info', 'test', 'test');
expect(scope.toasters.length).toBe(1);
});
it('should unsubscribe events on $destroy if handlers exist', function () {
var toasterEventRegistry;
inject(function (_toasterEventRegistry_) {
toasterEventRegistry = _toasterEventRegistry_;
});
var container = compileContainer();
var scope = container.scope();
spyOn(toasterEventRegistry, 'unsubscribeToNewToastEvent').and.callThrough();
spyOn(toasterEventRegistry, 'unsubscribeToClearToastsEvent').and.callThrough();
scope.$destroy();
expect(toasterEventRegistry.unsubscribeToNewToastEvent).toHaveBeenCalled();
expect(toasterEventRegistry.unsubscribeToClearToastsEvent).toHaveBeenCalled();
});
describe('addToast', function () {
it('should default to icon-class config value if toast.type not found in icon-classes', function () {
var toasterConfig;
inject(function (_toasterConfig_) {
toasterConfig = _toasterConfig_;
});
compileContainer();
expect(toasterConfig['icon-class']).toBe('toast-info');
toaster.pop({ type: 'invalid' });
rootScope.$digest();
expect(toaster.toast.type).toBe('toast-info');
});
it('should allow subsequent duplicates if prevent-duplicates is not set', function () {
var container = compileContainer();
var scope = container.scope();
expect(scope.toasters.length).toBe(0);
toaster.pop({ type: 'info', title: 'title', body: 'body' });
toaster.pop({ type: 'info', title: 'title', body: 'body' });
rootScope.$digest();
expect(scope.toasters.length).toBe(2);
});
it('should not allow subsequent duplicates if prevent-duplicates is true without toastId param', function () {
var container = angular.element(
'');
$compile(container)(rootScope);
rootScope.$digest();
var scope = container.scope();
expect(scope.toasters.length).toBe(0);
toaster.pop({ type: 'info', title: 'title', body: 'body' });
toaster.pop({ type: 'info', title: 'title', body: 'body' });
rootScope.$digest();
expect(scope.toasters.length).toBe(1);
});
it('should allow subsequent duplicates if prevent-duplicates is true with unique toastId params', function () {
var container = angular.element(
'');
$compile(container)(rootScope);
rootScope.$digest();
var scope = container.scope();
expect(scope.toasters.length).toBe(0);
toaster.pop({ type: 'info', title: 'title', body: 'body', toastId: 1 });
toaster.pop({ type: 'info', title: 'title', body: 'body', toastId: 2 });
rootScope.$digest();
expect(scope.toasters.length).toBe(2);
});
it('should not allow subsequent duplicates if prevent-duplicates is true with identical toastId params', function () {
var container = angular.element(
'');
$compile(container)(rootScope);
rootScope.$digest();
var scope = container.scope();
expect(scope.toasters.length).toBe(0);
toaster.pop({ type: 'info', title: 'title', body: 'body', toastId: 1 });
toaster.pop({ type: 'info', title: 'title', body: 'body', toastId: 1 });
rootScope.$digest();
expect(scope.toasters.length).toBe(1);
});
it('should not render the close button if showCloseButton is false', function () {
var container = compileContainer();
toaster.pop({ type: 'info', body: 'With a close button' });
rootScope.$digest();
expect(container.find('button')[0]).toBeUndefined();
});
it('should use the default close html if toast.closeHtml is undefined', function () {
var container = compileContainer();
toaster.pop({ type: 'info', body: 'With a close button', showCloseButton: true });
rootScope.$digest();
var buttons = container.find('button');
expect(buttons.length).toBe(1);
expect(buttons[0].outerHTML).toBe('');
});
it('should use the toast.closeHtml argument if passed', function () {
var container = compileContainer();
toaster.pop({ type: 'info', body: 'With a close button', showCloseButton: true,
closeHtml: ''
});
rootScope.$digest();
var buttons = container.find('button');
expect(buttons.length).toBe(1);
expect(buttons[0].outerHTML).toBe('');
});
it('should render toast.closeHtml argument if not a button element', function () {
var container = compileContainer();
toaster.pop({ type: 'info', body: 'With close text', showCloseButton: true,
closeHtml: 'Close'
});
rootScope.$digest();
var spans = container.find('span');
expect(spans.length).toBe(1);
expect(spans[0].outerHTML).toBe('Close');
});
it('should show the close button if mergedConfig close-button is an object set to true for toast-info', function () {
var container = angular.element(
'');
$compile(container)(rootScope);
rootScope.$digest();
toaster.pop({ type: 'info' });
rootScope.$digest();
var buttons = container.find('button');
expect(buttons.length).toBe(1);
expect(buttons[0].outerHTML).toBe('');
});
it('should not render the close button if mergedConfig close-button type cannot be found', function () {
var container = angular.element(
'');
$compile(container)(rootScope);
rootScope.$digest();
toaster.pop({ type: 'info' });
rootScope.$digest();
var buttons = container.find('button');
expect(buttons.length).toBe(0);
expect(buttons[0]).toBeUndefined();
});
it('should not render the close button if mergedConfig close-button is not an object', function () {
var container = angular.element(
'');
$compile(container)(rootScope);
rootScope.$digest();
toaster.pop({ type: 'info' });
rootScope.$digest();
var buttons = container.find('button');
expect(buttons.length).toBe(0);
expect(buttons[0]).toBeUndefined();
});
it('should render trustedHtml bodyOutputType', function () {
var container = compileContainer();
toaster.pop({ bodyOutputType: 'trustedHtml', body: 'Body' });
rootScope.$digest();
var body = container.find('section');
expect(body.length).toBe(1);
expect(body[0].outerHTML).toBe('Body');
});
it('should render template bodyOutputType when body is passed', function () {
inject(function($templateCache) {
$templateCache.put('/templatepath/template.html', 'Template');
});
var container = compileContainer();
toaster.pop({ bodyOutputType: 'template', body: '/templatepath/template.html' });
rootScope.$digest();
expect(toaster.toast.body).toBe('/templatepath/template.html');
var body = container.find('section');
expect(body.length).toBe(1);
expect(body[0].outerHTML).toBe('Template');
});
it('should render default template bodyOutputType when body is not passed', function () {
inject(function($templateCache) {
$templateCache.put('toasterBodyTmpl.html', 'Template');
});
var container = compileContainer();
toaster.pop({ bodyOutputType: 'template' });
rootScope.$digest();
expect(toaster.toast.bodyTemplate).toBe('toasterBodyTmpl.html');
var body = container.find('section');
expect(body.length).toBe(1);
expect(body[0].outerHTML).toBe('Template');
});
it('should render templateWithData bodyOutputType when body is passed', function () {
inject(function($templateCache) {
$templateCache.put('template.html', 'Template {{toaster.data}}');
});
var container = compileContainer();
toaster.pop({ bodyOutputType: 'templateWithData', body: "{template: 'template.html', data: 123 }" });
rootScope.$digest();
var body = container.find('section');
expect(body.length).toBe(1);
expect(body[0].outerHTML).toBe('Template 123');
});
it('should throw exception for default templateWithData bodyOutputType when body is not passed', function () {
// TODO: If the default fallback template cannot be parsed to an object
// composed of template and data, an exception is thrown. This seems to
// be undesirable behavior. A clearer exception should be thrown, or better
// handling should be handled, or the fallback option should be removed.
inject(function($templateCache) {
$templateCache.put('template.html', 'Template {{toaster.data}}');
});
compileContainer();
var hasException = false;
try {
toaster.pop({ bodyOutputType: 'templateWithData' });
} catch (e) {
expect(e.message).toBe("Cannot read property 'template' of undefined");
hasException = true;
}
expect(hasException).toBe(true);
});
it('should remove first in toast if limit is met and newest-on-top is true', function () {
var container = angular.element(
'');
$compile(container)(rootScope);
rootScope.$digest();
var scope = container.scope();
toaster.pop({ type: 'info', body: 'first' });
toaster.pop({ type: 'info', body: 'second' });
rootScope.$digest();
expect(scope.toasters.length).toBe(2);
expect(scope.toasters[0].body).toBe('second');
expect(scope.toasters[1].body).toBe('first');
toaster.pop({ type: 'info', body: 'third' });
rootScope.$digest();
expect(scope.toasters.length).toBe(2);
expect(scope.toasters[0].body).toBe('third');
expect(scope.toasters[1].body).toBe('second');
});
it('should remove last in toast if limit is met and newest-on-top is false', function () {
var container = angular.element(
'');
$compile(container)(rootScope);
rootScope.$digest();
var scope = container.scope();
toaster.pop({ type: 'info', body: 'first' });
toaster.pop({ type: 'info', body: 'second' });
rootScope.$digest();
expect(scope.toasters.length).toBe(2);
expect(scope.toasters[0].body).toBe('first');
expect(scope.toasters[1].body).toBe('second');
toaster.pop({ type: 'info', body: 'third' });
rootScope.$digest();
expect(scope.toasters.length).toBe(2);
expect(scope.toasters[0].body).toBe('second');
expect(scope.toasters[1].body).toBe('third');
});
});
describe('removeToast', function () {
it('should not remove toast if id does not match a toast id', function() {
var container = compileContainer();
var scope = container.scope();
toaster.pop({ type: 'info', body: 'toast 1' });
toaster.pop({ type: 'info', body: 'toast 2' });
rootScope.$digest();
expect(scope.toasters.length).toBe(2);
expect(scope.toasters[0].id).toBe(2)
expect(scope.toasters[1].id).toBe(1)
scope.removeToast(3);
rootScope.$digest();
expect(scope.toasters.length).toBe(2);
});
it('should invoke onHideCallback if it exists when toast is removed', function () {
var container = compileContainer();
var scope = container.scope();
var mock = {
callback : function () { }
};
spyOn(mock, 'callback');
toaster.pop({ type: 'info', body: 'toast 1', onHideCallback: mock.callback });
rootScope.$digest();
scope.removeToast(1);
rootScope.$digest();
expect(mock.callback).toHaveBeenCalled();
});
});
describe('scope._onNewTest', function () {
it('should not add toast if toasterId is passed to scope._onNewToast but toasterId is not set via config', function () {
var container = compileContainer();
var scope = container.scope();
expect(scope.config.toasterId).toBeUndefined();
toaster.pop({ type: 'info', body: 'toast 1', toasterId: 1 });
rootScope.$digest();
expect(scope.toasters.length).toBe(0);
});
it('should add toast if toasterId is passed to scope._onNewToast and toasterId is set via config', function () {
var container = angular.element(
'');
$compile(container)(rootScope);
rootScope.$digest();
var scope = container.scope();
expect(scope.config.toasterId).toBe(1);
toaster.pop({ type: 'info', body: 'toast 1', toasterId: 1 });
rootScope.$digest();
expect(scope.toasters.length).toBe(1);
});
it('should add toasts to their respective container based on toasterId', function () {
var container1 = angular.element(
'');
var container2 = angular.element(
'');
$compile(container1)(rootScope);
$compile(container2)(rootScope);
rootScope.$digest();
var scope1 = container1.scope();
var scope2 = container2.scope();
toaster.pop({ type: 'info', body: 'toast 1', toasterId: 1 });
toaster.pop({ type: 'info', body: 'toast 2', toasterId: 2 });
rootScope.$digest();
expect(scope1.toasters.length).toBe(1);
expect(scope2.toasters.length).toBe(1);
});
});
describe('scope._onClearToasts', function (){
it('should remove all toasts from all containers if toasterId is *', function () {
var container1 = angular.element(
'');
var container2 = angular.element(
'');
$compile(container1)(rootScope);
$compile(container2)(rootScope);
rootScope.$digest();
var scope1 = container1.scope();
var scope2 = container2.scope();
toaster.pop({ type: 'info', body: 'toast 1', toasterId: 1 });
toaster.pop({ type: 'info', body: 'toast 2', toasterId: 2 });
rootScope.$digest();
expect(scope1.toasters.length).toBe(1);
expect(scope2.toasters.length).toBe(1);
toaster.clear('*');
rootScope.$digest();
expect(scope1.toasters.length).toBe(0);
expect(scope2.toasters.length).toBe(0);
});
it('should remove all toasts from all containers if config.toasterId and toastId are undefined', function () {
var container1 = angular.element(
'');
var container2 = angular.element(
'');
$compile(container1)(rootScope);
$compile(container2)(rootScope);
rootScope.$digest();
var scope1 = container1.scope();
var scope2 = container2.scope();
toaster.pop({ type: 'info', body: 'toast 1' });
toaster.pop({ type: 'info', body: 'toast 2' });
rootScope.$digest();
// since there are two separate instances of the container
// without a toasterId, both receive the newToast event
expect(scope1.toasters.length).toBe(2);
expect(scope2.toasters.length).toBe(2);
toaster.clear();
rootScope.$digest();
expect(scope1.toasters.length).toBe(0);
expect(scope2.toasters.length).toBe(0);
});
it('should not remove by toasterId / toastId from the correct container if toast.toasterId is defined and toast.toastId is undefined', function () {
var container1 = angular.element(
'');
var container2 = angular.element(
'');
$compile(container1)(rootScope);
$compile(container2)(rootScope);
rootScope.$digest();
var scope1 = container1.scope();
var scope2 = container2.scope();
// removeAllToasts explicitly looks for toast.uid, which is only set
// if toastId is passed as a parameter
toaster.pop({ type: 'info', body: 'toast 1', toasterId: 1 });
toaster.pop({ type: 'info', body: 'toast 2', toasterId: 2 });
toaster.pop({ type: 'info', body: 'toast 3', toasterId: 2 });
rootScope.$digest();
expect(scope1.toasters.length).toBe(1);
expect(scope2.toasters.length).toBe(2);
toaster.clear(2, 1);
rootScope.$digest();
expect(scope1.toasters.length).toBe(1);
expect(scope2.toasters.length).toBe(2);
});
it('should remove by toasterId / toastId from the correct container if toasterId is defined and toastId is defined', function () {
var container1 = angular.element(
'');
var container2 = angular.element(
'');
$compile(container1)(rootScope);
$compile(container2)(rootScope);
rootScope.$digest();
var scope1 = container1.scope();
var scope2 = container2.scope();
// removeAllToasts explicitly looks for toast.uid, which is only set
// if toastId is passed as a parameter
toaster.pop({ type: 'info', body: 'toast 1', toasterId: 1, toastId: 1 });
toaster.pop({ type: 'info', body: 'toast 2', toasterId: 2, toastId: 1 });
toaster.pop({ type: 'info', body: 'toast 3', toasterId: 2, toastId: 2 });
rootScope.$digest();
expect(scope1.toasters.length).toBe(1);
expect(scope2.toasters.length).toBe(2);
toaster.clear(2, 1);
rootScope.$digest();
expect(scope1.toasters.length).toBe(1);
expect(scope2.toasters.length).toBe(1);
});
});
});
describe('toasterContainer', function () {
var $interval, $intervalSpy;
inject(function (_$interval_) {
$interval = _$interval_;
});
beforeEach(function () {
$intervalSpy = jasmine.createSpy('$interval', $interval);
module('toaster', function ($provide) {
$provide.value('$interval', $intervalSpy);
});
// inject the toaster service
inject(function (_toaster_, _$rootScope_, _$compile_) {
toaster = _toaster_;
rootScope = _$rootScope_;
$compile = _$compile_;
});
});
it('should use the toast.timeout argument if it is a valid number', function () {
var container = compileContainer();
var scope = container.scope();
spyOn(scope, 'configureTimer').and.callThrough();
toaster.pop({ timeout: 2 });
expect(scope.configureTimer).toHaveBeenCalled();
expect(scope.configureTimer.calls.allArgs()[0][0].timeout).toBe(2);
expect($intervalSpy.calls.first().args[1]).toBe(2)
});
it('should not use the toast.timeout argument if not a valid number', function () {
var container = compileContainer();
var scope = container.scope();
spyOn(scope, 'configureTimer').and.callThrough();
toaster.pop({ timeout: "2" });
expect(scope.configureTimer).toHaveBeenCalled();
expect(scope.configureTimer.calls.allArgs()[0][0].timeout).toBe("2");
expect($intervalSpy.calls.first().args[1]).toBe(5000);
});
it('should call scope.removeToast when toast.timeoutPromise expires', function () {
var container = compileContainer();
var scope = container.scope();
spyOn(scope, 'removeToast').and.callThrough();
toaster.pop({ timeout: 2 });
$intervalSpy.calls.first().args[0]();
rootScope.$digest();
expect(scope.removeToast).toHaveBeenCalled();
});
it('should retrieve timeout by toast type if mergedConfig toast-timeout is an object', function () {
var element = angular.element(
'');
$compile(element)(rootScope);
rootScope.$digest();
toaster.pop({ type: 'info' });
expect($intervalSpy.calls.first().args[1]).toBe(5);
});
it('should not set a timeout if mergedConfig toast-timeout is an object and does not match toast type', function () {
// TODO: this seems to be a bug in the toast-timeout configuration option.
// It should fall back to a default value if the toast type configuration
// does not match the target toast type or throw an exception to warn of an
// invalid configuration.
var element = angular.element(
'');
$compile(element)(rootScope);
rootScope.$digest();
toaster.pop({ type: 'warning' });
expect($intervalSpy.calls.all().length).toBe(0);
});
});
function compileContainer() {
var element = angular.element('');
$compile(element)(rootScope);
rootScope.$digest();
return element;
}