Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions spa/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Human Cell Atlas
* https://www.humancellatlas.org/
*
* Spec for testing top-level app component.
*/

// Core dependencies
import { Location } from "@angular/common";
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { MatIconModule, MatToolbarModule } from "@angular/material";
import { ActivatedRoute, NavigationEnd, Router, RouterEvent, RouterModule } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing";
import { Store } from "@ngrx/store";
import { ReplaySubject } from "rxjs";

// App dependencies
import { AppComponent } from "./app.component";
import { ConfigService } from "./config/config.service";
import { SetViewStateAction } from "./files/_ngrx/file-facet-list/set-view-state.action";
import { FileFacetName } from "./files/shared/file-facet-name.model";
import { GenusSpecies } from "./files/shared/genus-species.model";
import { LibraryConstructionApproach } from "./files/shared/library-construction-approach.model";
import { QueryStringFacet } from "./files/shared/query-string-facet.model";
import { EntityName } from "./files/shared/entity-name.model";
import { DeviceDetectorService } from "ngx-device-detector";
import { CCToolbarNavComponent } from "./shared/cc-toolbar-nav/cc-toolbar-nav.component";
import { CCToolbarNavItemComponent } from "./shared/cc-toolbar-nav-item/cc-toolbar-nav-item.component";
import { DesktopFooterComponent } from "./site/desktop-footer/desktop-footer.component";
import { DataPolicyFooterComponent } from "./site/data-policy-footer/data-policy-footer.component";
import { HCAFooterComponent } from "./site/hca-footer/hca-footer.component";
import { HCAToolbarComponent } from "./site/hca-toolbar/hca-toolbar.component";
import { StickyFooterComponent } from "./site/sticky-footer/sticky-footer.component";
import { LocalStorageService } from "./storage/local-storage.service";
import { ActivatedRouteStub } from "./test/activated-route.stub";


describe("AppComponent:", () => {

const PROJECTS_PATH = "/projects";
const PROJECTS_PATH_WITH_FILTERS = "/projects?filter=%5B%7B%22facetName%22:%22libraryConstructionApproach%22,%22terms%22:%5B%22Smart-seq2%22%5D%7D%5D";

let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;

const locationSpy = jasmine.createSpyObj("Location", ["path"]);
const storeSpy = jasmine.createSpyObj("Store", ["pipe", "dispatch"]);

const navigation$ = new ReplaySubject<RouterEvent>(1);
const routerMock = {
events: navigation$.asObservable()
};

beforeEach(async(() => {

TestBed.configureTestingModule({
declarations: [
AppComponent,
CCToolbarNavComponent,
CCToolbarNavItemComponent,
DataPolicyFooterComponent,
DesktopFooterComponent,
HCAFooterComponent,
HCAToolbarComponent,
StickyFooterComponent
],
imports: [
RouterTestingModule,
MatIconModule,
MatToolbarModule,
RouterModule
],
providers: [{
provide: DeviceDetectorService,
useValue: jasmine.createSpyObj("DeviceDetectorService", ["isMobile", "isTablet"])
}, {
provide: Store,
useValue: storeSpy
}, {
provide: ActivatedRoute,
useClass: ActivatedRouteStub
}, {
provide: Location,
useValue: locationSpy
}, {
provide: Router,
useValue: routerMock
}, {
provide: ConfigService,
useValue: jasmine.createSpyObj("ConfigService", ["getPortalURL"])
}, {
provide: LocalStorageService,
useValue: jasmine.createSpyObj("LocalStorageService", ["get", "set"])
}]
});

fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
}));

/**
* Smoke test
*/
it("should create component", () => {

expect(component).toBeTruthy();
});

/**
* Default to homo sapiens if there are initially no filters set.
*/
it("defaults search terms to human if no filters set on load of app", () => {

const activatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
spyOnProperty(activatedRoute, "snapshot").and.returnValue({
queryParams: {}
});
locationSpy.path.and.returnValue(PROJECTS_PATH);
navigation$.next(new NavigationEnd(1, "/", PROJECTS_PATH));

component["setAppStateFromURL"]();

const filters = [
new QueryStringFacet(FileFacetName.GENUS_SPECIES, [GenusSpecies.HOMO_SAPIENS])
];
const setViewAction = new SetViewStateAction(EntityName.PROJECTS, filters);
expect(storeSpy.dispatch).toHaveBeenCalledWith(setViewAction);
});

/**
* Do not default to homo sapiens if there are initially filters set.
*/
it("does not default search terms to human if filters are already set on load of app", () => {

const activatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
spyOnProperty(activatedRoute, "snapshot").and.returnValue({
queryParams: {
filter: `[{"facetName": "libraryConstructionApproach", "terms": ["Smart-seq2"]}]`
}
});
locationSpy.path.and.returnValue(PROJECTS_PATH_WITH_FILTERS);
navigation$.next(new NavigationEnd(1, "/", PROJECTS_PATH_WITH_FILTERS));

component["setAppStateFromURL"]();

const filters = [
new QueryStringFacet(FileFacetName.LIBRARY_CONSTRUCTION_APPROACH, [LibraryConstructionApproach.SMART_SEQ2])
];
const setViewAction = new SetViewStateAction(EntityName.PROJECTS, filters);
expect(storeSpy.dispatch).toHaveBeenCalledWith(setViewAction);
});

/**
* Tests are incomplete - review component functionality and add necessary tests.
*/
xit("TBD", () => {
});
});
30 changes: 22 additions & 8 deletions spa/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

// Core dependencies
import { Location } from "@angular/common";
import { Component, Inject, OnDestroy, OnInit, Renderer2 } from "@angular/core";
import { Component, OnDestroy, OnInit, Renderer2 } from "@angular/core";
import { ActivatedRoute, NavigationEnd, Params, Router } from "@angular/router";
import { select, Store } from "@ngrx/store";
import { combineLatest, Observable, Subscription, Subject } from "rxjs";
Expand All @@ -25,6 +25,8 @@ import { HealthRequestAction } from "./system/_ngrx/health/health-request.action
import { selectHealth, selectIndex } from "./system/_ngrx/system.selectors";
import { IndexRequestAction } from "./system/_ngrx/index/index-request.action";
import { SystemState } from "./system.state";
import { FileFacetName } from "./files/shared/file-facet-name.model";
import { GenusSpecies } from "./files/shared/genus-species.model";

@Component({
selector: "app-root",
Expand All @@ -49,15 +51,13 @@ export class AppComponent implements OnInit, OnDestroy {
* @param {Location} location
* @param {Router} router
* @param {Renderer2} renderer
* @param {Window} window
*/
constructor(private deviceService: DeviceDetectorService,
private store: Store<AppState>,
private activatedRoute: ActivatedRoute,
private location: Location,
private router: Router,
private renderer: Renderer2,
@Inject("Window") private window: Window) {
private renderer: Renderer2) {
}

/**
Expand Down Expand Up @@ -194,17 +194,31 @@ export class AppComponent implements OnInit, OnDestroy {
private setAppStateFromURL() {

// Using NavigationEnd here as subscribing to activatedRoute.queryParamsMap always emits an initial value,
// making it difficult to detect the difference between the initial value or an intentionally empty value. We
// are therefore unable to determine when app state setup is complete and can safely unsubscribe.
// making it difficult to detect the difference between the initial value or an intentionally empty value. Using
// activatedRoute.queryParamsMap would therefore make it difficult to determine when app state setup is complete,
// and when we can safely unsubscribe.
this.routerEventsSubscription = this.router.events.subscribe((evt) => {

if ( evt instanceof NavigationEnd ) {

const params = this.activatedRoute.snapshot.queryParams;
const filter = this.parseQueryStringFacets(params);
const tab = this.parseTab();

const filter = this.parseQueryStringFacets(params);

// Default app state to have human selected. This is only necessary if there is currently no filter
// applied.
if ( filter.length === 0 ) {
const queryStringFacet =
new QueryStringFacet(FileFacetName.GENUS_SPECIES, [GenusSpecies.HOMO_SAPIENS]);
filter.push(queryStringFacet);
}

this.store.dispatch(new SetViewStateAction(tab, filter));
this.routerEventsSubscription.unsubscribe();

if ( this.routerEventsSubscription ) {
this.routerEventsSubscription.unsubscribe();
}
}
});
}
Expand Down
20 changes: 20 additions & 0 deletions spa/src/app/test/activated-route.stub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Human Cell Atlas
* https://www.humancellatlas.org/
*
* Stub ActivatedRoute object, to be used when spying on snapshot values.
*/

// Core dependencies
import { Injectable } from "@angular/core";

@Injectable()
export class ActivatedRouteStub {

/**
* @returns {{}}
*/
get snapshot() {
return {};
}
}