From b052435549dbf8fae0b50db1f16a731035ddc7f2 Mon Sep 17 00:00:00 2001
From: Tifenn Guillas <tifenn.guillas@gmail.com>
Date: Fri, 24 Sep 2021 11:44:55 +0200
Subject: [PATCH] WIP: Add tests for instance module

---
 .../app/instance/instance-routing.module.ts   |   4 +
 .../app/instance/instance.component.spec.ts   | 104 ++++++++++++++++++
 client/src/app/instance/instance.component.ts |  56 +++++++---
 client/src/app/instance/instance.module.ts    |   4 +
 client/src/app/instance/instance.reducer.ts   |   5 +
 5 files changed, 160 insertions(+), 13 deletions(-)
 create mode 100644 client/src/app/instance/instance.component.spec.ts

diff --git a/client/src/app/instance/instance-routing.module.ts b/client/src/app/instance/instance-routing.module.ts
index 53ae1296..e6795b8b 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 00000000..67713f0b
--- /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 fa981f4b..bb78d58c 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 ffedd2ae..33a65143 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 d14c08ae..2c3b3c97 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,
-- 
GitLab