From b79319d1b5e8b1adcea7aa1d2a55acf90722c1aa Mon Sep 17 00:00:00 2001 From: Will Abson Date: Wed, 16 Nov 2016 17:02:55 +0000 Subject: [PATCH] Reset form when cancel button clicked - Remove use of any in component and add types - Small refactoring of method name - Add many more tests Refs #1066 --- .../activiti-start-process.component.mock.ts | 172 +++++++++++ .../activiti-start-process.component.html | 8 +- .../activiti-start-process.component.spec.ts | 280 +++++++++++++----- .../activiti-start-process.component.ts | 31 +- 4 files changed, 402 insertions(+), 89 deletions(-) create mode 100644 ng2-components/ng2-activiti-processlist/src/assets/activiti-start-process.component.mock.ts diff --git a/ng2-components/ng2-activiti-processlist/src/assets/activiti-start-process.component.mock.ts b/ng2-components/ng2-activiti-processlist/src/assets/activiti-start-process.component.mock.ts new file mode 100644 index 0000000000..da1ed613fe --- /dev/null +++ b/ng2-components/ng2-activiti-processlist/src/assets/activiti-start-process.component.mock.ts @@ -0,0 +1,172 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ProcessInstance } from './../models/process-instance.model'; +import { ProcessDefinitionRepresentation } from './../models/process-definition.model'; + +export var newProcess = new ProcessInstance({ + id: '32323', + name: 'Process' +}); + +export var fakeProcessDefs = [new ProcessDefinitionRepresentation({ + id: 'my:process1', + name: 'My Process 1', + hasStartForm: false +}), new ProcessDefinitionRepresentation({ + id: 'my:process2', + name: 'My Process 2', + hasStartForm: false +})]; + +export var fakeProcessDefWithForm = [new ProcessDefinitionRepresentation({ + id: 'my:process1', + name: 'My Process 1', + hasStartForm: true +})]; + +export var taskFormMock = { + 'id': 4, + 'name': 'Translation request', + 'processDefinitionId': 'TranslationProcess:2:8', + 'processDefinitionName': 'Translation Process', + 'processDefinitionKey': 'TranslationProcess', + 'taskId': '91', + 'taskName': 'Request translation', + 'taskDefinitionKey': 'sid-DDECD9E4-0299-433F-9193-C3D905C3EEBE', + 'tabs': [], + 'fields': [{ + 'fieldType': 'ContainerRepresentation', + 'id': '1478093984155', + 'name': 'Label', + 'type': 'container', + 'value': null, + 'required': false, + 'readOnly': false, + 'overrideId': false, + 'colspan': 1, + 'placeholder': null, + 'minLength': 0, + 'maxLength': 0, + 'minValue': null, + 'maxValue': null, + 'regexPattern': null, + 'optionType': null, + 'hasEmptyValue': null, + 'options': null, + 'restUrl': null, + 'restResponsePath': null, + 'restIdProperty': null, + 'restLabelProperty': null, + 'tab': null, + 'className': null, + 'dateDisplayFormat': null, + 'layout': null, + 'sizeX': 2, + 'sizeY': 1, + 'row': -1, + 'col': -1, + 'visibilityCondition': null, + 'numberOfColumns': 2, + 'fields': { + '1': [{ + 'fieldType': 'AttachFileFieldRepresentation', + 'id': 'originalcontent', + 'name': 'Original content', + 'type': 'upload', + 'value': [], + 'required': true, + 'readOnly': false, + 'overrideId': false, + 'colspan': 1, + 'placeholder': null, + 'minLength': 0, + 'maxLength': 0, + 'minValue': null, + 'maxValue': null, + 'regexPattern': null, + 'optionType': null, + 'hasEmptyValue': null, + 'options': null, + 'restUrl': null, + 'restResponsePath': null, + 'restIdProperty': null, + 'restLabelProperty': null, + 'tab': null, + 'className': null, + 'params': { + }, + 'dateDisplayFormat': null, + 'layout': {'row': -1, 'column': -1, 'colspan': 1}, + 'sizeX': 1, + 'sizeY': 1, + 'row': -1, + 'col': -1, + 'visibilityCondition': null, + 'metaDataColumnDefinitions': [] + }], + '2': [{ + 'fieldType': 'RestFieldRepresentation', + 'id': 'language', + 'name': 'Language', + 'type': 'dropdown', + 'value': 'Choose one...', + 'required': true, + 'readOnly': false, + 'overrideId': false, + 'colspan': 1, + 'placeholder': null, + 'minLength': 0, + 'maxLength': 0, + 'minValue': null, + 'maxValue': null, + 'regexPattern': null, + 'optionType': null, + 'hasEmptyValue': true, + 'options': [{'id': 'empty', 'name': 'Choose one...'}, {'id': 'fr', 'name': 'French'}, { + 'id': 'de', + 'name': 'German' + }, {'id': 'es', 'name': 'Spanish'}], + 'restUrl': null, + 'restResponsePath': null, + 'restIdProperty': null, + 'restLabelProperty': null, + 'tab': null, + 'className': null, + 'params': {'existingColspan': 1, 'maxColspan': 1}, + 'dateDisplayFormat': null, + 'layout': {'row': -1, 'column': -1, 'colspan': 1}, + 'sizeX': 1, + 'sizeY': 1, + 'row': -1, + 'col': -1, + 'visibilityCondition': null, + 'endpoint': null, + 'requestHeaders': null + }] + } + }], + 'outcomes': [], + 'javascriptEvents': [], + 'className': '', + 'style': '', + 'customFieldTemplates': {}, + 'metadata': {}, + 'variables': [], + 'gridsterForm': false, + 'globalDateFormat': 'D-M-YYYY' +}; diff --git a/ng2-components/ng2-activiti-processlist/src/components/activiti-start-process.component.html b/ng2-components/ng2-activiti-processlist/src/components/activiti-start-process.component.html index c506e6f7f9..fba8ef3380 100644 --- a/ng2-components/ng2-activiti-processlist/src/components/activiti-start-process.component.html +++ b/ng2-components/ng2-activiti-processlist/src/components/activiti-start-process.component.html @@ -4,7 +4,7 @@

{{'START_PROCESS.DIALOG.TITLE'|translate}}

-

- +
- - + +
diff --git a/ng2-components/ng2-activiti-processlist/src/components/activiti-start-process.component.spec.ts b/ng2-components/ng2-activiti-processlist/src/components/activiti-start-process.component.spec.ts index 2e9ddef461..708cd042e0 100644 --- a/ng2-components/ng2-activiti-processlist/src/components/activiti-start-process.component.spec.ts +++ b/ng2-components/ng2-activiti-processlist/src/components/activiti-start-process.component.spec.ts @@ -17,12 +17,13 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { DebugElement, SimpleChange } from '@angular/core'; import { Observable } from 'rxjs/Rx'; -import { ActivitiFormModule } from 'ng2-activiti-form'; import { AlfrescoTranslationService, CoreModule } from 'ng2-alfresco-core'; -import { FormService } from 'ng2-activiti-form'; +import { ActivitiFormModule, FormService } from 'ng2-activiti-form'; + import { TranslationMock } from './../assets/translation.service.mock'; +import { newProcess, fakeProcessDefs, fakeProcessDefWithForm, taskFormMock } from './../assets/activiti-start-process.component.mock'; import { ActivitiStartProcessButton } from './activiti-start-process.component'; import { ActivitiProcessService } from '../services/activiti-process.service'; @@ -34,14 +35,10 @@ describe('ActivitiStartProcessButton', () => { let processService: ActivitiProcessService; let formService: FormService; let getDefinitionsSpy: jasmine.Spy; + let getStartFormDefinitionSpy: jasmine.Spy; let startProcessSpy: jasmine.Spy; let debugElement: DebugElement; - let newProcess = { - id: '32323', - name: 'Process' - }; - beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ CoreModule, ActivitiFormModule ], @@ -64,14 +61,9 @@ describe('ActivitiStartProcessButton', () => { processService = fixture.debugElement.injector.get(ActivitiProcessService); formService = fixture.debugElement.injector.get(FormService); - getDefinitionsSpy = spyOn(processService, 'getProcessDefinitions').and.returnValue(Observable.of([{ - id: 'my:process1', - name: 'My Process 1' - }, { - id: 'my:process2', - name: 'My Process 2' - }])); + getDefinitionsSpy = spyOn(processService, 'getProcessDefinitions').and.returnValue(Observable.of(fakeProcessDefs)); startProcessSpy = spyOn(processService, 'startProcess').and.returnValue(Observable.of(newProcess)); + getStartFormDefinitionSpy = spyOn(formService, 'getStartFormDefinition').and.returnValue(Observable.of(taskFormMock)); componentHandler = jasmine.createSpyObj('componentHandler', [ 'upgradeAllRegistered', @@ -80,79 +72,217 @@ describe('ActivitiStartProcessButton', () => { window['componentHandler'] = componentHandler; }); - it('should display the correct number of processes in the select list', () => { - fixture.detectChanges(); - let selectElement = debugElement.query(By.css('select')); - expect(selectElement.children.length).toBe(3); + describe('process definitions list', () => { + + it('should call service to fetch process definitions', () => { + fixture.detectChanges(); + expect(getDefinitionsSpy).toHaveBeenCalled(); + }); + + it('should call service to fetch process definitions with appId when provided', () => { + let appId = '123'; + component.appId = appId; + fixture.detectChanges(); + expect(getDefinitionsSpy).toHaveBeenCalledWith(appId); + }); + + it('should display the correct number of processes in the select list', () => { + fixture.detectChanges(); + let selectElement = debugElement.query(By.css('select')); + expect(selectElement.children.length).toBe(3); + }); + + it('should display the correct process def details', (done) => { + fixture.detectChanges(); + fixture.whenStable().then(() => { + let optionEl: HTMLOptionElement = debugElement.queryAll(By.css('select option'))[1].nativeElement; + expect(optionEl.value).toBe('my:process1'); + expect(optionEl.textContent.trim()).toBe('My Process 1'); + done(); + }); + }); + }); - it('should display the correct process def details', (done) => { - fixture.detectChanges(); - fixture.whenStable().then(() => { - let optionEl: HTMLOptionElement = debugElement.queryAll(By.css('select option'))[1].nativeElement; - expect(optionEl.value).toBe('my:process1'); - expect(optionEl.textContent.trim()).toBe('My Process 1'); - done(); + describe('input changes', () => { + + let change = new SimpleChange('123', '456'); + let nullChange = new SimpleChange('123', null); + + beforeEach(async(() => { + component.appId = '123'; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + getDefinitionsSpy.calls.reset(); + }); + })); + + it('should reload processes when appId input changed', () => { + component.ngOnChanges({ appId: change }); + expect(getDefinitionsSpy).toHaveBeenCalledWith('456'); }); + + it('should reload processes when appId input changed to null', () => { + component.ngOnChanges({ appId: nullChange }); + expect(getDefinitionsSpy).toHaveBeenCalledWith(null); + }); + + it('should not reload processes when changes do not include appId input', () => { + component.ngOnChanges({}); + expect(getDefinitionsSpy).not.toHaveBeenCalled(); + }); + }); - it('should call service to start process if required fields provided', (done) => { - component.name = 'My new process'; - component.showDialog(); - fixture.detectChanges(); - component.onChange('my:process1'); - component.startProcess(); - fixture.whenStable().then(() => { - expect(startProcessSpy).toHaveBeenCalled(); - done(); + describe('start process', () => { + + beforeEach(() => { + component.name = 'My new process'; + component.showDialog(); + fixture.detectChanges(); }); + + it('should call service to start process if required fields provided', async(() => { + component.onProcessDefChange('my:process1'); + component.startProcess(); + fixture.whenStable().then(() => { + expect(startProcessSpy).toHaveBeenCalled(); + }); + })); + + it('should avoid calling service to start process if required fields NOT provided', async(() => { + component.name = ''; + component.startProcess(); + fixture.whenStable().then(() => { + expect(startProcessSpy).not.toHaveBeenCalled(); + }); + })); + + it('should call service to start process with the correct parameters', async(() => { + component.onProcessDefChange('my:process1'); + component.startProcess(); + fixture.whenStable().then(() => { + expect(startProcessSpy).toHaveBeenCalledWith('my:process1', 'My new process', undefined); + }); + })); + + it('should output start event when process started successfully', async(() => { + let emitSpy = spyOn(component.start, 'emit'); + component.onProcessDefChange('my:process1'); + component.startProcess(); + fixture.whenStable().then(() => { + expect(emitSpy).toHaveBeenCalledWith(newProcess); + }); + })); + }); - it('should avoid calling service to start process if required fields NOT provided', (done) => { - component.showDialog(); - fixture.detectChanges(); - component.startProcess(); - fixture.whenStable().then(() => { - expect(startProcessSpy).not.toHaveBeenCalled(); - done(); + describe('start forms', () => { + + describe('without start form', () => { + + beforeEach(async(() => { + component.name = 'My new process'; + component.showDialog(); + fixture.detectChanges(); + component.onProcessDefChange('my:process1'); + fixture.whenStable(); + })); + + it('should indicate start form is missing', async(() => { + expect(component.isStartFormMissingOrValid()).toBe(true); + })); + + it('should enable start button when name and process filled out', async(() => { + fixture.detectChanges(); + let startBtn = debugElement.query(By.css('[data-automation-id="btn-start"]')); + expect(startBtn.properties['disabled']).toBe(false); + })); + }); + + describe('with start form', () => { + + beforeEach(() => { + getDefinitionsSpy.and.returnValue(Observable.of(fakeProcessDefWithForm)); + component.showDialog(); + fixture.detectChanges(); + component.onProcessDefChange('my:process1'); + fixture.detectChanges(); + fixture.whenStable(); + }); + + it('should initialize start form', () => { + expect(component.startForm).toBeDefined(); + expect(component.startForm).not.toBeNull(); + }); + + it('should load start form from service', () => { + expect(getStartFormDefinitionSpy).toHaveBeenCalled(); + }); + + it('should indicate start form is invalid', async(() => { + expect(component.isStartFormMissingOrValid()).toBe(false); + })); + + it('should leave start button disabled when mandatory fields not filled out', async(() => { + component.name = 'My new process'; + fixture.detectChanges(); + let startBtn = debugElement.query(By.css('[data-automation-id="btn-start"]')); + expect(startBtn.properties['disabled']).toBe(true); + })); + + }); + }); - it('should call service to start process with the correct parameters', (done) => { - component.name = 'My new process'; - component.showDialog(); - fixture.detectChanges(); - component.onChange('my:process1'); - component.startProcess(); - fixture.whenStable().then(() => { - expect(startProcessSpy).toHaveBeenCalledWith('my:process1', 'My new process', undefined); - done(); - }); - }); + describe('cancel dialog', () => { - it('should output start event when process started successfully', (done) => { - let emitSpy = spyOn(component.start, 'emit'); - component.name = 'My new process'; - component.showDialog(); - fixture.detectChanges(); - component.onChange('my:process1'); - component.startProcess(); - fixture.whenStable().then(() => { - expect(emitSpy).toHaveBeenCalledWith(newProcess); - done(); - }); - }); + let setupDialog = (definitions?: any) => { + if (definitions) { + getDefinitionsSpy.and.returnValue(Observable.of(definitions)); + } + component.showDialog(); + fixture.detectChanges(); + component.onProcessDefChange('my:process1'); + fixture.detectChanges(); + }; + + let clickCancelButton = () => { + let closeButton: DebugElement = debugElement.query(By.css('[data-automation-id="btn-close"]')); + closeButton.triggerEventHandler('click', null); + }; + + it('should close dialog when cancel button clicked', async(() => { + let closeSpy = spyOn(component.dialog.nativeElement, 'close'); + component.showDialog(); + fixture.detectChanges(); + clickCancelButton(); + expect(closeSpy).toHaveBeenCalled(); + })); + + it('should clear processId and name when dialog cancelled', async(() => { + setupDialog(); + clickCancelButton(); + expect(component.currentProcessDef.id).toBeNull(); + expect(component.name).toBe(''); + })); + + it('should clear form values when dialog cancelled', async(() => { + setupDialog(fakeProcessDefWithForm); + fixture.whenStable().then(() => { + component.startForm.data = { + language: 'fr', + upload: {} + }; + fixture.detectChanges(); + clickCancelButton(); + expect(component.startForm.data['language']).toBeUndefined(); + expect(component.startForm.data['upload']).toBeUndefined(); + }); + })); - it('should indicate start form is missing when process does not have a start form', (done) => { - component.name = 'My new process'; - component.showDialog(); - fixture.detectChanges(); - component.onChange('my:process1'); - component.startProcess(); - fixture.whenStable().then(() => { - expect(component.isStartFormMissingOrValid()).toBe(true); - done(); - }); }); }); diff --git a/ng2-components/ng2-activiti-processlist/src/components/activiti-start-process.component.ts b/ng2-components/ng2-activiti-processlist/src/components/activiti-start-process.component.ts index e1136e5418..8de557fdf1 100644 --- a/ng2-components/ng2-activiti-processlist/src/components/activiti-start-process.component.ts +++ b/ng2-components/ng2-activiti-processlist/src/components/activiti-start-process.component.ts @@ -18,6 +18,8 @@ import { Component, EventEmitter, Input, Output, OnInit, ViewChild, DebugElement, OnChanges, SimpleChanges } from '@angular/core'; import { AlfrescoTranslationService } from 'ng2-alfresco-core'; import { ActivitiStartForm } from 'ng2-activiti-form'; +import { ProcessInstance } from './../models/process-instance.model'; +import { ProcessDefinitionRepresentation } from './../models/process-definition.model'; import { ActivitiProcessService } from './../services/activiti-process.service'; declare let componentHandler: any; @@ -35,7 +37,7 @@ export class ActivitiStartProcessButton implements OnInit, OnChanges { appId: string; @Output() - start: EventEmitter = new EventEmitter(); + start: EventEmitter = new EventEmitter(); @ViewChild('dialog') dialog: DebugElement; @@ -43,11 +45,11 @@ export class ActivitiStartProcessButton implements OnInit, OnChanges { @ViewChild('startForm') startForm: ActivitiStartForm; - processDefinitions: any[] = []; + processDefinitions: ProcessDefinitionRepresentation[] = []; name: string; - currentProcessDef: any; + currentProcessDef: ProcessDefinitionRepresentation = new ProcessDefinitionRepresentation(); constructor(private translate: AlfrescoTranslationService, private activitiProcess: ActivitiProcessService) { @@ -70,9 +72,9 @@ export class ActivitiStartProcessButton implements OnInit, OnChanges { } public load(appId: string) { - this.reset(); - this.activitiProcess.getProcessDefinitions(this.appId).subscribe( - (res: any[]) => { + this.resetSelectedProcessDefinition(); + this.activitiProcess.getProcessDefinitions(appId).subscribe( + (res) => { this.processDefinitions = res; }, (err) => { @@ -92,7 +94,7 @@ export class ActivitiStartProcessButton implements OnInit, OnChanges { if (this.currentProcessDef.id && this.name) { let formValues = this.startForm ? this.startForm.form.values : undefined; this.activitiProcess.startProcess(this.currentProcessDef.id, this.name, formValues).subscribe( - (res: any) => { + (res) => { this.name = ''; this.start.emit(res); this.cancel(); @@ -105,10 +107,11 @@ export class ActivitiStartProcessButton implements OnInit, OnChanges { } public cancel() { + this.reset(); this.dialog.nativeElement.close(); } - onChange(processDefinitionId) { + onProcessDefChange(processDefinitionId) { let processDef = this.processDefinitions.find((processDefinition) => { return processDefinition.id === processDefinitionId; }); @@ -128,7 +131,15 @@ export class ActivitiStartProcessButton implements OnInit, OnChanges { return this.currentProcessDef.id && this.name && this.isStartFormMissingOrValid(); } - reset() { - this.currentProcessDef = {}; + private resetSelectedProcessDefinition() { + this.currentProcessDef = new ProcessDefinitionRepresentation(); + } + + private reset() { + this.resetSelectedProcessDefinition(); + this.name = ''; + if (this.startForm) { + this.startForm.data = {}; + } } }