Skip to content
Snippets Groups Projects
Commit 031aa7da authored by Xiao Gui's avatar Xiao Gui
Browse files

fix https://github.com/HumanBrainProject/interactive-viewer/issues/453

chore: added tests for history.service
chore: added tests to urlUtil
parent db769029
No related branches found
No related tags found
No related merge requests found
import { AtlasViewerHistoryUseEffect } from './atlasViewer.history.service'
import { TestBed, tick, fakeAsync, flush } from '@angular/core/testing'
import { provideMockActions } from '@ngrx/effects/testing'
import { provideMockStore } from '@ngrx/store/testing'
import { Observable, of, Subscription } from 'rxjs'
import { Action, Store } from '@ngrx/store'
import { defaultRootState } from '../services/stateStore.service'
import { HttpClientModule } from '@angular/common/http'
import { cold } from 'jasmine-marbles'
const bigbrainJson = require('!json-loader!src/res/ext/bigbrain.json')
const actions$: Observable<Action> = of({type: 'TEST'})
describe('atlasviewer.history.service.ts', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule
],
providers: [
AtlasViewerHistoryUseEffect,
provideMockActions(() => actions$),
provideMockStore({ initialState: defaultRootState })
]
})
})
afterEach(() => {
})
describe('currentStateSearchParam$', () => {
it('should fire when template is set', () => {
const effect = TestBed.get(AtlasViewerHistoryUseEffect)
const store = TestBed.get(Store)
const { viewerState } = defaultRootState
store.setState({
...defaultRootState,
viewerState: {
...viewerState,
templateSelected: bigbrainJson
}
})
const expected = cold('(a)', {
a: 'templateSelected=Big+Brain+%28Histology%29'
})
expect(effect.currentStateSearchParam$).toBeObservable(expected)
})
it('should fire when template and parcellation is set', () => {
const effect = TestBed.get(AtlasViewerHistoryUseEffect)
const store = TestBed.get(Store)
const { viewerState } = defaultRootState
store.setState({
...defaultRootState,
viewerState: {
...viewerState,
templateSelected: bigbrainJson,
parcellationSelected: bigbrainJson.parcellations[0]
}
})
const expected = cold('(a)', {
a: 'templateSelected=Big+Brain+%28Histology%29&parcellationSelected=Cytoarchitectonic+Maps'
})
expect(effect.currentStateSearchParam$).toBeObservable(expected)
})
})
describe('setNewSearchString$', () => {
const obj = {
spiedFn: () => {}
}
const subscriptions: Subscription[] = []
let spy
beforeAll(() => {
spy = spyOn(obj, 'spiedFn')
})
beforeEach(() => {
spy.calls.reset()
})
afterEach(() => {
while (subscriptions.length > 0) subscriptions.pop().unsubscribe()
})
it('should fire when set', fakeAsync(() => {
const store = TestBed.get(Store)
const effect = TestBed.get(AtlasViewerHistoryUseEffect)
subscriptions.push(
effect.setNewSearchString$.subscribe(obj.spiedFn)
)
const { viewerState } = defaultRootState
store.setState({
...defaultRootState,
viewerState: {
...viewerState,
templateSelected: bigbrainJson,
parcellationSelected: bigbrainJson.parcellations[0]
}
})
tick(100)
expect(spy).toHaveBeenCalledTimes(1)
}))
it('should not call window.history.pushState on start', fakeAsync(() => {
tick(100)
expect(spy).toHaveBeenCalledTimes(0)
}))
})
})
\ No newline at end of file
...@@ -2,7 +2,7 @@ import { Injectable, OnDestroy } from "@angular/core"; ...@@ -2,7 +2,7 @@ import { Injectable, OnDestroy } from "@angular/core";
import { Actions, Effect, ofType } from '@ngrx/effects' import { Actions, Effect, ofType } from '@ngrx/effects'
import { Store } from "@ngrx/store"; import { Store } from "@ngrx/store";
import { fromEvent, merge, of, Subscription } from "rxjs"; import { fromEvent, merge, of, Subscription } from "rxjs";
import { catchError, debounceTime, distinctUntilChanged, filter, map, startWith, switchMap, switchMapTo, take, withLatestFrom } from "rxjs/operators"; import { debounceTime, distinctUntilChanged, filter, map, startWith, switchMap, switchMapTo, take, withLatestFrom, shareReplay } from "rxjs/operators";
import { defaultRootState, GENERAL_ACTION_TYPES, IavRootStoreInterface } from "src/services/stateStore.service"; import { defaultRootState, GENERAL_ACTION_TYPES, IavRootStoreInterface } from "src/services/stateStore.service";
import { AtlasViewerConstantsServices } from "src/ui/databrowserModule/singleDataset/singleDataset.base"; import { AtlasViewerConstantsServices } from "src/ui/databrowserModule/singleDataset/singleDataset.base";
import { cvtSearchParamToState, cvtStateToSearchParam } from "./atlasViewer.urlUtil"; import { cvtSearchParamToState, cvtStateToSearchParam } from "./atlasViewer.urlUtil";
...@@ -79,53 +79,56 @@ export class AtlasViewerHistoryUseEffect implements OnDestroy { ...@@ -79,53 +79,56 @@ export class AtlasViewerHistoryUseEffect implements OnDestroy {
private subscriptions: Subscription[] = [] private subscriptions: Subscription[] = []
private currentStateSearchParam$ = this.store$.pipe( private currentStateSearchParam$ = this.store$.pipe(
map(getSearchParamStringFromState), map(s => {
catchError((err, _obs) => { try {
// TODO error parsing current state search param. let user know return getSearchParamStringFromState(s)
return of(null) } catch (e) {
// TODO parsing state to search param error
return null
}
}), }),
filter(v => v !== null), filter(v => v !== null),
) )
// GENERAL_ACTION_TYPES.APPLY_STATE is triggered by pop state or initial
// conventiently, the action has a state property
public setNewSearchString$ = this.actions$.pipe(
ofType(GENERAL_ACTION_TYPES.APPLY_STATE),
// subscribe to inner obs on init
startWith({}),
switchMap(({ state }: any) =>
this.currentStateSearchParam$.pipe(
shareReplay(1),
distinctUntilChanged(),
debounceTime(100),
// compares the searchParam triggerd by change of state with the searchParam generated by GENERAL_ACTION_TYPES.APPLY_STATE
// if the same, the change is induced by GENERAL_ACTION_TYPES.APPLY_STATE, and should NOT be pushed to history
filter((newSearchParam, index) => {
try {
const oldSearchParam = (state && getSearchParamStringFromState(state)) || ''
// in the unlikely event that user returns to the exact same state without use forward/back button
return index > 0 || newSearchParam !== oldSearchParam
} catch (e) {
return index > 0 || newSearchParam !== ''
}
})
)
)
)
constructor( constructor(
private store$: Store<IavRootStoreInterface>, private store$: Store<IavRootStoreInterface>,
private actions$: Actions, private actions$: Actions,
private constantService: AtlasViewerConstantsServices, private constantService: AtlasViewerConstantsServices
) { ) {
this.subscriptions.push(
// GENERAL_ACTION_TYPES.APPLY_STATE is triggered by pop state or initial
// conventiently, the action has a state property
this.actions$.pipe(
ofType(GENERAL_ACTION_TYPES.APPLY_STATE),
// subscribe to inner obs on init
startWith({}),
switchMap(({ state }: any) =>
this.currentStateSearchParam$.pipe(
distinctUntilChanged(),
debounceTime(100),
// compares the searchParam triggerd by change of state with the searchParam generated by GENERAL_ACTION_TYPES.APPLY_STATE this.setNewSearchString$.subscribe(newSearchString => {
// if the same, the change is induced by GENERAL_ACTION_TYPES.APPLY_STATE, and should NOT be pushed to history const url = new URL(window.location.toString())
filter((newSearchParam, index) => { url.search = newSearchString
try { window.history.pushState(newSearchString, '', url.toString())
})
const oldSearchParam = (state && getSearchParamStringFromState(state)) || ''
// in the unlikely event that user returns to the exact same state without use forward/back button
return index > 0 || newSearchParam !== oldSearchParam
} catch (e) {
return index > 0 || newSearchParam !== ''
}
}),
),
),
).subscribe(newSearchString => {
const url = new URL(window.location.toString())
url.search = newSearchString
window.history.pushState(newSearchString, '', url.toString())
}),
)
} }
public ngOnDestroy() { public ngOnDestroy() {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import {} from 'jasmine' import {} from 'jasmine'
import { defaultRootState } from 'src/services/stateStore.service' import { defaultRootState } from 'src/services/stateStore.service'
import { cvtSearchParamToState, PARSING_SEARCHPARAM_ERROR } from './atlasViewer.urlUtil' import { cvtSearchParamToState, PARSING_SEARCHPARAM_ERROR, cvtStateToSearchParam } from './atlasViewer.urlUtil'
const bigbrainJson = require('!json-loader!src/res/ext/bigbrain.json') const bigbrainJson = require('!json-loader!src/res/ext/bigbrain.json')
const colin = require('!json-loader!src/res/ext/colin.json') const colin = require('!json-loader!src/res/ext/colin.json')
...@@ -19,6 +19,7 @@ const fetchedTemplateRootState = { ...@@ -19,6 +19,7 @@ const fetchedTemplateRootState = {
}, },
} }
// TODO finish writing tests
describe('atlasViewer.urlService.service.ts', () => { describe('atlasViewer.urlService.service.ts', () => {
describe('cvtSearchParamToState', () => { describe('cvtSearchParamToState', () => {
it('convert empty search param to empty state', () => { it('convert empty search param to empty state', () => {
...@@ -67,5 +68,34 @@ describe('atlasViewer.urlService.service.ts', () => { ...@@ -67,5 +68,34 @@ describe('atlasViewer.urlService.service.ts', () => {
describe('cvtStateToSearchParam', () => { describe('cvtStateToSearchParam', () => {
it('should convert template selected', () => {
const { viewerState } = defaultRootState
const searchParam = cvtStateToSearchParam({
...defaultRootState,
viewerState: {
...viewerState,
templateSelected: bigbrainJson,
}
})
const stringified = searchParam.toString()
expect(stringified).toBe('templateSelected=Big+Brain+%28Histology%29')
})
})
it('should convert template selected and parcellation selected', () => {
const { viewerState } = defaultRootState
const searchParam = cvtStateToSearchParam({
...defaultRootState,
viewerState: {
...viewerState,
templateSelected: bigbrainJson,
parcellationSelected: bigbrainJson.parcellations[0]
}
})
const stringified = searchParam.toString()
expect(stringified).toBe('templateSelected=Big+Brain+%28Histology%29&parcellationSelected=Cytoarchitectonic+Maps')
}) })
}) })
...@@ -28,7 +28,7 @@ export const cvtStateToSearchParam = (state: IavRootStoreInterface): URLSearchPa ...@@ -28,7 +28,7 @@ export const cvtStateToSearchParam = (state: IavRootStoreInterface): URLSearchPa
// encoding states // encoding states
searchParam.set('templateSelected', templateSelected.name) searchParam.set('templateSelected', templateSelected.name)
searchParam.set('parcellationSelected', parcellationSelected.name) if (!!parcellationSelected) searchParam.set('parcellationSelected', parcellationSelected.name)
// encoding selected regions // encoding selected regions
const accumulatorMap = new Map<string, number[]>() const accumulatorMap = new Map<string, number[]>()
...@@ -41,28 +41,34 @@ export const cvtStateToSearchParam = (state: IavRootStoreInterface): URLSearchPa ...@@ -41,28 +41,34 @@ export const cvtStateToSearchParam = (state: IavRootStoreInterface): URLSearchPa
for (const [key, arr] of accumulatorMap) { for (const [key, arr] of accumulatorMap) {
cRegionObj[key] = arr.map(n => encodeNumber(n)).join(separator) cRegionObj[key] = arr.map(n => encodeNumber(n)).join(separator)
} }
searchParam.set('cRegionsSelected', JSON.stringify(cRegionObj)) if (Object.keys(cRegionObj).length > 0) searchParam.set('cRegionsSelected', JSON.stringify(cRegionObj))
// encoding navigation // encoding navigation
const { orientation, perspectiveOrientation, perspectiveZoom, position, zoom } = navigation if (navigation) {
const cNavString = [ const { orientation, perspectiveOrientation, perspectiveZoom, position, zoom } = navigation
orientation.map(n => encodeNumber(n, {float: true})).join(separator), if (orientation && perspectiveOrientation && perspectiveZoom && position && zoom) {
perspectiveOrientation.map(n => encodeNumber(n, {float: true})).join(separator), const cNavString = [
encodeNumber(Math.floor(perspectiveZoom)), orientation.map(n => encodeNumber(n, {float: true})).join(separator),
Array.from(position).map((v: number) => Math.floor(v)).map(n => encodeNumber(n)).join(separator), perspectiveOrientation.map(n => encodeNumber(n, {float: true})).join(separator),
encodeNumber(Math.floor(zoom)), encodeNumber(Math.floor(perspectiveZoom)),
].join(`${separator}${separator}`) Array.from(position).map((v: number) => Math.floor(v)).map(n => encodeNumber(n)).join(separator),
searchParam.set('cNavigation', cNavString) encodeNumber(Math.floor(zoom)),
].join(`${separator}${separator}`)
searchParam.set('cNavigation', cNavString)
}
}
// encode nifti layers // encode nifti layers
const initialNgState = templateSelected.nehubaConfig.dataset.initialNgState if (!!templateSelected.nehubaConfig) {
const { layers } = ngViewerState const initialNgState = templateSelected.nehubaConfig.dataset.initialNgState
const additionalLayers = layers.filter(layer => const { layers } = ngViewerState
/^blob:/.test(layer.name) && const additionalLayers = layers.filter(layer =>
Object.keys(initialNgState.layers).findIndex(layerName => layerName === layer.name) < 0, /^blob:/.test(layer.name) &&
) Object.keys(initialNgState.layers).findIndex(layerName => layerName === layer.name) < 0,
const niftiLayers = additionalLayers.filter(layer => /^nifti:\/\//.test(layer.source)) )
if (niftiLayers.length > 0) { searchParam.set('niftiLayers', niftiLayers.join('__')) } const niftiLayers = additionalLayers.filter(layer => /^nifti:\/\//.test(layer.source))
if (niftiLayers.length > 0) { searchParam.set('niftiLayers', niftiLayers.join('__')) }
}
// plugin state // plugin state
const { initManifests } = pluginState const { initManifests } = pluginState
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment