From e56ea2aff894ea21537d66340b74b8a7b6a59787 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Fri, 13 Apr 2018 15:18:35 +0200 Subject: [PATCH] Applied feedback and other fixes --- resources/i18n/en.json | 3 +- .../search-facet-filter.component.html | 2 +- .../search-facet-filter.component.spec.ts | 7 +-- .../search-facet-filter.component.ts | 19 ++----- .../+search-page/search-page.component.html | 4 +- src/app/+search-page/search-page.component.ts | 2 + .../search-results.component.html | 5 +- .../search-results.component.ts | 3 +- .../search-service/search.service.spec.ts | 16 ++++-- .../search-service/search.service.ts | 54 +++++-------------- .../builders/remote-data-build.service.ts | 7 ++- .../data/base-response-parsing.service.ts | 5 +- .../data/config-response-parsing.service.ts | 2 +- .../core/data/dso-response-parsing.service.ts | 2 +- ...acet-value-map-response-parsing.service.ts | 2 +- .../facet-value-response-parsing.service.ts | 6 +-- src/app/core/data/paginated-list.ts | 36 ++++++++++++- .../data/search-response-parsing.service.ts | 2 +- src/app/core/shared/page-info.model.ts | 11 ++++ src/app/shared/testing/router-stub.ts | 2 + 20 files changed, 107 insertions(+), 83 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 68dfcbe147..5c750b2397 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -80,7 +80,8 @@ "search_dspace": "Search DSpace" }, "results": { - "head": "Search Results" + "head": "Search Results", + "no-results": "There were no results for this search" }, "sidebar": { "close": "Back to results", diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html index 8687aeb25b..074c5700d7 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html @@ -20,7 +20,7 @@
- {{"search.filters.filter.show-more" | translate}} { let comp: SearchFacetFilterComponent; @@ -56,7 +57,7 @@ describe('SearchFacetFilterComponent', () => { let router; const page = Observable.of(0); - const mockValues = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(null, values))); + const mockValues = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), values))); beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], @@ -121,14 +122,14 @@ describe('SearchFacetFilterComponent', () => { }); describe('when the getAddParams method is called wih a value', () => { - it('should return the selectedValueq list with the new parameter value', () => { + it('should return the selectedValue list with the new parameter value', () => { const result = comp.getAddParams(value3); expect(result).toEqual({ [mockFilterConfig.paramName]: [value1, value2, value3] }); }); }); describe('when the getRemoveParams method is called wih a value', () => { - it('should return the selectedValueq list with the parameter value left out', () => { + it('should return the selectedValue list with the parameter value left out', () => { const result = comp.getRemoveParams(value1); expect(result).toEqual({ [mockFilterConfig.paramName]: [value2] }); }); diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index 04258fbd35..bf78bbe5ec 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -4,7 +4,7 @@ import { SearchFilterConfig } from '../../../search-service/search-filter-config import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { SearchFilterService } from '../search-filter.service'; -import { hasValue, isNotEmpty } from '../../../../shared/empty.util'; +import { hasNoValue, hasValue, isNotEmpty } from '../../../../shared/empty.util'; import { RemoteData } from '../../../../core/data/remote-data'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { SearchService } from '../../../search-service/search.service'; @@ -30,6 +30,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { filterValues: Array>>> = []; filterValues$: BehaviorSubject = new BehaviorSubject(this.filterValues); currentPage: Observable; + isLastPage$: BehaviorSubject = new BehaviorSubject(false); filter: string; pageChange = false; sub: Subscription; @@ -50,12 +51,12 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { this.pageChange = false; this.unsubscribe(); - this.sub = this.currentPage.distinctUntilChanged().map((page) => { return this.searchService.getFacetValuesFor(this.filterConfig, page, options); }).subscribe((newValues$) => { this.filterValues = [...this.filterValues, newValues$]; this.filterValues$.next(this.filterValues); + newValues$.first().subscribe((rd) => this.isLastPage$.next(hasNoValue(rd.payload.next))); }); } @@ -98,18 +99,6 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { hasValue(o: any): boolean { return hasValue(o); } - - isLastPage(): Observable { - return this.filterValues$.flatMap((map) => { - - if (isNotEmpty(map)) { - return map.pop().map((rd: RemoteData>) => rd.payload.currentPage >= rd.payload.totalPages); - } else { - return false; - } - }); - } - getRemoveParams(value: string) { return { [this.filterConfig.paramName]: this.selectedValues.filter((v) => v !== value) }; } @@ -123,7 +112,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { } unsubscribe(): void { - if (this.sub !== undefined) { + if (hasValue(this.sub)) { this.sub.unsubscribe(); } } diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index c50bb30696..d53e4776b1 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -36,6 +36,4 @@
- - - + \ No newline at end of file diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index c9efc65676..03f664de59 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -65,6 +65,8 @@ export class SearchPageComponent implements OnInit { this.resultsRD$ = this.searchOptions$.pipe( flatMap((searchOptions) => this.service.search(searchOptions)) ); + this.resultsRD$.subscribe((t) => console.log(t)); + } public closeSidebar(): void { diff --git a/src/app/+search-page/search-results/search-results.component.html b/src/app/+search-page/search-results/search-results.component.html index 7d7c169380..ec103e8957 100644 --- a/src/app/+search-page/search-results/search-results.component.html +++ b/src/app/+search-page/search-results/search-results.component.html @@ -1,10 +1,11 @@ +

{{ 'search.results.head' | translate }}

-

{{ 'search.results.head' | translate }}

- + + \ No newline at end of file diff --git a/src/app/+search-page/search-results/search-results.component.ts b/src/app/+search-page/search-results/search-results.component.ts index 4b3fec4565..14ccb5d541 100644 --- a/src/app/+search-page/search-results/search-results.component.ts +++ b/src/app/+search-page/search-results/search-results.component.ts @@ -5,6 +5,7 @@ import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { SearchOptions, ViewMode } from '../search-options.model'; import { SortOptions } from '../../core/cache/models/sort-options.model'; import { SearchResult } from '../search-result.model'; +import { PaginatedList } from '../../core/data/paginated-list'; /** * This component renders a simple item page. @@ -20,7 +21,7 @@ import { SearchResult } from '../search-result.model'; ] }) export class SearchResultsComponent { - @Input() searchResults: RemoteData>>; + @Input() searchResults: RemoteData>>; @Input() searchConfig: SearchOptions; @Input() sortConfig: SortOptions; @Input() viewMode: ViewMode; diff --git a/src/app/+search-page/search-service/search.service.spec.ts b/src/app/+search-page/search-service/search.service.spec.ts index ef6efe3deb..4b558f8726 100644 --- a/src/app/+search-page/search-service/search.service.spec.ts +++ b/src/app/+search-page/search-service/search.service.spec.ts @@ -10,7 +10,7 @@ import { ViewMode } from '../../+search-page/search-options.model'; import { RouteService } from '../../shared/route.service'; import { GLOBAL_CONFIG } from '../../../config'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Router, UrlTree } from '@angular/router'; import { RequestService } from '../../core/data/request.service'; import { ResponseCacheService } from '../../core/cache/response-cache.service'; import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; @@ -79,7 +79,8 @@ describe('SearchService', () => { const halService = { /* tslint:disable:no-empty */ - getEndpoint: () => {} + getEndpoint: () => { + } /* tslint:enable:no-empty */ }; @@ -118,6 +119,8 @@ describe('SearchService', () => { ], }); searchService = TestBed.get(SearchService); + const urlTree = Object.assign(new UrlTree(), { root: { children: { primary: 'search' } } }); + router.parseUrl.and.returnValue(urlTree); }); it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => { @@ -160,7 +163,8 @@ describe('SearchService', () => { spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint)); (searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry)); /* tslint:disable:no-empty */ - searchService.search(searchOptions).subscribe((t) => {}); // subscribe to make sure all methods are called + searchService.search(searchOptions).subscribe((t) => { + }); // subscribe to make sure all methods are called /* tslint:enable:no-empty */ }); @@ -189,7 +193,8 @@ describe('SearchService', () => { spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint)); (searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry)); /* tslint:disable:no-empty */ - searchService.getConfig(null).subscribe((t) => {}); // subscribe to make sure all methods are called + searchService.getConfig(null).subscribe((t) => { + }); // subscribe to make sure all methods are called /* tslint:enable:no-empty */ }); @@ -220,7 +225,8 @@ describe('SearchService', () => { spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint)); (searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry)); /* tslint:disable:no-empty */ - searchService.getConfig(scope).subscribe((t) => {}); // subscribe to make sure all methods are called + searchService.getConfig(scope).subscribe((t) => { + }); // subscribe to make sure all methods are called /* tslint:enable:no-empty */ }); diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 6b1034fa33..ec4ccc9742 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -1,5 +1,8 @@ import { Injectable, OnDestroy } from '@angular/core'; -import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; +import { + ActivatedRoute, NavigationExtras, PRIMARY_OUTLET, Router, + UrlSegmentGroup +} from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { flatMap, map, tap } from 'rxjs/operators'; import { ViewMode } from '../../+search-page/search-options.model'; @@ -21,13 +24,12 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { GenericConstructor } from '../../core/shared/generic-constructor'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { URLCombiner } from '../../core/url-combiner/url-combiner'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { NormalizedSearchResult } from '../normalized-search-result.model'; import { SearchOptions } from '../search-options.model'; import { SearchResult } from '../search-result.model'; import { FacetValue } from './facet-value.model'; -import { FilterType } from './filter-type.model'; import { SearchFilterConfig } from './search-filter-config.model'; import { SearchResponseParsingService } from '../../core/data/search-response-parsing.service'; import { SearchQueryResponse } from './search-query-response.model'; @@ -37,48 +39,16 @@ import { ListableObject } from '../../shared/object-collection/shared/listable-o import { FacetValueResponseParsingService } from '../../core/data/facet-value-response-parsing.service'; import { FacetConfigResponseParsingService } from '../../core/data/facet-config-response-parsing.service'; import { PaginatedSearchOptions } from '../paginated-search-options.model'; +import { observable } from 'rxjs/symbol/observable'; @Injectable() export class SearchService implements OnDestroy { private searchLinkPath = 'discover/search/objects'; - private facetValueLinkPath = 'discover/search/facets'; private facetValueLinkPathPrefix = 'discover/facets/'; private facetConfigLinkPath = 'discover/facets'; private sub; - uiSearchRoute = '/search'; - config: SearchFilterConfig[] = [ - // Object.assign(new SearchFilterConfig(), - // { - // name: 'scope', - // type: FilterType.hierarchical, - // hasFacets: true, - // isOpenByDefault: true - // }), - Object.assign(new SearchFilterConfig(), - { - name: 'author', - type: FilterType.text, - hasFacets: true, - isOpenByDefault: false - }), - Object.assign(new SearchFilterConfig(), - { - name: 'dateIssued', - type: FilterType.date, - hasFacets: true, - isOpenByDefault: false - }), - Object.assign(new SearchFilterConfig(), - { - name: 'subject', - type: FilterType.text, - hasFacets: false, - isOpenByDefault: false - }) - ]; - // searchOptions: BehaviorSubject; searchOptions: SearchOptions; constructor(private router: Router, @@ -96,7 +66,6 @@ export class SearchService implements OnDestroy { } search(searchOptions?: PaginatedSearchOptions): Observable>>> { - // this.halService.getEndpoint(this.searchLinkPath).subscribe((t) => console.log(t)); const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe( map((url: string) => { if (hasValue(searchOptions)) { @@ -136,7 +105,8 @@ export class SearchService implements OnDestroy { ); // Create search results again with the correct dso objects linked to each result - const tDomainListObs: Observable>> = Observable.combineLatest(sqrObs, dsoObs, (sqr: SearchQueryResponse, dsos: RemoteData) => { + const tDomainListObs = Observable.combineLatest(sqrObs, dsoObs, (sqr: SearchQueryResponse, dsos: RemoteData) => { + return sqr.objects.map((object: NormalizedSearchResult, index: number) => { let co = DSpaceObject; if (dsos.payload[index]) { @@ -150,7 +120,7 @@ export class SearchService implements OnDestroy { } }); }); - + const pageInfoObs: Observable = responseCacheObs.pipe( map((entry: ResponseCacheEntry) => entry.response), map((response: FacetValueSuccessResponse) => response.pageInfo) @@ -262,11 +232,13 @@ export class SearchService implements OnDestroy { queryParamsHandling: 'merge' }; - this.router.navigate([this.uiSearchRoute], navigationExtras); + this.router.navigate([this.getSearchLink()], navigationExtras); } getSearchLink() { - return this.uiSearchRoute; + const urlTree = this.router.parseUrl(this.router.url); + const g: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET]; + return '/' + g.toString(); } ngOnDestroy(): void { diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index af2c80c9aa..ece80cf4ca 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -4,7 +4,7 @@ import { map, tap } from 'rxjs/operators'; import { NormalizedSearchResult } from '../../../+search-page/normalized-search-result.model'; import { SearchResult } from '../../../+search-page/search-result.model'; import { SearchQueryResponse } from '../../../+search-page/search-service/search-query-response.model'; -import { hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; import { RemoteDataError } from '../../data/remote-data-error'; @@ -200,6 +200,11 @@ export class RemoteDataBuildService { } aggregate(input: Array>>): Observable> { + + if (isEmpty(input)) { + return Observable.of(new RemoteData(false, false, true, null, [])); + } + return Observable.combineLatest( ...input, (...arr: Array>) => { diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index 9d6a5851e5..bde0857946 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -117,8 +117,9 @@ export abstract class BaseResponseParsingService { this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref); } - processPageInfo(pageObj: any): PageInfo { - if (isNotEmpty(pageObj)) { + processPageInfo(payload: any): PageInfo { + if (isNotEmpty(payload.page)) { + const pageObj = Object.assign({}, payload.page, {_links: payload._links}); const pageInfoObject = new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj); if (pageInfoObject.currentPage >= 0) { Object.assign(pageInfoObject, { currentPage: pageInfoObject.currentPage + 1 }); diff --git a/src/app/core/data/config-response-parsing.service.ts b/src/app/core/data/config-response-parsing.service.ts index 69be4bbc02..033c9ddc68 100644 --- a/src/app/core/data/config-response-parsing.service.ts +++ b/src/app/core/data/config-response-parsing.service.ts @@ -29,7 +29,7 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && data.statusCode === '200') { const configDefinition = this.process(data.payload, request.href); - return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload.page)); + return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload)); } else { return new ErrorResponse( Object.assign( diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index 11590d0431..9651eb3157 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -28,7 +28,7 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const processRequestDTO = this.process(data.payload, request.href); const selfLinks = this.flattenSingleKeyObject(processRequestDTO).map((no) => no.self); - return new DSOSuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload.page)) + return new DSOSuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload)) } } diff --git a/src/app/core/data/facet-value-map-response-parsing.service.ts b/src/app/core/data/facet-value-map-response-parsing.service.ts index dfd72c0cc5..8588e4aa0b 100644 --- a/src/app/core/data/facet-value-map-response-parsing.service.ts +++ b/src/app/core/data/facet-value-map-response-parsing.service.ts @@ -37,7 +37,7 @@ export class FacetValueMapResponseParsingService extends BaseResponseParsingServ payload._embedded.facets.map((facet) => { const values = facet._embedded.values.map((value) => {value.search = value._links.search.href; return value;}); const facetValues = serializer.deserializeArray(values); - const valuesResponse = new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload.page)); + const valuesResponse = new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload)); facetMap[facet.name] = valuesResponse; }); diff --git a/src/app/core/data/facet-value-response-parsing.service.ts b/src/app/core/data/facet-value-response-parsing.service.ts index 17f0730566..bc3f4e5368 100644 --- a/src/app/core/data/facet-value-response-parsing.service.ts +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -30,9 +30,9 @@ export class FacetValueResponseParsingService extends BaseResponseParsingService const payload = data.payload; const serializer = new DSpaceRESTv2Serializer(FacetValue); - const values = payload._embedded.values.map((value) => {value.search = value._links.search.href; return value;}); + // const values = payload._embedded.values.map((value) => {value.search = value._links.search.href; return value;}); - const facetValues = serializer.deserializeArray(values); - return new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload.page)); + const facetValues = serializer.deserializeArray(payload._embedded.values); + return new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload)); } } diff --git a/src/app/core/data/paginated-list.ts b/src/app/core/data/paginated-list.ts index 7e4a57f84e..fe095ecc45 100644 --- a/src/app/core/data/paginated-list.ts +++ b/src/app/core/data/paginated-list.ts @@ -45,10 +45,44 @@ export class PaginatedList { return this.pageInfo.currentPage; } return 1; - } set currentPage(value: number) { this.pageInfo.currentPage = value; } + + get first(): string { + return this.pageInfo.first; + } + + set first(first: string) { + this.pageInfo.first = first; + } + + get prev(): string { + return this.pageInfo.prev; + } + set prev(prev: string) { + this.pageInfo.prev = prev; + } + + get next(): string { + return this.pageInfo.next; + } + + set next(next: string) { + this.pageInfo.next = next; + } + + get last(): string { + return this.pageInfo.last; + } + + set last(last: string) { + this.pageInfo.last = last; + } + + + + } diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index 927c5ff010..707875911d 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -56,6 +56,6 @@ export class SearchResponseParsingService implements ResponseParsingService { })); payload.objects = objects; const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload); - return new SearchSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload.page)); + return new SearchSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload)); } } diff --git a/src/app/core/shared/page-info.model.ts b/src/app/core/shared/page-info.model.ts index 0cb5cac5b1..ba2af24dce 100644 --- a/src/app/core/shared/page-info.model.ts +++ b/src/app/core/shared/page-info.model.ts @@ -28,4 +28,15 @@ export class PageInfo { @autoserializeAs(Number, 'number') currentPage: number; + @autoserialize + last: string; + + @autoserialize + next: string; + + @autoserialize + prev: string; + + @autoserialize + first: string; } diff --git a/src/app/shared/testing/router-stub.ts b/src/app/shared/testing/router-stub.ts index ca4f7642b8..31c09c41e3 100644 --- a/src/app/shared/testing/router-stub.ts +++ b/src/app/shared/testing/router-stub.ts @@ -1,7 +1,9 @@ + export class RouterStub { url: string; //noinspection TypeScriptUnresolvedFunction navigate = jasmine.createSpy('navigate'); + parseUrl = jasmine.createSpy('parseUrl'); navigateByUrl(url): void { this.url = url; }