[ACA-3448] Candidate user is able to complete a task without a form attached before claiming it (#5780)

* [ACA-3448] Candidate user is able to complete a task without a form attached before claiming it

* * Added unit test to the recent changes

* * Updated unit test
* Fixed comment

* * Updated doc

* * Revered claim changes

* * Fixed comments

* * Added unit tests to the recent changes

* * Removed errorModel
This commit is contained in:
siva kumar 2020-06-22 16:29:07 +05:30 committed by GitHub
parent 29d953e2d1
commit 9a6fd0125b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 212 additions and 34 deletions

View File

@ -206,6 +206,41 @@ export let initiatorCanCompleteTaskDetailsMock = new TaskDetailsModel({
taskDefinitionKey: 'sid-DDECD9E4-0299-433F-9193-C3D905C3EEBE', taskDefinitionKey: 'sid-DDECD9E4-0299-433F-9193-C3D905C3EEBE',
executionId: '86', executionId: '86',
involvedGroups: [], involvedGroups: [],
involvedPeople: [],
memberOfCandidateUsers: false,
managerOfCandidateGroup: false,
memberOfCandidateGroup: false
});
export let initiatorWithCandidatesTaskDetailsMock = new TaskDetailsModel({
id: '91',
name: 'Request translation',
description: null,
category: null,
assignee: null,
created: '2016-11-03T15:25:42.749+0000',
dueDate: null,
endDate: null,
duration: null,
priority: 50,
parentTaskId: null,
parentTaskName: null,
processInstanceId: '86',
processInstanceName: null,
processDefinitionId: 'TranslationProcess:2:8',
processDefinitionName: 'Translation Process',
processDefinitionDescription: null,
processDefinitionKey: 'TranslationProcess',
processDefinitionCategory: 'http://www.activiti.org/processdef',
processDefinitionVersion: 2,
processDefinitionDeploymentId: '5',
formKey: '4',
processInstanceStartUserId: '1001',
initiatorCanCompleteTask: true,
adhocTaskCanBeReassigned: false,
taskDefinitionKey: 'sid-DDECD9E4-0299-433F-9193-C3D905C3EEBE',
executionId: '86',
involvedGroups: [],
involvedPeople: [ involvedPeople: [
{ {
id: 1001, id: 1001,
@ -220,9 +255,9 @@ export let initiatorCanCompleteTaskDetailsMock = new TaskDetailsModel({
email: 'fake@app.activiti.com' email: 'fake@app.activiti.com'
} }
], ],
memberOfCandidateUsers: false, memberOfCandidateUsers: true,
managerOfCandidateGroup: false, managerOfCandidateGroup: true,
memberOfCandidateGroup: false memberOfCandidateGroup: true
}); });
export let taskDetailsWithOutAssigneeMock = new TaskDetailsModel({ export let taskDetailsWithOutAssigneeMock = new TaskDetailsModel({
@ -316,10 +351,13 @@ export let claimedTaskDetailsMock = new TaskDetailsModel({
endDate: null, endDate: null,
duration: null, duration: null,
priority: 50, priority: 50,
formKey: '4',
parentTaskId: null, parentTaskId: null,
parentTaskName: null, parentTaskName: null,
processInstanceId: '86', processInstanceId: '86',
processInstanceName: null, processInstanceName: null,
processInstanceStartUserId: '1002',
initiatorCanCompleteTask: false,
processDefinitionId: 'TranslationProcess:2:8', processDefinitionId: 'TranslationProcess:2:8',
processDefinitionName: 'Translation Process', processDefinitionName: 'Translation Process',
involvedGroups: [ involvedGroups: [
@ -507,7 +545,7 @@ export let taskDetailsWithOutFormMock = new TaskDetailsModel({
'name': 'Request translation', 'name': 'Request translation',
'description': 'fake description', 'description': 'fake description',
'category': null, 'category': null,
'assignee': {'id': 1001, 'firstName': 'Admin', 'lastName': 'Paul', 'email': 'my@mymail.com' }, 'assignee': {'id': 1001, 'firstName': 'Admin', 'lastName': 'Paul', 'email': 'fake-email@gmail.com' },
'created': '2016-11-03T15:25:42.749+0000', 'created': '2016-11-03T15:25:42.749+0000',
'dueDate': '2016-11-03T15:25:42.749+0000', 'dueDate': '2016-11-03T15:25:42.749+0000',
'endDate': null, 'endDate': null,

View File

@ -5,7 +5,6 @@
[showValidationIcon]="showFormValidationIcon" [showValidationIcon]="showFormValidationIcon"
[showRefreshButton]="showFormRefreshButton" [showRefreshButton]="showFormRefreshButton"
[showCompleteButton]="showFormCompleteButton" [showCompleteButton]="showFormCompleteButton"
[disableCompleteButton]="!isCompleteButtonEnabled()"
[showSaveButton]="isSaveButtonVisible()" [showSaveButton]="isSaveButtonVisible()"
[readOnly]="isReadOnlyForm()" [readOnly]="isReadOnlyForm()"
[fieldValidators]="fieldValidators" [fieldValidators]="fieldValidators"
@ -61,10 +60,11 @@
</mat-card-content> </mat-card-content>
<mat-card-actions class="adf-task-form-actions"> <mat-card-actions class="adf-task-form-actions">
<ng-template [ngTemplateOutlet]="taskFormButtons"></ng-template> <ng-template [ngTemplateOutlet]="taskFormButtons"></ng-template>
<button id="adf-no-form-cancel-button" mat-button *ngIf="showCancelButton" (click)="onCancel()"> <button mat-button
{{'ADF_TASK_FORM.EMPTY_FORM.BUTTONS.CANCEL' | translate}} *ngIf="!isCompletedTask()" id="adf-no-form-complete-button"
</button> color="primary"
<button mat-button *ngIf="!isCompletedTask()" color="primary" (click)="onCompleteTask()" id="adf-no-form-complete-button"> [disabled]="canCompleteNoFormTask()"
(click)="onCompleteTask()">
{{'ADF_TASK_FORM.EMPTY_FORM.BUTTONS.COMPLETE' | translate}} {{'ADF_TASK_FORM.EMPTY_FORM.BUTTONS.COMPLETE' | translate}}
</button> </button>
</mat-card-actions> </mat-card-actions>
@ -73,18 +73,25 @@
</ng-template> </ng-template>
<ng-template #taskFormButtons> <ng-template #taskFormButtons>
<button mat-button id="adf-no-form-cancel-button"
*ngIf="showCancelButton"
(click)="onCancel()">
{{'ADF_TASK_FORM.EMPTY_FORM.BUTTONS.CANCEL' | translate}}
</button>
<button mat-button data-automation-id="adf-task-form-claim-button" <button mat-button data-automation-id="adf-task-form-claim-button"
*ngIf="isTaskClaimable()" *ngIf="isTaskClaimable()"
adf-claim-task adf-claim-task
[taskId]="taskId" [taskId]="taskId"
(success)="onClaimTask($event)"> (success)="onClaimTask($event)"
(error)="onClaimTaskError($event)">
{{ 'ADF_TASK_LIST.DETAILS.BUTTON.CLAIM' | translate }} {{ 'ADF_TASK_LIST.DETAILS.BUTTON.CLAIM' | translate }}
</button> </button>
<button mat-button data-automation-id="adf-task-form-unclaim-button" <button mat-button data-automation-id="adf-task-form-unclaim-button"
*ngIf="isTaskClaimedByCandidateMember()" *ngIf="isTaskClaimedByCandidateMember()"
adf-unclaim-task adf-unclaim-task
[taskId]="taskId" [taskId]="taskId"
(success)="onUnclaimTask($event)"> (success)="onUnclaimTask($event)"
(error)="onUnclaimTaskError($event)">
{{ 'ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM' | translate }} {{ 'ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM' | translate }}
</button> </button>
</ng-template> </ng-template>

View File

@ -40,7 +40,8 @@ import {
initiatorCanCompleteTaskDetailsMock, initiatorCanCompleteTaskDetailsMock,
taskDetailsWithOutCandidateGroup, taskDetailsWithOutCandidateGroup,
claimedTaskDetailsMock, claimedTaskDetailsMock,
claimedByGroupMemberMock claimedByGroupMemberMock,
initiatorWithCandidatesTaskDetailsMock
} from '../../../mock/task/task-details.mock'; } from '../../../mock/task/task-details.mock';
import { TaskDetailsModel } from '../../models/task-details.model'; import { TaskDetailsModel } from '../../models/task-details.model';
import { ProcessTestingModule } from '../../../testing/process.testing.module'; import { ProcessTestingModule } from '../../../testing/process.testing.module';
@ -80,7 +81,7 @@ describe('TaskFormComponent', () => {
taskDetailsMock.processDefinitionId = null; taskDetailsMock.processDefinitionId = null;
spyOn(formService, 'getTask').and.returnValue(of(taskDetailsMock)); spyOn(formService, 'getTask').and.returnValue(of(taskDetailsMock));
authService = TestBed.get(AuthenticationService); authService = TestBed.get(AuthenticationService);
getBpmLoggedUserSpy = spyOn(authService, 'getBpmLoggedUser').and.returnValue(of({ email: 'fake-email' })); getBpmLoggedUserSpy = spyOn(authService, 'getBpmLoggedUser').and.returnValue(of({ id: 1001, email: 'fake-email@gmail.com' }));
}); });
afterEach(async() => { afterEach(async() => {
@ -129,22 +130,6 @@ describe('TaskFormComponent', () => {
expect(formCompletedSpy).toHaveBeenCalled(); expect(formCompletedSpy).toHaveBeenCalled();
}); });
it('Should be able to complete the task as a process initiator', async () => {
const formCompletedSpy: jasmine.Spy = spyOn(component.formCompleted, 'emit');
const completeTaskFormSpy = spyOn(formService, 'completeTaskForm').and.returnValue(of({}));
getTaskDetailsSpy.and.returnValue(of(initiatorCanCompleteTaskDetailsMock));
component.taskId = '123';
fixture.detectChanges();
await fixture.whenStable();
const activitFormSelector = element.querySelector('adf-form');
const completeButton = fixture.debugElement.nativeElement.querySelector('#adf-form-complete');
expect(activitFormSelector).toBeDefined();
expect(completeButton['disabled']).toEqual(false);
completeButton.click();
expect(completeTaskFormSpy).toHaveBeenCalled();
expect(formCompletedSpy).toHaveBeenCalled();
});
it('Should emit error event in case form complete service fails', async () => { it('Should emit error event in case form complete service fails', async () => {
const errorSpy: jasmine.Spy = spyOn(component.error, 'emit'); const errorSpy: jasmine.Spy = spyOn(component.error, 'emit');
const completeTaskFormSpy = spyOn(formService, 'completeTaskForm').and.returnValue(throwError({message: 'servce failed'})); const completeTaskFormSpy = spyOn(formService, 'completeTaskForm').and.returnValue(throwError({message: 'servce failed'}));
@ -160,7 +145,6 @@ describe('TaskFormComponent', () => {
expect(completeTaskFormSpy).toHaveBeenCalled(); expect(completeTaskFormSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalled(); expect(errorSpy).toHaveBeenCalled();
}); });
}); });
describe('change detection', () => { describe('change detection', () => {
@ -500,6 +484,97 @@ describe('TaskFormComponent', () => {
}); });
}); });
describe('Complete task', () => {
it('Should be able to complete the assigned task in case process initiator not allowed to complete the task', async () => {
getBpmLoggedUserSpy.and.returnValue(of({ id: 1002, firstName: 'Wilbur', lastName: 'Adams', email: 'wilbur@app.activiti.com' }));
getTaskDetailsSpy.and.returnValue(of(taskDetailsMock));
const formCompletedSpy: jasmine.Spy = spyOn(component.formCompleted, 'emit');
const completeTaskFormSpy = spyOn(formService, 'completeTaskForm').and.returnValue(of({}));
component.taskId = '123';
component.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
const completeButton = fixture.debugElement.nativeElement.querySelector('#adf-form-complete');
expect(component.isProcessInitiator()).toEqual(false);
expect(completeButton['disabled']).toEqual(false);
completeButton.click();
expect(completeTaskFormSpy).toHaveBeenCalled();
expect(formCompletedSpy).toHaveBeenCalled();
});
it('Should be able to complete the task if process initiator allowed to complete the task', async () => {
getBpmLoggedUserSpy.and.returnValue(of({ id: 1001, firstName: 'Wilbur', lastName: 'Adams', email: 'wilbur@app.activiti.com' }));
const formCompletedSpy: jasmine.Spy = spyOn(component.formCompleted, 'emit');
const completeTaskFormSpy = spyOn(formService, 'completeTaskForm').and.returnValue(of({}));
getTaskDetailsSpy.and.returnValue(of(initiatorCanCompleteTaskDetailsMock));
component.taskId = '123';
fixture.detectChanges();
await fixture.whenStable();
const activitFormSelector = element.querySelector('adf-form');
const completeButton = fixture.debugElement.nativeElement.querySelector('#adf-form-complete');
expect(activitFormSelector).toBeDefined();
expect(completeButton['disabled']).toEqual(false);
expect(component.isProcessInitiator()).toEqual(true);
completeButton.click();
expect(completeTaskFormSpy).toHaveBeenCalled();
expect(formCompletedSpy).toHaveBeenCalled();
});
it('Should not be able to complete a task with candidates users if process initiator allowed to complete the task', async () => {
getTaskDetailsSpy.and.returnValue(of(initiatorWithCandidatesTaskDetailsMock));
component.taskId = '123';
fixture.detectChanges();
await fixture.whenStable();
expect(component.canInitiatorComplete()).toEqual(true);
expect(component.isProcessInitiator()).toEqual(true);
expect(component.isCandidateMember()).toEqual(true);
const activitFormSelector = element.querySelector('adf-form');
const completeButton = fixture.debugElement.nativeElement.querySelector('#adf-form-complete');
expect(activitFormSelector).toBeDefined();
expect(completeButton['disabled']).toEqual(true);
});
it('Should be able to complete a task with candidates users if process initiator not allowed to complete the task', async () => {
const formCompletedSpy: jasmine.Spy = spyOn(component.formCompleted, 'emit');
getBpmLoggedUserSpy.and.returnValue(of({ id: 1001, firstName: 'Wilbur', lastName: 'Adams', email: 'wilbur@app.activiti.com' }));
const completeTaskFormSpy = spyOn(formService, 'completeTaskForm').and.returnValue(of({}));
getTaskDetailsSpy.and.returnValue(of(claimedTaskDetailsMock));
component.taskId = '123';
fixture.detectChanges();
await fixture.whenStable();
const activitFormSelector = element.querySelector('adf-form');
const completeButton = fixture.debugElement.nativeElement.querySelector('#adf-form-complete');
expect(activitFormSelector).toBeDefined();
expect(component.canInitiatorComplete()).toEqual(false);
expect(component.isProcessInitiator()).toEqual(false);
expect(completeButton['disabled']).toEqual(false);
completeButton.click();
expect(completeTaskFormSpy).toHaveBeenCalled();
expect(formCompletedSpy).toHaveBeenCalled();
});
});
describe('Claim/Unclaim buttons', () => { describe('Claim/Unclaim buttons', () => {
it('should display the claim button if no assignee', async() => { it('should display the claim button if no assignee', async() => {
@ -589,7 +664,7 @@ describe('TaskFormComponent', () => {
component.taskId = 'mock-task-id'; component.taskId = 'mock-task-id';
component.taskClaimed.subscribe((taskId) => { component.taskClaimed.subscribe((taskId: string) => {
expect(taskId).toEqual(component.taskId); expect(taskId).toEqual(component.taskId);
done(); done();
}); });
@ -601,6 +676,25 @@ describe('TaskFormComponent', () => {
claimBtn.nativeElement.click(); claimBtn.nativeElement.click();
}); });
it('should emit error event in case claim task api fails', (done) => {
const mockError = { message: 'Api Failed' };
spyOn(taskListService, 'claimTask').and.returnValue(throwError(mockError));
getTaskDetailsSpy.and.returnValue(of(claimableTaskDetailsMock));
component.taskId = 'mock-task-id';
component.error.subscribe((error: any) => {
expect(error).toEqual(mockError);
done();
});
component.ngOnInit();
fixture.detectChanges();
const claimBtn = fixture.debugElement.query(By.css('[adf-claim-task]'));
claimBtn.nativeElement.click();
});
it('should emit taskUnClaimed when task is unclaimed', (done) => { it('should emit taskUnClaimed when task is unclaimed', (done) => {
spyOn(taskListService, 'unclaimTask').and.returnValue(of({})); spyOn(taskListService, 'unclaimTask').and.returnValue(of({}));
getBpmLoggedUserSpy.and.returnValue(of(claimedTaskDetailsMock.assignee)); getBpmLoggedUserSpy.and.returnValue(of(claimedTaskDetailsMock.assignee));
@ -619,5 +713,25 @@ describe('TaskFormComponent', () => {
const unclaimBtn = fixture.debugElement.query(By.css('[adf-unclaim-task]')); const unclaimBtn = fixture.debugElement.query(By.css('[adf-unclaim-task]'));
unclaimBtn.nativeElement.click(); unclaimBtn.nativeElement.click();
}); });
it('should emit error event in case unclaim task api fails', (done) => {
const mockError = { message: 'Api Failed' };
spyOn(taskListService, 'unclaimTask').and.returnValue(throwError(mockError));
getBpmLoggedUserSpy.and.returnValue(of(claimedTaskDetailsMock.assignee));
getTaskDetailsSpy.and.returnValue(of(claimedTaskDetailsMock));
component.taskId = 'mock-task-id';
component.error.subscribe((error: any) => {
expect(error).toEqual(mockError);
done();
});
component.ngOnInit();
fixture.detectChanges();
const unclaimBtn = fixture.debugElement.query(By.css('[adf-unclaim-task]'));
unclaimBtn.nativeElement.click();
});
}); });
}); });

View File

@ -272,15 +272,26 @@ export class TaskFormComponent implements OnInit {
} }
isReadOnlyForm(): boolean { isReadOnlyForm(): boolean {
return this.internalReadOnlyForm || !(this.isAssignedToMe() || this.canInitiatorComplete()); let readOnlyForm: boolean;
if (this.isCandidateMember()) {
readOnlyForm = this.internalReadOnlyForm || !this.isAssignedToMe();
} else {
readOnlyForm = this.internalReadOnlyForm || !(this.isAssignedToMe() || (this.canInitiatorComplete() && this.isProcessInitiator()));
}
return readOnlyForm;
}
isProcessInitiator(): boolean {
return this.currentLoggedUser && ( this.currentLoggedUser.id === +this.taskDetails.processInstanceStartUserId);
} }
isSaveButtonVisible(): boolean { isSaveButtonVisible(): boolean {
return this.showFormSaveButton && (!this.canInitiatorComplete() || this.isAssignedToMe()); return this.showFormSaveButton && (!this.canInitiatorComplete() || this.isAssignedToMe());
} }
canCompleteTask(): boolean { canCompleteNoFormTask(): boolean {
return !this.isCompletedTask() && this.isAssignedToMe(); return this.isReadOnlyForm();
} }
getCompletedTaskTranslatedMessage(): Observable<string> { getCompletedTaskTranslatedMessage(): Observable<string> {
@ -307,7 +318,15 @@ export class TaskFormComponent implements OnInit {
this.taskClaimed.emit(taskId); this.taskClaimed.emit(taskId);
} }
onClaimTaskError(error: any) {
this.error.emit(error);
}
onUnclaimTask(taskId: string) { onUnclaimTask(taskId: string) {
this.taskUnclaimed.emit(taskId); this.taskUnclaimed.emit(taskId);
} }
onUnclaimTaskError(error: any) {
this.error.emit(error);
}
} }