From 5fb27215a42fcd475dd91636760aac6f5186a0f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com> Date: Thu, 7 Mar 2024 02:05:01 +0100 Subject: [PATCH] feat: add tests for bio entities events --- .../createOverlayGeometryFeature.test.ts | 21 +++- ...eOverlaySubmapLinkRectangleFeature.test.ts | 10 +- .../mapSingleClick/handleAliasResults.test.ts | 23 +++- .../handleFeaturesClick.test.ts | 117 ++++++++++++++++++ .../mapSingleClick/onMapSingleClick.test.ts | 71 ++++++++++- .../utils/listeners/onPointerMove.test.ts | 75 +++++++++++ .../utils/listeners/onPointerMove.ts | 5 +- 7 files changed, 303 insertions(+), 19 deletions(-) create mode 100644 src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.test.ts create mode 100644 src/components/Map/MapViewer/utils/listeners/onPointerMove.test.ts diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.test.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.test.ts index 8ee12221..4c94444e 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.test.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.test.ts @@ -1,3 +1,4 @@ +import { FEATURE_TYPE } from '@/constants/features'; import { createOverlayGeometryFeature } from './createOverlayGeometryFeature'; describe('createOverlayGeometryFeature', () => { @@ -7,8 +8,13 @@ describe('createOverlayGeometryFeature', () => { const xMax = 10; const yMax = 10; const colorHexString = '#FF0000'; + const entityId = 2007; - const feature = createOverlayGeometryFeature([xMin, yMin, xMax, yMax], colorHexString); + const feature = createOverlayGeometryFeature( + [xMin, yMin, xMax, yMax], + colorHexString, + entityId, + ); expect(feature.getGeometry()!.getCoordinates()).toEqual([ [ @@ -22,6 +28,9 @@ describe('createOverlayGeometryFeature', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - getStyle() is not typed expect(feature.getStyle().getFill().getColor()).toEqual(colorHexString); + + expect(feature.get('id')).toBe(entityId); + expect(feature.get('type')).toBe(FEATURE_TYPE.SURFACE_OVERLAY); }); it('should create a feature with the correct geometry and style when using a different color', () => { @@ -30,8 +39,13 @@ describe('createOverlayGeometryFeature', () => { const xMax = 5; const yMax = 5; const colorHexString = '#00FF00'; + const entityId = 'a6e21d64-fd3c-4f7c-8acc-5fc305f4395a'; - const feature = createOverlayGeometryFeature([xMin, yMin, xMax, yMax], colorHexString); + const feature = createOverlayGeometryFeature( + [xMin, yMin, xMax, yMax], + colorHexString, + entityId, + ); expect(feature.getGeometry()!.getCoordinates()).toEqual([ [ @@ -45,5 +59,8 @@ describe('createOverlayGeometryFeature', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - getStyle() is not typed expect(feature.getStyle().getFill().getColor()).toEqual(colorHexString); + + expect(feature.get('id')).toBe(entityId); + expect(feature.get('type')).toBe(FEATURE_TYPE.SURFACE_MARKER); }); }); diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlaySubmapLinkRectangleFeature.test.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlaySubmapLinkRectangleFeature.test.ts index 06d6074a..cd5ba930 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlaySubmapLinkRectangleFeature.test.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlaySubmapLinkRectangleFeature.test.ts @@ -25,13 +25,13 @@ const CASES = [ describe('createOverlaySubmapLinkRectangleFeature - util', () => { it.each(CASES)('should return Feature instance', points => { - const feature = createOverlaySubmapLinkRectangleFeature(points, COLOR); + const feature = createOverlaySubmapLinkRectangleFeature(points, COLOR, 1234); expect(feature).toBeInstanceOf(Feature); }); it.each(CASES)('should return Feature instance with valid style and stroke', points => { - const feature = createOverlaySubmapLinkRectangleFeature(points, COLOR); + const feature = createOverlaySubmapLinkRectangleFeature(points, COLOR, 1234); const style = feature.getStyle(); expect(style).toMatchObject({ @@ -43,7 +43,7 @@ describe('createOverlaySubmapLinkRectangleFeature - util', () => { }); }); it('should return object with transparent fill and black stroke color when color is null', () => { - const feature = createOverlaySubmapLinkRectangleFeature([0, 0, 0, 0], null); + const feature = createOverlaySubmapLinkRectangleFeature([0, 0, 0, 0], null, 1234); const style = feature.getStyle(); expect(style).toMatchObject({ @@ -55,13 +55,13 @@ describe('createOverlaySubmapLinkRectangleFeature - util', () => { }); }); it.each(CASES)('should return Feature instance with valid geometry', (points, extent) => { - const feature = createOverlaySubmapLinkRectangleFeature(points, COLOR); + const feature = createOverlaySubmapLinkRectangleFeature(points, COLOR, 1234); const geometry = feature.getGeometry(); expect(geometry?.getExtent()).toEqual(extent); }); it('should throw error if extent is not valid', () => { - expect(() => createOverlaySubmapLinkRectangleFeature([100, 100, 0, 0], COLOR)).toThrow(); + expect(() => createOverlaySubmapLinkRectangleFeature([100, 100, 0, 0], COLOR, 1234)).toThrow(); }); }); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts index 44ac11c9..a12adfdf 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts @@ -1,4 +1,9 @@ -import { FIRST_ARRAY_ELEMENT, SECOND_ARRAY_ELEMENT, SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; +import { + FIRST_ARRAY_ELEMENT, + SECOND_ARRAY_ELEMENT, + SIZE_OF_EMPTY_ARRAY, + THIRD_ARRAY_ELEMENT, +} from '@/constants/common'; import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { ELEMENT_SEARCH_RESULT_MOCK_ALIAS } from '@/models/mocks/elementSearchResultMock'; import { apiPath } from '@/redux/apiPath'; @@ -27,19 +32,27 @@ describe('handleAliasResults - util', () => { handleAliasResults(dispatch)(ELEMENT_SEARCH_RESULT_MOCK_ALIAS); }); - it('should run openBioEntityDrawerById as first action', async () => { + it('should run selectTab as first action', async () => { await waitFor(() => { const actions = store.getActions(); expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY); - expect(actions[FIRST_ARRAY_ELEMENT].type).toEqual('drawer/openBioEntityDrawerById'); + expect(actions[FIRST_ARRAY_ELEMENT].type).toEqual('drawer/selectTab'); }); }); - it('should run getMultiBioEntity as second action', async () => { + it('should run openBioEntityDrawerById as second action', async () => { await waitFor(() => { const actions = store.getActions(); expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY); - expect(actions[SECOND_ARRAY_ELEMENT].type).toEqual('project/getMultiBioEntity/pending'); + expect(actions[SECOND_ARRAY_ELEMENT].type).toEqual('drawer/openBioEntityDrawerById'); + }); + }); + + it('should run getMultiBioEntity as third action', async () => { + await waitFor(() => { + const actions = store.getActions(); + expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY); + expect(actions[THIRD_ARRAY_ELEMENT].type).toEqual('project/getMultiBioEntity/pending'); }); }); }); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.test.ts new file mode 100644 index 00000000..a2f23be1 --- /dev/null +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.test.ts @@ -0,0 +1,117 @@ +import { FEATURE_TYPE } from '@/constants/features'; +import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; +import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener'; +import { Feature } from 'ol'; +import { handleFeaturesClick } from './handleFeaturesClick'; + +describe('handleFeaturesClick - util', () => { + beforeEach(() => { + PluginsEventBus.events = []; + }); + + describe('when feature contains pin icon marker', () => { + const { store } = getReduxStoreWithActionsListener(); + const { dispatch } = store; + const featureId = 1234; + const features = [ + new Feature({ + id: featureId, + type: FEATURE_TYPE.PIN_ICON_MARKER, + }), + ]; + + it('should dispatch event onPinIconClick', () => { + const dispatchEventSpy = jest.spyOn(PluginsEventBus, 'dispatchEvent'); + handleFeaturesClick(features, dispatch); + expect(dispatchEventSpy).toHaveBeenCalledWith('onPinIconClick', { id: featureId }); + }); + + it('should return shouldBlockCoordSearch=false', () => { + expect(handleFeaturesClick(features, dispatch)).toStrictEqual({ + shouldBlockCoordSearch: false, + }); + }); + }); + + describe('when feature contains pin icon bioentity', () => { + const { store } = getReduxStoreWithActionsListener(); + const { dispatch } = store; + const featureId = 1234; + const features = [ + new Feature({ + id: featureId, + type: FEATURE_TYPE.PIN_ICON_BIOENTITY, + }), + ]; + + it('should dispatch event onPinIconClick', () => { + const dispatchEventSpy = jest.spyOn(PluginsEventBus, 'dispatchEvent'); + handleFeaturesClick(features, dispatch); + expect(dispatchEventSpy).toHaveBeenCalledWith('onPinIconClick', { id: featureId }); + }); + + it('should dispatch actions regarding opening entity drawer', () => { + const { store: localStore } = getReduxStoreWithActionsListener(); + const { dispatch: localDispatch } = localStore; + handleFeaturesClick(features, localDispatch); + expect(store.getActions()).toStrictEqual([ + { payload: undefined, type: 'search/clearSearchData' }, + { payload: 1234, type: 'drawer/openBioEntityDrawerById' }, + ]); + }); + + it('should return shouldBlockCoordSearch=true', () => { + expect(handleFeaturesClick(features, dispatch)).toStrictEqual({ + shouldBlockCoordSearch: true, + }); + }); + }); + + describe('when feature contains surface overlay', () => { + const { store } = getReduxStoreWithActionsListener(); + const { dispatch } = store; + const featureId = 1234; + const features = [ + new Feature({ + id: featureId, + type: FEATURE_TYPE.SURFACE_OVERLAY, + }), + ]; + + it('should dispatch event onSurfaceClick', () => { + const dispatchEventSpy = jest.spyOn(PluginsEventBus, 'dispatchEvent'); + handleFeaturesClick(features, dispatch); + expect(dispatchEventSpy).toHaveBeenCalledWith('onSurfaceClick', { id: featureId }); + }); + + it('should return shouldBlockCoordSearch=false', () => { + expect(handleFeaturesClick(features, dispatch)).toStrictEqual({ + shouldBlockCoordSearch: false, + }); + }); + }); + + describe('when feature contains surface marker', () => { + const { store } = getReduxStoreWithActionsListener(); + const { dispatch } = store; + const featureId = 1234; + const features = [ + new Feature({ + id: featureId, + type: FEATURE_TYPE.SURFACE_MARKER, + }), + ]; + + it('should dispatch event onSurfaceClick', () => { + const dispatchEventSpy = jest.spyOn(PluginsEventBus, 'dispatchEvent'); + handleFeaturesClick(features, dispatch); + expect(dispatchEventSpy).toHaveBeenCalledWith('onSurfaceClick', { id: featureId }); + }); + + it('should return shouldBlockCoordSearch=false', () => { + expect(handleFeaturesClick(features, dispatch)).toStrictEqual({ + shouldBlockCoordSearch: false, + }); + }); + }); +}); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.test.ts index 806d2a7c..4270a324 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.test.ts @@ -1,4 +1,6 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-magic-numbers */ +import { FEATURE_TYPE } from '@/constants/features'; import { ELEMENT_SEARCH_RESULT_MOCK_ALIAS, ELEMENT_SEARCH_RESULT_MOCK_REACTION, @@ -8,7 +10,7 @@ import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener'; import { waitFor } from '@testing-library/react'; import { HttpStatusCode } from 'axios'; -import { MapBrowserEvent } from 'ol'; +import { Feature, Map, MapBrowserEvent } from 'ol'; import * as handleDataReset from './handleDataReset'; import * as handleSearchResultAction from './handleSearchResultAction'; import { onMapSingleClick } from './onMapSingleClick'; @@ -56,8 +58,12 @@ describe('onMapSingleClick - util', () => { const coordinate = [90, 90]; const event = getEvent(coordinate); + const mapInstanceMock = { + forEachFeatureAtPixel: (): void => {}, + } as unknown as Map; + it('should fire data reset handler', async () => { - await handler(event); + await handler(event, mapInstanceMock); expect(handleDataResetSpy).toBeCalled(); }); }); @@ -82,8 +88,12 @@ describe('onMapSingleClick - util', () => { .onGet(apiPath.getSingleBioEntityContentsStringWithCoordinates(point, modelId)) .reply(HttpStatusCode.Ok, undefined); + const mapInstanceMock = { + forEachFeatureAtPixel: (): void => {}, + } as unknown as Map; + it('does not fire search result action', async () => { - await handler(event); + await handler(event, mapInstanceMock); expect(handleSearchResultActionSpy).not.toBeCalled(); }); }); @@ -110,12 +120,53 @@ describe('onMapSingleClick - util', () => { .onGet(apiPath.getSingleBioEntityContentsStringWithCoordinates(point, modelId)) .reply(HttpStatusCode.Ok, []); + const mapInstanceMock = { + forEachFeatureAtPixel: (): void => {}, + } as unknown as Map; + it('does not fire search result action', async () => { - await handler(event); + await handler(event, mapInstanceMock); expect(handleSearchResultActionSpy).not.toBeCalled(); }); }); + describe('when clicked on feature type = pin icon bioentity', () => { + const { store } = getReduxStoreWithActionsListener(); + const { dispatch } = store; + const { modelId } = ELEMENT_SEARCH_RESULT_MOCK_ALIAS; + const mapSize = { + width: 270, + height: 270, + tileSize: 256, + minZoom: 2, + maxZoom: 9, + }; + const coordinate = [270, 270]; + const point = { x: 540.0072763538013, y: 539.9927236461986 }; + const event = getEvent(coordinate); + + mockedAxiosOldClient + .onGet(apiPath.getSingleBioEntityContentsStringWithCoordinates(point, modelId)) + .reply(HttpStatusCode.Ok, [ELEMENT_SEARCH_RESULT_MOCK_ALIAS]); + + const mapInstanceMock = { + forEachFeatureAtPixel: (pixel: any, mappingFunction: (feature: Feature) => void): void => { + [ + new Feature({ + id: 1000, + type: FEATURE_TYPE.PIN_ICON_BIOENTITY, + }), + ].forEach(mappingFunction); + }, + } as unknown as Map; + + it('does NOT fire search result action handler', async () => { + const handler = onMapSingleClick(mapSize, modelId, dispatch); + await handler(event, mapInstanceMock); + await waitFor(() => expect(handleSearchResultActionSpy).not.toBeCalled()); + }); + }); + describe('when searchResults are valid', () => { describe('when results type is ALIAS', () => { const { store } = getReduxStoreWithActionsListener(); @@ -136,9 +187,13 @@ describe('onMapSingleClick - util', () => { .onGet(apiPath.getSingleBioEntityContentsStringWithCoordinates(point, modelId)) .reply(HttpStatusCode.Ok, [ELEMENT_SEARCH_RESULT_MOCK_ALIAS]); + const mapInstanceMock = { + forEachFeatureAtPixel: (): void => {}, + } as unknown as Map; + it('does fire search result action handler', async () => { const handler = onMapSingleClick(mapSize, modelId, dispatch); - await handler(event); + await handler(event, mapInstanceMock); await waitFor(() => expect(handleSearchResultActionSpy).toBeCalled()); }); }); @@ -165,9 +220,13 @@ describe('onMapSingleClick - util', () => { .onGet(apiPath.getSingleBioEntityContentsStringWithCoordinates(point, modelId)) .reply(HttpStatusCode.Ok, [ELEMENT_SEARCH_RESULT_MOCK_REACTION]); + const mapInstanceMock = { + forEachFeatureAtPixel: (): void => {}, + } as unknown as Map; + it('does fire search result action - handle reaction', async () => { const handler = onMapSingleClick(mapSize, modelId, dispatch); - await handler(event); + await handler(event, mapInstanceMock); await waitFor(() => expect(handleSearchResultActionSpy).toBeCalled()); }); }); diff --git a/src/components/Map/MapViewer/utils/listeners/onPointerMove.test.ts b/src/components/Map/MapViewer/utils/listeners/onPointerMove.test.ts new file mode 100644 index 00000000..db8737b1 --- /dev/null +++ b/src/components/Map/MapViewer/utils/listeners/onPointerMove.test.ts @@ -0,0 +1,75 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Feature, Map, MapBrowserEvent } from 'ol'; +import { onPointerMove } from './onPointerMove'; + +const TARGET_STRING = 'abcd'; + +const EVENT_DRAGGING_MOCK = { + dragging: true, +} as unknown as MapBrowserEvent<PointerEvent>; + +const EVENT_MOCK = { + dragging: false, + originalEvent: undefined, +} as unknown as MapBrowserEvent<PointerEvent>; + +const MAP_INSTANCE_BASE_MOCK = { + getEventPixel: (): void => {}, + forEachFeatureAtPixel: (): void => {}, +}; + +describe('onPointerMove - util', () => { + describe('when event dragging', () => { + const target = document.createElement('div'); + const mapInstance = { + ...MAP_INSTANCE_BASE_MOCK, + getTarget: () => target, + } as unknown as Map; + + it('should return nothing and not modify target', () => { + expect(onPointerMove(mapInstance, EVENT_DRAGGING_MOCK)).toBe(undefined); + expect((mapInstance as any).getTarget().style.cursor).toBe(''); + }); + }); + + describe('when pin feature present and target is html', () => { + const target = document.createElement('div'); + const mapInstance = { + ...MAP_INSTANCE_BASE_MOCK, + forEachFeatureAtPixel: () => new Feature(), + getTarget: () => target, + } as unknown as Map; + + it('should return nothing and modify target', () => { + expect(onPointerMove(mapInstance, EVENT_MOCK)).toBe(undefined); + expect((mapInstance as any).getTarget().style.cursor).toBe('pointer'); + }); + }); + + describe('when pin feature present and target is string', () => { + const mapInstance = { + ...MAP_INSTANCE_BASE_MOCK, + forEachFeatureAtPixel: () => new Feature(), + getTarget: () => TARGET_STRING, + } as unknown as Map; + + it('should return nothing and not modify target', () => { + expect(onPointerMove(mapInstance, EVENT_MOCK)).toBe(undefined); + expect((mapInstance as any).getTarget()).toBe(TARGET_STRING); + }); + }); + + describe('when pin feature is not present and target is html', () => { + const target = document.createElement('div'); + const mapInstance = { + ...MAP_INSTANCE_BASE_MOCK, + forEachFeatureAtPixel: () => undefined, + getTarget: () => target, + } as unknown as Map; + + it('should return nothing and not modify target', () => { + expect(onPointerMove(mapInstance, EVENT_MOCK)).toBe(undefined); + expect((mapInstance as any).getTarget().style.cursor).toBe(''); + }); + }); +}); diff --git a/src/components/Map/MapViewer/utils/listeners/onPointerMove.ts b/src/components/Map/MapViewer/utils/listeners/onPointerMove.ts index 47f99775..868c3f33 100644 --- a/src/components/Map/MapViewer/utils/listeners/onPointerMove.ts +++ b/src/components/Map/MapViewer/utils/listeners/onPointerMove.ts @@ -2,6 +2,9 @@ import { PIN_ICON_ANY } from '@/constants/features'; import { Map } from 'ol'; import MapBrowserEvent from 'ol/MapBrowserEvent'; +const isTargetHTMLElement = (target: string | HTMLElement | undefined): target is HTMLElement => + !!target && typeof target !== 'string' && 'style' in target; + /* prettier-ignore */ export const onPointerMove = (mapInstance: Map, event: MapBrowserEvent<PointerEvent>): void => { @@ -20,7 +23,7 @@ export const onPointerMove = }); const target = mapInstance.getTarget(); - if (target && typeof target !== 'string' && 'style' in target) { + if (isTargetHTMLElement(target)) { target.style.cursor = feature ? 'pointer' : ''; } }; -- GitLab