[ADF-1625] removed wrong select into typeahed widget, readded validati… (#2411)

* #ADF-1625 removed wrong select into typeahed widget, readded validation on blur

* #ADF-1625 added validator for typeahead fixed tests

* #ADF-1625 removed wrong fdescribe

* [ADF-1625] cleaned behaviour on typeahed

* [ADF-1625] fixed some selection bug on typeahed and start fixing test

* [ADF-1625] fixed typeahead test for autocomplete
This commit is contained in:
Vito
2017-10-06 12:23:12 +01:00
committed by Eugenio Romano
parent 100479f017
commit 8289492905
5 changed files with 247 additions and 193 deletions
@@ -18,6 +18,7 @@
import { FormFieldOption } from './form-field-option';
import { FormFieldTypes } from './form-field-types';
import {
FixedValueFieldValidator,
MaxLengthFieldValidator,
MaxValueFieldValidator,
MinLengthFieldValidator,
@@ -533,4 +534,57 @@ describe('FormFieldValidator', () => {
});
});
describe('FixedValueFieldValidator', () => {
let validator: FixedValueFieldValidator;
beforeEach(() => {
validator = new FixedValueFieldValidator();
});
it('should support only typeahead field', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.isSupported(field)).toBeFalsy();
field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TYPEAHEAD
});
expect(validator.isSupported(field)).toBeTruthy();
});
it('should allow empty values', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TYPEAHEAD,
value: null,
regexPattern: 'pattern'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed for a valid input value in options', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TYPEAHEAD,
value: '1',
options: [{id: '1', name: 'Leanne Graham'}, {id: '2', name: 'Ervin Howell'}]
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for an invalid input value in options', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TYPEAHEAD,
value: 'Lean',
options: [{id: '1', name: 'Leanne Graham'}, {id: '2', name: 'Ervin Howell'}]
});
expect(validator.validate(field)).toBeFalsy();
});
});
});
@@ -366,6 +366,47 @@ export class RegExFieldValidator implements FormFieldValidator {
}
export class FixedValueFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.TYPEAHEAD
];
isSupported(field: FormFieldModel): boolean {
return field && this.supportedTypes.indexOf(field.type) > -1;
}
hasValidNameOrValidId(field: FormFieldModel): boolean {
return this.hasValidName(field) || this.hasValidId(field);
}
hasValidName(field: FormFieldModel) {
return field.options.find(item => item.name && item.name.toLocaleLowerCase() === field.value.toLocaleLowerCase()) ? true : false;
}
hasValidId(field: FormFieldModel) {
return field.options[field.value - 1] ? true : false;
}
hasStringValue(field: FormFieldModel) {
return field.value && typeof field.value === 'string';
}
hasOptions(field: FormFieldModel) {
return field.options && field.options.length > 0;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field)) {
if (this.hasStringValue(field) && this.hasOptions(field) && !this.hasValidNameOrValidId(field)) {
field.validationSummary = 'Invalid data inserted';
return false;
}
}
return true;
}
}
export const FORM_FIELD_VALIDATORS = [
new RequiredFieldValidator(),
new NumberFieldValidator(),
@@ -376,5 +417,6 @@ export const FORM_FIELD_VALIDATORS = [
new RegExFieldValidator(),
new DateFieldValidator(),
new MinDateFieldValidator(),
new MaxDateFieldValidator()
new MaxDateFieldValidator(),
new FixedValueFieldValidator()
];
@@ -10,18 +10,16 @@
type="text"
[id]="field.id"
[(ngModel)]="value"
(ngModelChange)="validate()"
(keyup)="onKeyUp($event)"
[disabled]="field.readOnly"
placeholder="{{field.placeholder}}"
[mdAutocomplete]="auto">
<md-autocomplete #auto="mdAutocomplete" (optionSelected)="onItemSelect($event.option.value)">
<md-option *ngFor="let item of options" (click)="onItemClick(item, $event)" [value]="item">
<span>{{item.name}}</span>
<md-option *ngFor="let item of options" [value]="item">
<span [id]="field.name+'_option_'+item.id">{{item.name}}</span>
</md-option>
</md-autocomplete>
<md-select class="adf-select" [id]="field.id" [(ngModel)]="field.value">
<md-option id="readonlyOption" *ngIf="isReadOnlyType()" [value]="field.value">{{field.value}}</md-option>
</md-select>
</md-input-container>
<error-widget [error]="field.validationSummary"></error-widget>
@@ -15,6 +15,7 @@
* limitations under the License.
*/
import { OverlayContainer } from '@angular/cdk/overlay';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CoreModule, LogServiceMock } from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Rx';
@@ -37,6 +38,7 @@ describe('TypeaheadWidgetComponent', () => {
let widget: TypeaheadWidgetComponent;
let visibilityService: WidgetVisibilityService;
let logService: LogServiceMock;
let overlayContainerElement: HTMLElement;
beforeEach(() => {
logService = new LogServiceMock();
@@ -108,23 +110,6 @@ describe('TypeaheadWidgetComponent', () => {
expect(widget.handleError).toHaveBeenCalledWith(err);
});
it('should prevent default behaviour on option item click', () => {
let event = jasmine.createSpyObj('event', ['preventDefault']);
widget.onItemClick(null, event);
expect(event.preventDefault).toHaveBeenCalled();
});
it('should update values on option item click', () => {
let option: FormFieldOption = <FormFieldOption> {
id: '1',
name: 'name'
};
spyOn(visibilityService, 'refreshVisibility').and.stub();
widget.onItemClick(option, null);
expect(widget.field.value).toBe(option.id);
expect(widget.value).toBe(option.name);
});
it('should setup initial value', () => {
spyOn(formService, 'getRestFieldValues').and.returnValue(Observable.create(observer => {
observer.next([
@@ -208,89 +193,6 @@ describe('TypeaheadWidgetComponent', () => {
expect(filtered[0]).toEqual(options[1]);
});
it('should update form on value flush', () => {
spyOn(widget.field, 'updateForm').and.callThrough();
widget.flushValue();
expect(widget.field.updateForm).toHaveBeenCalled();
});
it('should flush selected value', () => {
let options: FormFieldOption[] = [
{ id: '1', name: 'Item one' },
{ id: '2', name: 'Item Two' }
];
widget.field.options = options;
widget.value = 'Item Two';
widget.flushValue();
expect(widget.value).toBe(options[1].name);
expect(widget.field.value).toBe(options[1].id);
});
it('should be case insensitive when flushing value', () => {
let options: FormFieldOption[] = [
{ id: '1', name: 'Item one' },
{ id: '2', name: 'iTEM TWo' }
];
widget.field.options = options;
widget.value = 'ITEM TWO';
widget.flushValue();
expect(widget.value).toBe(options[1].name);
expect(widget.field.value).toBe(options[1].id);
});
it('should reset fields when flushing missing option value', () => {
widget.field.options = [
{ id: '1', name: 'Item one' },
{ id: '2', name: 'Item two' }
];
widget.value = 'Missing item';
widget.flushValue();
expect(widget.value).toBeNull();
expect(widget.field.value).toBeNull();
});
it('should reset fields when flushing incorrect value', () => {
widget.field.options = [
{ id: '1', name: 'Item one' },
{ id: '2', name: 'Item two' }
];
widget.field.value = 'Item two';
widget.value = 'Item two!';
widget.flushValue();
expect(widget.value).toBeNull();
expect(widget.field.value).toBeNull();
});
it('should reset fields when flushing value having no options', () => {
widget.field.options = null;
widget.field.value = 'item 1';
widget.value = 'new item';
widget.flushValue();
expect(widget.value).toBeNull();
expect(widget.field.value).toBeNull();
});
it('should emit field change event on item click', () => {
let event = jasmine.createSpyObj('event', ['preventDefault']);
let fakeField = new FormFieldModel(new FormModel(), { id: 'fakeField', value: 'fakeValue' });
widget.field = fakeField;
let item = { id: 'fake-id-opt', name: 'fake-name-opt' };
widget.onItemClick(item, event);
widget.fieldChanged.subscribe((field) => {
expect(field).toBeDefined();
expect(field.id).toEqual('fakeField');
expect(field.value).toEqual('fake-id-opt');
});
});
describe('when template is ready', () => {
let typeaheadWidgetComponent: TypeaheadWidgetComponent;
let fixture: ComponentFixture<TypeaheadWidgetComponent>;
@@ -308,7 +210,19 @@ describe('TypeaheadWidgetComponent', () => {
TestBed.configureTestingModule({
imports: [CoreModule, MaterialModule],
declarations: [TypeaheadWidgetComponent, ErrorWidgetComponent],
providers: [FormService, EcmModelService, WidgetVisibilityService]
providers: [
{provide: OverlayContainer, useFactory: () => {
overlayContainerElement = document.createElement('div');
overlayContainerElement.classList.add('cdk-overlay-container');
document.body.appendChild(overlayContainerElement);
// remove body padding to keep consistent cross-browser
document.body.style.padding = '0';
document.body.style.margin = '0';
return {getContainerElement: () => overlayContainerElement};
}}, FormService, EcmModelService, WidgetVisibilityService]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(TypeaheadWidgetComponent);
typeaheadWidgetComponent = fixture.componentInstance;
@@ -321,6 +235,33 @@ describe('TypeaheadWidgetComponent', () => {
TestBed.resetTestingModule();
});
describe ('and typeahead is in readonly mode', () => {
it('should show typeahead value with input disabled', async(() => {
typeaheadWidgetComponent.field = new FormFieldModel(
new FormModel({ processVariables: [{ name: 'typeahead-id_LABEL', value: 'FakeProcessValue' }] }), {
id: 'typeahead-id',
name: 'typeahead-name',
type: 'readonly',
params: { field: { id: 'typeahead-id', name: 'typeahead-name', type: 'typeahead' } }
});
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let readonlyInput: HTMLInputElement = <HTMLInputElement> element.querySelector('#typeahead-id');
expect(readonlyInput.disabled).toBeTruthy();
expect(readonlyInput).not.toBeNull();
expect(readonlyInput.value).toBe('FakeProcessValue');
});
}));
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
});
describe('and typeahead is populated via taskId', () => {
beforeEach(async(() => {
@@ -344,14 +285,58 @@ describe('TypeaheadWidgetComponent', () => {
}));
it('should show typeahead options', async(() => {
let keyboardEvent = new KeyboardEvent('keypress');
let typeahedElement = fixture.debugElement.query(By.css('#typeahead-id'));
let typeahedHTMLElement: HTMLInputElement = <HTMLInputElement> typeahedElement.nativeElement;
typeahedHTMLElement.focus();
typeaheadWidgetComponent.value = 'F';
typeahedHTMLElement.value = 'F';
typeahedHTMLElement.dispatchEvent(new Event('keyup'));
typeahedHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_1"]'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_2"]'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_3"]'))).not.toBeNull();
});
}));
it('should hide the option when the value is empty', async(() => {
let typeahedElement = fixture.debugElement.query(By.css('#typeahead-id'));
let typeahedHTMLElement: HTMLInputElement = <HTMLInputElement> typeahedElement.nativeElement;
typeahedHTMLElement.focus();
typeaheadWidgetComponent.value = 'F';
typeahedHTMLElement.value = 'F';
typeahedHTMLElement.dispatchEvent(new Event('keyup'));
typeahedHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_1"]'))).not.toBeNull();
typeahedHTMLElement.focus();
typeaheadWidgetComponent.value = '';
typeahedHTMLElement.dispatchEvent(new Event('keyup'));
typeahedHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_1"]'))).toBeNull();
});
});
}));
it('should show error message when the value is not valid', async(() => {
typeaheadWidgetComponent.value = 'Fake Name';
typeaheadWidgetComponent.field.value = 'Fake Name';
typeaheadWidgetComponent.field.options = fakeOptionList;
expect(element.querySelector('.adf-error-text')).toBeNull();
let keyboardEvent = new KeyboardEvent('keypress');
typeaheadWidgetComponent.onKeyUp(keyboardEvent);
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(fixture.debugElement.queryAll(By.css('[id="md-option-1"]'))).toBeDefined();
expect(fixture.debugElement.queryAll(By.css('[id="md-option-2"]'))).toBeDefined();
expect(fixture.debugElement.queryAll(By.css('[id="md-option-3"]'))).toBeDefined();
fixture.detectChanges();
expect(element.querySelector('.adf-error-text')).not.toBeNull();
expect(element.querySelector('.adf-error-text').textContent).toContain('Invalid data inserted');
});
}));
@@ -362,27 +347,6 @@ describe('TypeaheadWidgetComponent', () => {
expect(element.querySelector('#typeahead-id')).toBeNull();
});
}));
it('should show typeahead value when the type is readonly', async(() => {
typeaheadWidgetComponent.field = new FormFieldModel(
new FormModel({ taskId: 'fake-task-id', processVariables: [{ name: 'typeahead-id_LABEL', value: 'FakeProcessValue' }] }), {
id: 'typeahead-id',
name: 'typeahead-name',
type: 'readonly',
value: '9',
params: { field: { id: 'typeahead-id', name: 'typeahead-name', type: 'typeahead' } }
});
fixture.detectChanges();
const trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
trigger.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(element.querySelector('#typeahead-id')).not.toBeNull();
let optionElement: HTMLElement = <HTMLElement> document.body.querySelector('.mat-option');
expect(optionElement.innerText).toEqual('FakeProcessValue');
});
}));
});
describe('and typeahead is populated via processDefinitionId', () => {
@@ -15,7 +15,7 @@
* limitations under the License.
*/
/* tslint:disable:component-selector */
/* tslint:disable:component-selector */
import { ENTER, ESCAPE } from '@angular/cdk/keycodes';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
@@ -23,7 +23,7 @@ import { LogService } from 'ng2-alfresco-core';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
import { FormService } from './../../../services/form.service';
import { FormFieldOption } from './../core/form-field-option';
import { baseHost , WidgetComponent } from './../widget.component';
import { baseHost, WidgetComponent } from './../widget.component';
@Component({
selector: 'typeahead-widget',
@@ -42,7 +42,7 @@ export class TypeaheadWidgetComponent extends WidgetComponent implements OnInit
constructor(public formService: FormService,
private visibilityService: WidgetVisibilityService,
private logService: LogService) {
super(formService);
super(formService);
}
ngOnInit() {
@@ -51,101 +51,89 @@ export class TypeaheadWidgetComponent extends WidgetComponent implements OnInit
} else if (this.field.form.processDefinitionId) {
this.getValuesByProcessDefinitionId();
}
if (this.isReadOnlyType()) {
this.value = this.field.value;
}
}
getValuesByTaskId() {
this.formService
.getRestFieldValues(
this.field.form.taskId,
this.field.id
this.field.form.taskId,
this.field.id
)
.subscribe(
(result: FormFieldOption[]) => {
let options = result || [];
this.field.options = options;
(result: FormFieldOption[]) => {
let options = result || [];
this.field.options = options;
let fieldValue = this.field.value;
if (fieldValue) {
let toSelect = options.find(item => item.id === fieldValue);
if (toSelect) {
this.value = toSelect.name;
}
let fieldValue = this.field.value;
if (fieldValue) {
let toSelect = options.find(item => item.id === fieldValue);
if (toSelect) {
this.value = toSelect.name;
}
this.field.updateForm();
this.visibilityService.refreshEntityVisibility(this.field);
},
err => this.handleError(err)
}
this.field.updateForm();
this.visibilityService.refreshEntityVisibility(this.field);
},
err => this.handleError(err)
);
}
getValuesByProcessDefinitionId() {
this.formService
.getRestFieldValuesByProcessId(
this.field.form.processDefinitionId,
this.field.id
this.field.form.processDefinitionId,
this.field.id
)
.subscribe(
(result: FormFieldOption[]) => {
let options = result || [];
this.field.options = options;
(result: FormFieldOption[]) => {
let options = result || [];
this.field.options = options;
let fieldValue = this.field.value;
if (fieldValue) {
let toSelect = options.find(item => item.id === fieldValue);
if (toSelect) {
this.value = toSelect.name;
}
let fieldValue = this.field.value;
if (fieldValue) {
let toSelect = options.find(item => item.id === fieldValue);
if (toSelect) {
this.value = toSelect.name;
}
this.field.updateForm();
this.visibilityService.refreshEntityVisibility(this.field);
},
err => this.handleError(err)
}
this.field.updateForm();
this.visibilityService.refreshEntityVisibility(this.field);
},
err => this.handleError(err)
);
}
getOptions(): FormFieldOption[] {
let val = this.value.toLocaleLowerCase();
let val = this.value.trim().toLocaleLowerCase();
return this.field.options.filter(item => {
let name = item.name.toLocaleLowerCase();
return name.indexOf(val) > -1;
});
}
isValidOptionName(optionName: string): boolean {
let option = this.field.options.find(item => item.name && item.name.toLocaleLowerCase() === optionName.toLocaleLowerCase());
return option ? true : false;
}
onKeyUp(event: KeyboardEvent) {
if (this.value && this.value.length >= this.minTermLength && this.oldValue !== this.value) {
if (this.value && this.value.trim().length >= this.minTermLength && this.oldValue !== this.value) {
if (event.keyCode !== ESCAPE && event.keyCode !== ENTER) {
if (this.value.length >= this.minTermLength) {
this.options = this.getOptions();
this.oldValue = this.value;
if (this.isValidOptionName(this.value)) {
this.field.value = this.options[0].id;
}
}
}
}
}
flushValue() {
let options = this.field.options || [];
let lValue = this.value ? this.value.toLocaleLowerCase() : null;
let field = options.find(item => item.name && item.name.toLocaleLowerCase() === lValue);
if (field) {
this.field.value = field.id;
this.value = field.name;
} else {
this.field.value = null;
this.value = null;
}
this.field.updateForm();
}
onItemClick(item: FormFieldOption, event: Event) {
if (item) {
this.field.value = item.id;
this.value = item.name;
this.checkVisibility();
}
if (event) {
event.preventDefault();
if (this.isValueDefined() && this.value.trim().length === 0) {
this.oldValue = this.value;
this.options = [];
}
}
@@ -157,6 +145,14 @@ export class TypeaheadWidgetComponent extends WidgetComponent implements OnInit
}
}
validate() {
this.field.value = this.value;
}
isValueDefined() {
return this.value !== null && this.value !== undefined;
}
handleError(error: any) {
this.logService.error(error);
}