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