diff --git a/ui/src/app/metadata/configuration/action/restore.action.ts b/ui/src/app/metadata/configuration/action/restore.action.ts index 0407e830b..978e9eee4 100644 --- a/ui/src/app/metadata/configuration/action/restore.action.ts +++ b/ui/src/app/metadata/configuration/action/restore.action.ts @@ -34,12 +34,12 @@ export class RestoreVersionError implements Action { export class UpdateRestorationChangesRequest implements Action { readonly type = RestoreActionTypes.UPDATE_RESTORATION_REQUEST; - constructor(public payload: any) { } + constructor(public payload: Partial) { } } export class UpdateRestorationChangesSuccess implements Action { readonly type = RestoreActionTypes.UPDATE_RESTORATION_SUCCESS; - constructor(public payload: any) { } + constructor(public payload: Partial) { } } export class UpdateRestoreFormStatus implements Action { diff --git a/ui/src/app/metadata/configuration/container/configuration.component.spec.ts b/ui/src/app/metadata/configuration/container/configuration.component.spec.ts index 71bb05ab0..b1d9bc2f2 100644 --- a/ui/src/app/metadata/configuration/container/configuration.component.spec.ts +++ b/ui/src/app/metadata/configuration/container/configuration.component.spec.ts @@ -1,10 +1,9 @@ -import { Component, ViewChild, Input } from '@angular/core'; +import { Component, ViewChild } from '@angular/core'; import { TestBed, async, ComponentFixture } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { StoreModule, combineReducers } from '@ngrx/store'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; -import { MetadataConfiguration } from '../model/metadata-configuration'; import { ConfigurationComponent } from './configuration.component'; import * as fromConfiguration from '../reducer'; import * as fromProviders from '../../provider/reducer'; @@ -19,11 +18,6 @@ import { MockI18nModule } from '../../../../testing/i18n.stub'; class TestHostComponent { @ViewChild(ConfigurationComponent) public componentUnderTest: ConfigurationComponent; - - configuration: MetadataConfiguration = { - dates: [], - sections: [] - }; } describe('Metadata Configuration Page Component', () => { diff --git a/ui/src/app/metadata/configuration/container/restore-edit-step.component.spec.ts b/ui/src/app/metadata/configuration/container/restore-edit-step.component.spec.ts new file mode 100644 index 000000000..7d468b5af --- /dev/null +++ b/ui/src/app/metadata/configuration/container/restore-edit-step.component.spec.ts @@ -0,0 +1,107 @@ +import { Component, ViewChild, NO_ERRORS_SCHEMA } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, combineReducers, Store } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; + +import { RestoreEditStepComponent } from './restore-edit-step.component'; +import * as fromConfiguration from '../reducer'; +import * as fromProviders from '../../provider/reducer'; +import * as fromResolvers from '../../resolver/reducer'; +import * as fromWizard from '../../../wizard/reducer'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; + +import { + RestoreActionTypes +} from '../action/restore.action'; +import { WizardActionTypes } from '../../../wizard/action/wizard.action'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(RestoreEditStepComponent) + public componentUnderTest: RestoreEditStepComponent; +} + +describe('Restore Version Edit Step Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: RestoreEditStepComponent; + let store: Store; + let dispatchSpy; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + StoreModule.forRoot({ + 'metadata-configuration': combineReducers(fromConfiguration.reducers), + 'provider': combineReducers(fromProviders.reducers), + 'resolver': combineReducers(fromResolvers.reducers), + 'wizard': combineReducers(fromWizard.reducers) + }), + MockI18nModule, + RouterTestingModule + ], + declarations: [ + RestoreEditStepComponent, + TestHostComponent + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + + store = TestBed.get(Store); + dispatchSpy = spyOn(store, 'dispatch'); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should compile', () => { + expect(app).toBeTruthy(); + }); + + describe('onChange', () => { + it('should dispatch an update changes event', () => { + app.onChange({ name: 'test' }); + expect(store.dispatch).toHaveBeenCalled(); + expect(dispatchSpy.calls.mostRecent().args[0].type).toBe(RestoreActionTypes.UPDATE_RESTORATION_REQUEST); + }); + }); + + describe('updateStatus', () => { + it('should dispatch an update form status event', () => { + app.updateStatus([{ value: 'foo' }, 'common']); + expect(store.dispatch).toHaveBeenCalled(); + expect(dispatchSpy.calls.mostRecent().args[0].type).toBe(RestoreActionTypes.UPDATE_STATUS); + }); + + it('should dispatch an update form status event', () => { + app.updateStatus([{}, 'common']); + expect(store.dispatch).toHaveBeenCalled(); + expect(dispatchSpy.calls.mostRecent().args[0].type).toBe(RestoreActionTypes.UPDATE_STATUS); + }); + }); + + describe('updateLock', () => { + it('should dispatch a LockEditor event when passed a locked status', () => { + app.updateLock(true); + expect(store.dispatch).toHaveBeenCalled(); + expect(dispatchSpy.calls.mostRecent().args[0].type).toBe(WizardActionTypes.LOCK); + }); + + it('should dispatch a UnlockEditor event when passed a locked status', () => { + app.updateLock(false); + expect(store.dispatch).toHaveBeenCalled(); + expect(dispatchSpy.calls.mostRecent().args[0].type).toBe(WizardActionTypes.UNLOCK); + }); + }); +}); diff --git a/ui/src/app/metadata/configuration/container/restore-edit-step.component.ts b/ui/src/app/metadata/configuration/container/restore-edit-step.component.ts index 3d93f5ebd..904cf43f6 100644 --- a/ui/src/app/metadata/configuration/container/restore-edit-step.component.ts +++ b/ui/src/app/metadata/configuration/container/restore-edit-step.component.ts @@ -8,7 +8,7 @@ import { import { getWizardDefinition, getSchema, getValidators, getCurrent, getWizardIndex } from '../../../wizard/reducer'; import { Wizard, WizardStep } from '../../../wizard/model'; import { Metadata } from '../../domain/domain.type'; -import { map, switchMap, withLatestFrom } from 'rxjs/operators'; +import { map, switchMap, withLatestFrom, filter } from 'rxjs/operators'; import { NAV_FORMATS } from '../../domain/component/editor-nav.component'; import { UpdateRestorationChangesRequest, UpdateRestoreFormStatus } from '../action/restore.action'; import { LockEditor, UnlockEditor } from '../../../wizard/action/wizard.action'; @@ -32,45 +32,51 @@ export class RestoreEditStepComponent implements OnDestroy { model$: Observable = this.store.select(getFormattedModel); step$: Observable = this.store.select(getCurrent); - lockable$: Observable = this.step$.pipe(map(step => step.locked)); + lockable$: Observable = this.step$.pipe(filter(s => !!s), map(step => step.locked)); validators$: Observable; formats = NAV_FORMATS; constructor( - private store: Store, - private route: ActivatedRoute + private store: Store ) { this.validators$ = this.definition$.pipe( + filter(def => !!def), map(def => def.validatorParams), switchMap(params => combineLatest(params.map(p => this.store.select(p)))), switchMap(selections => this.store.select(getValidators(selections))) ); this.step$ - .pipe(takeUntil(this.ngUnsubscribe)) - .subscribe(s => this.lockChange$.next(s && s.locked ? true : false)); + .pipe(takeUntil(this.ngUnsubscribe), filter(step => !!step)) + .subscribe(s => this.lockChange$.next(s.locked ? true : false)); this.lockChange$ .pipe(takeUntil(this.ngUnsubscribe)) - .subscribe(locked => this.store.dispatch(locked ? new LockEditor() : new UnlockEditor())); + .subscribe(this.updateLock); this.statusChange$ .pipe( takeUntil(this.ngUnsubscribe), withLatestFrom(this.store.select(getWizardIndex)) ) - .subscribe(([errors, currentPage]) => { - const status = { [currentPage]: !(errors.value) ? 'VALID' : 'INVALID' }; - this.store.dispatch(new UpdateRestoreFormStatus(status)); - }); + .subscribe(this.updateStatus); } onChange(changes: any): void { this.store.dispatch(new UpdateRestorationChangesRequest(changes)); } + updateStatus([errors, currentPage]) { + const status = { [currentPage]: !(errors.value) ? 'VALID' : 'INVALID' }; + this.store.dispatch(new UpdateRestoreFormStatus(status)); + } + + updateLock(locked: boolean) { + this.store.dispatch(locked ? new LockEditor() : new UnlockEditor()); + } + ngOnDestroy() { this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); diff --git a/ui/src/app/metadata/configuration/container/restore-edit.component.spec.ts b/ui/src/app/metadata/configuration/container/restore-edit.component.spec.ts new file mode 100644 index 000000000..847ab5623 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/restore-edit.component.spec.ts @@ -0,0 +1,79 @@ +import { Component, ViewChild, NO_ERRORS_SCHEMA } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, combineReducers, Store } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; + +import { RestoreEditComponent } from './restore-edit.component'; +import * as fromConfiguration from '../reducer'; +import * as fromProviders from '../../provider/reducer'; +import * as fromResolvers from '../../resolver/reducer'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { RestoreActionTypes } from '../action/restore.action'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(RestoreEditComponent) + public componentUnderTest: RestoreEditComponent; +} + +describe('Restore Version Edit Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: RestoreEditComponent; + let store: Store; + let dispatchSpy; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + StoreModule.forRoot({ + 'metadata-configuration': combineReducers(fromConfiguration.reducers), + 'provider': combineReducers(fromProviders.reducers), + 'resolver': combineReducers(fromResolvers.reducers) + }), + MockI18nModule, + RouterTestingModule + ], + declarations: [ + RestoreEditComponent, + TestHostComponent + ], + schemas: [ NO_ERRORS_SCHEMA ] + }).compileComponents(); + + store = TestBed.get(Store); + dispatchSpy = spyOn(store, 'dispatch'); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should compile', () => { + expect(app).toBeTruthy(); + }); + + describe('save', () => { + it('should dispatch a save request event', () => { + app.save(); + expect(store.dispatch).toHaveBeenCalled(); + expect(dispatchSpy.calls.mostRecent().args[0].type).toBe(RestoreActionTypes.RESTORE_VERSION_REQUEST); + }); + }); + + describe('cancel', () => { + it('should dispatch a cancel request event', () => { + app.cancel(); + expect(store.dispatch).toHaveBeenCalled(); + expect(dispatchSpy.calls.mostRecent().args[0].type).toBe(RestoreActionTypes.CANCEL_RESTORE); + }); + }); +}); diff --git a/ui/src/app/metadata/configuration/container/version-options.component.spec.ts b/ui/src/app/metadata/configuration/container/version-options.component.spec.ts new file mode 100644 index 000000000..86f78f839 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/version-options.component.spec.ts @@ -0,0 +1,82 @@ +import { Component, ViewChild, NO_ERRORS_SCHEMA } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, combineReducers } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; + +import { VersionOptionsComponent } from './version-options.component'; +import * as fromConfiguration from '../reducer'; +import * as fromProviders from '../../provider/reducer'; +import * as fromResolvers from '../../resolver/reducer'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { Metadata } from '../../domain/domain.type'; +import { ViewportScroller } from '@angular/common'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(VersionOptionsComponent) + public componentUnderTest: VersionOptionsComponent; +} + +describe('Metadata Version Options Page Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: VersionOptionsComponent; + let scroller: ViewportScroller; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + StoreModule.forRoot({ + 'metadata-configuration': combineReducers(fromConfiguration.reducers), + 'provider': combineReducers(fromProviders.reducers), + 'resolver': combineReducers(fromResolvers.reducers) + }), + MockI18nModule, + RouterTestingModule + ], + declarations: [ + VersionOptionsComponent, + TestHostComponent + ], + schemas: [ NO_ERRORS_SCHEMA ] + }).compileComponents(); + + scroller = TestBed.get(ViewportScroller); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should compile', () => { + expect(app).toBeTruthy(); + }); + + describe('setModel method', () => { + it('should set attributes based on the passed data', () => { + app.setModel({ id: 'foo', '@type': 'bar' } as Metadata); + expect(app.id).toBe('foo'); + expect(app.kind).toBe('provider'); + + app.setModel({ resourceId: 'baz' } as Metadata); + expect(app.id).toBe('baz'); + expect(app.kind).toBe('resolver'); + }); + }); + + describe('onScrollTo method', () => { + it('should set attributes based on the passed data', () => { + spyOn(scroller, 'scrollToAnchor'); + app.onScrollTo('foo'); + expect(scroller.scrollToAnchor).toHaveBeenCalledWith('foo'); + }); + }); +}); diff --git a/ui/src/app/metadata/configuration/container/version.component.spec.ts b/ui/src/app/metadata/configuration/container/version.component.spec.ts new file mode 100644 index 000000000..ae745f1b4 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/version.component.spec.ts @@ -0,0 +1,56 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, combineReducers } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; + +import { VersionComponent } from './version.component'; +import * as fromConfiguration from '../reducer'; +import * as fromProviders from '../../provider/reducer'; +import * as fromResolvers from '../../resolver/reducer'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(VersionComponent) + public componentUnderTest: VersionComponent; +} + +describe('Metadata Version Page Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: VersionComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + StoreModule.forRoot({ + 'metadata-configuration': combineReducers(fromConfiguration.reducers), + 'provider': combineReducers(fromProviders.reducers), + 'resolver': combineReducers(fromResolvers.reducers) + }), + MockI18nModule, + RouterTestingModule + ], + declarations: [ + VersionComponent, + TestHostComponent + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should compile', () => { + expect(app).toBeTruthy(); + }); +}); diff --git a/ui/src/app/metadata/configuration/reducer/restore.reducer.spec.ts b/ui/src/app/metadata/configuration/reducer/restore.reducer.spec.ts index 9aa588b98..97795549b 100644 --- a/ui/src/app/metadata/configuration/reducer/restore.reducer.spec.ts +++ b/ui/src/app/metadata/configuration/reducer/restore.reducer.spec.ts @@ -1,6 +1,13 @@ import { reducer } from './restore.reducer'; import * as fromRestore from './restore.reducer'; -import * as actions from '../action/restore.action'; +import { + RestoreActionTypes, + RestoreActionsUnion, + UpdateRestorationChangesSuccess, + SetSavingStatus, + UpdateRestoreFormStatus +} from '../action/restore.action'; +import { Metadata } from '../../domain/domain.type'; describe('Restore Reducer', () => { @@ -14,20 +21,77 @@ describe('Restore Reducer', () => { }); }); - describe('SET_HISTORY action', () => { + describe(`${RestoreActionTypes.UPDATE_RESTORATION_SUCCESS} action`, () => { it('should set the state metadata model', () => { const serviceEnabled = true; - const action = new actions.UpdateRestorationChangesSuccess({ serviceEnabled }); + const action = new UpdateRestorationChangesSuccess({ serviceEnabled }); const result = reducer(fromRestore.initialState, action); - expect(Object.keys(result.changes)).toEqual({serviceEnabled: true}); + expect(result.changes).toEqual({serviceEnabled} as Metadata); + }); + }); + + describe(`${RestoreActionTypes.SET_SAVING_STATUS} action`, () => { + it('should set the state saving status', () => { + const action = new SetSavingStatus(true); + const result = reducer(fromRestore.initialState, action); + + expect(result.saving).toBe(true); + }); + }); + + describe(`${RestoreActionTypes.UPDATE_STATUS} action`, () => { + it('should set the state saving status', () => { + const action = new UpdateRestoreFormStatus({foo: 'INVALID'}); + const result = reducer(fromRestore.initialState, action); + + expect(result.status.foo).toBe('INVALID'); }); }); describe('selector function', () => { describe('getChanges', () => { it('should return the selected version id', () => { - expect(fromRestore.getChanges({ ...baseState, serviceEnabled: false })).toEqual({ serviceEnabled: false }); + expect(fromRestore.getChanges({ ...baseState, changes: { serviceEnabled: false } })).toEqual({ serviceEnabled: false }); + }); + }); + + describe('isRestorationSaved', () => { + it('should return false if there are outstanding changes', () => { + expect(fromRestore.isRestorationSaved({ ...baseState, changes: { name: 'too' } })).toBe(false); + }); + + it('should return true if there are no outstanding changes', () => { + expect(fromRestore.isRestorationSaved({ ...baseState, changes: {} })).toBe(true); + }); + }); + + describe('getFormStatus', () => { + it('should return the current form status', () => { + expect(fromRestore.getFormStatus({ ...baseState, status: { common: 'INVALID' } })).toEqual({ common: 'INVALID' }); + }); + }); + + describe('isRestorationSaving', () => { + it('should return the saving status', () => { + expect(fromRestore.isRestorationSaving({ ...baseState })).toBe(false); + expect(fromRestore.isRestorationSaving({ ...baseState, saving: true })).toBe(true); + }); + }); + + describe('isRestorationValid', () => { + it('should return false if any forms have an invalid status', () => { + expect(fromRestore.isRestorationValid({ ...baseState, status: { common: 'INVALID' } })).toBe(false); + }); + + it('should return true if all forms have a valid status', () => { + expect(fromRestore.isRestorationValid({ ...baseState, status: { common: 'VALID' } })).toBe(true); + }); + }); + + describe('getInvalidRestorationForms', () => { + it('should return the form names that are invalid', () => { + expect(fromRestore.getInvalidRestorationForms({ ...baseState, status: { common: 'INVALID' } })).toEqual(['common']); }); }); }); diff --git a/ui/src/app/metadata/configuration/reducer/restore.reducer.ts b/ui/src/app/metadata/configuration/reducer/restore.reducer.ts index f2e000728..e0c58c2b7 100644 --- a/ui/src/app/metadata/configuration/reducer/restore.reducer.ts +++ b/ui/src/app/metadata/configuration/reducer/restore.reducer.ts @@ -4,7 +4,7 @@ import { RestoreActionTypes, RestoreActionsUnion } from '../action/restore.actio export interface RestoreState { saving: boolean; status: { [key: string]: string }; - changes: Metadata; + changes: Partial; } export const initialState: RestoreState = { diff --git a/ui/src/app/metadata/configuration/reducer/version.reducer.spec.ts b/ui/src/app/metadata/configuration/reducer/version.reducer.spec.ts index e69de29bb..a564df384 100644 --- a/ui/src/app/metadata/configuration/reducer/version.reducer.spec.ts +++ b/ui/src/app/metadata/configuration/reducer/version.reducer.spec.ts @@ -0,0 +1,108 @@ +import { + reducer, + getVersionModel, + getVersionModelLoaded, + getSelectedMetadataId, + getSelectedVersionId, + getSelectedVersionType +} from './version.reducer'; +import * as fromVersion from './version.reducer'; +import { + VersionActionTypes, + SelectVersionRequest, + ClearVersion, + SelectVersionSuccess +} from '../action/version.action'; +import { Metadata } from '../../domain/domain.type'; +import { VersionRequest } from '../model/request'; + +describe('Restore Reducer', () => { + + let baseState; + + const req: VersionRequest = { + type: 'provider', + version: 'foo', + id: 'bar' + }; + + const model: Metadata = { + id: 'bar', + name: 'foo', + '@type': 'MetadataProvider', + type: 'provider', + resourceId: 'foo', + createdBy: 'bar' + }; + + beforeEach(() => { + baseState = { ...fromVersion.initialState }; + }); + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + + expect(result).toEqual(baseState); + }); + }); + + describe(`${VersionActionTypes.SELECT_VERSION_REQUEST} action`, () => { + it('should set the needed metadata properties', () => { + const action = new SelectVersionRequest(req); + const result = reducer(baseState, action); + + expect(result.selectedMetadataId).toEqual(req.id); + }); + }); + + describe(`${VersionActionTypes.SELECT_VERSION_SUCCESS} action`, () => { + it('should set the needed metadata properties', () => { + const action = new SelectVersionSuccess(model as Metadata); + const result = reducer(baseState, action); + + expect(result).toEqual({ ...baseState, model, loaded: true }); + }); + }); + + describe(`${VersionActionTypes.CLEAR_VERSION} action`, () => { + it('should set the needed metadata properties', () => { + const action = new ClearVersion(); + const result = reducer(baseState, action); + + expect(result).toEqual(baseState); + }); + }); + + describe('selector function', () => { + describe('getSelectedMetadataId', () => { + it('should return the selected version id', () => { + expect(getVersionModel({ ...baseState, model })).toEqual(model); + }); + }); + + describe('getSelectedMetadataVersion', () => { + it('should return the selected version id', () => { + expect(getVersionModelLoaded({ ...baseState, loaded: true })).toBe(true); + }); + }); + + describe('getSelectedMetadataId', () => { + it('should return the selected resource id', () => { + expect(getSelectedMetadataId({ ...baseState, selectedMetadataId: req.id})).toEqual(req.id); + }); + }); + + describe('getSelectedMetadataType', () => { + it('should return the selected version type', () => { + expect(getSelectedVersionType({ ...baseState, selectedVersionType: req.type })).toEqual(req.type); + }); + }); + + describe('getSelectedMetadataType', () => { + it('should return the selected version id', () => { + expect(getSelectedVersionId({ ...baseState, selectedVersionId: req.version })).toEqual(req.version); + }); + }); + }); +});