diff --git a/libs/shared/base-abstract-classes/src/service/editors/single-row-editor.service.spec.ts b/libs/shared/base-abstract-classes/src/service/editors/single-row-editor.service.spec.ts index 83b6098d79..93faab5d94 100644 --- a/libs/shared/base-abstract-classes/src/service/editors/single-row-editor.service.spec.ts +++ b/libs/shared/base-abstract-classes/src/service/editors/single-row-editor.service.spec.ts @@ -1,6 +1,4 @@ import { TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - import { ToastrService } from 'ngx-toastr'; import { instance, mock } from 'ts-mockito'; import { MysqlQueryService } from '@keira/shared/db-layer'; @@ -14,7 +12,7 @@ describe('SingleRowEditorService', () => { beforeEach(() => TestBed.configureTestingModule({ - imports: [RouterTestingModule], + imports: [], providers: [ { provide: MysqlQueryService, useValue: instance(mock(MysqlQueryService)) }, { provide: ToastrService, useValue: instance(mock(ToastrService)) }, @@ -162,4 +160,77 @@ describe('SingleRowEditorService', () => { expect(updateFullQuerySpy).toHaveBeenCalledTimes(1); }); }); + + describe('updateFormAfterReload()', () => { + let consoleErrorSpy: Spy; + let consoleWarnSpy: Spy; + let mockForm: any; + + beforeEach(() => { + // Mock the form and its controls + mockForm = { + controls: { + id: { setValue: jasmine.createSpy('setValue') }, + name: { setValue: jasmine.createSpy('setValue') }, + guid: { setValue: jasmine.createSpy('setValue') }, // Add guid control + }, + }; + service['_form'] = mockForm; + + // Mock originalValue + service['_originalValue'] = { id: 123, name: 'Test Name', guid: 456 }; // Add guid value + + // Spy on console.error and console.warn + consoleErrorSpy = spyOn(console, 'error'); + consoleWarnSpy = spyOn(console, 'warn'); + + // Temporarily override `fields` for testing + Object.defineProperty(service, 'fields', { + value: ['id', 'name', 'guid', 123 as any, null as any], + writable: true, + }); + }); + + it('should set values for valid fields in the form', () => { + service['updateFormAfterReload'](); + + expect(mockForm.controls.id.setValue).toHaveBeenCalledWith(123); // Valid field + expect(mockForm.controls.name.setValue).toHaveBeenCalledWith('Test Name'); // Valid field + expect(mockForm.controls.guid.setValue).toHaveBeenCalledWith(456); // Valid field + }); + + it('should log an error for missing controls', () => { + // Override `fields` to include an invalid field + Object.defineProperty(service, 'fields', { + value: ['id', 'missingField'], + }); + + service['updateFormAfterReload'](); + + expect(consoleErrorSpy).toHaveBeenCalledWith("Control 'missingField' does not exist!"); + }); + + it('should log a warning for invalid field types', () => { + service['updateFormAfterReload'](); + + expect(consoleWarnSpy).toHaveBeenCalledWith("Field '123' is not a valid string key."); + expect(consoleWarnSpy).toHaveBeenCalledWith("Field 'null' is not a valid string key."); + }); + + it('should not throw errors for valid but empty fields', () => { + Object.defineProperty(service, 'fields', { + value: [], // No fields to iterate + }); + + expect(() => service['updateFormAfterReload']()).not.toThrow(); + }); + + it('should reset loading to false after execution', () => { + service['_loading'] = true; + + service['updateFormAfterReload'](); + + expect(service['_loading']).toBe(false); // Ensure loading is reset + }); + }); }); diff --git a/libs/shared/common-services/src/validation.service.spec.ts b/libs/shared/common-services/src/validation.service.spec.ts new file mode 100644 index 0000000000..da1601a185 --- /dev/null +++ b/libs/shared/common-services/src/validation.service.spec.ts @@ -0,0 +1,36 @@ +import { TestBed } from '@angular/core/testing'; +import { ValidationService } from './validation.service'; + +describe('ValidationService', () => { + let service: ValidationService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ValidationService], + }); + + service = TestBed.inject(ValidationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should have a default value of true for validationPassed$', (done: DoneFn) => { + service.validationPassed$.subscribe((value) => { + expect(value).toBe(true); + done(); + }); + }); + + it('should emit the updated value when validationPassed$ changes', (done: DoneFn) => { + // Emit a new value + service.validationPassed$.next(false); + + // Subscribe and verify the updated value + service.validationPassed$.subscribe((value) => { + expect(value).toBe(false); + done(); + }); + }); +}); diff --git a/libs/shared/directives/src/validate-input.directive.spec.ts b/libs/shared/directives/src/validate-input.directive.spec.ts new file mode 100644 index 0000000000..384344ef28 --- /dev/null +++ b/libs/shared/directives/src/validate-input.directive.spec.ts @@ -0,0 +1,196 @@ +import { Component, DebugElement } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { ReactiveFormsModule, FormControl, FormsModule, NgControl } from '@angular/forms'; +import { InputValidationDirective } from './validate-input.directive'; +import { ValidationService } from '@keira/shared/common-services'; +import { take } from 'rxjs'; + +@Component({ + template: ` +
+
+ +
+
+ `, +}) +class TestComponent { + testControl = new FormControl(''); +} + +describe('InputValidationDirective', () => { + let fixture: ComponentFixture; + let validationService: ValidationService; + let debugElement: DebugElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent], + imports: [ReactiveFormsModule, FormsModule, InputValidationDirective], // Add the directive to imports + providers: [ValidationService], + }).compileComponents(); + + fixture = TestBed.createComponent(TestComponent); + validationService = TestBed.inject(ValidationService); + debugElement = fixture.debugElement.query(By.directive(InputValidationDirective)); + fixture.detectChanges(); + }); + + it('should create an instance', () => { + const directive = debugElement.injector.get(InputValidationDirective); + expect(directive).toBeTruthy(); + }); + + it('should display error message when control is invalid and touched', () => { + const control = debugElement.injector.get(NgControl).control; + control?.setValidators(() => ({ required: true })); + control?.markAsTouched(); + control?.updateValueAndValidity(); + + fixture.detectChanges(); + + const errorDiv = debugElement.nativeElement.parentNode.querySelector('.error-message'); + expect(errorDiv).toBeTruthy(); + expect(errorDiv.textContent).toBe('This field is required'); + }); + + it('should remove error message when control becomes valid', () => { + const control = debugElement.injector.get(NgControl).control; + control?.setValidators(() => ({ required: true })); + control?.markAsTouched(); + control?.updateValueAndValidity(); + + fixture.detectChanges(); + + let errorDiv = debugElement.nativeElement.parentNode.querySelector('.error-message'); + expect(errorDiv).toBeTruthy(); + + // Make the control valid + control?.clearValidators(); + control?.setValue('Valid value'); // Set a valid value + control?.updateValueAndValidity(); + + fixture.detectChanges(); + + errorDiv = debugElement.nativeElement.parentNode.querySelector('.error-message'); + expect(errorDiv).toBeNull(); + }); + + it('should handle empty error object gracefully', () => { + const control = debugElement.injector.get(NgControl).control; + control?.setValidators(() => ({})); // No errors + control?.markAsTouched(); + control?.updateValueAndValidity(); + + fixture.detectChanges(); + + const errorDiv = debugElement.nativeElement.parentNode.querySelector('.error-message'); + expect(errorDiv).toBeNull(); + }); + + it('should handle multiple error types gracefully', () => { + const control = debugElement.injector.get(NgControl).control; + control?.setValidators(() => ({ required: true, minlength: true })); // Multiple errors + control?.markAsTouched(); + control?.updateValueAndValidity(); + + fixture.detectChanges(); + + const errorDiv = debugElement.nativeElement.parentNode.querySelector('.error-message'); + expect(errorDiv).toBeTruthy(); + expect(errorDiv.textContent).toBe('This field is required'); // Test only the first error message + }); + + it('should not throw when control is null', () => { + const ngControl = debugElement.injector.get(NgControl); + spyOnProperty(ngControl, 'control', 'get').and.returnValue(null); // Mock control as null + + expect(() => { + fixture.detectChanges(); + }).not.toThrow(); + }); + + it('should not create duplicate error messages if errorDiv already exists', () => { + const control = debugElement.injector.get(NgControl).control; + control?.setValidators(() => ({ required: true })); + control?.markAsTouched(); + control?.updateValueAndValidity(); + + fixture.detectChanges(); + + const initialErrorDiv = debugElement.nativeElement.parentNode.querySelector('.error-message'); + expect(initialErrorDiv).toBeTruthy(); + + // Trigger the updateErrorMessage logic again + fixture.detectChanges(); + + const updatedErrorDiv = debugElement.nativeElement.parentNode.querySelector('.error-message'); + expect(updatedErrorDiv).toBe(initialErrorDiv); // Same errorDiv should remain + }); + + it('should not add error message if parentNode is null', () => { + const control = debugElement.injector.get(NgControl).control; + + // Mock parentNode as null + spyOnProperty(debugElement.nativeElement, 'parentNode', 'get').and.returnValue(null); + + control?.setValidators(() => ({ required: true })); + control?.markAsTouched(); + control?.updateValueAndValidity(); + + fixture.detectChanges(); + + const errorDiv = debugElement.nativeElement.parentNode?.querySelector('.error-message'); + expect(errorDiv).toBeFalsy(); // Error message should not be added + }); + + it('should update validationPassed$ in ValidationService', (done: DoneFn) => { + const control = debugElement.injector.get(NgControl).control; + + control?.setValidators(() => ({ required: true })); + control?.markAsTouched(); + control?.updateValueAndValidity(); + + validationService.validationPassed$ + .pipe(take(1)) // Take only the first emission + .subscribe((isValid) => { + expect(isValid).toBe(false); // Initially invalid + done(); + }); + + // Test invalid state + control?.setValue(''); + fixture.detectChanges(); + + // Test valid state + control?.setValue('Valid value'); + control?.updateValueAndValidity(); + fixture.detectChanges(); + }); + + it('should set touched when control is invalid', () => { + const control = debugElement.injector.get(NgControl).control; + + spyOn(control!, 'markAsTouched'); + control?.setValidators(() => ({ required: true })); + control?.updateValueAndValidity(); + + fixture.detectChanges(); + + expect(control?.markAsTouched).toHaveBeenCalled(); + }); + + it('should not create errorDiv if parentNode is null', () => { + const control = debugElement.injector.get(NgControl).control; + spyOnProperty(debugElement.nativeElement, 'parentNode', 'get').and.returnValue(null); + + control?.setValidators(() => ({ required: true })); + control?.markAsTouched(); + control?.updateValueAndValidity(); + + fixture.detectChanges(); + + expect(debugElement.nativeElement.parentNode?.querySelector('.error-message')).toBeFalsy(); + }); +}); diff --git a/libs/shared/directives/src/validate-input.directive.ts b/libs/shared/directives/src/validate-input.directive.ts index 79dab3325b..15081b4298 100644 --- a/libs/shared/directives/src/validate-input.directive.ts +++ b/libs/shared/directives/src/validate-input.directive.ts @@ -29,28 +29,34 @@ export class InputValidationDirective extends SubscriptionHandler implements OnI ); } - private updateErrorMessage(control: AbstractControl): void { + private updateErrorMessage(control: AbstractControl | null): void { + // Safely remove the existing errorDiv if it exists if (this.errorDiv) { - this.renderer.removeChild(this.el.nativeElement.parentNode, this.errorDiv); + const parent = this.el.nativeElement.parentNode; + if (parent) { + this.renderer.removeChild(parent, this.errorDiv); + } this.errorDiv = null; } if (control?.invalid) { - control?.markAsTouched(); + control.markAsTouched(); } - if (control?.touched && control?.invalid) { - this.errorDiv = this.renderer.createElement('div'); - this.renderer.addClass(this.errorDiv, 'error-message'); - const errorMessage = control?.errors?.['required'] ? 'This field is required' : 'Invalid field'; + if (control?.touched && control?.invalid && control.errors && Object.keys(control.errors).length && this.el.nativeElement.parentNode) { + const parent = this.el.nativeElement.parentNode; + if (parent) { + this.errorDiv = this.renderer.createElement('div'); + this.renderer.addClass(this.errorDiv, 'error-message'); + const errorMessage = control.errors?.['required'] ? 'This field is required' : 'Invalid field'; - const text = this.renderer.createText(errorMessage); - this.renderer.appendChild(this.errorDiv, text); + const text = this.renderer.createText(errorMessage); + this.renderer.appendChild(this.errorDiv, text); - const parent = this.el.nativeElement.parentNode; - this.renderer.appendChild(parent, this.errorDiv); + this.renderer.appendChild(parent, this.errorDiv); + } } - this.validationService.validationPassed$.next(control?.valid); + this.validationService.validationPassed$.next(!!control?.valid); } }