diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index c2e0485..a9fe9ea 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,10 +1,8 @@ -import { Component, NgModule } from '@angular/core'; +import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { AuthGuard } from './auth/auth.guard'; import { HomeComponent } from './home/home.component'; import { AboutComponent } from './about/about.component'; -import { LoginComponent } from './auth/login/login.component'; import { DatasetsComponent } from './datasets/datasets.component'; import { DatasetDetailComponent } from './datasets/dataset-detail/dataset-detail.component'; import { DatasetResolver } from './datasets/dataset-detail/dataset-resolver.service'; @@ -21,43 +19,35 @@ import { VariantResolver } from './variants/variant-detail/variant-resolver.serv const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'about', component: AboutComponent }, - { path: 'login', component: LoginComponent }, { path: 'breeds', - component: BreedsComponent, - canActivate: [ AuthGuard ] + component: BreedsComponent }, { path: 'datasets', - component: DatasetsComponent, - canActivate: [ AuthGuard ] + component: DatasetsComponent }, { path: 'datasets/:_id', component: DatasetDetailComponent, - canActivate: [ AuthGuard ], resolve: { dataset: DatasetResolver } }, { path: 'samples', component: SamplesComponent, - canActivate: [ AuthGuard ] }, { path: 'samples/:species/:_id', component: SampleDetailComponent, - canActivate: [ AuthGuard ], resolve: { sample: SampleResolver } }, { path: 'variants', component: VariantsComponent, - canActivate: [ AuthGuard ], }, { path: 'variants/:species/:_id', component: VariantDetailComponent, - canActivate: [ AuthGuard ], resolve: { variant: VariantResolver } }, { diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 55194c2..091a2f2 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,19 +1,11 @@ -import { Component, OnInit } from '@angular/core'; - -import { AuthService } from './auth/auth.service'; +import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) -export class AppComponent implements OnInit { +export class AppComponent { title = 'SMARTER-frontend'; - constructor(private authService: AuthService) { } - - ngOnInit(): void { - // try authologin when starting application - this.authService.autoLogin(); - } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index da8b55c..c341e9b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -9,11 +9,9 @@ import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { MaterialModule } from './material/material.module'; import { HomeComponent } from './home/home.component'; -import { LoginComponent } from './auth/login/login.component'; import { HeaderComponent } from './navigation/header/header.component'; import { SidenavListComponent } from './navigation/sidenav-list/sidenav-list.component'; import { DatasetsComponent } from './datasets/datasets.component'; -import { AuthInterceptorService } from './auth/auth-interceptor.service'; import { NotFoundComponent } from './not-found/not-found.component'; import { ShortenPipe } from './shared/shorten.pipe'; import { BreedsComponent } from './breeds/breeds.component'; @@ -33,7 +31,6 @@ import { AboutComponent } from './about/about.component'; declarations: [ AppComponent, HomeComponent, - LoginComponent, HeaderComponent, SidenavListComponent, DatasetsComponent, @@ -62,12 +59,7 @@ import { AboutComponent } from './about/about.component'; MaterialModule ], providers: [ - { - provide: HTTP_INTERCEPTORS, - useClass: AuthInterceptorService, - // required, even if it is the only interceptor defined - multi: true - } + ], bootstrap: [AppComponent] }) diff --git a/src/app/auth/auth-data.model.ts b/src/app/auth/auth-data.model.ts deleted file mode 100644 index 6c34d22..0000000 --- a/src/app/auth/auth-data.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface AuthData { - username: string; - password: string; - redirectTo?: string; -} diff --git a/src/app/auth/auth-interceptor.service.spec.ts b/src/app/auth/auth-interceptor.service.spec.ts deleted file mode 100644 index e400c5f..0000000 --- a/src/app/auth/auth-interceptor.service.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -import { MaterialModule } from '../material/material.module'; -import { AuthInterceptorService } from './auth-interceptor.service'; - -describe('AuthInterceptorService', () => { - let service: AuthInterceptorService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - RouterTestingModule, - MaterialModule, - ], - providers: [ - AuthInterceptorService - ], - }); - service = TestBed.inject(AuthInterceptorService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/auth/auth-interceptor.service.ts b/src/app/auth/auth-interceptor.service.ts deleted file mode 100644 index 8034b85..0000000 --- a/src/app/auth/auth-interceptor.service.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpHandler, HttpHeaders, HttpInterceptor, HttpRequest } from '@angular/common/http'; - -import { exhaustMap, take } from 'rxjs/operators'; - -import { AuthService } from './auth.service'; - -// don't provide interceptors in root, it needs a custom configuration in -// app.module 'providers' section -@Injectable() -export class AuthInterceptorService implements HttpInterceptor { - - constructor(private authService: AuthService) {} - - intercept(req: HttpRequest, next: HttpHandler) { - // this will be executed for each request - return this.authService.user.pipe( - // take: subscribe to user subject, get N objects and then unsubscribe - take(1), - // take the results of the first subscribe and returns a new observable - exhaustMap(user => { - // during login, I don't have a token. So I need to return the unmodified request - if (!user) { - return next.handle(req); - } - - // copy the request in a new object that I can modify - const modifiedReq = req.clone({ - headers: new HttpHeaders({ - 'Authorization': 'Bearer ' + user.token - }) - }); - - // now I will add a token to each requests - return next.handle(modifiedReq); - }) - ); - } -} diff --git a/src/app/auth/auth.guard.ts b/src/app/auth/auth.guard.ts deleted file mode 100644 index fcb3be5..0000000 --- a/src/app/auth/auth.guard.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Injectable } from "@angular/core"; -import { ActivatedRouteSnapshot, CanActivate, Params, Router, RouterStateSnapshot, UrlTree } from "@angular/router"; - -import { Observable } from "rxjs"; -import { map, take } from "rxjs/operators"; - -import { AuthService } from "./auth.service"; - -@Injectable({ - providedIn: 'root' -}) -export class AuthGuard implements CanActivate { - - constructor( - private authService: AuthService, - private router: Router, - ) { } - - canActivate( - route: ActivatedRouteSnapshot, - router: RouterStateSnapshot - ): boolean | Promise | Observable { - // determine if a user is authenticated or not by watching user BehaviourSubject - return this.authService.user.pipe( - // take the latest user value and then unsusbscribe - take(1), - map(user => { - // convert a value in a true boolean, or a null/underfined value in false - const isAuth = !!user; - - if (isAuth) { - return true; - } - - // get the requested url to redirect after login - const params: Params = { - next: router.url - }; - - // if not authenticated, redirect to login page - return this.router.createUrlTree(['/login'], {queryParams: params}); - }) - ); - } -} diff --git a/src/app/auth/auth.service.spec.ts b/src/app/auth/auth.service.spec.ts deleted file mode 100644 index 2e2ff7e..0000000 --- a/src/app/auth/auth.service.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ - -import { TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; - -import { MaterialModule } from '../material/material.module'; -import { AuthService, AuthResponseData } from './auth.service'; -import { AuthData } from './auth-data.model'; -import { environment } from 'src/environments/environment'; -import { LoginComponent } from './login/login.component'; - -describe('AuthService', () => { - let service: AuthService; - let controller: HttpTestingController; - - let authData: AuthData = { - username: 'test', - password: 'test', - redirectTo: "/" - } - let authResponse: AuthResponseData = { - token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NTY3MDMyNCwianRpIjoibWlhbyIsInR5cGUiOiJhY2Nlc3MiLCJzdWIiOiI2MGU4MThlMWYyMzBiZjQ2OWMwODNiYjUiLCJuYmYiOjE2NjU2NzAzMjQsImV4cCI6MTY2NjI3NTEyNH0.UVrnF8Ss6sNGul5-Ab59L4vZQEziRAHAkQ_egGBhAcY', - expires: new Date().toLocaleString() - } - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - RouterTestingModule.withRoutes([ - { path: 'login', component: LoginComponent }, - ]), - HttpClientTestingModule, - MaterialModule, - ], - }); - service = TestBed.inject(AuthService); - controller = TestBed.inject(HttpTestingController); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('test authentication', () => { - service.user.subscribe(user => { - if (user) { - expect(user.username).toBe('test'); - } - }) - - const expectedUrl = `${environment.backend_url}/auth/login`; - - service.login(authData); - const request = controller.expectOne(expectedUrl); - - // Answer the request so the Observable emits a value. - request.flush(authResponse); - }); - - it('test logout', () => { - // fake a login - const expectedUrl = `${environment.backend_url}/auth/login`; - service.login(authData); - const request = controller.expectOne(expectedUrl); - request.flush(authResponse); - - // do a logout - service.logout(); - - service.user.subscribe(user => { - expect(user).toBeNull(); - }); - }); -}); diff --git a/src/app/auth/auth.service.ts b/src/app/auth/auth.service.ts deleted file mode 100644 index 95b3bf3..0000000 --- a/src/app/auth/auth.service.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Router } from '@angular/router'; -import { HttpClient, HttpErrorResponse } from '@angular/common/http'; - -import { BehaviorSubject } from 'rxjs'; - -import { environment } from '../../environments/environment'; -import { UIService } from '../shared/ui.service'; -import { AuthData } from './auth-data.model'; -import { User } from './user.model'; - -// declared here and not exported: I don't need it outside this module -export interface AuthResponseData { - token: string; - expires: string; -} - -@Injectable({ - providedIn: 'root' -}) -export class AuthService { - // A variant of Subject that requires an initial value and emits its current - // value whenever it is subscribed to. A normal Subject is optimal - // to see status changes, like when user login and logout since UI need to change - // immediately. However, I need a user to do my request, so I need this value - // even after I made the subscription (for instance, when fetching data after - // login) - user = new BehaviorSubject(null); - - // track the timer for autoLogout. If I logout manually, this need to be cleared - private tokenExpirationTimer: any; - - constructor( - private http: HttpClient, - private uiService: UIService, - private router: Router, - ) { } - - login(authData: AuthData) { - // set loading state (required to show progress spinner during queries) - this.uiService.loadingStateChanged.next(true); - - // sending the auth request - this.http.post(environment.backend_url + '/auth/login', authData) - .subscribe({ - next: (authResponseData: AuthResponseData) => { - // create a new user object - const user = new User(authData.username, authResponseData.token); - - // emit user as a currently logged user - this.user.next(user); - - // every time I emit a new user, I need to set the timer for autoLogout - // passing expiresIn (milliseconds) - this.autoLogout(user.expiresIn); - - /* we need also to save data somewhere since when I reload the page, the application - start a new instance and so all the data I have (ie, the token) is lost. I can - use localStorage which is a persistent location on the browser which can store - key->value pairs. 'userData' is the key. The value can't be a JS object, need to - be converted as a string with JSON.stringify method, which can serialize a JS object */ - localStorage.setItem('userData', JSON.stringify(user)); - - // redirect to a path or "/" - this.router.navigate([authData.redirectTo]); - }, - error: (error: HttpErrorResponse) => { - this.uiService.showSnackbar(error.message, "Dismiss"); - } - }); - - // reset loading state - this.uiService.loadingStateChanged.next(false); - } - - /* when application starts (or is reloaded), search for userData saved in localStorage - an try to setUp a user object */ - autoLogin() { - const userData: { - username: string; - _token: string; - _tokenExpirationDate: string; - // https://stackoverflow.com/a/46915314/4385116 - // JSON.parse need a string as an argument - } = JSON.parse(localStorage.getItem('userData') || '{}'); - - if (!userData._token) { - // no user data: you must sign in - return; - } - - const loadedUser = new User( - userData.username, - userData._token - ); - - // check token validity. token property is a getter method, which returns - // null if token is expired. So: - if (loadedUser.token) { - // emit loaded user with our subject - this.user.next(loadedUser); - - // set the autoLogout timer - this.autoLogout(loadedUser.expiresIn); - } - } - - autoLogout(expirationDuration: number) { - // set a timer to log out the user after a certain time. however, if I log out - // manually, this timer need to be disabled - this.tokenExpirationTimer = setTimeout(() => { - this.logout(); - }, expirationDuration) - } - - logout() { - this.user.next(null); - this.router.navigate(["/login"]); - - // if I logout, I need to clear out the localStorage from user data, since - // the token won't be valid forever - localStorage.removeItem('userData'); - - // clear the logout timer - if (this.tokenExpirationTimer) { - clearTimeout(this.tokenExpirationTimer); - this.tokenExpirationTimer = null; - } - } -} diff --git a/src/app/auth/login/login.component.html b/src/app/auth/login/login.component.html deleted file mode 100644 index c20e673..0000000 --- a/src/app/auth/login/login.component.html +++ /dev/null @@ -1,32 +0,0 @@ -
-
- - - Please enter a valid user name - Invalid or missing user name - - - - - Please enter your password - Missing password - - - -
-
diff --git a/src/app/auth/login/login.component.scss b/src/app/auth/login/login.component.scss deleted file mode 100644 index c219af1..0000000 --- a/src/app/auth/login/login.component.scss +++ /dev/null @@ -1,5 +0,0 @@ - -mat-form-field { - margin-top: 2rem; - width: 300px; -} diff --git a/src/app/auth/login/login.component.spec.ts b/src/app/auth/login/login.component.spec.ts deleted file mode 100644 index cfa1c0b..0000000 --- a/src/app/auth/login/login.component.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ReactiveFormsModule } from '@angular/forms'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -import { MaterialModule } from '../../material/material.module'; -import { LoginComponent } from './login.component'; - -describe('LoginComponent', () => { - let component: LoginComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - RouterTestingModule.withRoutes([]), - BrowserAnimationsModule, - HttpClientTestingModule, - ReactiveFormsModule, - MaterialModule, - ], - declarations: [ LoginComponent ] - }) - .compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(LoginComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/auth/login/login.component.ts b/src/app/auth/login/login.component.ts deleted file mode 100644 index f7d60a0..0000000 --- a/src/app/auth/login/login.component.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; -import { Subscription } from 'rxjs'; - -import { UIService } from '../../shared/ui.service'; -import { AuthService } from '../auth.service'; - -@Component({ - selector: 'app-login', - templateUrl: './login.component.html', - styleUrls: ['./login.component.scss'] -}) -export class LoginComponent implements OnInit, OnDestroy { - // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#definite-assignment-assertions - // tell TS that this property will be used as this, even if I don't assign a value here or in constructor - loginForm!: FormGroup; - isLoading = false; - private loadingSubscription!: Subscription; - redirectTo!: string; - hide = true; - - constructor( - private route: ActivatedRoute, - private authService: AuthService, - private uiService: UIService - ) { } - - ngOnInit(): void { - // partially inspired from https://jasonwatmore.com/post/2016/12/08/angular-2-redirect-to-previous-url-after-login-with-auth-guard - // get return url from route parameters or default to '/' - this.redirectTo = this.route.snapshot.queryParams['next'] || '/'; - - // subscribe to see when request are performed by the server - this.loadingSubscription = this.uiService.loadingStateChanged.subscribe(isLoading => { - this.isLoading = isLoading; - }); - - this.loginForm = new FormGroup({ - // same ways to define validators - username: new FormControl('', {validators: [Validators.required]}), - password: new FormControl('', [Validators.required]) - }); - } - - onSubmit(): void { - this.authService.login({ - // this is reactive approach - username: this.loginForm.value.username, - password: this.loginForm.value.password, - redirectTo: this.redirectTo - }); - } - - ngOnDestroy() { - if (this.loadingSubscription) { - this.loadingSubscription.unsubscribe(); - } - } - -} diff --git a/src/app/auth/user.model.ts b/src/app/auth/user.model.ts deleted file mode 100644 index 318d0d7..0000000 --- a/src/app/auth/user.model.ts +++ /dev/null @@ -1,46 +0,0 @@ - -import jwt_decode, { JwtPayload } from 'jwt-decode' - -export class User { - private _tokenExpirationDate: Date; - private decoded: JwtPayload; - - // constructor enable the new keyword to create an object - constructor( - public username: string, - /* _ and private: I don't want to access the token from outside, if I need the - Token, I will call a method that will also check the validity before returning - a token string */ - private _token: string - ) { - // determining expiration time from token - // https://github.com/auth0/jwt-decode/issues/82#issuecomment-782941154 - this.decoded = jwt_decode(_token); - this._tokenExpirationDate = new Date(Number(this.decoded.exp) * 1000); - } - - // getter: is like a function but is accessed as a property (ex user.token) - // code will be executed when accessing this property. Assign a value to - // token will throw and error: we need a setter method to set this property - get token() { - // if i don't have an expiration date or this is less that current time (new Date()) - if (!this._tokenExpirationDate || new Date() > this._tokenExpirationDate) { - // the token is expired, return null even if I have a token - return null - } - - // return value of a private string - return this._token; - } - - get expiresIn() { - const now = new Date().getTime(); - const expiresIn = this._tokenExpirationDate.getTime() - now; - - if (expiresIn < 0) { - return 0; - } else { - return expiresIn; - } - } -} diff --git a/src/app/breeds/breeds-example.json b/src/app/breeds/breeds-example.json new file mode 100644 index 0000000..bc1d3e1 --- /dev/null +++ b/src/app/breeds/breeds-example.json @@ -0,0 +1,131 @@ +{ + "items": [ + { + "_id": { + "$oid": "66544d35719e7ca41a58d257" + }, + "aliases": [ + { + "dataset_id": { + "$oid": "604f75a61a08c53cebd09b67" + }, + "fid": "TEXEL_UY" + }, + { + "dataset_id": { + "$oid": "638a3fc844981838fd398504" + }, + "fid": "TEX" + }, + { + "dataset_id": { + "$oid": "638a3fc844981838fd398505" + }, + "fid": "TEX" + }, + { + "dataset_id": { + "$oid": "638a3fc944981838fd398506" + }, + "fid": "TEX" + }, + { + "dataset_id": { + "$oid": "638a3fca44981838fd398507" + }, + "fid": "TEX" + }, + { + "dataset_id": { + "$oid": "638a3fcb44981838fd398508" + }, + "fid": "TEX" + }, + { + "dataset_id": { + "$oid": "638a3fcc44981838fd398509" + }, + "fid": "TEX" + }, + { + "country": "Netherlands", + "dataset_id": { + "$oid": "604f75a61a08c53cebd09b58" + }, + "fid": "TEX" + }, + { + "country": "Netherlands", + "dataset_id": { + "$oid": "632addae76fa33fed2d2cc6d" + }, + "fid": "TEX" + }, + { + "country": "Unknown", + "dataset_id": { + "$oid": "632c869f0b8c366746d6e8b7" + }, + "fid": "TEX" + } + ], + "code": "TEX", + "n_individuals": 686, + "name": "Texel", + "species": "Sheep" + }, + { + "_id": { + "$oid": "66544d36655c76b775dbebd1" + }, + "aliases": [ + { + "dataset_id": { + "$oid": "604f74db1a08c53cebd09ae1" + }, + "fid": "0" + }, + { + "dataset_id": { + "$oid": "614ece6d6ac707ecf33bd641" + }, + "fid": "FRI" + }, + { + "dataset_id": { + "$oid": "614ece766ac707ecf33bd642" + }, + "fid": "FRI" + }, + { + "dataset_id": { + "$oid": "618d4568d65f4170c18fb2a4" + }, + "fid": "0" + }, + { + "dataset_id": { + "$oid": "6197b5a58aaeeb2699e4c925" + }, + "fid": "FRI" + }, + { + "dataset_id": { + "$oid": "621d081f020817ce8440ca9b" + }, + "fid": "FRZ" + } + ], + "code": "FRZ", + "n_individuals": 688, + "name": "Frizarta", + "species": "Sheep" + } + ], + "next": "/smarter-api/breeds?species=Sheep&size=2&page=2", + "page": 1, + "pages": 1, + "prev": null, + "size": 2, + "total": 2 +} diff --git a/src/app/breeds/breeds.model.ts b/src/app/breeds/breeds.model.ts index 9c570ed..7943ac6 100644 --- a/src/app/breeds/breeds.model.ts +++ b/src/app/breeds/breeds.model.ts @@ -10,10 +10,10 @@ export interface Breed { export interface BreedsAPI { items: Breed[]; - next?: string; + next?: string | null; page: number; pages: number; - prev?: string; + prev?: string | null; size: number; total: number; } diff --git a/src/app/datasets/dataset-detail/dataset-detail.component.spec.ts b/src/app/datasets/dataset-detail/dataset-detail.component.spec.ts index d4c935e..60d2d80 100644 --- a/src/app/datasets/dataset-detail/dataset-detail.component.spec.ts +++ b/src/app/datasets/dataset-detail/dataset-detail.component.spec.ts @@ -1,12 +1,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SpyLocation } from '@angular/common/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { of } from 'rxjs'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Location } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { DatasetDetailComponent } from './dataset-detail.component'; import { Dataset } from '../datasets.model'; +import { SamplesComponent } from 'src/app/samples/samples.component'; const dataset: Dataset = { "_id": { @@ -38,11 +41,17 @@ const route = { data: of({ dataset: dataset }) }; describe('DatasetDetailComponent', () => { let component: DatasetDetailComponent; let fixture: ComponentFixture; + let location: SpyLocation; + let mockRouter = { + navigate: jasmine.createSpy('navigate') // Create a spy for the navigate method + }; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ - RouterTestingModule, + RouterTestingModule.withRoutes( + [{path: 'samples', component: SamplesComponent}] + ), HttpClientTestingModule, ], declarations: [ DatasetDetailComponent ], @@ -50,12 +59,15 @@ describe('DatasetDetailComponent', () => { schemas: [ CUSTOM_ELEMENTS_SCHEMA ], providers: [ { provide: ActivatedRoute, useValue: route }, + { provide: Location, useClass: SpyLocation }, + { provide: Router, useValue: mockRouter }, ] }) .compileComponents(); fixture = TestBed.createComponent(DatasetDetailComponent); component = fixture.componentInstance; + location = TestBed.inject(Location); fixture.detectChanges(); }); @@ -63,10 +75,24 @@ describe('DatasetDetailComponent', () => { expect(component).toBeTruthy(); }); - describe('ngOnInit', () => { - it('should get dataset data', () => { - component.ngOnInit(); - expect(component.dataset).toEqual(dataset); - }); + it('should get dataset data', () => { + component.ngOnInit(); + expect(component.dataset).toEqual(dataset); }); + + // https://codeutility.org/unit-testing-angular-6-location-go-back-stack-overflow/ + it('should go back to previous page on back button click', () => { + spyOn(location, 'back'); + component.goBack(); + expect(location.back).toHaveBeenCalled(); + }); + + it('should navigate to /samples with query params when getSamples is called', () => { + component.getSamples(); + expect(mockRouter.navigate).toHaveBeenCalledWith( + ['/samples'], + { queryParams: { dataset: component.dataset._id.$oid, species: component.dataset.species } } + ); + }); + }); diff --git a/src/app/datasets/datasets.component.spec.ts b/src/app/datasets/datasets.component.spec.ts index 8a773a1..e112134 100644 --- a/src/app/datasets/datasets.component.spec.ts +++ b/src/app/datasets/datasets.component.spec.ts @@ -1,25 +1,50 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { RouterTestingModule } from '@angular/router/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Observable } from 'rxjs/internal/Observable'; +import { of } from 'rxjs/internal/observable/of'; +import { map } from 'rxjs/operators'; import { MaterialModule } from '../material/material.module'; import { DatasetsComponent } from './datasets.component'; +import { DatasetsService } from './datasets.service'; describe('DatasetsComponent', () => { let component: DatasetsComponent; let fixture: ComponentFixture; + let datasetsService: DatasetsService; + let router: Router; + + // Creates an observable that will be used for testing ActivatedRoute params + const paramsMock = new Observable((observer) => { + observer.next({ + page: 1, + size: 5, + sort: 'name', + order: 'desc', + search: null + }); + observer.complete(); + }); beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ BrowserAnimationsModule, HttpClientTestingModule, - RouterTestingModule, + RouterTestingModule.withRoutes( + [{path: 'datasets', component: DatasetsComponent}] + ), MaterialModule, ], declarations: [ DatasetsComponent ], + providers: [ + { provide: DatasetsService, useValue: { getDatasets: () => of([]) } }, + { provide: ActivatedRoute, useValue: { queryParams: paramsMock }} + ], // https://testing-angular.com/testing-components-with-children/#unit-test schemas: [ CUSTOM_ELEMENTS_SCHEMA ], }) @@ -27,12 +52,45 @@ describe('DatasetsComponent', () => { }); beforeEach(() => { + router = jasmine.createSpyObj('Router', ['navigate']); fixture = TestBed.createComponent(DatasetsComponent); component = fixture.componentInstance; + datasetsService = TestBed.inject(DatasetsService); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have the correct columns', () => { + expect(component.displayedColumns).toEqual(['file', 'species', 'breed', 'country', 'type']); + }); + + it('should call getDatasets on init', fakeAsync(() => { + spyOn(datasetsService, 'getDatasets').and.callThrough(); + component.ngAfterViewInit(); + tick(1); + expect(datasetsService.getDatasets).toHaveBeenCalled(); + })); + + it('should update searchValue and navigate to /datasets with query params', () => { + const event = { target: { value: 'test' } }; + const queryParams = component.getQueryParams(); + + of(event).pipe( + map((event: any) => { + component.searchValue = event.target.value; + router.navigate( + ["/datasets"], + { + queryParams: queryParams + } + ) + }) + ).subscribe(); + + expect(component.searchValue).toBe('test'); + expect(router.navigate).toHaveBeenCalledWith(['/datasets'], { queryParams: queryParams }); + }); }); diff --git a/src/app/datasets/datasets.service.spec.ts b/src/app/datasets/datasets.service.spec.ts index de66d65..6891122 100644 --- a/src/app/datasets/datasets.service.spec.ts +++ b/src/app/datasets/datasets.service.spec.ts @@ -22,13 +22,13 @@ describe('DatasetsService', () => { }); it('Test for searching datasets', () => { - const pageIndex = 0; - const pageSize = 10; - const sortActive = ''; + const pageIndex = 1; + const pageSize = 2; + const sortActive = 'file'; const sortDirection: SortDirection = "desc"; const searchValue = 'adaptmap'; - const expectedUrl = `${environment.backend_url}/datasets?size=${pageSize}&search=${searchValue}`; + const expectedUrl = `${environment.backend_url}/datasets?page=${pageSize}&size=${pageSize}&sort=${sortActive}&order=${sortDirection}&search=${searchValue}`; service.getDatasets(sortActive, sortDirection, pageIndex, pageSize, searchValue).subscribe( (datasets) => {}); diff --git a/src/app/navigation/header/header.component.html b/src/app/navigation/header/header.component.html index b751c4c..a6feb7c 100644 --- a/src/app/navigation/header/header.component.html +++ b/src/app/navigation/header/header.component.html @@ -26,12 +26,6 @@
  • Variants
  • -
  • - Login -
  • -
  • - Logout -
  • diff --git a/src/app/navigation/header/header.component.spec.ts b/src/app/navigation/header/header.component.spec.ts index 0fec6ad..d7748bd 100644 --- a/src/app/navigation/header/header.component.spec.ts +++ b/src/app/navigation/header/header.component.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { By } from '@angular/platform-browser'; import { MaterialModule } from '../../material/material.module'; import { HeaderComponent } from './header.component'; @@ -32,4 +33,19 @@ describe('HeaderComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should call onToggleSidenav when the menu button is clicked', () => { + spyOn(component, 'onToggleSidenav'); + const button = fixture.debugElement.query(By.css('button[mat-icon-button]')).nativeElement; + button.click(); + expect(component.onToggleSidenav).toHaveBeenCalled(); + }); + + it('should emit sidenavToggle event when onToggleSidenav is called', () => { + spyOn(component.sidenavToggle, 'emit'); + + component.onToggleSidenav(); + + expect(component.sidenavToggle.emit).toHaveBeenCalled(); + }); }); diff --git a/src/app/navigation/header/header.component.ts b/src/app/navigation/header/header.component.ts index 28d669c..b671866 100644 --- a/src/app/navigation/header/header.component.ts +++ b/src/app/navigation/header/header.component.ts @@ -1,42 +1,22 @@ -import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Output } from '@angular/core'; import { Subscription } from 'rxjs'; -import { AuthService } from '../../auth/auth.service'; - @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.scss'] }) -export class HeaderComponent implements OnInit, OnDestroy { - // listed to sidenav frout outside +export class HeaderComponent { + // listed to sidenav from outside @Output() sidenavToggle = new EventEmitter(); // mind authentication isAuthenticated = false; authSubscription!: Subscription; - constructor(private authService: AuthService) { } - - ngOnInit(): void { - this.authSubscription = this.authService.user.subscribe(user => { - // if I have a user, I'm authenticated - this.isAuthenticated = !user ? false : true; - }); - } - onToggleSidenav() { this.sidenavToggle.emit(); } - onLogout() { - this.authService.logout(); - } - - ngOnDestroy() { - if (this.authSubscription) { - this.authSubscription.unsubscribe(); - } - } } diff --git a/src/app/navigation/sidenav-list/sidenav-list.component.html b/src/app/navigation/sidenav-list/sidenav-list.component.html index 39e7ad0..a7e2ccb 100644 --- a/src/app/navigation/sidenav-list/sidenav-list.component.html +++ b/src/app/navigation/sidenav-list/sidenav-list.component.html @@ -23,14 +23,4 @@ list Variants - - face - Login - - - - diff --git a/src/app/navigation/sidenav-list/sidenav-list.component.spec.ts b/src/app/navigation/sidenav-list/sidenav-list.component.spec.ts index 3010d17..8058175 100644 --- a/src/app/navigation/sidenav-list/sidenav-list.component.spec.ts +++ b/src/app/navigation/sidenav-list/sidenav-list.component.spec.ts @@ -32,4 +32,21 @@ describe('SidenavListComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should emit closeSidenav event when onClose is called', () => { + spyOn(component.closeSidenav, 'emit'); + + component.onClose(); + + expect(component.closeSidenav.emit).toHaveBeenCalled(); + }); + + it('should call onClose when onLogout is called', () => { + spyOn(component, 'onClose'); + + component.onLogout(); + + expect(component.onClose).toHaveBeenCalled(); + }); + }); diff --git a/src/app/navigation/sidenav-list/sidenav-list.component.ts b/src/app/navigation/sidenav-list/sidenav-list.component.ts index 0e2773d..81e477d 100644 --- a/src/app/navigation/sidenav-list/sidenav-list.component.ts +++ b/src/app/navigation/sidenav-list/sidenav-list.component.ts @@ -1,30 +1,19 @@ -import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Output } from '@angular/core'; import { Subscription } from 'rxjs'; -import { AuthService } from '../../auth/auth.service'; - @Component({ selector: 'app-sidenav-list', templateUrl: './sidenav-list.component.html', styleUrls: ['./sidenav-list.component.scss'] }) -export class SidenavListComponent implements OnInit, OnDestroy { +export class SidenavListComponent { @Output() closeSidenav = new EventEmitter(); // mind authentication isAuthenticated = false; authSubscription!: Subscription; - constructor(private authService: AuthService) { } - - ngOnInit(): void { - this.authSubscription = this.authService.user.subscribe(user => { - // if I have a user, I'm authenticated - this.isAuthenticated = !user ? false : true; - }); - } - onClose() { this.closeSidenav.emit(); } @@ -32,13 +21,6 @@ export class SidenavListComponent implements OnInit, OnDestroy { onLogout() { // required to close the sidebar this.onClose(); - this.authService.logout(); - } - - ngOnDestroy() { - if (this.authSubscription) { - this.authSubscription.unsubscribe(); - } } } diff --git a/src/app/samples/countries-example.json b/src/app/samples/countries-example.json new file mode 100644 index 0000000..8abb726 --- /dev/null +++ b/src/app/samples/countries-example.json @@ -0,0 +1,32 @@ +{ + "items": [ + { + "_id": { + "$oid": "665679be2c1c55629d917d14" + }, + "alpha_2": "AL", + "alpha_3": "ALB", + "name": "Albania", + "numeric": 8, + "official_name": "Republic of Albania", + "species": ["Sheep"] + }, + { + "_id": { + "$oid": "665679be2c1c55629d917d15" + }, + "alpha_2": "DZ", + "alpha_3": "DZA", + "name": "Algeria", + "numeric": 12, + "official_name": "People's Democratic Republic of Algeria", + "species": ["Sheep"] + } + ], + "next": "/smarter-api/countries?species=Sheep&size=2&page=2", + "page": 1, + "pages": 1, + "prev": null, + "size": 2, + "total": 2 +} diff --git a/src/app/samples/samples.model.ts b/src/app/samples/samples.model.ts index e54fa6c..06b4791 100644 --- a/src/app/samples/samples.model.ts +++ b/src/app/samples/samples.model.ts @@ -55,10 +55,10 @@ export interface Country { export interface CountriesAPI { items: Country[]; - next?: string; + next?: string | null; page: number; pages: number; - prev?: string; + prev?: string | null; size: number; total: number; } diff --git a/src/app/samples/samples.service.spec.ts b/src/app/samples/samples.service.spec.ts index 872cacb..a0a0678 100644 --- a/src/app/samples/samples.service.spec.ts +++ b/src/app/samples/samples.service.spec.ts @@ -4,11 +4,16 @@ import { SortDirection } from '@angular/material/sort'; import { environment } from '../../environments/environment'; import { SamplesService } from './samples.service'; -import { SamplesSearch } from './samples.model'; +import { CountriesAPI, SamplesSearch } from './samples.model'; +import countriesData from './countries-example.json'; +import breedsData from '../breeds/breeds-example.json'; +import { BreedsAPI } from '../breeds/breeds.model'; describe('SamplesService', () => { let service: SamplesService; let controller: HttpTestingController; + let mockCountries: CountriesAPI = countriesData; + let mockBreeds: BreedsAPI = breedsData; beforeEach(() => { TestBed.configureTestingModule({ @@ -23,15 +28,15 @@ describe('SamplesService', () => { }); it('Test for searching samples', () => { - const pageIndex = 0; - const pageSize = 10; - const sortActive = ''; + const pageIndex = 1; + const pageSize = 2; + const sortActive = 'sample_id'; const sortDirection: SortDirection = "desc"; const samplesSearch: SamplesSearch = { breed: "Merino" }; - const expectedUrl = `${environment.backend_url}/samples/${service.selectedSpecie.toLowerCase()}?size=${pageSize}&breed=Merino`; + const expectedUrl = `${environment.backend_url}/samples/${service.selectedSpecie.toLowerCase()}?page=2&size=2&sort=sample_id&order=desc&breed=Merino`; service.getSamples(sortActive, sortDirection, pageIndex, pageSize, samplesSearch).subscribe( (samples) => {}); @@ -55,6 +60,8 @@ describe('SamplesService', () => { service.getCountries(); const request = controller.expectOne(expectedUrl); + request.flush(mockCountries); + expect(service.countries.length).toBe(mockCountries.total) controller.verify(); }); @@ -63,6 +70,8 @@ describe('SamplesService', () => { service.getBreeds(); const request = controller.expectOne(expectedUrl); + request.flush(mockBreeds); + expect(service.breeds.length).toBe(mockBreeds.total); controller.verify(); }) }); diff --git a/src/app/shared/overlay.service.spec.ts b/src/app/shared/overlay.service.spec.ts new file mode 100644 index 0000000..1e8cab4 --- /dev/null +++ b/src/app/shared/overlay.service.spec.ts @@ -0,0 +1,60 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { Overlay } from '@angular/cdk/overlay'; +import { OverlayService } from './overlay.service'; +import { Component, ViewChild, TemplateRef, ViewContainerRef } from '@angular/core'; + +// A fake component with ViewContainerRef +@Component({ + template: ` +
    + + ` +}) +class TestComponent { + @ViewChild('testContainer', { read: ViewContainerRef }) vcRef!: ViewContainerRef; + @ViewChild('testTemplate') templateRef!: TemplateRef; +} + +describe('OverlayService', () => { + let component: TestComponent; + let fixture: ComponentFixture; + let service: OverlayService; + let overlay: Overlay; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent], + providers: [OverlayService, Overlay] + }).compileComponents(); + + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + service = TestBed.inject(OverlayService); + overlay = TestBed.inject(Overlay); + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should create an overlay', () => { + const spy = spyOn(overlay, 'create').and.callThrough(); + service.createOverlay({}); + expect(spy).toHaveBeenCalled(); + }); + + it('should attach a template portal', () => { + const overlayRef = service.createOverlay({}); + const spy = spyOn(overlayRef, 'attach').and.callThrough(); + const templateRef = component.templateRef; + const vcRef = component.vcRef; + service.attachTemplatePortal(overlayRef, templateRef, vcRef); + expect(spy).toHaveBeenCalled(); + }); + + it('should position globally center', () => { + const positionStrategy = service.positionGloballyCenter(); + expect(positionStrategy).toBeTruthy(); + }); +}); diff --git a/src/app/shared/progress-spinner/progress-spinner.component.spec.ts b/src/app/shared/progress-spinner/progress-spinner.component.spec.ts index 54a5e19..88eab8b 100644 --- a/src/app/shared/progress-spinner/progress-spinner.component.spec.ts +++ b/src/app/shared/progress-spinner/progress-spinner.component.spec.ts @@ -1,25 +1,74 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Overlay } from '@angular/cdk/overlay'; - +import { Overlay, OverlayRef, PositionStrategy } from '@angular/cdk/overlay'; import { ProgressSpinnerComponent } from './progress-spinner.component'; +import { OverlayService } from '../overlay.service'; describe('ProgressSpinnerComponent', () => { let component: ProgressSpinnerComponent; let fixture: ComponentFixture; + let overlayService: OverlayService; + let overlayRefSpy: jasmine.SpyObj; + let positionStrategySpy: jasmine.SpyObj; + + beforeEach(() => { + overlayRefSpy = jasmine.createSpyObj( + 'OverlayRef', + ['hasAttached', 'detach'] + ); + positionStrategySpy = jasmine.createSpyObj('PositionStrategy', ['global', 'centerHorizontally', 'centerVertically']); - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ ProgressSpinnerComponent ], - providers: [ Overlay ], - }) - .compileComponents(); + TestBed.configureTestingModule({ + declarations: [ProgressSpinnerComponent], + providers: [ + { + provide: Overlay, + useValue: { + create: () => overlayRefSpy, + position: () => positionStrategySpy + } + }, + OverlayService + ] + }); fixture = TestBed.createComponent(ProgressSpinnerComponent); component = fixture.componentInstance; - fixture.detectChanges(); + overlayService = TestBed.inject(OverlayService); + + // add a custom overlayRef to the component for testing purposes + component.setOverlayRefForTesting(overlayRefSpy); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should attach the template portal when displayProgressSpinner is true', () => { + component.displayProgressSpinner = true; + overlayRefSpy.hasAttached.and.returnValue(false); + const attachSpy = spyOn(overlayService, 'attachTemplatePortal'); + + component.ngDoCheck(); + + expect(attachSpy).toHaveBeenCalled(); + }); + + it('should detach the template portal when displayProgressSpinner is false', () => { + component.displayProgressSpinner = false; + overlayRefSpy.hasAttached.and.returnValue(true); + + component.ngDoCheck(); + + expect(overlayRefSpy.detach).toHaveBeenCalled(); + }); + + it('should create overlay on init', () => { + const createSpy = spyOn(overlayService, 'createOverlay'); + const positionSpy = spyOn(overlayService, 'positionGloballyCenter'); + + component.ngOnInit(); + + expect(createSpy).toHaveBeenCalled(); + expect(positionSpy).toHaveBeenCalled(); + }); }); diff --git a/src/app/shared/progress-spinner/progress-spinner.component.ts b/src/app/shared/progress-spinner/progress-spinner.component.ts index 0d2dd90..9930855 100644 --- a/src/app/shared/progress-spinner/progress-spinner.component.ts +++ b/src/app/shared/progress-spinner/progress-spinner.component.ts @@ -29,6 +29,11 @@ export class ProgressSpinnerComponent { private vcRef: ViewContainerRef, private overlayService: OverlayService) { } + // For testing purposes + public setOverlayRefForTesting(overlayRef: OverlayRef): void { + this.overlayRef = overlayRef; + } + ngOnInit() { // Config for Overlay Service this.progressSpinnerOverlayConfig = { diff --git a/src/app/shared/shared.model.spec.ts b/src/app/shared/shared.model.spec.ts new file mode 100644 index 0000000..fa92705 --- /dev/null +++ b/src/app/shared/shared.model.spec.ts @@ -0,0 +1,21 @@ +import { ObjectDate } from './shared.model'; + +describe('ObjectDate', () => { + it('should create an instance with a Date object', () => { + const date = new Date(); + const objectDate = new ObjectDate(date); + expect(objectDate.$date).toEqual(date); + }); + + it('should create an instance with a string', () => { + const dateString = '2022-01-01T00:00:00Z'; + const objectDate = new ObjectDate(dateString); + expect(objectDate.$date).toEqual(new Date(dateString)); + }); + + it('should copy the original date for for invalid date string', () => { + const invalidDateString = 'invalid-date'; + const objectDate = new ObjectDate(invalidDateString); + expect(objectDate.$date).toEqual(invalidDateString); + }); +}); diff --git a/src/app/shared/shared.model.ts b/src/app/shared/shared.model.ts index a4043b7..67fdba8 100644 --- a/src/app/shared/shared.model.ts +++ b/src/app/shared/shared.model.ts @@ -5,6 +5,23 @@ export interface ObjectID { $oid: string; } +export class ObjectDate { + $date: Date | string; + + constructor(value: string | Date) { + if (value instanceof Date) { + this.$date = value; + } else { + const date = new Date(value); + if (isNaN(date.getTime())) { + this.$date = value; // Store the original string + } else { + this.$date = date; + } + } + } +} + // https://dev.to/ankittanna/how-to-create-a-type-for-complex-json-object-in-typescript-d81 export type JSONValue = | string diff --git a/src/app/shared/ui.service.spec.ts b/src/app/shared/ui.service.spec.ts new file mode 100644 index 0000000..1023ac0 --- /dev/null +++ b/src/app/shared/ui.service.spec.ts @@ -0,0 +1,35 @@ +import { TestBed } from '@angular/core/testing'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { UIService } from './ui.service'; + +describe('UIService', () => { + let service: UIService; + let snackBar: MatSnackBar; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [MatSnackBarModule, NoopAnimationsModule], + providers: [UIService] + }); + + service = TestBed.inject(UIService); + snackBar = TestBed.inject(MatSnackBar); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should show a snackbar', () => { + const spy = spyOn(snackBar, 'open').and.callThrough(); + service.showSnackbar('Test message', 'Test action', 3000); + expect(spy).toHaveBeenCalledWith('Test message', 'Test action', { duration: 3000 }); + }); + + it('should show a snackbar with default duration when no duration is provided', () => { + const spy = spyOn(snackBar, 'open').and.callThrough(); + service.showSnackbar('Test message', 'Test action'); + expect(spy).toHaveBeenCalledWith('Test message', 'Test action', { duration: 5000 }); + }); +}); diff --git a/src/app/variants/variant-detail/variant-detail.component.html b/src/app/variants/variant-detail/variant-detail.component.html index 4ca3d54..1fbec0b 100644 --- a/src/app/variants/variant-detail/variant-detail.component.html +++ b/src/app/variants/variant-detail/variant-detail.component.html @@ -48,6 +48,7 @@

    id: {{ variant._id.$oid }}

    Chrom: {{ location.chrom }} Position: {{ location.position }} + Date: {{ location.date.$date | date:'longDate':'en-US' }} Illumina: {{ location.illumina }} Illumina TOP: {{ location.illumina_top }} Illumina Forward: {{ location.illumina_forward }} diff --git a/src/app/variants/variant-detail/variant-detail.component.spec.ts b/src/app/variants/variant-detail/variant-detail.component.spec.ts index 6414b0e..cb5a5f1 100644 --- a/src/app/variants/variant-detail/variant-detail.component.spec.ts +++ b/src/app/variants/variant-detail/variant-detail.component.spec.ts @@ -21,7 +21,7 @@ const variant: Variant = { { "chrom": "15", "date": { - "$date": "2009-01-07T00:00:00Z" + "$date": new Date("2009-01-07T00:00:00Z") }, "illumina": "A/G", "illumina_strand": "TOP", @@ -47,7 +47,7 @@ const variant: Variant = { { "chrom": "15", "date": { - "$date": "2017-03-13T00:00:00Z" + "$date": new Date("2017-03-13T00:00:00Z") }, "illumina": "A/G", "illumina_strand": "TOP", diff --git a/src/app/variants/variants.model.ts b/src/app/variants/variants.model.ts index 7a1fa9f..73f98f8 100644 --- a/src/app/variants/variants.model.ts +++ b/src/app/variants/variants.model.ts @@ -1,5 +1,5 @@ -import { JSONObject, ObjectID } from "../shared/shared.model"; +import { JSONObject, ObjectID, ObjectDate } from "../shared/shared.model"; export interface Location { ss_id?: string; @@ -14,7 +14,7 @@ export interface Location { affymetrix_ab?: string; strand?: string; imported_from: string; - date?: JSONObject; + date?: ObjectDate; } export interface Probeset { diff --git a/src/app/variants/variants.service.ts b/src/app/variants/variants.service.ts index 458989c..e1c44d4 100644 --- a/src/app/variants/variants.service.ts +++ b/src/app/variants/variants.service.ts @@ -1,3 +1,4 @@ +import { map } from 'rxjs/operators'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { SortDirection } from '@angular/material/sort'; @@ -5,6 +6,7 @@ import { environment } from 'src/environments/environment'; import { SupportedChip, SupportedChipsAPI, Variant, VariantsAPI, VariantsSearch } from './variants.model'; import { Observable, Subject, forkJoin } from 'rxjs'; +import { ObjectDate } from '../shared/shared.model'; @Injectable({ providedIn: 'root' @@ -69,7 +71,16 @@ export class VariantsService { getVariant(_id: string, species: string) { const url = environment.backend_url + '/variants/' + species + "/" + _id; - return this.http.get(url); + return this.http.get(url).pipe( + map((variant: Variant) => { + variant.locations.forEach((location) => { + if (location.date) { + location.date = new ObjectDate(location.date.$date); + } + }); + return variant; + }) + ); } getSupportedChips(species: string,): void {