From 751282a1bb748083b0f57b5de396541cfac6b84a Mon Sep 17 00:00:00 2001 From: Tifenn Guillas <tifenn.guillas@lam.fr> Date: Thu, 30 Sep 2021 15:57:07 +0200 Subject: [PATCH] Tests on search containers => DONE --- .../abstract-search.component.spec.ts | 61 ++++++++ .../containers/abstract-search.component.ts | 15 +- .../search/containers/criteria.component.html | 10 +- .../containers/criteria.component.spec.ts | 125 +++++++++++++++ .../search/containers/criteria.component.ts | 25 ++- .../search/containers/dataset.component.html | 4 +- .../containers/dataset.component.spec.ts | 142 +++++++++++------- .../search/containers/dataset.component.ts | 4 +- .../containers/result.component.spec.ts | 2 +- 9 files changed, 314 insertions(+), 74 deletions(-) create mode 100644 client/src/app/instance/search/containers/abstract-search.component.spec.ts create mode 100644 client/src/app/instance/search/containers/criteria.component.spec.ts diff --git a/client/src/app/instance/search/containers/abstract-search.component.spec.ts b/client/src/app/instance/search/containers/abstract-search.component.spec.ts new file mode 100644 index 00000000..60b38e48 --- /dev/null +++ b/client/src/app/instance/search/containers/abstract-search.component.spec.ts @@ -0,0 +1,61 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { of } from 'rxjs'; + +import { AbstractSearchComponent } from './abstract-search.component'; +import * as searchActions from '../../store/actions/search.actions'; + +describe('[Instance][Search][Container] AbstractSearchComponent', () => { + @Component({ + selector: 'app-fake', + template: '' + }) + class MyFakeComponent extends AbstractSearchComponent { + ngOnInit() { + super.ngOnInit(); + } + + ngOnDestroy() { + super.ngOnDestroy(); + } + } + + let component: MyFakeComponent; + let fixture: ComponentFixture<MyFakeComponent>; + let store: MockStore; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [MyFakeComponent], + providers: [provideMockStore({ })] + }); + fixture = TestBed.createComponent(MyFakeComponent); + component = fixture.componentInstance; + store = TestBed.inject(MockStore); + })); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should execute ngOnInit lifecycle', (done) => { + component.attributeListIsLoaded = of(true); + const spy = jest.spyOn(store, 'dispatch'); + component.ngOnInit(); + Promise.resolve(null).then(function() { + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith(searchActions.initSearch()); + expect(spy).toHaveBeenCalledWith(searchActions.loadDefaultFormParameters()); + done(); + }); + }); + + it('#ngOnDestroy() should unsubscribe from attributeListIsLoadedSubscription', () => { + component.attributeListIsLoadedSubscription = of().subscribe(); + const spy = jest.spyOn(component.attributeListIsLoadedSubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(spy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/client/src/app/instance/search/containers/abstract-search.component.ts b/client/src/app/instance/search/containers/abstract-search.component.ts index 54e40851..afe55c5d 100644 --- a/client/src/app/instance/search/containers/abstract-search.component.ts +++ b/client/src/app/instance/search/containers/abstract-search.component.ts @@ -16,6 +16,14 @@ import * as searchActions from '../../store/actions/search.actions'; import * as searchSelector from '../../store/selectors/search.selector'; import * as coneSearchSelector from '../../store/selectors/cone-search.selector'; +/** + * @abstract + * @class + * @classdesc Abstract search container. + * + * @implements OnInit + * @implements OnDestroy + */ @Directive() export abstract class AbstractSearchComponent implements OnInit, OnDestroy { public datasetSelected: Observable<string>; @@ -41,8 +49,7 @@ export abstract class AbstractSearchComponent implements OnInit, OnDestroy { public outputList: Observable<number[]>; public queryParams: Observable<SearchQueryParams>; public coneSearch: Observable<ConeSearch>; - - private attributeListIsLoadedSubscription: Subscription; + public attributeListIsLoadedSubscription: Subscription; constructor(protected store: Store<{ }>) { this.datasetSelected = store.select(datasetSelector.selectDatasetNameByRoute); @@ -70,7 +77,7 @@ export abstract class AbstractSearchComponent implements OnInit, OnDestroy { this.coneSearch = this.store.select(coneSearchSelector.selectConeSearch); } - ngOnInit() { + ngOnInit(): void { // Create a micro task that is processed after the current synchronous code // This micro task prevent the expression has changed after view init error Promise.resolve(null).then(() => this.store.dispatch(searchActions.initSearch())); @@ -81,7 +88,7 @@ export abstract class AbstractSearchComponent implements OnInit, OnDestroy { }); } - ngOnDestroy() { + ngOnDestroy(): void { if (this.attributeListIsLoadedSubscription) this.attributeListIsLoadedSubscription.unsubscribe(); } } diff --git a/client/src/app/instance/search/containers/criteria.component.html b/client/src/app/instance/search/containers/criteria.component.html index c2beb345..0afddb0a 100644 --- a/client/src/app/instance/search/containers/criteria.component.html +++ b/client/src/app/instance/search/containers/criteria.component.html @@ -18,11 +18,11 @@ (deleteConeSearch)="deleteConeSearch()" (retrieveCoordinates)="retrieveCoordinates($event)"> </app-cone-search-tab> - <app-criteria-tabs + <app-criteria-tabs [attributeList]="attributeList | async | sortByCriteriaDisplay" [criteriaFamilyList]="criteriaFamilyList | async" [criteriaList]="criteriaList | async" - (addCriterion)="addCriterion($event)" + (addCriterion)="addCriterion($event)" (deleteCriterion)="deleteCriterion($event)"> </app-criteria-tabs> </div> @@ -32,7 +32,7 @@ [currentStep]="currentStep | async" [datasetSelected]="datasetSelected | async" [datasetList]="datasetList | async" - [attributeList]="attributeList | async | SortByOutputDisplayPipe" + [attributeList]="attributeList | async | sortByCriteriaDisplay" [criteriaFamilyList]="criteriaFamilyList | async" [outputFamilyList]="outputFamilyList | async" [outputCategoryList]="outputCategoryList | async" @@ -45,8 +45,8 @@ </div> <div class="row mt-5 justify-content-between"> <div class="col"> - <a routerLink="/instance/{{ instanceSelected | async }}/search/dataset/{{ datasetSelected | async }}" - [queryParams]="queryParams | async" + <a routerLink="/instance/{{ instanceSelected | async }}/search/dataset/{{ datasetSelected | async }}" + [queryParams]="queryParams | async" class="btn btn-outline-secondary"> <span class="fas fa-arrow-left"></span> Dataset </a> diff --git a/client/src/app/instance/search/containers/criteria.component.spec.ts b/client/src/app/instance/search/containers/criteria.component.spec.ts new file mode 100644 index 00000000..a1c1e2eb --- /dev/null +++ b/client/src/app/instance/search/containers/criteria.component.spec.ts @@ -0,0 +1,125 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { Component, Input } from '@angular/core'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { MockStore, provideMockStore } from '@ngrx/store/testing'; + +import { CriteriaComponent } from './criteria.component'; +import { Attribute, CriteriaFamily, Dataset, OutputCategory, OutputFamily } from '../../../metamodel/models'; +import { ConeSearch, Criterion, Resolver, SearchQueryParams } from '../../store/models'; +import { SortByCriteriaDisplayPipe } from '../pipes/sort-by-criteria-display.pipe'; +import * as searchActions from '../../store/actions/search.actions'; +import { AbstractSearchComponent } from './abstract-search.component'; +import * as coneSearchActions from '../../store/actions/cone-search.actions'; + +describe('[Instance][Search][Container] CriteriaComponent', () => { + @Component({ selector: 'app-spinner', template: '' }) + class SpinnerStubComponent { } + + @Component({ selector: 'app-cone-search-tab', template: '' }) + class ConeSearchStubComponent { + @Input() datasetSelected: string; + @Input() datasetList: Dataset[]; + @Input() coneSearch: ConeSearch; + @Input() resolver: Resolver; + @Input() resolverIsLoading: boolean; + @Input() resolverIsLoaded: boolean; + } + + @Component({ selector: 'app-criteria-tabs', template: '' }) + class CriteriaTabsStubComponent { + @Input() attributeList: Attribute[]; + @Input() criteriaFamilyList: CriteriaFamily[]; + @Input() criteriaList: Criterion[]; + } + + @Component({ selector: 'app-summary', template: '' }) + class SummaryStubComponent { + @Input() currentStep: string; + @Input() datasetSelected: string; + @Input() datasetList: Dataset[]; + @Input() attributeList: Attribute[]; + @Input() criteriaFamilyList: CriteriaFamily[]; + @Input() outputFamilyList: OutputFamily[]; + @Input() outputCategoryList: OutputCategory[]; + @Input() criteriaList: Criterion[]; + @Input() outputList: number[]; + @Input() queryParams: SearchQueryParams; + @Input() coneSearch: ConeSearch; + } + + let component: CriteriaComponent; + let fixture: ComponentFixture<CriteriaComponent>; + let store: MockStore; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [ + CriteriaComponent, + SpinnerStubComponent, + ConeSearchStubComponent, + CriteriaTabsStubComponent, + SummaryStubComponent, + SortByCriteriaDisplayPipe + ], + providers: [provideMockStore({ })] + }); + fixture = TestBed.createComponent(CriteriaComponent); + component = fixture.componentInstance; + store = TestBed.inject(MockStore); + })); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should execute ngOnInit lifecycle', (done) => { + const spy = jest.spyOn(store, 'dispatch'); + jest.spyOn(AbstractSearchComponent.prototype, 'ngOnInit').mockReturnThis(); + component.ngOnInit(); + Promise.resolve(null).then(function() { + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith(searchActions.changeStep({ step: 'criteria' })); + expect(spy).toHaveBeenCalledWith(searchActions.checkCriteria()); + done(); + }); + }); + + it('#addCriterion() should dispatch addCriterion action', () => { + const criterion: Criterion = { id: 1, type: 'field' }; + const spy = jest.spyOn(store, 'dispatch'); + component.addCriterion(criterion); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(searchActions.addCriterion({ criterion })); + }); + + it('#deleteCriterion() should dispatch deleteCriterion action', () => { + const spy = jest.spyOn(store, 'dispatch'); + component.deleteCriterion(1); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(searchActions.deleteCriterion({ idCriterion: 1 })); + }); + + it('#addConeSearch() should dispatch addConeSearch action', () => { + const coneSearch: ConeSearch = { ra: 1, dec: 2, radius: 3 }; + const spy = jest.spyOn(store, 'dispatch'); + component.addConeSearch(coneSearch); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(coneSearchActions.addConeSearch({ coneSearch })); + }); + + it('#deleteConeSearch() should dispatch deleteConeSearch action', () => { + const spy = jest.spyOn(store, 'dispatch'); + component.deleteConeSearch(); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(coneSearchActions.deleteConeSearch()); + }); + + it('#retrieveCoordinates() should dispatch deleteConeSearch action', () => { + const spy = jest.spyOn(store, 'dispatch'); + component.retrieveCoordinates('myObject'); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(coneSearchActions.retrieveCoordinates({ name: 'myObject' })); + }); +}); diff --git a/client/src/app/instance/search/containers/criteria.component.ts b/client/src/app/instance/search/containers/criteria.component.ts index b03d239d..2530489e 100644 --- a/client/src/app/instance/search/containers/criteria.component.ts +++ b/client/src/app/instance/search/containers/criteria.component.ts @@ -18,14 +18,14 @@ import * as searchActions from '../../store/actions/search.actions'; import * as coneSearchActions from '../../store/actions/cone-search.actions'; import * as coneSearchSelector from '../../store/selectors/cone-search.selector'; -@Component({ - selector: 'app-criteria', - templateUrl: 'criteria.component.html' -}) /** * @class * @classdesc Search criteria container. */ +@Component({ + selector: 'app-criteria', + templateUrl: 'criteria.component.html' +}) export class CriteriaComponent extends AbstractSearchComponent { public resolver: Observable<Resolver>; public resolverIsLoading: Observable<boolean>; @@ -38,7 +38,9 @@ export class CriteriaComponent extends AbstractSearchComponent { this.resolverIsLoaded = this.store.select(coneSearchSelector.selectResolverIsLoaded); } - ngOnInit() { + ngOnInit(): void { + // Create a micro task that is processed after the current synchronous code + // This micro task prevent the expression has changed after view init error Promise.resolve(null).then(() => this.store.dispatch(searchActions.changeStep({ step: 'criteria' }))); Promise.resolve(null).then(() => this.store.dispatch(searchActions.checkCriteria())); super.ngOnInit(); @@ -62,14 +64,27 @@ export class CriteriaComponent extends AbstractSearchComponent { this.store.dispatch(searchActions.deleteCriterion({ idCriterion })); } + /** + * Dispatches action to add cone search. + * + * @param {ConeSearch} coneSearch - The cone search. + */ addConeSearch(coneSearch: ConeSearch): void { this.store.dispatch(coneSearchActions.addConeSearch({ coneSearch })); } + /** + * Dispatches action to remove the cone search. + */ deleteConeSearch(): void { this.store.dispatch(coneSearchActions.deleteConeSearch()); } + /** + * Dispatches action to retrieve object coordinates. + * + * @param {string} name - The object name. + */ retrieveCoordinates(name: string): void { this.store.dispatch(coneSearchActions.retrieveCoordinates({ name })); } diff --git a/client/src/app/instance/search/containers/dataset.component.html b/client/src/app/instance/search/containers/dataset.component.html index 032a3fce..c1a85d26 100644 --- a/client/src/app/instance/search/containers/dataset.component.html +++ b/client/src/app/instance/search/containers/dataset.component.html @@ -4,7 +4,7 @@ </app-spinner> <div *ngIf="(datasetFamilyListIsLoaded | async) - && (datasetListIsLoaded | async) + && (datasetListIsLoaded | async) && (surveyListIsLoaded | async)" class="row mt-4"> <ng-container *ngIf="(datasetList | async).length === 0"> <div class="col-12 lead text-center"> @@ -30,7 +30,7 @@ [currentStep]="currentStep | async" [datasetSelected]="datasetSelected | async" [datasetList]="datasetList | async" - [attributeList]="attributeList | async | SortByOutputDisplayPipe" + [attributeList]="attributeList | async | sortByOutputDisplay" [criteriaFamilyList]="criteriaFamilyList | async" [outputFamilyList]="outputFamilyList | async" [outputCategoryList]="outputCategoryList | async" diff --git a/client/src/app/instance/search/containers/dataset.component.spec.ts b/client/src/app/instance/search/containers/dataset.component.spec.ts index aba154ca..0bd43368 100644 --- a/client/src/app/instance/search/containers/dataset.component.spec.ts +++ b/client/src/app/instance/search/containers/dataset.component.spec.ts @@ -1,65 +1,95 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { Component, Input } from '@angular/core'; +import { RouterTestingModule } from '@angular/router/testing'; -import { Component} from '@angular/core'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { of } from 'rxjs'; -import { Store } from '@ngrx/store'; -import { Observable, Subscription } from 'rxjs'; - -import { AbstractSearchComponent } from './abstract-search.component'; -import { Survey, DatasetFamily } from 'src/app/metamodel/models'; +import { DatasetComponent } from './dataset.component'; +import { + Attribute, + CriteriaFamily, + Dataset, + DatasetFamily, + OutputCategory, + OutputFamily, + Survey +} from '../../../metamodel/models'; +import { ConeSearch, Criterion, SearchQueryParams } from '../../store/models'; +import { SortByOutputDisplayPipe } from '../pipes/sort-by-output-display.pipe'; import * as searchActions from '../../store/actions/search.actions'; -import * as authSelector from 'src/app/auth/auth.selector'; -import * as datasetFamilySelector from 'src/app/metamodel/selectors/dataset-family.selector'; -import * as surveySelector from 'src/app/metamodel/selectors/survey.selector'; +import { AbstractSearchComponent } from './abstract-search.component'; -/** - * @class - * @classdesc Search dataset container. - */ -@Component({ - selector: 'app-dataset', - templateUrl: 'dataset.component.html' -}) -export class DatasetComponent extends AbstractSearchComponent { - public isAuthenticated: Observable<boolean>; - public datasetFamilyListIsLoading: Observable<boolean>; - public datasetFamilyListIsLoaded: Observable<boolean>; - public datasetFamilyList: Observable<DatasetFamily[]>; - public surveyListIsLoading: Observable<boolean>; - public surveyListIsLoaded: Observable<boolean>; - public surveyList: Observable<Survey[]>; - public datasetSelectedSubscription: Subscription; +describe('[Instance][Search][Container] DatasetComponent', () => { + @Component({ selector: 'app-spinner', template: '' }) + class SpinnerStubComponent { } - constructor(protected store: Store<{ }>) { - super(store); - this.isAuthenticated = store.select(authSelector.selectIsAuthenticated); - this.datasetFamilyListIsLoading = store.select(datasetFamilySelector.selectDatasetFamilyListIsLoading); - this.datasetFamilyListIsLoaded = store.select(datasetFamilySelector.selectDatasetFamilyListIsLoaded); - this.datasetFamilyList = store.select(datasetFamilySelector.selectAllDatasetFamilies); - this.surveyListIsLoading = store.select(surveySelector.selectSurveyListIsLoading); - this.surveyListIsLoaded = store.select(surveySelector.selectSurveyListIsLoaded); - this.surveyList = store.select(surveySelector.selectAllSurveys); + @Component({ selector: 'app-dataset-tabs', template: '' }) + class DatasetTabsStubComponent { + @Input() surveyList: Survey[]; + @Input() datasetList: Dataset[]; + @Input() datasetFamilyList: DatasetFamily[]; + @Input() instanceSelected: string; + @Input() datasetSelected: string; } - ngOnInit(): void { - Promise.resolve(null).then(() => this.store.dispatch(searchActions.changeStep({ step: 'dataset' }))); - this.datasetSelectedSubscription = this.datasetSelected.subscribe(datasetSelected => { - if (datasetSelected) { - Promise.resolve(null).then(() => this.store.dispatch(searchActions.initSearch())); - } - }) - super.ngOnInit(); + @Component({ selector: 'app-summary', template: '' }) + class SummaryStubComponent { + @Input() currentStep: string; + @Input() datasetSelected: string; + @Input() datasetList: Dataset[]; + @Input() attributeList: Attribute[]; + @Input() criteriaFamilyList: CriteriaFamily[]; + @Input() outputFamilyList: OutputFamily[]; + @Input() outputCategoryList: OutputCategory[]; + @Input() criteriaList: Criterion[]; + @Input() outputList: number[]; + @Input() queryParams: SearchQueryParams; + @Input() coneSearch: ConeSearch; } - ngOnDestroy(): void { - this.datasetSelectedSubscription.unsubscribe(); - super.ngOnDestroy(); - } -} + let component: DatasetComponent; + let fixture: ComponentFixture<DatasetComponent>; + let store: MockStore; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [ + DatasetComponent, + SpinnerStubComponent, + DatasetTabsStubComponent, + SummaryStubComponent, + SortByOutputDisplayPipe + ], + providers: [provideMockStore({ })] + }); + fixture = TestBed.createComponent(DatasetComponent); + component = fixture.componentInstance; + store = TestBed.inject(MockStore); + })); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should execute ngOnInit lifecycle', (done) => { + component.datasetSelected = of('myDataset'); + const spy = jest.spyOn(store, 'dispatch'); + jest.spyOn(AbstractSearchComponent.prototype, 'ngOnInit').mockReturnThis(); + component.ngOnInit(); + Promise.resolve(null).then(function() { + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith(searchActions.changeStep({ step: 'dataset' })); + expect(spy).toHaveBeenCalledWith(searchActions.initSearch()); + done(); + }); + }); + + it('#ngOnDestroy() should unsubscribe from datasetSelectedSubscription', () => { + component.datasetSelectedSubscription = of().subscribe(); + const spy = jest.spyOn(component.datasetSelectedSubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(spy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/client/src/app/instance/search/containers/dataset.component.ts b/client/src/app/instance/search/containers/dataset.component.ts index aba154ca..cbd886fd 100644 --- a/client/src/app/instance/search/containers/dataset.component.ts +++ b/client/src/app/instance/search/containers/dataset.component.ts @@ -49,6 +49,8 @@ export class DatasetComponent extends AbstractSearchComponent { } ngOnInit(): void { + // Create a micro task that is processed after the current synchronous code + // This micro task prevent the expression has changed after view init error Promise.resolve(null).then(() => this.store.dispatch(searchActions.changeStep({ step: 'dataset' }))); this.datasetSelectedSubscription = this.datasetSelected.subscribe(datasetSelected => { if (datasetSelected) { @@ -59,7 +61,7 @@ export class DatasetComponent extends AbstractSearchComponent { } ngOnDestroy(): void { - this.datasetSelectedSubscription.unsubscribe(); + if (this.datasetSelectedSubscription) this.datasetSelectedSubscription.unsubscribe(); super.ngOnDestroy(); } } diff --git a/client/src/app/instance/search/containers/result.component.spec.ts b/client/src/app/instance/search/containers/result.component.spec.ts index c6c157af..16462a1f 100644 --- a/client/src/app/instance/search/containers/result.component.spec.ts +++ b/client/src/app/instance/search/containers/result.component.spec.ts @@ -161,7 +161,7 @@ describe('[Instance][Search][Container] ResultComponent', () => { expect(spy).toHaveBeenCalledWith(searchActions.deleteSelectedData({ id: 1 })); }); - it('#ngOnDestroy() should dispatch destroyResults action, unsubscribe from pristineSubscription and call #ngOnDestroy() on extended class', () => { + it('#ngOnDestroy() should dispatch destroyResults action and unsubscribe from pristineSubscription', () => { component.pristineSubscription = of().subscribe(); const unsubscribeSpy = jest.spyOn(component.pristineSubscription, 'unsubscribe'); const dispatchSpy = jest.spyOn(store, 'dispatch'); -- GitLab