From 416abe87a5b6acb9bd426ad5c3314b797bde2441 Mon Sep 17 00:00:00 2001 From: Peter Haight Date: Sat, 29 Nov 2014 16:04:29 -0800 Subject: [PATCH] The start of a test framework for Angular. To run: npm install npm test The beginnings of some tests for the new Angular code. Includes some tests for the case type stuff and the new mailing stuff. --- .gitignore | 2 + CRM/Core/Page/Angular.php | 17 ++- bower.json | 17 +++ js/angular-crmApp.js | 30 ++++ js/angular-crmCaseType.js | 2 +- js/angular-crmMailing.js | 12 +- package.json | 18 +++ templates/CRM/Core/Page/Angular.tpl | 45 +----- tests/karma.conf.js | 36 +++++ tests/karma/lib/crmJsonComparitor.js | 89 ++++++++++++ tests/karma/modules.js | 15 ++ tests/karma/unit/crmCaseTypeSpec.js | 197 ++++++++++++++++++++++++++ tests/karma/unit/crmJsonComparitor.js | 76 ++++++++++ tests/karma/unit/crmMailingSpec.js | 31 ++++ 14 files changed, 528 insertions(+), 59 deletions(-) create mode 100644 bower.json create mode 100644 js/angular-crmApp.js create mode 100644 package.json create mode 100644 tests/karma.conf.js create mode 100644 tests/karma/lib/crmJsonComparitor.js create mode 100644 tests/karma/modules.js create mode 100644 tests/karma/unit/crmCaseTypeSpec.js create mode 100644 tests/karma/unit/crmJsonComparitor.js create mode 100644 tests/karma/unit/crmMailingSpec.js diff --git a/.gitignore b/.gitignore index 29d3ae3714..e1901e79e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *~ *.bak +bower_components CRM/ACL/DAO CRM/Activity/DAO CRM/Auction/DAO @@ -113,6 +114,7 @@ civicrm-version.php civicrm-version.txt civicrm.config.php install/langs.php +node_modules packages/.channels packages/.depdb packages/.depdblock diff --git a/CRM/Core/Page/Angular.php b/CRM/Core/Page/Angular.php index 0ab871872c..8c6f732031 100644 --- a/CRM/Core/Page/Angular.php +++ b/CRM/Core/Page/Angular.php @@ -40,8 +40,8 @@ class CRM_Core_Page_Angular extends CRM_Core_Page { ); }); - $res->addScriptFile('civicrm', 'packages/bower_components/angular/angular.min.js', 100, 'html-header', FALSE); - $res->addScriptFile('civicrm', 'packages/bower_components/angular-route/angular-route.min.js', 110, 'html-header', FALSE); + $res->addScriptFile('civicrm', 'bower_components/angular/angular.min.js', 100, 'html-header', FALSE); + $res->addScriptFile('civicrm', 'bower_components/angular-route/angular-route.min.js', 110, 'html-header', FALSE); $headOffset = 0; foreach ($modules as $module) { if (!empty($module['css'])) { @@ -64,16 +64,19 @@ class CRM_Core_Page_Angular extends CRM_Core_Page { */ public static function getAngularModules() { $angularModules = array(); - $angularModules['ui.utils'] = array('ext' => 'civicrm', 'js' => array('packages/bower_components/angular-ui-utils/ui-utils.min.js')); - $angularModules['ui.sortable'] = array('ext' => 'civicrm', 'js' => array('packages/bower_components/angular-ui-sortable/sortable.min.js')); - $angularModules['unsavedChanges'] = array('ext' => 'civicrm', 'js' => array('packages/bower_components/angular-unsavedChanges/dist/unsavedChanges.min.js')); + $angularModules['ui.utils'] = array('ext' => 'civicrm', 'js' => array('bower_components/angular-ui-utils/ui-utils.min.js')); + $angularModules['ui.sortable'] = array('ext' => 'civicrm', 'js' => array('bower_components/angular-ui-sortable/sortable.min.js')); + $angularModules['unsavedChanges'] = array('ext' => 'civicrm', 'js' => array('bower_components/angular-unsavedChanges/dist/unsavedChanges.min.js')); // https://github.com/jwstadler/angular-jquery-dialog-service - $angularModules['angularFileUpload'] = array('ext' => 'civicrm', 'js' => array('packages/bower_components/angular-file-upload/angular-file-upload.min.js')); - $angularModules['dialogService'] = array('ext' => 'civicrm' , 'js' => array('packages/bower_components/angular-jquery-dialog-service/dialog-service.js')); + $angularModules['angularFileUpload'] = array('ext' => 'civicrm', 'js' => array('bower_components/angular-file-upload/angular-file-upload.min.js')); + $angularModules['dialogService'] = array('ext' => 'civicrm' , 'js' => array('bower_components/angular-jquery-dialog-service/dialog-service.js')); + $angularModules['crmApp'] = array('ext' => 'civicrm', 'js' => array('js/angular-crmApp.js')); $angularModules['crmAttachment'] = array('ext' => 'civicrm', 'js' => array('js/angular-crmAttachment.js'), 'css' => array('css/angular-crmAttachment.css')); $angularModules['crmUi'] = array('ext' => 'civicrm', 'js' => array('js/angular-crm-ui.js', 'packages/ckeditor/ckeditor.js')); $angularModules['crmUtil'] = array('ext' => 'civicrm', 'js' => array('js/angular-crm-util.js')); $angularModules['ngSanitize'] = array('ext' => 'civicrm', 'js' => array('js/angular-sanitize.js')); + $angularModules['unsavedChanges'] = array('ext' => 'civicrm', 'js' => array('bower_components/angular-unsavedChanges/dist/unsavedChanges.min.js')); + $angularModules['crmUi'] = array('ext' => 'civicrm', 'js' => array('js/angular-crm-ui.js')); foreach (CRM_Core_Component::getEnabledComponents() as $component) { $angularModules = array_merge($angularModules, $component->getAngularModules()); diff --git a/bower.json b/bower.json new file mode 100644 index 0000000000..cbae39c0cd --- /dev/null +++ b/bower.json @@ -0,0 +1,17 @@ +{ + "name": "civicrm", + "description": "CiviCRM", + "version": "5.0.0", + "license": "AGPL-3.0", + "private": true, + "dependencies": { + "angular": "1.3.x", + "angular-file-upload": "~1.1.5", + "angular-jquery-dialog-service": "totten/angular-jquery-dialog-service#jquery-closure", + "angular-mocks": "1.3.x", + "angular-route": "1.3.x", + "angular-ui-sortable": "0.12.x", + "angular-ui-utils": "0.1.x", + "angular-unsavedChanges": "~0.1.1" + } +} diff --git a/js/angular-crmApp.js b/js/angular-crmApp.js new file mode 100644 index 0000000000..ed7d1c2922 --- /dev/null +++ b/js/angular-crmApp.js @@ -0,0 +1,30 @@ +(function(angular, CRM) { + var crmApp = angular.module('crmApp', CRM.angular.modules); + crmApp.config(['$routeProvider', + function($routeProvider) { + $routeProvider.otherwise({ + template: ts('Unknown path') + }); + } + ]); + crmApp.factory('crmApi', function() { + return function(entity, action, params, message) { + // JSON serialization in CRM.api3 is not aware of Angular metadata like $$hash + if (CRM._.isObject(entity)) { + return CRM.api3(eval('('+angular.toJson(entity)+')'), message); + } else { + return CRM.api3(entity, action, eval('('+angular.toJson(params)+')'), message); + } + }; + }); + crmApp.factory('crmLegacy', function() { + return CRM; + }); + crmApp.factory('crmNavigator', ['$window', function($window) { + return { + redirect: function(path) { + $window.location.href = path; + } + }; + }]); +})(angular, CRM); diff --git a/js/angular-crmCaseType.js b/js/angular-crmCaseType.js index d27c9655e5..6195137272 100644 --- a/js/angular-crmCaseType.js +++ b/js/angular-crmCaseType.js @@ -4,7 +4,7 @@ return CRM.resourceUrls['civicrm'] + '/partials/crmCaseType/' + relPath; }; - var crmCaseType = angular.module('crmCaseType', ['ngRoute', 'ui.utils', 'crmUi', 'unsavedChanges']); + var crmCaseType = angular.module('crmCaseType', ['ngRoute', 'ui.utils', 'crmUi', 'unsavedChanges', 'crmApp']); // Note: This template will be passed to cloneDeep(), so don't put any funny stuff in here! var newCaseTypeTemplate = { diff --git a/js/angular-crmMailing.js b/js/angular-crmMailing.js index b383acb571..7595d67abd 100644 --- a/js/angular-crmMailing.js +++ b/js/angular-crmMailing.js @@ -4,7 +4,7 @@ }; angular.module('crmMailing', [ - 'crmUtil', 'crmAttachment', 'ngRoute', 'ui.utils', 'crmUi', 'dialogService' + 'crmUtil', 'crmAttachment', 'ngRoute', 'ui.utils', 'crmUi', 'dialogService', 'crmApp' ]); // TODO ngSanitize, unsavedChanges // Time to wait before triggering AJAX update to recipients list @@ -57,14 +57,12 @@ } ]); - angular.module('crmMailing').controller('ListMailingsCtrl', function ListMailingsCtrl() { + angular.module('crmMailing').controller('ListMailingsCtrl', ['crmLegacy', 'crmNavigator', function ListMailingsCtrl(crmLegacy, crmNavigator) { // We haven't implemented this in Angular, but some users may get clever // about typing URLs, so we'll provide a redirect. - window.location = CRM.url('civicrm/mailing/browse/unscheduled', { - reset: 1, - scheduled: 'false' - }); - }); + var new_url = crmLegacy.url('civicrm/mailing/browse/unscheduled', {reset: 1, scheduled: 'false'}); + crmNavigator.redirect(new_url); + }]); angular.module('crmMailing').controller('EditMailingCtrl', function EditMailingCtrl($scope, selectedMail, $location, crmMailingMgr, crmStatus, CrmAttachments, crmMailingPreviewMgr) { $scope.mailing = selectedMail; diff --git a/package.json b/package.json new file mode 100644 index 0000000000..f1cb2e01e5 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "description": "CiviCRM", + "main": "index.js", + "license": "MIT", + "name": "civicrm", + "version": "5.0.0", + "devDependencies": { + "bower": "^1.3.1", + "karma": "^0.12.16", + "karma-chrome-launcher": "^0.1.4", + "jasmine-core": "~2.1.2", + "karma-jasmine": "~0.3.2" + }, + "scripts": { + "postinstall": "bower install", + "test": "node node_modules/karma/bin/karma start tests/karma.conf.js" + } +} diff --git a/templates/CRM/Core/Page/Angular.tpl b/templates/CRM/Core/Page/Angular.tpl index 706a810d0c..63eb5e3e45 100644 --- a/templates/CRM/Core/Page/Angular.tpl +++ b/templates/CRM/Core/Page/Angular.tpl @@ -2,47 +2,4 @@
- - - -{/literal} \ No newline at end of file +{/literal} diff --git a/tests/karma.conf.js b/tests/karma.conf.js new file mode 100644 index 0000000000..1f977dc7e8 --- /dev/null +++ b/tests/karma.conf.js @@ -0,0 +1,36 @@ +module.exports = function(config) { + config.set({ + autoWatch: true, + basePath: '..', + browsers: ['Chrome'], + exclude: [ + ], + files: [ + 'bower_components/jquery/dist/jquery.min.js', + 'bower_components/jquery-ui/jquery-ui.min.js', + 'packages/backbone/lodash.compat.min.js', + 'packages/jquery/plugins/select2/select2.min.js', + 'packages/jquery/plugins/jquery.blockUI.js', + 'packages/jquery/plugins/jquery.validate.js', + 'js/Common.js', + 'bower_components/angular/angular.js', + 'bower_components/angular-file-upload/angular-file-upload.js', + 'bower_components/angular-jquery-dialog-service/dialog-service.js', + 'bower_components/angular-route/angular-route.js', + 'bower_components/angular-mocks/angular-mocks.js', + 'bower_components/angular-ui-sortable/sortable.js', + 'bower_components/angular-ui-utils/ui-utils.js', + 'bower_components/angular-unsavedChanges/dist/unsavedChanges.js', + 'tests/karma/modules.js', + 'js/crm.ajax.js', + 'js/angular-*.js', + 'tests/karma/lib/*.js', + 'tests/karma/**/*.js', + ], + frameworks: ['jasmine'], + logLevel: config.LOG_INFO, + port: 9876, + reporters: ['progress'], + singleRun: false + }); +}; diff --git a/tests/karma/lib/crmJsonComparitor.js b/tests/karma/lib/crmJsonComparitor.js new file mode 100644 index 0000000000..44a3207037 --- /dev/null +++ b/tests/karma/lib/crmJsonComparitor.js @@ -0,0 +1,89 @@ +'use strict'; + +(function(root) { + var Comparitor = function() {}; + Comparitor.prototype = { + compare: function(actual, expected) { + this.result = { + 'pass': true, + }; + this.internal_compare('root', actual, expected); + return this.result; + }, + internal_compare: function(context, actual, expected) { + if (expected instanceof Array) { + return this.internal_compare_array(context, actual, expected); + } else if (expected instanceof Object) { + return this.internal_compare_object(context, actual, expected); + } else { + return this.internal_compare_value(context, actual, expected); + } + return true; + }, + internal_compare_array: function(context, actual, expected) { + if (!(actual instanceof Array)) { + this.result.pass = false; + this.result.message = "The expected data has an array at " + context + ", but the actual data has something else (" + actual + ")"; + return false; + } + if (expected.length != actual.length) { + this.result.pass = false; + this.result.message = "The expected data has an array with " + expected.length + " items in it, but the actual data has " + actual.length + " items."; + return false; + } + for (var i = 0; i < expected.length; i++) { + var still_matches = this.internal_compare(context + "[" + i + "]", actual[i], expected[i]); + if (!still_matches) { + return false; + } + } + return true; + }, + internal_compare_object: function(context, actual, expected) { + if (!(actual instanceof Object) || actual instanceof Array) { + this.result.pass = false; + this.result.message = "The expected data has an object at root, but the actual data has something else (" + actual + ")"; + return false; + } + for (var key in expected) { + if (!(key in actual)) { + this.result.pass = false; + this.result.message = "Could not find key '" + key + "' in actual data at " + context + "."; + return false; + } + var still_matches = this.internal_compare(context + "[" + key + "]", actual[key], expected[key]); + if (!still_matches) { + return false; + } + } + for (var key in actual) { + if (!(key in expected)) { + this.result.pass = false; + this.result.message = "Did not expect key " + key + " in actual data at " + context + "."; + return false; + } + } + return true; + }, + internal_compare_value: function(context, actual, expected) { + if (expected === actual) { + return true; + } + this.result.pass = false; + this.result.message = "Expected '" + actual + "' to be '" + expected + "' at " + context + "."; + return false; + }, + register: function(jasmine) { + var comparitor = this; + jasmine.addMatchers({ + toEqualData: function(expected) { + return { + compare: $.proxy(comparitor.compare, comparitor) + } + } + }); + } + }; + var module = angular.module('crmJsonComparitor', []); + module.service('crmJsonComparitor', Comparitor); +})(angular); diff --git a/tests/karma/modules.js b/tests/karma/modules.js new file mode 100644 index 0000000000..28419384a3 --- /dev/null +++ b/tests/karma/modules.js @@ -0,0 +1,15 @@ +CRM.angular = { + modules: [ + 'ngRoute', + 'ui.utils', + 'ui.sortable', + 'unsavedChanges', + 'angularFileUpload', + 'dialogService', + 'crmApp', + 'crmAttachment', + 'crmUi', + 'crmUtil', + 'ngSanitize', + ] +}; diff --git a/tests/karma/unit/crmCaseTypeSpec.js b/tests/karma/unit/crmCaseTypeSpec.js new file mode 100644 index 0000000000..ff826a4779 --- /dev/null +++ b/tests/karma/unit/crmCaseTypeSpec.js @@ -0,0 +1,197 @@ +'use strict'; + +describe('crmCaseType', function() { + + beforeEach(function() { + CRM.resourceUrls = { + 'civicrm': '' + }; + CRM.crmCaseType = { + 'REL_TYPE_CNAME': 'label_b_a' + }; + module('crmCaseType'); + module('crmJsonComparitor'); + inject(function(crmJsonComparitor) { + crmJsonComparitor.register(jasmine); + }); + }); + + describe('CaseTypeCtrl', function() { + var apiCalls; + var ctrl; + var compile; + var $httpBackend; + var scope; + var timeout; + + beforeEach(inject(function(_$httpBackend_, $rootScope, $controller, $compile, $timeout) { + $httpBackend = _$httpBackend_; + scope = $rootScope.$new(); + compile = $compile; + timeout = $timeout; + apiCalls = { + 'actStatuses': { + 'values': { + "272": { + "id": "272", + "option_group_id": "25", + "label": "Scheduled", + "value": "1", + "name": "Scheduled", + "filter": "0", + "is_default": "1", + "weight": "1", + "is_optgroup": "0", + "is_reserved": "1", + "is_active": "1" + }, + "273": { + "id": "273", + "option_group_id": "25", + "label": "Completed", + "value": "2", + "name": "Completed", + "filter": "0", + "weight": "2", + "is_optgroup": "0", + "is_reserved": "1", + "is_active": "1" + } + } + }, + 'actTypes': { + 'values': { + "784": { + "id": "784", + "option_group_id": "2", + "label": "ADC referral", + "value": "62", + "name": "ADC referral", + "filter": "0", + "is_default": "0", + "weight": "64", + "is_optgroup": "0", + "is_reserved": "0", + "is_active": "1", + "component_id": "7" + }, + "32": { + "id": "32", + "option_group_id": "2", + "label": "Add Client To Case", + "value": "27", + "name": "Add Client To Case", + "filter": "0", + "is_default": "0", + "weight": "26", + "description": "", + "is_optgroup": "0", + "is_reserved": "1", + "is_active": "1", + "component_id": "7" + } + } + }, + 'relTypes': { + 'values' : { + "14": { + "id": "14", + "name_a_b": "Benefits Specialist is", + "label_a_b": "Benefits Specialist is", + "name_b_a": "Benefits Specialist", + "label_b_a": "Benefits Specialist", + "description": "Benefits Specialist", + "contact_type_a": "Individual", + "contact_type_b": "Individual", + "is_reserved": "0", + "is_active": "1" + }, + "9": { + "id": "9", + "name_a_b": "Case Coordinator is", + "label_a_b": "Case Coordinator is", + "name_b_a": "Case Coordinator", + "label_b_a": "Case Coordinator", + "description": "Case Coordinator", + "contact_type_a": "Individual", + "contact_type_b": "Individual", + "is_reserved": "0", + "is_active": "1" + } + } + }, + "caseType": { + "id": "1", + "name": "housing_support", + "title": "Housing Support", + "description": "Help homeless individuals obtain temporary and long-term housing", + "is_active": "1", + "is_reserved": "0", + "weight": "1", + "is_forkable": "1", + "is_forked": "", + "definition": { + "activityTypes": [ + {"name": "Open Case", "max_instances": "1"} + ], + "activitySets": [ + { + "name": "standard_timeline", + "label": "Standard Timeline", + "timeline": "1", + "activityTypes": [ + { + "name": "Open Case", + "status": "Completed" + }, + { + "name": "Medical evaluation", + "reference_activity": "Open Case", + "reference_offset": "1", + "reference_select": "newest" + } + ] + } + ], + "caseRoles": [ + { + "name": "Homeless Services Coordinator", + "creator": "1", + "manager": "1" + } + ] + } + } + }; + ctrl = $controller('CaseTypeCtrl', {$scope: scope, apiCalls: apiCalls}); + })); + + it('should load activity statuses', function() { + expect(scope.activityStatuses).toEqualData([apiCalls['actStatuses']['values']['272'], apiCalls['actStatuses']['values']['273']]); + }); + + it('should load activity types', function() { + expect(scope.activityTypes).toEqualData(apiCalls['actTypes']['values']); + }); + + it('addActivitySet should add an activitySet to the case type', function() { + scope.addActivitySet('timeline'); + var activitySets = scope.caseType.definition.activitySets; + var newSet = activitySets[activitySets.length - 1]; + expect(newSet.name).toBe('timeline_1'); + expect(newSet.timeline).toBe('1'); + expect(newSet.label).toBe('Timeline'); + }); + + it('addActivitySet handles second timeline correctly', function() { + scope.addActivitySet('timeline'); + scope.addActivitySet('timeline'); + var activitySets = scope.caseType.definition.activitySets; + var newSet = activitySets[activitySets.length - 1]; + expect(newSet.name).toBe('timeline_2'); + expect(newSet.timeline).toBe('1'); + expect(newSet.label).toBe('Timeline #2'); + }); + + }); +}); diff --git a/tests/karma/unit/crmJsonComparitor.js b/tests/karma/unit/crmJsonComparitor.js new file mode 100644 index 0000000000..653a34b65a --- /dev/null +++ b/tests/karma/unit/crmJsonComparitor.js @@ -0,0 +1,76 @@ +'use strict'; + +describe('crmJsonComparitor', function() { + var comparitor; + + beforeEach(function() { + module('crmJsonComparitor'); + }); + + beforeEach(function() { + inject(function(crmJsonComparitor) { + comparitor = crmJsonComparitor; + }); + }); + + it('should return false when comparing different objects', function() { + var result = comparitor.compare({'foo': 'bar'}, {'bar': 'foo'}); + expect(result.pass).toBe(false); + }); + + it('should return true when comparing equal objects', function() { + var result = comparitor.compare({'bar': 'foo'}, {'bar': 'foo'}); + expect(result.pass).toBe(true); + }); + + it('should explain what part of the comparison failed when comparing objects', function() { + var result = comparitor.compare({'foo': 'bar'}, {'bar': 'foo'}); + expect(result.message).toBe('Could not find key \'bar\' in actual data at root.'); + }); + + it('should handle nested objects', function() { + var result = comparitor.compare({'foo': {'bif': 'bam'}}, {'foo': {'bif': 'bam'}}); + expect(result.pass).toBe(true); + }); + + it('should handle differences in nested objects', function() { + var result = comparitor.compare({'foo': {'bif': 'bam'}}, {'foo': {'bif': 'bop'}}); + expect(result.pass).toBe(false); + expect(result.message).toBe("Expected 'bam' to be 'bop' at root[foo][bif]."); + }); + + it('should handle arrays', function() { + var result = comparitor.compare([1, 2, 3, 4], [1, 2, 3, 4]); + expect(result.pass).toBe(true); + }); + + it('should handle arrays with differences', function() { + var result = comparitor.compare([1, 2, 2, 4], [1, 2, 3, 4]); + expect(result.pass).toBe(false); + expect(result.message).toBe("Expected '2' to be '3' at root[2]."); + }); + + it('should handle nested arrays and objects', function() { + var result = comparitor.compare([1, 2, {'foo': 'bar'}, 4], [1, 2, {'foo': 'bar'}, 4]); + expect(result.pass).toBe(true); + }); + + it('should handle nested arrays and objects with differences', function() { + var result = comparitor.compare([1, 2, {'foo': 'bar'}, 4], [1, 2, {'foo': 'bif'}, 4]); + expect(result.pass).toBe(false); + expect(result.message).toBe("Expected 'bar' to be 'bif' at root[2][foo]."); + }); + + it('should complain when comparing an object to an array', function() { + var result = comparitor.compare({'foo': 'bar'}, [1, 2, 3]); + expect(result.pass).toBe(false); + expect(result.message).toBe("The expected data has an array at root, but the actual data has something else ([object Object])"); + }); + + it('should complain when comparing an array to an object', function() { + var result = comparitor.compare([1, 2, 3], {'foo': 'bar'}); + expect(result.pass).toBe(false); + expect(result.message).toBe("The expected data has an object at root, but the actual data has something else (1,2,3)"); + }); + +}); diff --git a/tests/karma/unit/crmMailingSpec.js b/tests/karma/unit/crmMailingSpec.js new file mode 100644 index 0000000000..67382e983c --- /dev/null +++ b/tests/karma/unit/crmMailingSpec.js @@ -0,0 +1,31 @@ +'use strict'; + +describe('crmMailing', function() { + + beforeEach(function() { + module('crmApp'); + module('crmMailing'); + }); + + describe('ListMailingsCtrl', function() { + var ctrl; + var navigator; + + beforeEach(function() { + navigator = jasmine.createSpyObj('crmNavigator', ['redirect']); + module(function ($provide) { + $provide.value('crmNavigator', navigator) + }); + inject(['crmLegacy', function(crmLegacy) { + crmLegacy.url({back: '/*path*?*query*', front: '/*path*?*query*'}); + }]); + inject(['$controller', function($controller) { + ctrl = $controller('ListMailingsCtrl', {}); + }]); + }); + + it('should redirect to unscheduled', function() { + expect(navigator.redirect).toHaveBeenCalled(); + }); + }); +}); -- 2.25.1