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

View File

@@ -15,23 +15,25 @@
* limitations under the License. * limitations under the License.
*/ */
import { ElementRef } from '@angular/core'; import { of, timer } from 'rxjs';
import { Observable } from 'rxjs';
import { FormService } from '../../../services/form.service'; import { FormService } from '../../../services/form.service';
import { FormFieldModel } from '../core/form-field.model'; import { FormFieldModel } from '../core/form-field.model';
import { FormModel } from '../core/form.model'; import { FormModel } from '../core/form.model';
import { GroupModel } from '../core/group.model'; import { GroupModel } from '../core/group.model';
import { FunctionalGroupWidgetComponent } from './functional-group.widget'; import { FunctionalGroupWidgetComponent } from './functional-group.widget';
import { AlfrescoApiService } from '../../../../services'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import { CoreTestingModule, setupTestBed } from '../../../../testing'; import { CoreTestingModule, setupTestBed } from '../../../../testing';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
describe('FunctionalGroupWidgetComponent', () => { describe('FunctionalGroupWidgetComponent', () => {
let fixture: ComponentFixture<FunctionalGroupWidgetComponent>;
let component: FunctionalGroupWidgetComponent;
let formService: FormService; let formService: FormService;
let elementRef: ElementRef; let getWorkflowGroupsSpy: jasmine.Spy;
let widget: FunctionalGroupWidgetComponent; const groups: GroupModel[] = [
let alfrescoApiService: AlfrescoApiService; { id: '1', name: 'group 1' },
{ id: '2', name: 'group 2' }
];
setupTestBed({ setupTestBed({
imports: [ imports: [
@@ -41,170 +43,109 @@ describe('FunctionalGroupWidgetComponent', () => {
}); });
beforeEach(() => { beforeEach(() => {
alfrescoApiService = TestBed.inject(AlfrescoApiService); formService = TestBed.inject(FormService);
getWorkflowGroupsSpy = spyOn(formService, 'getWorkflowGroups').and.returnValue(of([]));
formService = new FormService(null, alfrescoApiService, null); fixture = TestBed.createComponent(FunctionalGroupWidgetComponent);
elementRef = new ElementRef(null); component = fixture.componentInstance;
widget = new FunctionalGroupWidgetComponent(formService, elementRef); component.field = new FormFieldModel(new FormModel());
widget.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'}; const group: GroupModel = { name: 'group-1'};
widget.field.value = group; component.field.value = group;
component.ngOnInit();
spyOn(formService, 'getWorkflowGroups').and.returnValue( expect(component.searchTerm.value).toEqual(group.name);
new Observable((observer) => {
observer.next(null);
observer.complete();
})
);
widget.ngOnInit();
expect(formService.getWorkflowGroups).toHaveBeenCalled();
expect(widget.value).toBe(group.name);
}); });
it('should not setup text on init', () => { it('should not setup text on init', () => {
widget.field.value = null; component.field.value = null;
widget.ngOnInit(); component.ngOnInit();
expect(widget.value).toBeUndefined(); expect(component.searchTerm.value).toBeNull();
});
it('should require form field to setup values on init', () => {
widget.field = null;
widget.ngOnInit();
expect(widget.value).toBeUndefined();
expect(widget.groupId).toBeUndefined();
}); });
it('should setup group restriction', () => { it('should setup group restriction', () => {
widget.ngOnInit(); component.ngOnInit();
expect(widget.groupId).toBeUndefined(); expect(component.groupId).toBeUndefined();
widget.field.params = { restrictWithGroup: { id: '<id>' } }; component.field.params = { restrictWithGroup: { id: '<id>' } };
widget.ngOnInit(); component.ngOnInit();
expect(widget.groupId).toBe('<id>'); expect(component.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);
}); });
it('should update form on value flush', () => { it('should update form on value flush', () => {
spyOn(widget.field, 'updateForm').and.callThrough(); spyOn(component.field, 'updateForm').and.callThrough();
widget.flushValue(); component.updateOption();
expect(widget.field.updateForm).toHaveBeenCalled(); expect(component.field.updateForm).toHaveBeenCalled();
}); });
it('should flush selected value', () => { it('should flush selected value', () => {
const groups: GroupModel[] = [ getWorkflowGroupsSpy.and.returnValue(of(groups));
{ id: '1', name: 'group 1' },
{ id: '2', name: 'group 2' }
];
widget.groups = groups; component.updateOption(groups[1]);
widget.value = 'group 2';
widget.flushValue();
expect(widget.value).toBe(groups[1].name); expect(component.field.value).toBe(groups[1]);
expect(widget.field.value).toBe(groups[1]);
}); });
it('should be case insensitive when flushing value', () => { it('should fetch groups and show popup on key up', async () => {
const groups: GroupModel[] = [ component.groupId = 'parentGroup';
{ id: '1', name: 'group 1' }, getWorkflowGroupsSpy.and.returnValue(of(groups));
{ id: '2', name: 'gRoUp 2' }
];
widget.groups = groups; await typeIntoInput('group');
widget.value = 'GROUP 2';
widget.flushValue();
expect(widget.value).toBe(groups[1].name); const options: HTMLElement[] = Array.from(document.querySelectorAll('[id="adf-group-label-name"]'));
expect(widget.field.value).toBe(groups[1]); 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', () => { it('should hide popup when fetching empty group list', async () => {
const groups: GroupModel[] = [{}, {}]; component.groupId = 'parentGroup';
spyOn(formService, 'getWorkflowGroups').and.returnValue( getWorkflowGroupsSpy.and.returnValues(of(groups), of([]));
new Observable((observer) => {
observer.next(groups);
observer.complete();
})
);
const keyboardEvent = new KeyboardEvent('keypress'); await typeIntoInput('group');
widget.value = 'group';
widget.onKeyUp(keyboardEvent);
expect(formService.getWorkflowGroups).toHaveBeenCalledWith('group', undefined); let options: HTMLElement[] = Array.from(document.querySelectorAll('[id="adf-group-label-name"]'));
expect(widget.groups).toBe(groups); 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', () => { it('should not fetch groups when value is missing', async () => {
const groups: GroupModel[] = [{}, {}]; await typeIntoInput('');
spyOn(formService, 'getWorkflowGroups').and.returnValue( expect(getWorkflowGroupsSpy).not.toHaveBeenCalled();
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 hide popup when fetching empty group list', () => { it('should not fetch groups when value violates constraints', async () => {
spyOn(formService, 'getWorkflowGroups').and.returnValue( component.minTermLength = 4;
new Observable((observer) => { await typeIntoInput('123');
observer.next(null); expect(getWorkflowGroupsSpy).not.toHaveBeenCalled();
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();
}); });
}); });

View File

@@ -17,11 +17,13 @@
/* tslint:disable:component-selector */ /* tslint:disable:component-selector */
import { ENTER, ESCAPE } from '@angular/cdk/keycodes';
import { Component, ElementRef, OnInit, ViewEncapsulation } from '@angular/core'; import { Component, ElementRef, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService } from '../../../services/form.service'; import { FormService } from '../../../services/form.service';
import { GroupModel } from './../core/group.model'; import { GroupModel } from './../core/group.model';
import { WidgetComponent } from './../widget.component'; 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({ @Component({
selector: 'functional-group-widget', selector: 'functional-group-widget',
@@ -42,11 +44,21 @@ import { WidgetComponent } from './../widget.component';
}) })
export class FunctionalGroupWidgetComponent extends WidgetComponent implements OnInit { export class FunctionalGroupWidgetComponent extends WidgetComponent implements OnInit {
value: string;
oldValue: string;
groups: GroupModel[] = [];
minTermLength: number = 1; minTermLength: number = 1;
groupId: string; 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, constructor(public formService: FormService,
public elementRef: ElementRef) { public elementRef: ElementRef) {
@@ -55,65 +67,52 @@ export class FunctionalGroupWidgetComponent extends WidgetComponent implements O
ngOnInit() { ngOnInit() {
if (this.field) { if (this.field) {
const group = this.field.value;
if (group) { if (this.field.readOnly) {
this.value = group.name; this.searchTerm.disable();
} }
const params = this.field.params; const params = this.field.params;
if (params && params['restrictWithGroup']) { if (params && params.restrictWithGroup) {
const restrictWithGroup = <GroupModel> params['restrictWithGroup']; const restrictWithGroup = <GroupModel> params.restrictWithGroup;
this.groupId = restrictWithGroup.id; this.groupId = restrictWithGroup.id;
} }
// Load auto-completion for previously saved value if (this.field.value?.name) {
if (this.value) { this.searchTerm.setValue(this.field.value.name);
this.formService
.getWorkflowGroups(this.value, this.groupId)
.subscribe(groups => this.groups = groups || []);
} }
} }
} }
onKeyUp(event: KeyboardEvent) { updateOption(option?: GroupModel) {
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());
if (option) { if (option) {
this.field.value = option; this.field.value = option;
this.value = option.name;
} else { } else {
this.field.value = null; this.field.value = null;
this.value = null;
} }
this.field.updateForm(); this.field.updateForm();
} }
onItemClick(item: GroupModel, event: Event) { validateGroup(valid: boolean, empty: boolean) {
if (item) { const isEmpty = !this.field.required && (empty || valid);
this.field.value = item; const hasValue = this.field.required && valid;
this.value = item.name;
} if (hasValue || isEmpty) {
if (event) { this.field.validationSummary.message = '';
event.preventDefault(); 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) { getDisplayName(model: GroupModel | string) {
if (item) { if (model) {
this.field.value = item; return typeof model === 'string' ? model : model.name;
this.value = item.name;
} }
return '';
} }
} }

View File

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

View File

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

View File

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