[ADF-5464] [form]Mandatory field validations are not applied once content removed (#7255)

* [ADF-5464] [form]Mandatory field validations are not applied once content is removed from these fields

* * fix test and people widget

* * fix flaky test
This commit is contained in:
Dharan
2021-09-28 18:11:43 +05:30
committed by GitHub
parent e73d3c3213
commit ccb17bb1a6
6 changed files with 154 additions and 213 deletions

View File

@@ -1,6 +1,9 @@
<div class="adf-group-widget {{field.className}}"
[class.is-dirty]="value"
[class.adf-invalid]="!field.isValid" [class.adf-readonly]="field.readOnly" id="functional-group-div">
[class.is-dirty]="!!field.value"
[class.adf-invalid]="!field.isValid"
[class.adf-readonly]="field.readOnly"
id="functional-group-div">
<mat-form-field>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span *ngIf="isRequired()">*</span></label>
<input matInput
@@ -8,15 +11,14 @@
type="text"
data-automation-id="adf-group-search-input"
[id]="field.id"
[(ngModel)]="value"
(keyup)="onKeyUp($event)"
[disabled]="field.readOnly"
placeholder="{{field.placeholder}}"
[formControl]="searchTerm"
[placeholder]="field.placeholder"
[matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="onItemSelect($event.option.value)">
<mat-option *ngFor="let item of groups; let i = index" id="adf-group-widget-user-{{i}}"
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="updateOption($event.option.value)" [displayWith]="getDisplayName">
<mat-option *ngFor="let item of groups$ | async; let i = index"
id="adf-group-widget-user-{{i}}"
[id]="field.id +'-'+item.id"
(click)="onItemClick(item, $event)" [value]="item">
[value]="item">
<span id="adf-group-label-name">{{item.name}}</span>
</mat-option>
</mat-autocomplete>

View File

@@ -15,23 +15,25 @@
* limitations under the License.
*/
import { ElementRef } from '@angular/core';
import { Observable } from 'rxjs';
import { of, timer } from 'rxjs';
import { FormService } from '../../../services/form.service';
import { FormFieldModel } from '../core/form-field.model';
import { FormModel } from '../core/form.model';
import { GroupModel } from '../core/group.model';
import { FunctionalGroupWidgetComponent } from './functional-group.widget';
import { AlfrescoApiService } from '../../../../services';
import { TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CoreTestingModule, setupTestBed } from '../../../../testing';
import { TranslateModule } from '@ngx-translate/core';
describe('FunctionalGroupWidgetComponent', () => {
let fixture: ComponentFixture<FunctionalGroupWidgetComponent>;
let component: FunctionalGroupWidgetComponent;
let formService: FormService;
let elementRef: ElementRef;
let widget: FunctionalGroupWidgetComponent;
let alfrescoApiService: AlfrescoApiService;
let getWorkflowGroupsSpy: jasmine.Spy;
const groups: GroupModel[] = [
{ id: '1', name: 'group 1' },
{ id: '2', name: 'group 2' }
];
setupTestBed({
imports: [
@@ -41,170 +43,109 @@ describe('FunctionalGroupWidgetComponent', () => {
});
beforeEach(() => {
alfrescoApiService = TestBed.inject(AlfrescoApiService);
formService = TestBed.inject(FormService);
getWorkflowGroupsSpy = spyOn(formService, 'getWorkflowGroups').and.returnValue(of([]));
formService = new FormService(null, alfrescoApiService, null);
elementRef = new ElementRef(null);
widget = new FunctionalGroupWidgetComponent(formService, elementRef);
widget.field = new FormFieldModel(new FormModel());
fixture = TestBed.createComponent(FunctionalGroupWidgetComponent);
component = fixture.componentInstance;
component.field = new FormFieldModel(new FormModel());
fixture.detectChanges();
});
it('should setup text from underlying field on init', () => {
afterEach(() => {
getWorkflowGroupsSpy.calls.reset();
fixture.destroy();
});
async function typeIntoInput(text: string) {
component.searchTerm.setValue(text);
fixture.detectChanges();
await timer(300).toPromise();
await fixture.whenStable();
fixture.detectChanges();
const input = fixture.nativeElement.querySelector('input');
input.focus();
input.dispatchEvent(new Event('focusin'));
input.dispatchEvent(new Event('input'));
await fixture.whenStable();
fixture.detectChanges();
}
it('should setup text from underlying field on init', async () => {
const group: GroupModel = { name: 'group-1'};
widget.field.value = group;
component.field.value = group;
component.ngOnInit();
spyOn(formService, 'getWorkflowGroups').and.returnValue(
new Observable((observer) => {
observer.next(null);
observer.complete();
})
);
widget.ngOnInit();
expect(formService.getWorkflowGroups).toHaveBeenCalled();
expect(widget.value).toBe(group.name);
expect(component.searchTerm.value).toEqual(group.name);
});
it('should not setup text on init', () => {
widget.field.value = null;
widget.ngOnInit();
expect(widget.value).toBeUndefined();
});
it('should require form field to setup values on init', () => {
widget.field = null;
widget.ngOnInit();
expect(widget.value).toBeUndefined();
expect(widget.groupId).toBeUndefined();
component.field.value = null;
component.ngOnInit();
expect(component.searchTerm.value).toBeNull();
});
it('should setup group restriction', () => {
widget.ngOnInit();
expect(widget.groupId).toBeUndefined();
component.ngOnInit();
expect(component.groupId).toBeUndefined();
widget.field.params = { restrictWithGroup: { id: '<id>' } };
widget.ngOnInit();
expect(widget.groupId).toBe('<id>');
});
it('should prevent default behaviour on option item click', () => {
const event = jasmine.createSpyObj('event', ['preventDefault']);
widget.onItemClick(null, event);
expect(event.preventDefault).toHaveBeenCalled();
});
it('should update values on item click', () => {
const item: GroupModel = { name: 'group-1' };
widget.onItemClick(item, null);
expect(widget.field.value).toBe(item);
expect(widget.value).toBe(item.name);
component.field.params = { restrictWithGroup: { id: '<id>' } };
component.ngOnInit();
expect(component.groupId).toBe('<id>');
});
it('should update form on value flush', () => {
spyOn(widget.field, 'updateForm').and.callThrough();
widget.flushValue();
expect(widget.field.updateForm).toHaveBeenCalled();
spyOn(component.field, 'updateForm').and.callThrough();
component.updateOption();
expect(component.field.updateForm).toHaveBeenCalled();
});
it('should flush selected value', () => {
const groups: GroupModel[] = [
{ id: '1', name: 'group 1' },
{ id: '2', name: 'group 2' }
];
getWorkflowGroupsSpy.and.returnValue(of(groups));
widget.groups = groups;
widget.value = 'group 2';
widget.flushValue();
component.updateOption(groups[1]);
expect(widget.value).toBe(groups[1].name);
expect(widget.field.value).toBe(groups[1]);
expect(component.field.value).toBe(groups[1]);
});
it('should be case insensitive when flushing value', () => {
const groups: GroupModel[] = [
{ id: '1', name: 'group 1' },
{ id: '2', name: 'gRoUp 2' }
];
it('should fetch groups and show popup on key up', async () => {
component.groupId = 'parentGroup';
getWorkflowGroupsSpy.and.returnValue(of(groups));
widget.groups = groups;
widget.value = 'GROUP 2';
widget.flushValue();
await typeIntoInput('group');
expect(widget.value).toBe(groups[1].name);
expect(widget.field.value).toBe(groups[1]);
const options: HTMLElement[] = Array.from(document.querySelectorAll('[id="adf-group-label-name"]'));
expect(options.map(option => option.innerText)).toEqual(['group 1', 'group 2']);
expect(getWorkflowGroupsSpy).toHaveBeenCalledWith('group', 'parentGroup');
});
it('should fetch groups and show popup on key up', () => {
const groups: GroupModel[] = [{}, {}];
spyOn(formService, 'getWorkflowGroups').and.returnValue(
new Observable((observer) => {
observer.next(groups);
observer.complete();
})
);
it('should hide popup when fetching empty group list', async () => {
component.groupId = 'parentGroup';
getWorkflowGroupsSpy.and.returnValues(of(groups), of([]));
const keyboardEvent = new KeyboardEvent('keypress');
widget.value = 'group';
widget.onKeyUp(keyboardEvent);
await typeIntoInput('group');
expect(formService.getWorkflowGroups).toHaveBeenCalledWith('group', undefined);
expect(widget.groups).toBe(groups);
let options: HTMLElement[] = Array.from(document.querySelectorAll('[id="adf-group-label-name"]'));
expect(options.map(option => option.innerText)).toEqual(['group 1', 'group 2']);
await typeIntoInput('unknown-group');
options = Array.from(document.querySelectorAll('[id="adf-group-label-name"]'));
expect(options).toEqual([]);
expect(getWorkflowGroupsSpy).toHaveBeenCalledTimes(2);
});
it('should fetch groups with a group filter', () => {
const groups: GroupModel[] = [{}, {}];
spyOn(formService, 'getWorkflowGroups').and.returnValue(
new Observable((observer) => {
observer.next(groups);
observer.complete();
})
);
const keyboardEvent = new KeyboardEvent('keypress');
widget.groupId = 'parentGroup';
widget.value = 'group';
widget.onKeyUp(keyboardEvent);
expect(formService.getWorkflowGroups).toHaveBeenCalledWith('group', 'parentGroup');
expect(widget.groups).toBe(groups);
it('should not fetch groups when value is missing', async () => {
await typeIntoInput('');
expect(getWorkflowGroupsSpy).not.toHaveBeenCalled();
});
it('should hide popup when fetching empty group list', () => {
spyOn(formService, 'getWorkflowGroups').and.returnValue(
new Observable((observer) => {
observer.next(null);
observer.complete();
})
);
const keyboardEvent = new KeyboardEvent('keypress');
widget.value = 'group';
widget.onKeyUp(keyboardEvent);
expect(formService.getWorkflowGroups).toHaveBeenCalledWith('group', undefined);
expect(widget.groups.length).toBe(0);
});
it('should not fetch groups when value is missing', () => {
spyOn(formService, 'getWorkflowGroups').and.stub();
const keyboardEvent = new KeyboardEvent('keypress');
widget.value = null;
widget.onKeyUp(keyboardEvent);
expect(formService.getWorkflowGroups).not.toHaveBeenCalled();
});
it('should not fetch groups when value violates constraints', () => {
spyOn(formService, 'getWorkflowGroups').and.stub();
const keyboardEvent = new KeyboardEvent('keypress');
widget.minTermLength = 4;
widget.value = '123';
widget.onKeyUp(keyboardEvent);
expect(formService.getWorkflowGroups).not.toHaveBeenCalled();
it('should not fetch groups when value violates constraints', async () => {
component.minTermLength = 4;
await typeIntoInput('123');
expect(getWorkflowGroupsSpy).not.toHaveBeenCalled();
});
});

View File

@@ -17,11 +17,13 @@
/* tslint:disable:component-selector */
import { ENTER, ESCAPE } from '@angular/cdk/keycodes';
import { Component, ElementRef, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService } from '../../../services/form.service';
import { GroupModel } from './../core/group.model';
import { WidgetComponent } from './../widget.component';
import { catchError, debounceTime, filter, switchMap, tap } from 'rxjs/operators';
import { merge, of } from 'rxjs';
import { FormControl } from '@angular/forms';
@Component({
selector: 'functional-group-widget',
@@ -42,11 +44,21 @@ import { WidgetComponent } from './../widget.component';
})
export class FunctionalGroupWidgetComponent extends WidgetComponent implements OnInit {
value: string;
oldValue: string;
groups: GroupModel[] = [];
minTermLength: number = 1;
groupId: string;
searchTerm = new FormControl();
groups$ = merge(this.searchTerm.valueChanges).pipe(
tap((search: GroupModel | string) => {
const isValid = typeof search !== 'string';
const empty = search === '';
this.updateOption( isValid ? search as GroupModel : null );
this.validateGroup(isValid, empty);
}),
filter((group: string | GroupModel) => typeof group === 'string' && group.length >= this.minTermLength),
debounceTime(300),
switchMap((searchTerm: string) => this.formService.getWorkflowGroups(searchTerm, this.groupId)
.pipe(catchError(() => of([]))))
);
constructor(public formService: FormService,
public elementRef: ElementRef) {
@@ -55,65 +67,52 @@ export class FunctionalGroupWidgetComponent extends WidgetComponent implements O
ngOnInit() {
if (this.field) {
const group = this.field.value;
if (group) {
this.value = group.name;
if (this.field.readOnly) {
this.searchTerm.disable();
}
const params = this.field.params;
if (params && params['restrictWithGroup']) {
const restrictWithGroup = <GroupModel> params['restrictWithGroup'];
if (params && params.restrictWithGroup) {
const restrictWithGroup = <GroupModel> params.restrictWithGroup;
this.groupId = restrictWithGroup.id;
}
// Load auto-completion for previously saved value
if (this.value) {
this.formService
.getWorkflowGroups(this.value, this.groupId)
.subscribe(groups => this.groups = groups || []);
if (this.field.value?.name) {
this.searchTerm.setValue(this.field.value.name);
}
}
}
onKeyUp(event: KeyboardEvent) {
if (this.value && this.value.length >= this.minTermLength && this.oldValue !== this.value) {
if (event.keyCode !== ESCAPE && event.keyCode !== ENTER) {
this.oldValue = this.value;
this.formService
.getWorkflowGroups(this.value, this.groupId)
.subscribe(groups => this.groups = groups || []);
}
}
}
flushValue() {
const option = this.groups.find((item) => item.name.toLocaleLowerCase() === this.value.toLocaleLowerCase());
updateOption(option?: GroupModel) {
if (option) {
this.field.value = option;
this.value = option.name;
} else {
this.field.value = null;
this.value = null;
}
this.field.updateForm();
}
onItemClick(item: GroupModel, event: Event) {
if (item) {
this.field.value = item;
this.value = item.name;
}
if (event) {
event.preventDefault();
validateGroup(valid: boolean, empty: boolean) {
const isEmpty = !this.field.required && (empty || valid);
const hasValue = this.field.required && valid;
if (hasValue || isEmpty) {
this.field.validationSummary.message = '';
this.field.validate();
this.field.form.validateForm();
} else {
this.field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_VALUE';
this.field.markAsInvalid();
this.field.form.markAsInvalid();
}
}
onItemSelect(item: GroupModel) {
if (item) {
this.field.value = item;
this.value = item.name;
getDisplayName(model: GroupModel | string) {
if (model) {
return typeof model === 'string' ? model : model.name;
}
return '';
}
}

View File

@@ -11,7 +11,7 @@
type="text"
[id]="field.id"
[formControl]="searchTerm"
placeholder="{{field.placeholder}}"
[placeholder]="field.placeholder"
[matAutocomplete]="auto"
[matTooltip]="field.tooltip"
matTooltipPosition="above"

View File

@@ -180,10 +180,11 @@ describe('PeopleWidgetComponent', () => {
element = fixture.nativeElement;
});
afterAll(() => {
if (fixture) {
afterEach(() => {
fixture.destroy();
}
});
afterAll(() => {
TestBed.resetTestingModule();
});
@@ -219,18 +220,19 @@ describe('PeopleWidgetComponent', () => {
expect(fixture.debugElement.query(By.css('#adf-people-widget-user-1'))).not.toBeNull();
});
it('should hide result list if input is empty', () => {
it('should hide result list if input is empty', async () => {
const peopleHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('input');
peopleHTMLElement.focus();
peopleHTMLElement.value = '';
peopleHTMLElement.dispatchEvent(new Event('keyup'));
peopleHTMLElement.dispatchEvent(new Event('focusin'));
peopleHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.debugElement.query(By.css('#adf-people-widget-user-0'))).toBeNull();
});
});
it('should display two options if we tap one letter', async () => {
fixture.detectChanges();

View File

@@ -55,29 +55,25 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit {
input: ElementRef;
@Output()
peopleSelected: EventEmitter<number>;
peopleSelected: EventEmitter<number> = new EventEmitter();
groupId: string;
value: any;
searchTerm = new FormControl();
errorMsg = '';
searchTerms$: Observable<any> = this.searchTerm.valueChanges;
users$ = this.searchTerms$.pipe(
tap(() => {
this.errorMsg = '';
tap((searchInput) => {
if (typeof searchInput === 'string') {
this.onItemSelect();
}
}),
distinctUntilChanged(),
switchMap((searchTerm) => {
const value = searchTerm.email ? this.getDisplayName(searchTerm) : searchTerm;
return this.formService.getWorkflowUsers(value, this.groupId)
.pipe(
catchError((err) => {
this.errorMsg = err.message;
return of();
})
);
.pipe(catchError(() => of([])));
}),
map((list: UserProcessModel[]) => {
const value = this.searchTerm.value.email ? this.getDisplayName(this.searchTerm.value) : this.searchTerm.value;
@@ -88,7 +84,6 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit {
constructor(public formService: FormService, public peopleProcessService: PeopleProcessService) {
super(formService);
this.peopleSelected = new EventEmitter();
}
ngOnInit() {
@@ -122,13 +117,13 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit {
isValidUser(users: UserProcessModel[], name: string): boolean {
if (users) {
return users.find((user) => {
return !!users.find((user) => {
const selectedUser = this.getDisplayName(user).toLocaleLowerCase() === name.toLocaleLowerCase();
if (selectedUser) {
this.peopleSelected.emit(user && user.id || undefined);
}
return selectedUser;
}) ? true : false;
});
}
return false;
}
@@ -141,9 +136,11 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit {
return '';
}
onItemSelect(item: UserProcessModel) {
onItemSelect(item?: UserProcessModel) {
if (item) {
this.field.value = item;
} else {
this.field.value = null;
}
}
}