Commit 416abe87 authored by peterh's avatar peterh

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.
parent e44431fe
*~
*.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
......
......@@ -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());
......
{
"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"
}
}
(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);
......@@ -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 = {
......
......@@ -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;
......
{
"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"
}
}
......@@ -2,47 +2,4 @@
<div ng-app="crmApp">
<div ng-view></div>
</div>
<script type="text/javascript">
(function(angular, _) {
var crmApp = angular.module('crmApp', CRM.angular.modules);
crmApp.config(['$routeProvider',
function($routeProvider) {
$routeProvider.otherwise({
template: ts('Unknown path')
});
}
]);
// crmApi is a function(entity,action,params,message) which is similar to CRM.api3, but
// it follows Angular conventions (e.g. it's an injectable service which returns a $q promise)
crmApp.factory('crmApi', function($q) {
return function(entity, action, params, message) {
// JSON serialization in CRM.api3 is not aware of Angular metadata like $$hash, so use angular.toJson()
var deferred = $q.defer();
var p;
if (_.isObject(entity)) {
p = CRM.api3(eval('('+angular.toJson(entity)+')'), message);
} else {
p = CRM.api3(entity, action, eval('('+angular.toJson(params)+')'), message);
}
// CRM.api3 returns a promise, but the promise doesn't really represent errors as errors, so we
// convert them
p.then(
function(result) {
if (result.is_error) {
deferred.reject(result);
} else {
deferred.resolve(result);
}
},
function(error) {
deferred.reject(error);
}
);
return deferred.promise;
};
});
})(angular, CRM._);
</script>
{/literal}
\ No newline at end of file
{/literal}
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
});
};
'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);
CRM.angular = {
modules: [
'ngRoute',
'ui.utils',
'ui.sortable',
'unsavedChanges',
'angularFileUpload',
'dialogService',
'crmApp',
'crmAttachment',
'crmUi',
'crmUtil',
'ngSanitize',
]
};
'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');
});
});
});
'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)");
});
});
'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();
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment