Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Jun 22, 2020
46 changes: 42 additions & 4 deletions lib/process-services/src/lib/mock/task/task-details.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,41 @@ export let initiatorCanCompleteTaskDetailsMock = new TaskDetailsModel({
taskDefinitionKey: 'sid-DDECD9E4-0299-433F-9193-C3D905C3EEBE',
executionId: '86',
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: [
{
id: 1001,
Expand All @@ -220,9 +255,9 @@ export let initiatorCanCompleteTaskDetailsMock = new TaskDetailsModel({
email: 'fake@app.activiti.com'
}
],
memberOfCandidateUsers: false,
managerOfCandidateGroup: false,
memberOfCandidateGroup: false
memberOfCandidateUsers: true,
managerOfCandidateGroup: true,
memberOfCandidateGroup: true
});

export let taskDetailsWithOutAssigneeMock = new TaskDetailsModel({
Expand Down Expand Up @@ -316,10 +351,13 @@ export let claimedTaskDetailsMock = new TaskDetailsModel({
endDate: null,
duration: null,
priority: 50,
formKey: '4',
parentTaskId: null,
parentTaskName: null,
processInstanceId: '86',
processInstanceName: null,
processInstanceStartUserId: '1002',
initiatorCanCompleteTask: false,
processDefinitionId: 'TranslationProcess:2:8',
processDefinitionName: 'Translation Process',
involvedGroups: [
Expand Down Expand Up @@ -507,7 +545,7 @@ export let taskDetailsWithOutFormMock = new TaskDetailsModel({
'name': 'Request translation',
'description': 'fake description',
'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',
'dueDate': '2016-11-03T15:25:42.749+0000',
'endDate': null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
[showValidationIcon]="showFormValidationIcon"
[showRefreshButton]="showFormRefreshButton"
[showCompleteButton]="showFormCompleteButton"
[disableCompleteButton]="!isCompleteButtonEnabled()"
[showSaveButton]="isSaveButtonVisible()"
[readOnly]="isReadOnlyForm()"
[fieldValidators]="fieldValidators"
Expand Down Expand Up @@ -61,10 +60,11 @@ <h4>
</mat-card-content>
<mat-card-actions class="adf-task-form-actions">
<ng-template [ngTemplateOutlet]="taskFormButtons"></ng-template>
<button id="adf-no-form-cancel-button" mat-button *ngIf="showCancelButton" (click)="onCancel()">
{{'ADF_TASK_FORM.EMPTY_FORM.BUTTONS.CANCEL' | translate}}
</button>
<button mat-button *ngIf="!isCompletedTask()" color="primary" (click)="onCompleteTask()" id="adf-no-form-complete-button">
<button mat-button
*ngIf="!isCompletedTask()" id="adf-no-form-complete-button"
color="primary"
[disabled]="canCompleteNoFormTask()"
(click)="onCompleteTask()">
{{'ADF_TASK_FORM.EMPTY_FORM.BUTTONS.COMPLETE' | translate}}
</button>
</mat-card-actions>
Expand All @@ -73,18 +73,25 @@ <h4>
</ng-template>

<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"
*ngIf="isTaskClaimable()"
adf-claim-task
[taskId]="taskId"
(success)="onClaimTask($event)">
(success)="onClaimTask($event)"
(error)="onClaimTaskError($event)">
{{ 'ADF_TASK_LIST.DETAILS.BUTTON.CLAIM' | translate }}
</button>
<button mat-button data-automation-id="adf-task-form-unclaim-button"
*ngIf="isTaskClaimedByCandidateMember()"
adf-unclaim-task
[taskId]="taskId"
(success)="onUnclaimTask($event)">
(success)="onUnclaimTask($event)"
(error)="onUnclaimTaskError($event)">
{{ 'ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM' | translate }}
</button>
</ng-template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ import {
initiatorCanCompleteTaskDetailsMock,
taskDetailsWithOutCandidateGroup,
claimedTaskDetailsMock,
claimedByGroupMemberMock
claimedByGroupMemberMock,
initiatorWithCandidatesTaskDetailsMock
} from '../../../mock/task/task-details.mock';
import { TaskDetailsModel } from '../../models/task-details.model';
import { ProcessTestingModule } from '../../../testing/process.testing.module';
Expand Down Expand Up @@ -80,7 +81,7 @@ describe('TaskFormComponent', () => {
taskDetailsMock.processDefinitionId = null;
spyOn(formService, 'getTask').and.returnValue(of(taskDetailsMock));
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() => {
Expand Down Expand Up @@ -129,22 +130,6 @@ describe('TaskFormComponent', () => {
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 () => {
const errorSpy: jasmine.Spy = spyOn(component.error, 'emit');
const completeTaskFormSpy = spyOn(formService, 'completeTaskForm').and.returnValue(throwError({message: 'servce failed'}));
Expand All @@ -160,7 +145,6 @@ describe('TaskFormComponent', () => {
expect(completeTaskFormSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalled();
});

});

describe('change detection', () => {
Expand Down Expand Up @@ -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', () => {

it('should display the claim button if no assignee', async() => {
Expand Down Expand Up @@ -589,7 +664,7 @@ describe('TaskFormComponent', () => {

component.taskId = 'mock-task-id';

component.taskClaimed.subscribe((taskId) => {
component.taskClaimed.subscribe((taskId: string) => {
expect(taskId).toEqual(component.taskId);
done();
});
Expand All @@ -601,6 +676,25 @@ describe('TaskFormComponent', () => {
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) => {
spyOn(taskListService, 'unclaimTask').and.returnValue(of({}));
getBpmLoggedUserSpy.and.returnValue(of(claimedTaskDetailsMock.assignee));
Expand All @@ -619,5 +713,25 @@ describe('TaskFormComponent', () => {
const unclaimBtn = fixture.debugElement.query(By.css('[adf-unclaim-task]'));
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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -272,15 +272,26 @@ export class TaskFormComponent implements OnInit {
}

isReadOnlyForm(): boolean {
return this.internalReadOnlyForm || !(this.isAssignedToMe() || this.canInitiatorComplete());
let readOnlyForm: boolean;
if (this.isCandidateMember()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any unit test related to this part. do we have it ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+this.taskDetails.processInstanceStartUserId is something wanted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah we need it, where UserRepresentation id is a number type and taskDetails.processInstanceStartUserId is string type.

}

isSaveButtonVisible(): boolean {
return this.showFormSaveButton && (!this.canInitiatorComplete() || this.isAssignedToMe());
}

canCompleteTask(): boolean {
return !this.isCompletedTask() && this.isAssignedToMe();
canCompleteNoFormTask(): boolean {
return this.isReadOnlyForm();
}

getCompletedTaskTranslatedMessage(): Observable<string> {
Expand All @@ -307,7 +318,15 @@ export class TaskFormComponent implements OnInit {
this.taskClaimed.emit(taskId);
}

onClaimTaskError(error: any) {
this.error.emit(error);
}

onUnclaimTask(taskId: string) {
this.taskUnclaimed.emit(taskId);
}

onUnclaimTaskError(error: any) {
this.error.emit(error);
}
}