diff --git a/src/app/shared/form/builder/parsers/row-parser.spec.ts b/src/app/shared/form/builder/parsers/row-parser.spec.ts index b60cf8d01c..b7bb083de7 100644 --- a/src/app/shared/form/builder/parsers/row-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/row-parser.spec.ts @@ -9,6 +9,13 @@ describe('RowParser test suite', () => { let row1: FormRowModel; let row2: FormRowModel; let row3: FormRowModel; + let row4: FormRowModel; + let row5: FormRowModel; + let row6: FormRowModel; + let row7: FormRowModel; + let row8: FormRowModel; + let row9: FormRowModel; + let row10: FormRowModel; const scopeUUID = 'testScopeUUID'; const initFormValues = {}; @@ -77,9 +84,10 @@ describe('RowParser test suite', () => { } ], languageCodes: [] - } + } as FormFieldModel ] } as FormRowModel; + row3 = { fields: [ { @@ -96,7 +104,7 @@ describe('RowParser test suite', () => { } ], languageCodes: [] - }, + } as FormFieldModel, { input: {type: 'onebox'}, label: 'Other title', @@ -112,14 +120,248 @@ describe('RowParser test suite', () => { } ], languageCodes: [] - } + } as FormFieldModel ] } as FormRowModel; + row4 = { + fields: [ + { + input: { + type: 'dropdown' + }, + label: 'Type', + mandatory: 'false', + repeatable: false, + hints: 'Select the tyupe.', + selectableMetadata: [ + { + metadata: 'type', + authority: 'common_types_dataset', + closed: false + } + ], + languageCodes: [] + } as FormFieldModel, + { + input: {type: 'series'}, + label: 'Series/Report No.', + mandatory: 'false', + repeatable: false, + hints: 'Enter the series and number assigned to this item by your community.', + selectableMetadata: [ + { + metadata: 'series', + } + ], + languageCodes: [] + } as FormFieldModel + ] + } as FormRowModel; + + row5 = { + fields: [ + { + input: { + type: 'lookup-name' + }, + label: 'Author', + mandatory: 'false', + repeatable: false, + hints: 'Enter the name of the author.', + selectableMetadata: [ + { + metadata: 'author', + authority: 'RPAuthority', + closed: false + } + ], + languageCodes: [] + } as FormFieldModel + ] + } as FormRowModel; + + row6 = { + fields: [ + { + input: { + type: 'list' + }, + label: 'Type', + mandatory: 'false', + repeatable: true, + hints: 'Select the type.', + selectableMetadata: [ + { + metadata: 'type', + authority: 'type_programme', + closed: false + } + ], + languageCodes: [] + } as FormFieldModel + ] + } as FormRowModel; + + row7 = { + fields: [ + { + input: { + type: 'date' + }, + label: 'Date of Issue.', + mandatory: 'true', + repeatable: false, + hints: 'Please give the date of previous publication or public distribution. You can leave out the day and/or month if they aren\'t applicable.', + mandatoryMessage: 'You must enter at least the year.', + selectableMetadata: [ + { + metadata: 'date', + } + ], + languageCodes: [] + } as FormFieldModel + ] + } as FormRowModel; + + row8 = { + fields: [ + { + input: { + type: 'tag' + }, + label: 'Keywords', + mandatory: 'false', + repeatable: false, + hints: 'Local controlled vocabulary.', + selectableMetadata: [ + { + metadata: 'subject', + authority: 'JOURNALAuthority', + closed: false + } + ], + languageCodes: [] + } as FormFieldModel + ] + } as FormRowModel; + + row9 = { + fields: [ + { + input: { + type: 'textarea' + }, + label: 'Description', + mandatory: 'false', + repeatable: false, + hints: 'Enter a description.', + selectableMetadata: [ + { + metadata: 'description' + } + ], + languageCodes: [] + } as FormFieldModel + ] + } as FormRowModel; + + row10 = { + fields: [ + { + input: { + type: 'group' + }, + rows: [ + { + fields: [ + { + input: { + type: 'onebox' + }, + label: 'Author', + mandatory: 'false', + repeatable: false, + hints: 'Enter the name of the author.', + selectableMetadata: [ + { + metadata: 'author' + } + ], + languageCodes: [] + }, + { + input: { + type: 'onebox' + }, + label: 'Affiliation', + mandatory: false, + repeatable: true, + hints: 'Enter the affiliation of the author.', + selectableMetadata: [ + { + metadata: 'affiliation' + } + ], + languageCodes: [] + } + ] + } + ], + label: 'Authors', + mandatory: 'true', + repeatable: false, + mandatoryMessage: 'Entering at least the first author is mandatory.', + hints: 'Enter the names of the authors of this item.', + selectableMetadata: [ + { + metadata: 'author' + } + ], + languageCodes: [] + } as FormFieldModel + ] + } as FormRowModel; }); it('should init parser properly', () => { - const parser = new RowParser(row1, scopeUUID, initFormValues, submissionScope, readOnly); + let parser = new RowParser(row1, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row2, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row3, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row4, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row5, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row6, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row7, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row8, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row9, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row10, scopeUUID, initFormValues, submissionScope, readOnly); expect(parser instanceof RowParser).toBe(true); }); diff --git a/src/app/shared/form/form.component.spec.ts b/src/app/shared/form/form.component.spec.ts index efc6383603..155e009fb1 100644 --- a/src/app/shared/form/form.component.spec.ts +++ b/src/app/shared/form/form.component.spec.ts @@ -1,13 +1,16 @@ -// Load the implementations that should be tested import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, inject, TestBed, } from '@angular/core/testing'; import { CommonModule } from '@angular/common'; import { BrowserModule } from '@angular/platform-browser'; -import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { Observable } from 'rxjs/Observable'; +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import 'rxjs/add/observable/of'; -import { DynamicFormControlModel, DynamicFormValidationService, DynamicInputModel } from '@ng-dynamic-forms/core'; +import { + DynamicFormArrayModel, + DynamicFormControlEvent, + DynamicFormControlModel, + DynamicFormValidationService, + DynamicInputModel +} from '@ng-dynamic-forms/core'; import { Store } from '@ngrx/store'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; @@ -16,6 +19,9 @@ import { FormComponent } from './form.component'; import { FormService } from './form.service'; import { FormBuilderService } from './builder/form-builder.service'; import { FormState } from './form.reducers'; +import { FormChangeAction, FormStatusChangeAction } from './form.actions'; +import { MockStore } from '../testing/mock-store'; +import { FormFieldMetadataValueObject } from './builder/models/form-field-metadata-value.model'; function createTestComponent(html: string, type: { new(...args: any[]): T }): ComponentFixture { TestBed.overrideComponent(type, { @@ -76,33 +82,47 @@ export const TEST_FORM_MODEL = [ ), ]; -export const TEST_FORM_GROUP = { - dc_title: new FormControl(), - dc_title_alternative: new FormControl(), - dc_publisher: new FormControl(), - dc_identifier_citation: new FormControl(), - dc_identifier_issn: new FormControl() -} +export const TEST_FORM_MODEL_WITH_ARRAY = [ + new DynamicFormArrayModel({ -describe('Form component', () => { + id: 'bootstrapFormArray', + initialCount: 1, + label: 'Form Array', + groupFactory: () => { + return [ + new DynamicInputModel({ + + id: 'bootstrapArrayGroupInput', + placeholder: 'example array group input', + readOnly: false + }) + ]; + } + }) +]; + +describe('FormComponent test suite', () => { let testComp: TestComponent; + let formComp: FormComponent; let testFixture: ComponentFixture; + let formFixture: ComponentFixture; + const formState: FormState = { + testForm: { + data: { + dc_title: null, + dc_title_alternative: null, + dc_publisher: null, + dc_identifier_citation: null, + dc_identifier_issn: null + }, + valid: false, + errors: [] + } + }; let html; - const formServiceStub = { - getFormData: (formId) => Observable.of([]) - } - const formBuilderServiceStub = { - createFormGroup: (formModel) => new FormGroup(TEST_FORM_GROUP) - } - const submissionFormsConfigServiceStub = {}; - const store: Store = jasmine.createSpyObj('store', { - /* tslint:disable:no-empty */ - dispatch: {}, - /* tslint:enable:no-empty */ - select: Observable.of({}) - }); + const store: MockStore = new MockStore(formState); // async beforeEach beforeEach(async(() => { @@ -135,23 +155,265 @@ describe('Form component', () => { })); - // synchronous beforeEach - beforeEach(() => { - html = ` - `; + describe('', () => { + // synchronous beforeEach + beforeEach(() => { + html = ` + `; - testFixture = createTestComponent(html, TestComponent) as ComponentFixture; - testComp = testFixture.componentInstance; + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + it('should create FormComponent', inject([FormComponent], (app: FormComponent) => { + + expect(app).toBeDefined(); + })); }); - it('should create Form Component', inject([FormComponent], (app: FormComponent) => { + describe('', () => { + beforeEach(() => { - expect(app).toBeDefined(); - })); + formFixture = TestBed.createComponent(FormComponent); + formComp = formFixture.componentInstance; // FormComponent test instance + formComp.formId = 'testForm'; + formComp.formModel = TEST_FORM_MODEL; + formComp.displaySubmit = false; + formFixture.detectChanges(); + spyOn(store, 'dispatch'); + }); + afterEach(() => { + formFixture.destroy(); + formComp = null; + }); + + it('should dispatch a FormStatusChangeAction when Form group status changes', () => { + const control = formComp.formGroup.get(['dc_title']); + control.setValue('Test Title'); + + expect(store.dispatch).toHaveBeenCalledWith(new FormStatusChangeAction('testForm', formComp.formGroup.valid)); + + }); + + it('should display form errors when errors are added to the state', () => { + const errors = [{ + fieldId: 'dc_title', + message: 'error.validation.required' + }]; + + formState.testForm.errors = errors; + store.nextState(formState); + formFixture.detectChanges(); + + expect((formComp as any).formErrors).toEqual(errors); + + }); + + it('should remove form errors when errors are empty in the state', () => { + (formComp as any).formErrors = [{ + fieldId: 'dc_title', + message: 'error.validation.required' + }]; + const errors = []; + + formState.testForm.errors = errors; + store.nextState(formState); + formFixture.detectChanges(); + + expect((formComp as any).formErrors).toEqual(errors); + + }); + + it('should dispatch FormChangeAction on form change', inject([FormBuilderService], (service: FormBuilderService) => { + const event = { + $event: new FormFieldMetadataValueObject('Test Title'), + context: null, + control: formComp.formGroup.get('dc_title'), + group: formComp.formGroup, + model: formComp.formModel[0], + type: 'change' + } as DynamicFormControlEvent; + + spyOn(formComp.change, 'emit'); + + formComp.onChange(event); + + expect(store.dispatch).toHaveBeenCalledWith(new FormChangeAction('testForm', service.getValueFromModel(formComp.formModel))); + expect(formComp.change.emit).toHaveBeenCalled(); + })); + + it('should emit change on form change', inject([FormBuilderService], (service: FormBuilderService) => { + const event = { + $event: new FormFieldMetadataValueObject('Test Title'), + context: null, + control: formComp.formGroup.get('dc_title'), + group: formComp.formGroup, + model: formComp.formModel[0], + type: 'change' + } as DynamicFormControlEvent; + + spyOn(formComp.change, 'emit'); + + formComp.onChange(event); + + expect(formComp.change.emit).toHaveBeenCalled(); + })); + + it('should not emit change Event on form change when emitChange is false', inject([FormBuilderService], (service: FormBuilderService) => { + const event = { + $event: new FormFieldMetadataValueObject('Test Title'), + context: null, + control: formComp.formGroup.get('dc_title'), + group: formComp.formGroup, + model: formComp.formModel[0], + type: 'change' + } as DynamicFormControlEvent; + + formComp.emitChange = false; + spyOn(formComp.change, 'emit'); + + formComp.onChange(event); + + expect(formComp.change.emit).not.toHaveBeenCalled(); + })); + + it('should emit blur Event on blur', () => { + const event = { + $event: new FocusEvent('blur'), + context: null, + control: formComp.formGroup.get('dc_title'), + group: formComp.formGroup, + model: formComp.formModel[0], + type: 'blur' + } as DynamicFormControlEvent; + + spyOn(formComp.blur, 'emit'); + + formComp.onBlur(event); + + expect(formComp.blur.emit).toHaveBeenCalled(); + }); + + it('should emit focus Event on focus', () => { + const event = { + $event: new FocusEvent('focus'), + context: null, + control: formComp.formGroup.get('dc_title'), + group: formComp.formGroup, + model: formComp.formModel[0], + type: 'focus' + } as DynamicFormControlEvent; + + spyOn(formComp.focus, 'emit'); + + formComp.onFocus(event); + + expect(formComp.focus.emit).toHaveBeenCalled(); + }); + + it('should return Observable of form status', () => { + + const control = formComp.formGroup.get(['dc_title']); + control.setValue('Test Title'); + formState.testForm.valid = true; + store.nextState(formState); + formFixture.detectChanges(); + + formComp.isValid().subscribe((valid) => { + expect(valid).toBe(true); + }); + }); + + it('should emit submit Event on form submit whether the form is valid', () => { + + const control = formComp.formGroup.get(['dc_title']); + control.setValue('Test Title'); + formState.testForm.valid = true; + spyOn(formComp.submit, 'emit'); + + store.nextState(formState); + formFixture.detectChanges(); + + formComp.onSubmit(); + expect(formComp.submit.emit).toHaveBeenCalled(); + }); + + it('should not emit submit Event on form submit whether the form is not valid', () => { + + spyOn((formComp as any).formService, 'validateAllFormFields'); + + store.nextState(formState); + formFixture.detectChanges(); + + formComp.onSubmit(); + expect((formComp as any).formService.validateAllFormFields).toHaveBeenCalled(); + }); + + it('should reset form group', () => { + + spyOn(formComp.formGroup, 'reset'); + + formComp.reset(); + + expect(formComp.formGroup.reset).toHaveBeenCalled(); + }); + }); + + describe('', () => { + beforeEach(() => { + + formFixture = TestBed.createComponent(FormComponent); + formComp = formFixture.componentInstance; // FormComponent test instance + formComp.formId = 'testFormArray'; + formComp.formModel = TEST_FORM_MODEL_WITH_ARRAY; + formComp.displaySubmit = false; + formFixture.detectChanges(); + spyOn(store, 'dispatch'); + }); + + afterEach(() => { + formFixture.destroy(); + formComp = null; + }); + + it('should return ReadOnly property from array item', inject([FormBuilderService], (service: FormBuilderService) => { + const readOnly = formComp.isItemReadOnly(formComp.formModel[0] as DynamicFormArrayModel, 0); + + expect(readOnly).toBe(false); + })); + + it('should dispatch FormChangeAction when an item has been added to an array', inject([FormBuilderService], (service: FormBuilderService) => { + formComp.insertItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1); + + expect(store.dispatch).toHaveBeenCalledWith(new FormChangeAction('testFormArray', service.getValueFromModel(formComp.formModel))); + })); + + it('should emit addArrayItem Event when an item has been added to an array', inject([FormBuilderService], (service: FormBuilderService) => { + spyOn(formComp.addArrayItem, 'emit'); + + formComp.insertItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1); + + expect(formComp.addArrayItem.emit).toHaveBeenCalled(); + })); + + it('should dispatch FormChangeAction when an item has been removed from an array', inject([FormBuilderService], (service: FormBuilderService) => { + formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1); + + expect(store.dispatch).toHaveBeenCalledWith(new FormChangeAction('testFormArray', service.getValueFromModel(formComp.formModel))); + })); + + it('should emit removeArrayItem Event when an item has been removed from an array', inject([FormBuilderService], (service: FormBuilderService) => { + spyOn(formComp.removeArrayItem, 'emit'); + + formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1); + + expect(formComp.removeArrayItem.emit).toHaveBeenCalled(); + })); + }) }); // declare a test component diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index 7a894ca3e5..922f2e40f8 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -21,7 +21,7 @@ import { import { FormBuilderService } from './builder/form-builder.service'; import { Observable } from 'rxjs/Observable'; import { Subscription } from 'rxjs/Subscription'; -import { hasValue, isNotNull, isNull } from '../empty.util'; +import { hasValue, isNotEmpty, isNotNull, isNull } from '../empty.util'; import { FormService } from './form.service'; import { formObjectFromIdSelector } from './selectors'; import { FormEntry, FormError } from './form.reducers'; @@ -154,7 +154,7 @@ export class FormComponent implements OnDestroy, OnInit { this.subs.push( this.store.select(formObjectFromIdSelector(this.formId)) - .filter((formState: FormEntry) => !!formState && !isEmpty(formState.errors)) + .filter((formState: FormEntry) => !!formState && (isNotEmpty(formState.errors) || isNotEmpty(this.formErrors))) .map((formState) => formState.errors) .distinctUntilChanged() // .delay(100) // this terrible delay is here to prevent the detection change error @@ -223,7 +223,7 @@ export class FormComponent implements OnDestroy, OnInit { /** * Method to keep synchronized form controls values with form state */ - private keepSync() { + private keepSync(): void { this.subs.push(this.formService.getFormData(this.formId) .subscribe((stateFormData) => { if (!Object.is(stateFormData, this.formGroup.value) && this.formGroup) { @@ -232,15 +232,15 @@ export class FormComponent implements OnDestroy, OnInit { })); } - onBlur(event) { + onBlur(event: DynamicFormControlEvent): void { this.blur.emit(event); } - onFocus(event) { + onFocus(event: DynamicFormControlEvent): void { this.focus.emit(event); } - onChange(event) { + onChange(event: DynamicFormControlEvent): void { const action: FormChangeAction = new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel)); this.store.dispatch(action); @@ -260,7 +260,7 @@ export class FormComponent implements OnDestroy, OnInit { * Method called on submit. * Emit a new submit Event whether the form is valid, mark fields with error otherwise */ - onSubmit() { + onSubmit(): void { if (this.getFormGroupValidStatus()) { this.submit.emit(this.formService.getFormData(this.formId)); } else { @@ -271,7 +271,7 @@ export class FormComponent implements OnDestroy, OnInit { /** * Method to reset form fields */ - reset() { + reset(): void { this.formGroup.reset(); } @@ -281,14 +281,14 @@ export class FormComponent implements OnDestroy, OnInit { return model.readOnly; } - removeItem($event, arrayContext: DynamicFormArrayModel, index: number) { + removeItem($event, arrayContext: DynamicFormArrayModel, index: number): void { const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; this.removeArrayItem.emit(this.getEvent($event, arrayContext, index, 'remove')); this.formBuilderService.removeFormArrayGroup(index, formArrayControl, arrayContext); this.store.dispatch(new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel))); } - insertItem($event, arrayContext: DynamicFormArrayModel, index: number) { + insertItem($event, arrayContext: DynamicFormArrayModel, index: number): void { const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; this.formBuilderService.insertFormArrayGroup(index, formArrayControl, arrayContext); this.addArrayItem.emit(this.getEvent($event, arrayContext, index, 'add')); diff --git a/src/app/shared/testing/mock-store.ts b/src/app/shared/testing/mock-store.ts index c619b5aa77..a3bca3c1b5 100644 --- a/src/app/shared/testing/mock-store.ts +++ b/src/app/shared/testing/mock-store.ts @@ -9,12 +9,13 @@ export class MockStore extends BehaviorSubject { } dispatch = (action: Action): void => { - console.info(); - } + // console.info(action); + }; select = (pathOrMapFn: any): Observable => { - return Observable.of(this.getValue()); - } + return this.asObservable() + .map((value) => pathOrMapFn.projector(value)) + }; nextState(_newState: T) { this.next(_newState);