Skip to content

Commit

Permalink
Add tests for directive and single editor.
Browse files Browse the repository at this point in the history
  • Loading branch information
Exitare committed Jan 27, 2025
1 parent 4196e13 commit b009f20
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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)) },
Expand Down Expand Up @@ -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
});
});
});
36 changes: 36 additions & 0 deletions libs/shared/common-services/src/validation.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
196 changes: 196 additions & 0 deletions libs/shared/directives/src/validate-input.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -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: `
<form>
<div>
<input type="text" [formControl]="testControl" keiraInputValidation />
</div>
</form>
`,
})
class TestComponent {
testControl = new FormControl('');
}

describe('InputValidationDirective', () => {
let fixture: ComponentFixture<TestComponent>;
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();
});
});
30 changes: 18 additions & 12 deletions libs/shared/directives/src/validate-input.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

0 comments on commit b009f20

Please sign in to comment.