diff --git a/client/src/app/instance/instance-routing.module.ts b/client/src/app/instance/instance-routing.module.ts index 53ae1296d4a2648f295956697e42626368d31be5..e6795b8b3773c388b49be1bf6f66322564b78b99 100644 --- a/client/src/app/instance/instance-routing.module.ts +++ b/client/src/app/instance/instance-routing.module.ts @@ -24,6 +24,10 @@ const routes: Routes = [ } ]; +/** + * @class + * @classdesc Instance routing module. + */ @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] diff --git a/client/src/app/instance/instance.component.spec.ts b/client/src/app/instance/instance.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..67713f0bddf0e877358a64e2af4622f23fff7bae --- /dev/null +++ b/client/src/app/instance/instance.component.spec.ts @@ -0,0 +1,104 @@ +import { TestBed, waitForAsync, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { of } from 'rxjs'; +import { provideMockStore, MockStore } from '@ngrx/store/testing'; + +import { InstanceComponent } from './instance.component'; +import { AppConfigService } from 'src/app/app-config.service'; +import * as authActions from 'src/app/auth/auth.actions'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Attribute, Instance } from '../metamodel/models'; +import { UserProfile } from '../auth/user-profile.model'; + +describe('InstanceComponent', () => { + @Component({ selector: 'app-navbar', template: '' }) + class NavbarStubComponent { + @Input() links: {label: string, icon: string, routerLink: string}[]; + @Input() isAuthenticated: boolean; + @Input() userProfile: UserProfile = null; + @Input() baseHref: string; + @Input() authenticationEnabled: boolean; + @Input() apiUrl: string; + @Input() instance: Instance; + } + + let component: InstanceComponent; + let fixture: ComponentFixture<InstanceComponent>; + let store: MockStore; + let appConfigServiceStub = new AppConfigService(); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [ + InstanceComponent, + NavbarStubComponent + ], + providers: [ + provideMockStore({ }), + { provide: AppConfigService, useValue: appConfigServiceStub } + ] + }).compileComponents(); + fixture = TestBed.createComponent(InstanceComponent); + component = fixture.componentInstance; + store = TestBed.inject(MockStore); + })); + + it('should create the component', () => { + expect(component).toBeDefined(); + }); + + // it('should execute ngOnInit lifecycle', (done) => { + // const spy = jest.spyOn(store, 'dispatch'); + // component.ngOnInit(); + // Promise.resolve(null).then(function() { + // expect(spy).toHaveBeenCalledTimes(1); + // expect(spy).toHaveBeenCalledWith(instanceActions.loadInstanceList()); + // done(); + // }); + // }); + + it('#getBaseHref() should return base href config key value', () => { + appConfigServiceStub.baseHref = '/my-project'; + expect(component.getBaseHref()).toBe('/my-project'); + }); + + it('#authenticationEnabled() should return authentication enabled config key value', () => { + appConfigServiceStub.authenticationEnabled = true; + expect(component.getAuthenticationEnabled()).toBeTruthy(); + }); + + it('#getApiUrl() should return API URL', () => { + appConfigServiceStub.apiUrl = 'http:test.com'; + expect(component.getApiUrl()).toEqual('http:test.com'); + }); + + it('#login() should dispatch login action', () => { + const spy = jest.spyOn(store, 'dispatch'); + component.login(); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(authActions.login()); + }); + + it('#logout() should dispatch logout action', () => { + const spy = jest.spyOn(store, 'dispatch'); + component.logout(); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(authActions.logout()); + }); + + it('#openEditProfile() should dispatch open edit profile action', () => { + const spy = jest.spyOn(store, 'dispatch'); + component.openEditProfile(); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(authActions.openEditProfile()); + }); + + it('should unsubscribe to instance when component is destroyed', () => { + component.instanceSubscription = of().subscribe(); + const spy = jest.spyOn(component.instanceSubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(spy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/client/src/app/instance/instance.component.ts b/client/src/app/instance/instance.component.ts index fa981f4b2c09c1d62b2d27f7ee1c1917a6ca8db7..bb78d58ce124b33a795c7e5f88dc0fee2a80bc7a 100644 --- a/client/src/app/instance/instance.component.ts +++ b/client/src/app/instance/instance.component.ts @@ -8,8 +8,9 @@ */ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Observable, Subscription } from 'rxjs'; + import { Store } from '@ngrx/store'; +import { Observable, Subscription } from 'rxjs'; import { UserProfile } from 'src/app/auth/user-profile.model'; import { Instance } from 'src/app/metamodel/models'; @@ -21,25 +22,25 @@ import * as surveyActions from 'src/app/metamodel/actions/survey.actions'; import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; import { AppConfigService } from 'src/app/app-config.service'; -@Component({ - selector: 'app-instance', - templateUrl: 'instance.component.html' -}) /** * @class * @classdesc Instance container + * + * @implements OnInit + * @implements OnDestroy */ +@Component({ + selector: 'app-instance', + templateUrl: 'instance.component.html' +}) export class InstanceComponent implements OnInit, OnDestroy { public favIcon: HTMLLinkElement = document.querySelector('#favicon'); public title: HTMLLinkElement = document.querySelector('#title'); - public links = [ - { label: 'Home', icon: 'fas fa-home', routerLink: 'home' } - ]; + public links = [{ label: 'Home', icon: 'fas fa-home', routerLink: 'home' }]; public instance: Observable<Instance>; public isAuthenticated: Observable<boolean>; public userProfile: Observable<UserProfile>; public userRoles: Observable<string[]>; - public instanceSubscription: Subscription; constructor(private store: Store<{ }>, private config: AppConfigService) { @@ -50,6 +51,8 @@ export class InstanceComponent implements OnInit, OnDestroy { } ngOnInit() { + // 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(datasetFamilyActions.loadDatasetFamilyList())); Promise.resolve(null).then(() => this.store.dispatch(datasetActions.loadDatasetList())); Promise.resolve(null).then(() => this.store.dispatch(surveyActions.loadSurveyList())); @@ -70,31 +73,58 @@ export class InstanceComponent implements OnInit, OnDestroy { }) } - getBaseHref() { + /** + * Returns application base href. + * + * @return string + */ + getBaseHref(): string { return this.config.baseHref; } - getAuthenticationEnabled() { + /** + * Checks if authentication is enabled. + * + * @return boolean + */ + getAuthenticationEnabled(): boolean { return this.config.authenticationEnabled; } - getApiUrl() { + /** + * Returns API URL. + * + * @return string + */ + getApiUrl(): string { return this.config.apiUrl; } + /** + * Dispatches action to log in. + */ login(): void { this.store.dispatch(authActions.login()); } + /** + * Dispatches action to log out. + */ logout(): void { this.store.dispatch(authActions.logout()); } + /** + * Dispatches action to open profile editor. + */ openEditProfile(): void { this.store.dispatch(authActions.openEditProfile()); } + /** + * Unsubscribes to instance when component is destroyed. + */ ngOnDestroy() { - this.instanceSubscription.unsubscribe(); + if (this.instanceSubscription) this.instanceSubscription.unsubscribe(); } } diff --git a/client/src/app/instance/instance.module.ts b/client/src/app/instance/instance.module.ts index ffedd2ae3a7e9336104f44a5f56aa14b9d54c56b..33a6514300c63f72977df896246e0cf0bc1135a9 100644 --- a/client/src/app/instance/instance.module.ts +++ b/client/src/app/instance/instance.module.ts @@ -18,6 +18,10 @@ import { instanceReducer } from './instance.reducer'; import { instanceEffects } from './store/effects'; import { instanceServices } from './store/services'; +/** + * @class + * @classdesc Instance module. + */ @NgModule({ imports: [ SharedModule, diff --git a/client/src/app/instance/instance.reducer.ts b/client/src/app/instance/instance.reducer.ts index d14c08ae52efefef8b043088ae0b74cddbb1a44d..2c3b3c970e64e3c185933ae2cd8f57515fe97bd8 100644 --- a/client/src/app/instance/instance.reducer.ts +++ b/client/src/app/instance/instance.reducer.ts @@ -16,6 +16,11 @@ import * as samp from './store/reducers/samp.reducer'; import * as coneSearch from './store/reducers/cone-search.reducer'; import * as detail from './store/reducers/detail.reducer'; +/** + * Interface for instance state. + * + * @interface State + */ export interface State { search: search.State, searchMultiple: searchMultiple.State,