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