Skip to content

Commit 7c026f6

Browse files
MillenniumFalconMechanicNoopDog
authored andcommitted
Added human as default selected species. Resolves #965. (#973)
1 parent ce8b615 commit 7c026f6

File tree

3 files changed

+200
-8
lines changed

3 files changed

+200
-8
lines changed

spa/src/app/app.component.spec.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Human Cell Atlas
3+
* https://www.humancellatlas.org/
4+
*
5+
* Spec for testing top-level app component.
6+
*/
7+
8+
// Core dependencies
9+
import { Location } from "@angular/common";
10+
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
11+
import { MatIconModule, MatToolbarModule } from "@angular/material";
12+
import { ActivatedRoute, NavigationEnd, Router, RouterEvent, RouterModule } from "@angular/router";
13+
import { RouterTestingModule } from "@angular/router/testing";
14+
import { Store } from "@ngrx/store";
15+
import { ReplaySubject } from "rxjs";
16+
17+
// App dependencies
18+
import { AppComponent } from "./app.component";
19+
import { ConfigService } from "./config/config.service";
20+
import { SetViewStateAction } from "./files/_ngrx/file-facet-list/set-view-state.action";
21+
import { FileFacetName } from "./files/shared/file-facet-name.model";
22+
import { GenusSpecies } from "./files/shared/genus-species.model";
23+
import { LibraryConstructionApproach } from "./files/shared/library-construction-approach.model";
24+
import { QueryStringFacet } from "./files/shared/query-string-facet.model";
25+
import { EntityName } from "./files/shared/entity-name.model";
26+
import { DeviceDetectorService } from "ngx-device-detector";
27+
import { CCToolbarNavComponent } from "./shared/cc-toolbar-nav/cc-toolbar-nav.component";
28+
import { CCToolbarNavItemComponent } from "./shared/cc-toolbar-nav-item/cc-toolbar-nav-item.component";
29+
import { DesktopFooterComponent } from "./site/desktop-footer/desktop-footer.component";
30+
import { DataPolicyFooterComponent } from "./site/data-policy-footer/data-policy-footer.component";
31+
import { HCAFooterComponent } from "./site/hca-footer/hca-footer.component";
32+
import { HCAToolbarComponent } from "./site/hca-toolbar/hca-toolbar.component";
33+
import { StickyFooterComponent } from "./site/sticky-footer/sticky-footer.component";
34+
import { LocalStorageService } from "./storage/local-storage.service";
35+
import { ActivatedRouteStub } from "./test/activated-route.stub";
36+
37+
38+
describe("AppComponent:", () => {
39+
40+
const PROJECTS_PATH = "/projects";
41+
const PROJECTS_PATH_WITH_FILTERS = "/projects?filter=%5B%7B%22facetName%22:%22libraryConstructionApproach%22,%22terms%22:%5B%22Smart-seq2%22%5D%7D%5D";
42+
43+
let component: AppComponent;
44+
let fixture: ComponentFixture<AppComponent>;
45+
46+
const locationSpy = jasmine.createSpyObj("Location", ["path"]);
47+
const storeSpy = jasmine.createSpyObj("Store", ["pipe", "dispatch"]);
48+
49+
const navigation$ = new ReplaySubject<RouterEvent>(1);
50+
const routerMock = {
51+
events: navigation$.asObservable()
52+
};
53+
54+
beforeEach(async(() => {
55+
56+
TestBed.configureTestingModule({
57+
declarations: [
58+
AppComponent,
59+
CCToolbarNavComponent,
60+
CCToolbarNavItemComponent,
61+
DataPolicyFooterComponent,
62+
DesktopFooterComponent,
63+
HCAFooterComponent,
64+
HCAToolbarComponent,
65+
StickyFooterComponent
66+
],
67+
imports: [
68+
RouterTestingModule,
69+
MatIconModule,
70+
MatToolbarModule,
71+
RouterModule
72+
],
73+
providers: [{
74+
provide: DeviceDetectorService,
75+
useValue: jasmine.createSpyObj("DeviceDetectorService", ["isMobile", "isTablet"])
76+
}, {
77+
provide: Store,
78+
useValue: storeSpy
79+
}, {
80+
provide: ActivatedRoute,
81+
useClass: ActivatedRouteStub
82+
}, {
83+
provide: Location,
84+
useValue: locationSpy
85+
}, {
86+
provide: Router,
87+
useValue: routerMock
88+
}, {
89+
provide: ConfigService,
90+
useValue: jasmine.createSpyObj("ConfigService", ["getPortalURL"])
91+
}, {
92+
provide: LocalStorageService,
93+
useValue: jasmine.createSpyObj("LocalStorageService", ["get", "set"])
94+
}]
95+
});
96+
97+
fixture = TestBed.createComponent(AppComponent);
98+
component = fixture.componentInstance;
99+
}));
100+
101+
/**
102+
* Smoke test
103+
*/
104+
it("should create component", () => {
105+
106+
expect(component).toBeTruthy();
107+
});
108+
109+
/**
110+
* Default to homo sapiens if there are initially no filters set.
111+
*/
112+
it("defaults search terms to human if no filters set on load of app", () => {
113+
114+
const activatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
115+
spyOnProperty(activatedRoute, "snapshot").and.returnValue({
116+
queryParams: {}
117+
});
118+
locationSpy.path.and.returnValue(PROJECTS_PATH);
119+
navigation$.next(new NavigationEnd(1, "/", PROJECTS_PATH));
120+
121+
component["setAppStateFromURL"]();
122+
123+
const filters = [
124+
new QueryStringFacet(FileFacetName.GENUS_SPECIES, [GenusSpecies.HOMO_SAPIENS])
125+
];
126+
const setViewAction = new SetViewStateAction(EntityName.PROJECTS, filters);
127+
expect(storeSpy.dispatch).toHaveBeenCalledWith(setViewAction);
128+
});
129+
130+
/**
131+
* Do not default to homo sapiens if there are initially filters set.
132+
*/
133+
it("does not default search terms to human if filters are already set on load of app", () => {
134+
135+
const activatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
136+
spyOnProperty(activatedRoute, "snapshot").and.returnValue({
137+
queryParams: {
138+
filter: `[{"facetName": "libraryConstructionApproach", "terms": ["Smart-seq2"]}]`
139+
}
140+
});
141+
locationSpy.path.and.returnValue(PROJECTS_PATH_WITH_FILTERS);
142+
navigation$.next(new NavigationEnd(1, "/", PROJECTS_PATH_WITH_FILTERS));
143+
144+
component["setAppStateFromURL"]();
145+
146+
const filters = [
147+
new QueryStringFacet(FileFacetName.LIBRARY_CONSTRUCTION_APPROACH, [LibraryConstructionApproach.SMART_SEQ2])
148+
];
149+
const setViewAction = new SetViewStateAction(EntityName.PROJECTS, filters);
150+
expect(storeSpy.dispatch).toHaveBeenCalledWith(setViewAction);
151+
});
152+
153+
/**
154+
* Tests are incomplete - review component functionality and add necessary tests.
155+
*/
156+
xit("TBD", () => {
157+
});
158+
});

spa/src/app/app.component.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

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

2931
@Component({
3032
selector: "app-root",
@@ -49,15 +51,13 @@ export class AppComponent implements OnInit, OnDestroy {
4951
* @param {Location} location
5052
* @param {Router} router
5153
* @param {Renderer2} renderer
52-
* @param {Window} window
5354
*/
5455
constructor(private deviceService: DeviceDetectorService,
5556
private store: Store<AppState>,
5657
private activatedRoute: ActivatedRoute,
5758
private location: Location,
5859
private router: Router,
59-
private renderer: Renderer2,
60-
@Inject("Window") private window: Window) {
60+
private renderer: Renderer2) {
6161
}
6262

6363
/**
@@ -194,17 +194,31 @@ export class AppComponent implements OnInit, OnDestroy {
194194
private setAppStateFromURL() {
195195

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

201202
if ( evt instanceof NavigationEnd ) {
202203

203204
const params = this.activatedRoute.snapshot.queryParams;
204-
const filter = this.parseQueryStringFacets(params);
205205
const tab = this.parseTab();
206+
207+
const filter = this.parseQueryStringFacets(params);
208+
209+
// Default app state to have human selected. This is only necessary if there is currently no filter
210+
// applied.
211+
if ( filter.length === 0 ) {
212+
const queryStringFacet =
213+
new QueryStringFacet(FileFacetName.GENUS_SPECIES, [GenusSpecies.HOMO_SAPIENS]);
214+
filter.push(queryStringFacet);
215+
}
216+
206217
this.store.dispatch(new SetViewStateAction(tab, filter));
207-
this.routerEventsSubscription.unsubscribe();
218+
219+
if ( this.routerEventsSubscription ) {
220+
this.routerEventsSubscription.unsubscribe();
221+
}
208222
}
209223
});
210224
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Human Cell Atlas
3+
* https://www.humancellatlas.org/
4+
*
5+
* Stub ActivatedRoute object, to be used when spying on snapshot values.
6+
*/
7+
8+
// Core dependencies
9+
import { Injectable } from "@angular/core";
10+
11+
@Injectable()
12+
export class ActivatedRouteStub {
13+
14+
/**
15+
* @returns {{}}
16+
*/
17+
get snapshot() {
18+
return {};
19+
}
20+
}

0 commit comments

Comments
 (0)