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);
}
}