diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts index 94f9f9636df31314f05d76cf815b5707d032be1c..c2afe9895a5617abd0414a25d2b6b21d00a6cfdb 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts @@ -31,7 +31,7 @@ import { } from '@alfa-client/tech-shared'; import { Injectable } from '@angular/core'; import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, UntypedFormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, UrlSegment } from '@angular/router'; import { faker } from '@faker-js/faker/.'; import { createDummy, Dummy } from 'libs/tech-shared/test/dummy'; @@ -220,22 +220,25 @@ describe('KeycloakFormService', () => { beforeEach(() => { service._doSubmit = jest.fn().mockReturnValue(singleHot(dummyStateResource)); + service._processInvalidForm = jest.fn().mockReturnValue(of(createEmptyStateResource())); service._processResponseValidationErrors = jest.fn().mockReturnValue(of(createEmptyStateResource())); service.form.setErrors(null); }); describe('on client validation error', () => { - it('should return empty state resource withouth loading after delay', fakeAsync(() => { + it('should process invalid form', () => { service.form.setErrors({ dummy: 'dummy error' }); - const results: StateResource<unknown>[] = []; - service.submit().subscribe((value: StateResource<unknown>) => { - results.push(value); - }); - tick(200); + service.submit().subscribe(); + + expect(service._processInvalidForm).toHaveBeenCalled(); + }); - expect(results).toEqual([createEmptyStateResource(true), createEmptyStateResource()]); - })); + it('should return invalid form processing result on invalid form', () => { + service.form.setErrors({ dummy: 'dummy error' }); + + expect(service.submit()).toBeObservable(singleColdCompleted(createEmptyStateResource())); + }); }); it('should call do submit', () => { @@ -269,6 +272,29 @@ describe('KeycloakFormService', () => { }); }); + describe('process invalid form', () => { + beforeEach(() => { + service._showValidationErrorForAllInvalidControls = jest.fn(); + }); + + it('should show validation errors on all invalid controls', () => { + service._processInvalidForm(); + + expect(service._showValidationErrorForAllInvalidControls).toHaveBeenCalledWith(service.form); + }); + + it('should return delayed empty state resource', fakeAsync(() => { + const results: StateResource<unknown>[] = []; + + service._processInvalidForm().subscribe((value: StateResource<unknown>) => { + results.push(value); + }); + tick(200); + + expect(results).toEqual([createEmptyStateResource(true), createEmptyStateResource()]); + })); + }); + describe('process response validation errors', () => { const keycloakHttpError: KeycloakHttpErrorResponse = createKeycloakHttpErrorResponse(); @@ -331,6 +357,66 @@ describe('KeycloakFormService', () => { }); }); + describe('show validation errors on all invalid controls', () => { + it('should update value and validity on invalid control', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity'); + + service._showValidationErrorForAllInvalidControls(control); + + expect(spy).toHaveBeenCalled(); + }); + + it('should update value and validity form invalid control from group', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(service, '_showValidationErrorForAllInvalidControls'); + const group: UntypedFormGroup = new FormGroup({ + someControl: control, + }); + + service._showValidationErrorForAllInvalidControls(group); + + expect(spy).toHaveBeenCalledWith(control); + }); + + it('should update value and validity on invalid control from group', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity'); + const group: UntypedFormGroup = new FormGroup({ + someControl: control, + }); + + service._showValidationErrorForAllInvalidControls(group); + + expect(spy).toHaveBeenCalled(); + }); + + it('should update value and validity form invalid control from array', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(service, '_showValidationErrorForAllInvalidControls'); + const array: FormArray = new FormArray([control]); + + service._showValidationErrorForAllInvalidControls(array); + + expect(spy).toHaveBeenCalledWith(control); + }); + + it('should update value and validity on invalid control from group', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity'); + const array: FormArray = new FormArray([control]); + + service._showValidationErrorForAllInvalidControls(array); + + expect(spy).toHaveBeenCalled(); + }); + }); + describe('set validation errors on controls', () => { it('should set invalid param validation error', () => { const invalidParam: InvalidParam = createInvalidParam(); diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts index c36e8fa65e00598ec42c2f5b86a6e92b7fbc04b6..0b10e2acef0ca225ffffd957e2990d8f0306d02a 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts @@ -22,7 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { - creatDelayedEmptyStateResource, + creatDelayedEmptyStateResourceObservable, createEmptyStateResource, InvalidParam, isLoaded, @@ -30,7 +30,7 @@ import { StateResource, } from '@alfa-client/tech-shared'; import { inject, Injectable } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { AbstractControl, FormArray, FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, UrlSegment } from '@angular/router'; import { catchError, first, Observable, of, tap } from 'rxjs'; import { ValidationMessageCode } from '../../../../tech-shared/src/lib/validation/tech.validation.messages'; @@ -80,13 +80,18 @@ export abstract class KeycloakFormService<T> { public submit(): Observable<StateResource<T>> { if (this.form.invalid) { - return creatDelayedEmptyStateResource(); + return this._processInvalidForm(); } return this._doSubmit().pipe( catchError((keycloakError: KeycloakHttpErrorResponse) => this._processResponseValidationErrors(keycloakError)), ); } + _processInvalidForm(): Observable<StateResource<T>> { + this._showValidationErrorForAllInvalidControls(this.form); + return creatDelayedEmptyStateResourceObservable<T>(); + } + _processResponseValidationErrors(keycloakError: KeycloakHttpErrorResponse): Observable<StateResource<T>> { try { this._setValidationErrorsOnControls( @@ -99,6 +104,13 @@ export abstract class KeycloakFormService<T> { return of(createEmptyStateResource<T>()); } + _showValidationErrorForAllInvalidControls(control: AbstractControl): void { + if (control.invalid) control.updateValueAndValidity(); + if (control instanceof FormGroup || control instanceof FormArray) { + Object.values(control.controls).forEach((control) => this._showValidationErrorForAllInvalidControls(control)); + } + } + _setValidationErrorsOnControls(invalidParams: InvalidParam[]): void { invalidParams.forEach((invalidParam: InvalidParam) => { setInvalidParamValidationError(this.form, invalidParam); diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts index 40f4dabc86ec168497e92529e23a1800d1649179..d2b4e27d565afcb0f57f01bf57d3d64031393fbd 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts @@ -55,7 +55,7 @@ export function createErrorStateResource<T>(error: HttpError): StateResource<any return { ...createEmptyStateResource<T>(), error, loaded: true }; } -export function creatDelayedEmptyStateResource<T>(): Observable<StateResource<T>> { +export function creatDelayedEmptyStateResourceObservable<T>(): Observable<StateResource<T>> { return of(createEmptyStateResource<T>()).pipe(delay(200), startWith(createEmptyStateResource<T>(true))); }