Merge pull request #14460 from eileenmcnaughton/ajax_test
[civicrm-core.git] / tests / karma / unit / crmCaseTypeSpec.js
1 /* global $, _, CRM:true */
2 'use strict';
3
4 describe('crmCaseType', function() {
5 var $controller;
6 var $compile;
7 var $httpBackend;
8 var $q;
9 var $rootScope;
10 var $timeout;
11 var apiCalls;
12 var ctrl;
13 var compile;
14 var defaultAssigneeDefaultValue;
15 var scope;
16
17 beforeEach(function() {
18 CRM.resourceUrls = {
19 'civicrm': ''
20 };
21 module('crmCaseType');
22 module('crmJsonComparator');
23 inject(function(crmJsonComparator) {
24 crmJsonComparator.register(jasmine);
25 });
26 });
27
28 beforeEach(inject(function(_$controller_, _$compile_, _$httpBackend_, _$q_, _$rootScope_, _$timeout_) {
29 $controller = _$controller_;
30 $compile = _$compile_;
31 $httpBackend = _$httpBackend_;
32 $q = _$q_;
33 $rootScope = _$rootScope_;
34 $timeout = _$timeout_;
35 }));
36
37 describe('CaseTypeCtrl', function() {
38 beforeEach(function () {
39 apiCalls = {
40 actStatuses: {
41 values: [
42 {
43 "id": "272",
44 "option_group_id": "25",
45 "label": "Scheduled",
46 "value": "1",
47 "name": "Scheduled",
48 "filter": "0",
49 "is_default": "1",
50 "weight": "1",
51 "is_optgroup": "0",
52 "is_reserved": "1",
53 "is_active": "1"
54 },
55 {
56 "id": "273",
57 "option_group_id": "25",
58 "label": "Completed",
59 "value": "2",
60 "name": "Completed",
61 "filter": "0",
62 "weight": "2",
63 "is_optgroup": "0",
64 "is_reserved": "1",
65 "is_active": "1"
66 }
67 ]
68 },
69 caseStatuses: {
70 values: [
71 {
72 "id": "290",
73 "option_group_id": "28",
74 "label": "Ongoing",
75 "value": "1",
76 "name": "Open",
77 "grouping": "Opened",
78 "filter": "0",
79 "is_default": "1",
80 "weight": "1",
81 "is_optgroup": "0",
82 "is_reserved": "1",
83 "is_active": "1"
84 },
85 {
86 "id": "291",
87 "option_group_id": "28",
88 "label": "Resolved",
89 "value": "2",
90 "name": "Closed",
91 "grouping": "Closed",
92 "filter": "0",
93 "weight": "2",
94 "is_optgroup": "0",
95 "is_reserved": "1",
96 "is_active": "1"
97 }
98 ]
99 },
100 actTypes: {
101 values: [
102 {
103 "id": "784",
104 "option_group_id": "2",
105 "label": "ADC referral",
106 "value": "62",
107 "name": "ADC referral",
108 "filter": "0",
109 "is_default": "0",
110 "weight": "64",
111 "is_optgroup": "0",
112 "is_reserved": "0",
113 "is_active": "1",
114 "component_id": "7"
115 },
116 {
117 "id": "32",
118 "option_group_id": "2",
119 "label": "Add Client To Case",
120 "value": "27",
121 "name": "Add Client To Case",
122 "filter": "0",
123 "is_default": "0",
124 "weight": "26",
125 "description": "",
126 "is_optgroup": "0",
127 "is_reserved": "1",
128 "is_active": "1",
129 "component_id": "7"
130 },
131 {
132 "id": "18",
133 "option_group_id": "2",
134 "label": "Open Case",
135 "value": "13",
136 "name": "Open Case",
137 "filter": "0",
138 "is_default": "0",
139 "weight": "13",
140 "is_optgroup": "0",
141 "is_reserved": "1",
142 "is_active": "1",
143 "component_id": "7",
144 "icon": "fa-folder-open-o"
145 },
146 {
147 "id": "857",
148 "option_group_id": "2",
149 "label": "Medical evaluation",
150 "value": "55",
151 "name": "Medical evaluation",
152 "filter": "0",
153 "is_default": "0",
154 "weight": "56",
155 "is_optgroup": "0",
156 "is_reserved": "0",
157 "is_active": "1",
158 "component_id": "7"
159 },
160 ]
161 },
162 relTypes: {
163 values: [
164 {
165 "id": "14",
166 "name_a_b": "Benefits Specialist is",
167 "label_a_b": "Benefits Specialist is",
168 "name_b_a": "Benefits Specialist",
169 "label_b_a": "Benefits Specialist",
170 "description": "Benefits Specialist",
171 "contact_type_a": "Individual",
172 "contact_type_b": "Individual",
173 "is_reserved": "0",
174 "is_active": "1"
175 },
176 {
177 "id": "9",
178 "name_a_b": "Case Coordinator is",
179 "label_a_b": "Case Coordinator is",
180 "name_b_a": "Case Coordinator",
181 "label_b_a": "Case Coordinator",
182 "description": "Case Coordinator",
183 "contact_type_a": "Individual",
184 "contact_type_b": "Individual",
185 "is_reserved": "0",
186 "is_active": "1"
187 },
188 {
189 "id": "2",
190 "name_a_b": "Spouse of",
191 "label_a_b": "Spouse of",
192 "name_b_a": "Spouse of",
193 "label_b_a": "Spouse of",
194 "description": "Spousal relationship.",
195 "contact_type_a": "Individual",
196 "contact_type_b": "Individual",
197 "is_reserved": "0",
198 "is_active": "1"
199 }
200 ]
201 },
202 relTypesForm: {
203 values: [
204 {
205 "key": "14_b_a",
206 "value": "Benefits Specialist"
207 },
208 {
209 "key": "14_a_b",
210 "value": "Benefits Specialist is"
211 },
212 {
213 "key": "9_b_a",
214 "value": "Case Coordinator"
215 },
216 {
217 "key": "9_a_b",
218 "value": "Case Coordinator is"
219 },
220 {
221 "key": "2_b_a",
222 "value": "Spouse of"
223 }
224 ]
225 },
226 caseType: {
227 "id": "1",
228 "name": "housing_support",
229 "title": "Housing Support",
230 "description": "Help homeless individuals obtain temporary and long-term housing",
231 "is_active": "1",
232 "is_reserved": "0",
233 "weight": "1",
234 "is_forkable": "1",
235 "is_forked": "",
236 "definition": {
237 "activityTypes": [
238 {"name": "Open Case", "max_instances": "1"}
239 ],
240 "activitySets": [
241 {
242 "name": "standard_timeline",
243 "label": "Standard Timeline",
244 "timeline": "1",
245 "activityTypes": [
246 {
247 "name": "Open Case",
248 "status": "Completed"
249 },
250 {
251 "name": "Medical evaluation",
252 "reference_activity": "Open Case",
253 "reference_offset": "1",
254 "reference_select": "newest"
255 }
256 ]
257 }
258 ],
259 "caseRoles": [
260 {
261 "name": "Homeless Services Coordinator",
262 "creator": "1",
263 "manager": "1"
264 }
265 ]
266 }
267 },
268 defaultAssigneeTypes: {
269 values: [
270 {
271 "id": "1174",
272 "option_group_id": "152",
273 "label": "None",
274 "value": "1",
275 "name": "NONE",
276 "filter": "0",
277 "is_default": "1",
278 "weight": "1",
279 "is_optgroup": "0",
280 "is_reserved": "0",
281 "is_active": "1"
282 },
283 {
284 "id": "1175",
285 "option_group_id": "152",
286 "label": "By relationship to workflow client",
287 "value": "2",
288 "name": "BY_RELATIONSHIP",
289 "filter": "0",
290 "is_default": "0",
291 "weight": "2",
292 "is_optgroup": "0",
293 "is_reserved": "0",
294 "is_active": "1"
295 },
296 {
297 "id": "1176",
298 "option_group_id": "152",
299 "label": "Specific contact",
300 "value": "3",
301 "name": "SPECIFIC_CONTACT",
302 "filter": "0",
303 "is_default": "0",
304 "weight": "3",
305 "is_optgroup": "0",
306 "is_reserved": "0",
307 "is_active": "1"
308 },
309 {
310 "id": "1177",
311 "option_group_id": "152",
312 "label": "User creating the workflow",
313 "value": "4",
314 "name": "USER_CREATING_THE_CASE",
315 "filter": "0",
316 "is_default": "0",
317 "weight": "4",
318 "is_optgroup": "0",
319 "is_reserved": "0",
320 "is_active": "1"
321 }
322 ]
323 }
324 };
325 defaultAssigneeDefaultValue = _.find(apiCalls.defaultAssigneeTypes.values, { is_default: '1' });
326 scope = $rootScope.$new();
327 ctrl = $controller('CaseTypeCtrl', {$scope: scope, apiCalls: apiCalls});
328 });
329
330 it('should load activity statuses', function() {
331 expect(scope.activityStatuses).toEqualData(apiCalls.actStatuses.values);
332 });
333
334 it('should load activity types', function() {
335 expect(scope.activityTypes['ADC referral']).toEqualData(apiCalls.actTypes.values[0]);
336 });
337
338 it('should store the default assignee types', function() {
339 expect(scope.defaultAssigneeTypes).toBe(apiCalls.defaultAssigneeTypes.values);
340 });
341
342 it('should store the default assignee types values indexed by name', function() {
343 var defaultAssigneeTypeValues = _.chain(apiCalls.defaultAssigneeTypes.values)
344 .indexBy('name').mapValues('value').value();
345
346 expect(scope.defaultAssigneeTypeValues).toEqual(defaultAssigneeTypeValues);
347 });
348
349 it('should store the default assignee relationship type options', function() {
350 var defaultRelationshipTypeOptions = _.transform(apiCalls.relTypes.values, function(result, relType) {
351 var isBidirectionalRelationship = relType.label_a_b === relType.label_b_a;
352
353 result.push({
354 label: relType.label_b_a,
355 value: relType.id + '_a_b'
356 });
357
358 if (!isBidirectionalRelationship) {
359 result.push({
360 label: relType.label_a_b,
361 value: relType.id + '_b_a'
362 });
363 }
364 }, []);
365
366 expect(scope.defaultRelationshipTypeOptions).toEqual(defaultRelationshipTypeOptions);
367 });
368
369 it('addActivitySet should add an activitySet to the case type', function() {
370 scope.addActivitySet('timeline');
371 var activitySets = scope.caseType.definition.activitySets;
372 var newSet = activitySets[activitySets.length - 1];
373 expect(newSet.name).toBe('timeline_1');
374 expect(newSet.timeline).toBe('1');
375 expect(newSet.label).toBe('Timeline');
376 });
377
378 it('addActivitySet handles second timeline correctly', function() {
379 scope.addActivitySet('timeline');
380 scope.addActivitySet('timeline');
381 var activitySets = scope.caseType.definition.activitySets;
382 var newSet = activitySets[activitySets.length - 1];
383 expect(newSet.name).toBe('timeline_2');
384 expect(newSet.timeline).toBe('1');
385 expect(newSet.label).toBe('Timeline #2');
386 });
387
388 describe('when clearing the activity\'s default assignee type values', function() {
389 var activity;
390
391 beforeEach(function() {
392 activity = {
393 default_assignee_relationship: 1,
394 default_assignee_contact: 2
395 };
396
397 scope.clearActivityDefaultAssigneeValues(activity);
398 });
399
400 it('clears the default assignee relationship for the activity', function() {
401 expect(activity.default_assignee_relationship).toBe(null);
402 });
403
404 it('clears the default assignee contact for the activity', function() {
405 expect(activity.default_assignee_contact).toBe(null);
406 });
407 });
408
409 describe('when adding a new activity to a set', function() {
410 var activitySet;
411
412 beforeEach(function() {
413 activitySet = { activityTypes: [] };
414 scope.activityTypes = { comment: { label: 'Add a new comment' } };
415
416 scope.addActivity(activitySet, 'comment');
417 });
418
419 it('adds a new Comment activity to the set', function() {
420 expect(activitySet.activityTypes[0]).toEqual({
421 name: 'comment',
422 label: scope.activityTypes.comment.label,
423 status: 'Scheduled',
424 reference_activity: 'Open Case',
425 reference_offset: '1',
426 reference_select: 'newest',
427 default_assignee_type: defaultAssigneeDefaultValue.value
428 });
429 });
430 });
431
432 describe('when creating a new workflow', function() {
433 beforeEach(inject(function ($controller) {
434 apiCalls.caseType = null;
435
436 ctrl = $controller('CaseTypeCtrl', {$scope: scope, apiCalls: apiCalls});
437 }));
438
439 it('sets default values for the case type title, name, and active status', function() {
440 expect(scope.caseType).toEqual(jasmine.objectContaining({
441 title: '',
442 name: '',
443 is_active: '1'
444 }));
445 });
446
447 it('adds an Open Case activty to the default activty set', function() {
448 expect(scope.caseType.definition.activitySets[0].activityTypes).toEqual([{
449 name: 'Open Case',
450 label: 'Open Case',
451 status: 'Completed',
452 default_assignee_type: defaultAssigneeDefaultValue.value
453 }]);
454 });
455 });
456 });
457
458 describe('crmAddName', function () {
459 var element;
460
461 beforeEach(function() {
462 scope = $rootScope.$new();
463 scope.activityTypeOptions = [1, 2, 3];
464 element = '<span crm-add-name crm-options="activityTypeOptions"></span>';
465
466 spyOn(CRM.$.fn, 'crmSelect2').and.callThrough();
467
468 element = $compile(element)(scope);
469 scope.$digest();
470 });
471
472 describe('when initialized', function () {
473 var returnValue;
474
475 beforeEach (function () {
476 var dataFunction = CRM.$.fn.crmSelect2.calls.argsFor(0)[0].data;
477 returnValue = dataFunction();
478 });
479
480 it('updates the UI with updated value of scope variable', function () {
481 expect(returnValue).toEqual({ results: scope.activityTypeOptions });
482 });
483 });
484 });
485
486 describe('crmEditableTabTitle', function () {
487 var element, titleLabel, penIcon, saveButton, cancelButton;
488
489 beforeEach(function() {
490 scope = $rootScope.$new();
491 element = '<div crm-editable-tab-title title="Click to edit">' +
492 '<span>{{ activitySet.label }}</span>' +
493 '</div>';
494
495 scope.activitySet = { label: 'Title'};
496 element = $compile(element)(scope);
497
498 titleLabel = $(element).find('span');
499 penIcon = $(element).find('i.fa-pencil');
500 saveButton = $(element).find('button[type=button]');
501 cancelButton = $(element).find('button[type=cancel]');
502
503 scope.$digest();
504 });
505
506 describe('when initialized', function () {
507 it('hides the save and cancel button', function () {
508 expect(saveButton.parent().css('display') === 'none').toBe(true);
509 expect(cancelButton.parent().css('display') === 'none').toBe(true);
510 });
511 });
512
513 describe('when clicked on title label', function () {
514 beforeEach(function () {
515 titleLabel.click();
516 });
517
518 it('hides the pen icon', function () {
519 expect(penIcon.css('display') === 'none').toBe(true);
520 });
521
522 it('shows the save button', function () {
523 expect(saveButton.parent().css('display') !== 'none').toBe(true);
524 });
525
526 it('makes the title editable', function () {
527 expect(titleLabel.attr('contenteditable')).toBe('true');
528 });
529 });
530
531 describe('when clicked outside of the editable area', function () {
532 beforeEach(function () {
533 titleLabel.click();
534 titleLabel.text('Updated Title');
535 titleLabel.blur();
536 $timeout.flush();
537 scope.$digest();
538 });
539
540 it('shows the pen icon', function () {
541 expect(penIcon.css('display') !== 'none').toBe(true);
542 });
543
544 it('hides the save and cancel button', function () {
545 expect(saveButton.parent().css('display') === 'none').toBe(true);
546 expect(cancelButton.parent().css('display') === 'none').toBe(true);
547 });
548
549 it('makes the title non editable', function () {
550 expect(titleLabel.attr('contenteditable')).not.toBe('true');
551 });
552
553 it('does not update the title in angular context', function () {
554 expect(scope.activitySet.label).toBe('Title');
555 });
556 });
557
558 describe('when ESCAPE key is pressed while typing', function () {
559 beforeEach(function () {
560 var eventObj = $.Event('keydown');
561 eventObj.key = 'Escape';
562
563 titleLabel.click();
564 titleLabel.text('Updated Title');
565 titleLabel.trigger(eventObj);
566 scope.$digest();
567 });
568
569 it('shows the pen icon', function () {
570 expect(penIcon.css('display') !== 'none').toBe(true);
571 });
572
573 it('hides the save and cancel button', function () {
574 expect(saveButton.parent().css('display') === 'none').toBe(true);
575 expect(cancelButton.parent().css('display') === 'none').toBe(true);
576 });
577
578 it('makes the title non editable', function () {
579 expect(titleLabel.attr('contenteditable')).not.toBe('true');
580 });
581
582 it('does not update the title', function () {
583 expect(scope.activitySet.label).toBe('Title');
584 });
585 });
586
587 describe('when ENTER key is pressed while typing', function () {
588 beforeEach(function () {
589 var eventObj = $.Event('keydown');
590 eventObj.key = 'Enter';
591
592 titleLabel.click();
593 titleLabel.text('Updated Title');
594 titleLabel.trigger(eventObj);
595 scope.$digest();
596 });
597
598 it('shows the pen icon', function () {
599 expect(penIcon.css('display') !== 'none').toBe(true);
600 });
601
602 it('hides the save and cancel button', function () {
603 expect(saveButton.parent().css('display') === 'none').toBe(true);
604 expect(cancelButton.parent().css('display') === 'none').toBe(true);
605 });
606
607 it('makes the title non editable', function () {
608 expect(titleLabel.attr('contenteditable')).not.toBe('true');
609 });
610
611 it('updates the title in angular context', function () {
612 expect(scope.activitySet.label).toBe('Updated Title');
613 });
614 });
615
616 describe('when SAVE button is clicked', function () {
617 beforeEach(function () {
618 titleLabel.click();
619 titleLabel.text('Updated Title');
620 saveButton.click();
621 scope.$digest();
622 });
623
624 it('shows the pen icon', function () {
625 expect(penIcon.css('display') !== 'none').toBe(true);
626 });
627
628 it('hides the save and cancel button', function () {
629 expect(saveButton.parent().css('display') === 'none').toBe(true);
630 expect(cancelButton.parent().css('display') === 'none').toBe(true);
631 });
632
633 it('makes the title non editable', function () {
634 expect(titleLabel.attr('contenteditable')).not.toBe('true');
635 });
636
637 it('updates the title in angular context', function () {
638 expect(scope.activitySet.label).toBe('Updated Title');
639 });
640 });
641
642 describe('when CANCEL button is clicked', function () {
643 beforeEach(function () {
644 titleLabel.click();
645 titleLabel.text('Updated Title');
646 cancelButton.click();
647 scope.$digest();
648 });
649
650 it('shows the pen icon', function () {
651 expect(penIcon.css('display') !== 'none').toBe(true);
652 });
653
654 it('hides the save and cancel button', function () {
655 expect(saveButton.parent().css('display') === 'none').toBe(true);
656 expect(cancelButton.parent().css('display') === 'none').toBe(true);
657 });
658
659 it('makes the title non editable', function () {
660 expect(titleLabel.attr('contenteditable')).not.toBe('true');
661 });
662
663 it('does not update the title in angular context', function () {
664 expect(scope.activitySet.label).toBe('Title');
665 });
666 });
667 });
668
669 describe('CaseTypeListCtrl', function() {
670 var caseTypes, crmApiSpy;
671
672 beforeEach(function() {
673 caseTypes = {
674 values: {
675 1: { id: 1 },
676 2: { id: 2 },
677 3: { id: 3 }
678 }
679 };
680 crmApiSpy = jasmine.createSpy('crmApi').and.returnValue($q.resolve());
681 scope = $rootScope.$new();
682 ctrl = $controller('CaseTypeListCtrl', {
683 $scope: scope,
684 caseTypes: caseTypes,
685 crmApi: crmApiSpy
686 });
687 });
688
689 it('should store an index of case types', function() {
690 expect(scope.caseTypes).toEqual(caseTypes.values);
691 });
692
693 describe('toggleCaseType', function() {
694 var caseType = { id: _.uniqueId() };
695
696 describe('when the case is active', function() {
697 beforeEach(function() {
698 caseType.is_active = '1';
699
700 scope.toggleCaseType(caseType);
701 });
702
703 it('sets the case type as inactive', function() {
704 expect(crmApiSpy).toHaveBeenCalledWith('CaseType', 'create', jasmine.objectContaining({
705 id: caseType.id,
706 is_active: '0'
707 }), true);
708 });
709 });
710
711 describe('when the case is inactive', function() {
712 beforeEach(function() {
713 caseType.is_active = '0';
714
715 scope.toggleCaseType(caseType);
716 });
717
718 it('sets the case type as active', function() {
719 expect(crmApiSpy).toHaveBeenCalledWith('CaseType', 'create', jasmine.objectContaining({
720 id: caseType.id,
721 is_active: '1'
722 }), true);
723 });
724 });
725 });
726
727 describe('deleteCaseType', function() {
728 var caseType = { id: _.uniqueId() };
729
730 beforeEach(function() {
731 crmApiSpy.and.returnValue($q.resolve(caseType));
732 scope.caseTypes[caseType.id] = caseType;
733
734 scope.deleteCaseType(caseType);
735 scope.$digest();
736 });
737
738 describe('when the case type can be deleted', function() {
739 it('deletes the case from the api', function() {
740 expect(crmApiSpy).toHaveBeenCalledWith('CaseType', 'delete', { id: caseType.id }, jasmine.any(Object));
741 });
742
743 it('removes the case type from the list', function() {
744 expect(scope.caseTypes[caseType.id]).toBeUndefined();
745 });
746 });
747
748 describe('when the case type cannot be delted', function() {
749 var error = { error_message: 'Error Message' };
750
751 beforeEach(function() {
752 var errorHandler;
753
754 crmApiSpy.and.returnValue($q.reject(error));
755 scope.caseTypes[caseType.id] = caseType;
756
757 spyOn(CRM, 'alert');
758 scope.deleteCaseType(caseType);
759 scope.$digest();
760
761 errorHandler = crmApiSpy.calls.mostRecent().args[3].error;
762 errorHandler(error);
763 });
764
765 it('displays the error message', function() {
766 expect(CRM.alert).toHaveBeenCalledWith(error.error_message, 'Error', 'error');
767 });
768 });
769
770 describe('revertCaseType', function() {
771 var caseType = {
772 id: _.uniqueId(),
773 definition: {},
774 is_forked: '1'
775 };
776
777 describe('when reverting a case type', function() {
778 beforeEach(function() {
779 scope.revertCaseType(caseType);
780 });
781
782 it('resets the case type information using the api', function() {
783 expect(crmApiSpy).toHaveBeenCalledWith('CaseType', 'create', jasmine.objectContaining({
784 id: caseType.id,
785 definition: 'null',
786 is_forked: '0'
787 }), true);
788 });
789 });
790 });
791 });
792 });
793 });