From 6bda29d6b71463cb81d71ca73976488f0009f7a9 Mon Sep 17 00:00:00 2001 From: Iuliia Kulagina Date: Tue, 9 Apr 2024 19:00:16 +0200 Subject: [PATCH] Add tests for memoryCache, localStorageCache and geocoder --- src/cache/CacheManager.ts | 8 +-- test/visualData.ts | 46 ++++++------ test/visualTest.ts | 144 ++++++++++++++++++++++++++++++++++---- 3 files changed, 156 insertions(+), 42 deletions(-) diff --git a/src/cache/CacheManager.ts b/src/cache/CacheManager.ts index 5f6cc95..bef6e0a 100644 --- a/src/cache/CacheManager.ts +++ b/src/cache/CacheManager.ts @@ -13,10 +13,10 @@ export class CacheManager { private localStorageCache: LocalStorageCache; private bingGeocoder: BingGeocoder; - constructor(localStorageService: IVisualLocalStorageV2Service) { - this.memoryCache = new MemoryCache(CacheSettings.MaxCacheSize, CacheSettings.MaxCacheSizeOverflow); - this.bingGeocoder = new BingGeocoder(); - this.localStorageCache = new LocalStorageCache(localStorageService); + constructor(localStorageService: IVisualLocalStorageV2Service, memoryCache?: MemoryCache, localStorageCache?: LocalStorageCache, bingGeocoder?: BingGeocoder) { + this.memoryCache = memoryCache ?? new MemoryCache(CacheSettings.MaxCacheSize, CacheSettings.MaxCacheSizeOverflow); + this.bingGeocoder = bingGeocoder ?? new BingGeocoder(); + this.localStorageCache = localStorageCache ?? new LocalStorageCache(localStorageService); } private createLocalStorageCache(): Promise { diff --git a/test/visualData.ts b/test/visualData.ts index e8755bb..72d297b 100644 --- a/test/visualData.ts +++ b/test/visualData.ts @@ -104,6 +104,28 @@ export class GlobeMapData extends TestDataViewBuilder { 80.237908 ]; + public coordinatesMock: ILocationDictionary = { + "addis ababa, ethiopia": {latitude: 9.03582859, longitude: 38.75241089}, + "ahmedabad, india": {latitude: 23.0145092, longitude: 72.59175873}, + "cairo, egypt": {latitude: 30.04348755, longitude: 31.23529243}, + "cape town, south africa": {latitude: -33.92710876, longitude: 18.42006111}, + "casablanca, morocco": {latitude: 33.59451294, longitude: -7.6200285}, + "chennai, india": {latitude: 13.07209206, longitude: 80.20185852}, + "durban, south africa": {latitude: -29.88188934, longitude: 30.98084259}, + "jakarta, indonesia": {latitude: -6.17475653, longitude: 106.82707214}, + "jeddah, saudi arabia": {latitude: 21.48730469, longitude: 39.18133545}, + "lagos, nigeria": {latitude: 6.45505762, longitude: 3.39417958}, + "lima, peru": {latitude: -12.06210613, longitude: -77.03652191}, + "london, united kingdom": {latitude: 51.50740814, longitude: -0.12772401}, + "mexico city, mexico": {latitude: 19.43267822, longitude: -99.13420868}, + "new taipei city, republic of china": {latitude: 25.01170921, longitude: 121.46588135}, + "riyadh, saudi arabia": {latitude: 24.69496918, longitude: 46.72412872}, + "shanghai, china": {latitude: 31.23036957, longitude: 121.47370148}, + "shenzhen, china": {latitude: 22.54368019, longitude: 114.0579071}, + "surat, india": {latitude: 21.20350838, longitude: 72.83922577}, + "tehran, iran": {latitude: 35.68925095, longitude: 51.38959885} + }; + public valuesValue: number[] = getRandomNumbers(this.valuesSourceDestination.length, 10, 500); public getDataView(columnNames?: string[]): DataView { @@ -147,28 +169,4 @@ export class GlobeMapData extends TestDataViewBuilder { } ], columnNames).build(); } - - public getCoordinatesMock(): ILocationDictionary{ - return { - "addis ababa, ethiopia": {latitude: 9.03582859, longitude: 38.75241089}, - "ahmedabad, india": {latitude: 23.0145092, longitude: 72.59175873}, - "cairo, egypt": {latitude: 30.04348755, longitude: 31.23529243}, - "cape town, south africa": {latitude: -33.92710876, longitude: 18.42006111}, - "casablanca, morocco": {latitude: 33.59451294, longitude: -7.6200285}, - "chennai, india": {latitude: 13.07209206, longitude: 80.20185852}, - "durban, south africa": {latitude: -29.88188934, longitude: 30.98084259}, - "jakarta, indonesia": {latitude: -6.17475653, longitude: 106.82707214}, - "jeddah, saudi arabia": {latitude: 21.48730469, longitude: 39.18133545}, - "lagos, nigeria": {latitude: 6.45505762, longitude: 3.39417958}, - "lima, peru": {latitude: -12.06210613, longitude: -77.03652191}, - "london, united kingdom": {latitude: 51.50740814, longitude: -0.12772401}, - "mexico city, mexico": {latitude: 19.43267822, longitude: -99.13420868}, - "new taipei city, republic of china": {latitude: 25.01170921, longitude: 121.46588135}, - "riyadh, saudi arabia": {latitude: 24.69496918, longitude: 46.72412872}, - "shanghai, china": {latitude: 31.23036957, longitude: 121.47370148}, - "shenzhen, china": {latitude: 22.54368019, longitude: 114.0579071}, - "surat, india": {latitude: 21.20350838, longitude: 72.83922577}, - "tehran, iran": {latitude: 35.68925095, longitude: 51.38959885} - }; - } } diff --git a/test/visualTest.ts b/test/visualTest.ts index 7621e17..edf64da 100644 --- a/test/visualTest.ts +++ b/test/visualTest.ts @@ -41,11 +41,15 @@ import { TileMap, ITileGapObject, IGlobeMapObject3DWithToolTipData } from "../sr import capabilities from '../capabilities.json'; import PrimitiveValue = powerbi.PrimitiveValue; -import { ILocationKeyDictionary } from "../src/interfaces/locationInterfaces"; -import { PointerType, createSelectionId, d3MouseDown, renderTimeout } from "powerbi-visuals-utils-testutils"; +import { ILocationDictionary, ILocationKeyDictionary } from "../src/interfaces/locationInterfaces"; +import { PointerType, createSelectionId, d3MouseDown, renderTimeout, MockIStorageV2Service } from "powerbi-visuals-utils-testutils"; import SubSelectionOutlineVisibility = powerbi.visuals.SubSelectionOutlineVisibility; import ArcSubSelectionOutline = powerbi.visuals.ArcSubSelectionOutline; -import { DataPointReferences } from "../src/settings"; +import { CacheSettings, DataPointReferences } from "../src/settings"; +import { BingGeocoder } from "../src/geocoder"; +import { LocalStorageCache } from "../src/cache/LocalStorageCache"; +import { MemoryCache } from "../src/cache/MemoryCache"; +import { CacheManager } from "../src/cache/CacheManager"; describe("GlobeMap", () => { let visualBuilder: GlobeMapBuilder, @@ -61,16 +65,8 @@ describe("GlobeMap", () => { let categoricalColumns = GlobeMapColumns.getCategoricalColumns(dataView); - let locations = categoricalColumns.Location.values as PrimitiveValue[]; - let locationsNeedToBeLoaded = {} as ILocationKeyDictionary; - - locations.forEach((locationName) => { - const name = (locationName as string).toLowerCase(); - locationsNeedToBeLoaded[name] = { - place: name, - locationType: "" - }; - }); + let locations = categoricalColumns.Location.values as string[]; + let locationsNeedToBeLoaded = convertLocationsForCacheManager(locations); const coordinates = await visualInstance.cacheManager.loadCoordinates(locationsNeedToBeLoaded); @@ -244,11 +240,119 @@ describe("GlobeMap", () => { } }); }); + + it("geocoder should return locations for all valid input", async () => { + const bingGeocoder = new BingGeocoder(); + const locations = Object.assign(defaultDataViewBuilder.valuesSourceDestination); + // set invalid location + locations[0] = 'lll, lll'; + const coordinatesFromBing: ILocationDictionary = await bingGeocoder.geocode(locations); + + expect(Object.keys(coordinatesFromBing).length).toBe(locations.length - 1); + expect(coordinatesFromBing[locations[0]]).toBeUndefined; + }); + + it("cacheManager should load coordinates for all input from bing, when memory cache is empty and local storage is empty", async () => { + const locations = Object.assign(defaultDataViewBuilder.valuesSourceDestination); + const locationsNeedToBeLoaded = convertLocationsForCacheManager(locations); + + const { memoryCacheMock, localStorageMock, geocoderMock } = setUpMocks(0, 0); + const cacheManager = new CacheManager(new MockIStorageV2Service(), memoryCacheMock, localStorageMock, geocoderMock); + + const result = await cacheManager.loadCoordinates(locationsNeedToBeLoaded); + expect(geocoderMock.geocode).toHaveBeenCalled(); + expect(Object.keys(result).length).toBe(Object.keys(locationsNeedToBeLoaded).length); + }); + + it("cacheManager should load coordinates for all input, when memory cache is empty and local storage has half of data", async () => { + const locations = Object.assign(defaultDataViewBuilder.valuesSourceDestination); + const locationsNeedToBeLoaded = convertLocationsForCacheManager(locations); + + const { memoryCacheMock, localStorageMock, geocoderMock } = setUpMocks(0, 10); + const cacheManager = new CacheManager(new MockIStorageV2Service(), memoryCacheMock, localStorageMock, geocoderMock); + + const result = await cacheManager.loadCoordinates(locationsNeedToBeLoaded); + expect(geocoderMock.geocode).toHaveBeenCalled(); + expect(Object.keys(result).length).toBe(Object.keys(locationsNeedToBeLoaded).length); + }); + + it("cacheManager should load coordinates for all input, when memory cache has half of data and local storage is empty", async () => { + const locations = Object.assign(defaultDataViewBuilder.valuesSourceDestination); + const locationsNeedToBeLoaded = convertLocationsForCacheManager(locations); + + const { memoryCacheMock, localStorageMock, geocoderMock } = setUpMocks(10, 0); + const cacheManager = new CacheManager(new MockIStorageV2Service(), memoryCacheMock, localStorageMock, geocoderMock); + + const result = await cacheManager.loadCoordinates(locationsNeedToBeLoaded); + expect(geocoderMock.geocode).toHaveBeenCalled(); + expect(Object.keys(result).length).toBe(Object.keys(locationsNeedToBeLoaded).length); + }); + + it("cacheManager should load coordinates for all input, when memory cache has 1/3 of data and local storage has 1/3 of data", async () => { + const locations = Object.assign(defaultDataViewBuilder.valuesSourceDestination); + const locationsNeedToBeLoaded = convertLocationsForCacheManager(locations); + + const { memoryCacheMock, localStorageMock, geocoderMock } = setUpMocks(5, 5); + const cacheManager = new CacheManager(new MockIStorageV2Service(), memoryCacheMock, localStorageMock, geocoderMock); + + const result = await cacheManager.loadCoordinates(locationsNeedToBeLoaded); + expect(geocoderMock.geocode).toHaveBeenCalled(); + expect(Object.keys(result).length).toBe(Object.keys(locationsNeedToBeLoaded).length); + }); + + it("cacheManager should load coordinates for all input, when memory cache has 1/2 of data and local storage has 1/2 of data", async () => { + const locations = Object.assign(defaultDataViewBuilder.valuesSourceDestination); + const locationsNeedToBeLoaded = convertLocationsForCacheManager(locations); + + const { memoryCacheMock, localStorageMock, geocoderMock } = setUpMocks(10, 10); + const cacheManager = new CacheManager(new MockIStorageV2Service(), memoryCacheMock, localStorageMock, geocoderMock); + + const result = await cacheManager.loadCoordinates(locationsNeedToBeLoaded); + expect(geocoderMock.geocode).not.toHaveBeenCalled(); + expect(Object.keys(result).length).toBe(Object.keys(locationsNeedToBeLoaded).length); + }); + + function setUpMocks(locationsInMemoryCache: number, locationsInLocalStorageCache: number) { + const fullResult = defaultDataViewBuilder.coordinatesMock; + + const locationsFromMemoryCache: ILocationDictionary = {}; + const locationsFromLocalStorageCache: ILocationDictionary = {}; + const locationsFromBing: ILocationDictionary = {}; + let counter = 0; + for (let key in fullResult) { + if (counter < locationsInMemoryCache){ + locationsFromMemoryCache[key] = fullResult[key]; + } + else if (counter < locationsInLocalStorageCache + locationsInMemoryCache){ + locationsFromLocalStorageCache[key] = fullResult[key]; + } + else { + locationsFromBing[key] = fullResult[key]; + } + counter++; + } + + // mock for local storage + const localStorageMock = new LocalStorageCache(new MockIStorageV2Service()); + const locationsInPromise = new Promise((resolve)=> resolve(locationsFromLocalStorageCache)); + spyOn(localStorageMock, 'loadCoordinates').and.returnValue(locationsInPromise); + + // mock for MemoryCache + const memoryCacheMock = new MemoryCache(CacheSettings.MaxCacheSize, CacheSettings.MaxCacheSizeOverflow); + spyOn(memoryCacheMock, 'loadCoordinates').and.returnValue(locationsFromMemoryCache); + + // mock for geocoder + const geocoderMock = new BingGeocoder(); + const locationsGeocoderInPromise = new Promise((resolve)=> resolve(locationsFromBing)); + spyOn(geocoderMock, 'geocode').and.returnValue(locationsGeocoderInPromise); + + return {memoryCacheMock, localStorageMock, geocoderMock}; + } }); describe("OnObject tests", () => { beforeAll((done) => { - const coordinates = defaultDataViewBuilder.getCoordinatesMock(); + const coordinates = defaultDataViewBuilder.coordinatesMock; if (Object.keys(coordinates).length > 0) { visualInstance.cacheManager.saveCoordinates(coordinates) .then(() => { @@ -739,4 +843,16 @@ describe("GlobeMap", () => { }, powerbi.VisualUpdateType.Data, true, 1500, subselectionsFromContextMenu); }); }); + + function convertLocationsForCacheManager(locations: string[]): ILocationKeyDictionary{ + const locationsNeedToBeLoaded = {} as ILocationKeyDictionary; + locations.forEach((locationName) => { + const name = (locationName as string).toLowerCase(); + locationsNeedToBeLoaded[name] = { + place: name, + locationType: "" + }; + }); + return locationsNeedToBeLoaded; + } });