From 946a224cdb5aeb1850b0b09fd803f523183ee60f Mon Sep 17 00:00:00 2001
From: Mateusz Winiarczyk <mateusz.winiarczyk@appunite.com>
Date: Fri, 2 Feb 2024 14:43:32 +0100
Subject: [PATCH] test(plugins): add tests for loading plugins

---
 .../LoadPlugin/hooks/useLoadPlugin.test.ts    | 88 +++++++++++++++++++
 .../LoadPluginFromUrl.component.test.tsx      | 86 +++++++++++++++++-
 .../hooks/useLoadPluginFromUrl.ts             | 45 ++++++----
 src/services/pluginsManager/pluginsManager.ts |  2 +
 .../pluginsManager/pluginsManager.types.ts    |  2 +-
 5 files changed, 205 insertions(+), 18 deletions(-)
 create mode 100644 src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts

diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts
new file mode 100644
index 00000000..04245fbf
--- /dev/null
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts
@@ -0,0 +1,88 @@
+/* eslint-disable no-magic-numbers */
+import { renderHook, waitFor } from '@testing-library/react';
+import { act } from 'react-dom/test-utils';
+import axios, { HttpStatusCode } from 'axios';
+import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener';
+import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
+import MockAdapter from 'axios-mock-adapter';
+import { PluginsManager } from '@/services/pluginsManager';
+import { pluginFixture } from '@/models/fixtures/pluginFixture';
+import { useLoadPlugin } from './useLoadPlugin';
+
+const mockedAxiosClient = new MockAdapter(axios);
+jest.mock('../../../../../../services/pluginsManager/pluginsManager');
+
+describe('useLoadPlugin', () => {
+  afterEach(() => {
+    jest.restoreAllMocks();
+  });
+  it('should unload plugin successfully', async () => {
+    const { Wrapper, store } = getReduxStoreWithActionsListener({
+      ...INITIAL_STORE_STATE_MOCK,
+      plugins: {
+        activePlugins: {
+          pluginsId: [pluginFixture.hash],
+          data: {
+            [pluginFixture.hash]: pluginFixture,
+          },
+        },
+        list: INITIAL_STORE_STATE_MOCK.plugins.list,
+      },
+    });
+
+    const {
+      result: {
+        current: { isPluginActive, isPluginLoading, togglePlugin },
+      },
+    } = renderHook(
+      () => useLoadPlugin({ hash: pluginFixture.hash, pluginUrl: pluginFixture.urls[0] }),
+      {
+        wrapper: Wrapper,
+      },
+    );
+
+    expect(isPluginActive).toBe(true);
+    expect(isPluginLoading).toBe(false);
+
+    act(() => {
+      togglePlugin();
+    });
+
+    const actions = store.getActions();
+    expect(actions[0]).toEqual({
+      payload: { pluginId: pluginFixture.hash },
+      type: 'plugins/removePlugin',
+    });
+  });
+  it('should load plugin successfully', async () => {
+    const hash = 'pluginHash';
+    const pluginUrl = 'http://example.com/plugin.js';
+    Math.max = jest.fn();
+    const { Wrapper } = getReduxStoreWithActionsListener(INITIAL_STORE_STATE_MOCK);
+    const pluginScript = `function init() {${Math.max(1, 2)}} init()`;
+
+    mockedAxiosClient.onGet(pluginUrl).reply(HttpStatusCode.Ok, pluginScript);
+
+    const {
+      result: {
+        current: { isPluginActive, isPluginLoading, togglePlugin },
+      },
+    } = renderHook(() => useLoadPlugin({ hash, pluginUrl }), {
+      wrapper: Wrapper,
+    });
+
+    expect(isPluginActive).toBe(false);
+    expect(isPluginLoading).toBe(false);
+
+    togglePlugin();
+
+    expect(Math.max).toHaveBeenCalledWith(1, 2);
+
+    await waitFor(() => {
+      expect(PluginsManager.setHashedPlugin).toHaveBeenCalledWith({
+        pluginScript,
+        pluginUrl,
+      });
+    });
+  });
+});
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx
index a229ab7e..10d387fb 100644
--- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx
@@ -9,6 +9,7 @@ import axios, { HttpStatusCode } from 'axios';
 import { apiPath } from '@/redux/apiPath';
 import { pluginFixture } from '@/models/fixtures/pluginFixture';
 import { act } from 'react-dom/test-utils';
+import { PLUGINS_INITIAL_STATE_LIST_MOCK } from '@/redux/plugins/plugins.mock';
 import { LoadPluginFromUrl } from './LoadPluginFromUrl.component';
 
 const mockedAxiosApiClient = mockNetworkResponse();
@@ -29,6 +30,11 @@ const renderComponent = (initialStore?: InitialStoreState): { store: StoreType }
 };
 
 describe('LoadPluginFromUrl - component', () => {
+  global.URL.canParse = jest.fn();
+
+  afterEach(() => {
+    jest.restoreAllMocks();
+  });
   describe('when always', () => {
     it('renders plugin input label', () => {
       renderComponent();
@@ -62,7 +68,7 @@ describe('LoadPluginFromUrl - component', () => {
       const input = screen.getByTestId('load-plugin-input-url');
       expect(input).toBeVisible();
 
-      await act(() => {
+      act(() => {
         fireEvent.change(input, { target: { value: pluginFixture.urls[0] } });
       });
 
@@ -71,11 +77,87 @@ describe('LoadPluginFromUrl - component', () => {
       const button = screen.queryByTestId('load-plugin-button');
       expect(button).toBeVisible();
 
-      await act(() => {
+      act(() => {
         button?.click();
       });
 
       expect(button).toBeDisabled();
     });
+    it('should not load plugin if it`s loaded already', async () => {
+      global.URL.canParse = jest.fn().mockReturnValue(true);
+
+      const plugin = {
+        ...pluginFixture,
+        urls: ['https://example.com/min.js'],
+      };
+      mockedAxiosClient.onGet(plugin.urls[0]).reply(HttpStatusCode.Ok, '');
+
+      const { store } = renderComponent({
+        plugins: {
+          activePlugins: {
+            pluginsId: [plugin.hash],
+            data: {
+              [plugin.hash]: plugin,
+            },
+          },
+          list: PLUGINS_INITIAL_STATE_LIST_MOCK,
+        },
+      });
+      const input = screen.getByTestId('load-plugin-input-url');
+      expect(input).toBeVisible();
+
+      act(() => {
+        fireEvent.change(input, { target: { value: plugin.urls[0] } });
+      });
+
+      expect(input).toHaveValue(plugin.urls[0]);
+
+      const button = screen.getByTestId('load-plugin-button');
+      expect(button).not.toBeDisabled();
+      await act(() => {
+        button.click();
+      });
+
+      const { activePlugins } = store.getState().plugins;
+
+      expect(activePlugins).toEqual({
+        pluginsId: [plugin.hash],
+        data: {
+          [plugin.hash]: plugin,
+        },
+      });
+
+      expect(input).toHaveValue('');
+    });
+    it('should disable url input if url is empty', async () => {
+      renderComponent();
+      const input = screen.getByTestId('load-plugin-input-url');
+      expect(input).toBeVisible();
+
+      act(() => {
+        fireEvent.change(input, { target: { value: '' } });
+      });
+
+      expect(input).toHaveValue('');
+
+      const button = screen.getByTestId('load-plugin-button');
+      expect(button).toBeDisabled();
+    });
+
+    it('should disable url input if url is not correct', async () => {
+      global.URL.canParse = jest.fn().mockReturnValue(false);
+      renderComponent();
+      const input = screen.getByTestId('load-plugin-input-url');
+      expect(input).toBeVisible();
+
+      act(() => {
+        fireEvent.change(input, { target: { value: 'abcd' } });
+      });
+
+      expect(input).toHaveValue('abcd');
+
+      const button = screen.getByTestId('load-plugin-button');
+      expect(button).toBeDisabled();
+    });
   });
 });
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/hooks/useLoadPluginFromUrl.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/hooks/useLoadPluginFromUrl.ts
index 54b17df5..123e220c 100644
--- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/hooks/useLoadPluginFromUrl.ts
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/hooks/useLoadPluginFromUrl.ts
@@ -1,6 +1,8 @@
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { activePluginsDataSelector } from '@/redux/plugins/plugins.selectors';
 import { PluginsManager } from '@/services/pluginsManager';
 import axios from 'axios';
-import { ChangeEvent, useState } from 'react';
+import { ChangeEvent, useMemo, useState } from 'react';
 
 type UseLoadPluginReturnType = {
   handleChangePluginUrl: (event: ChangeEvent<HTMLInputElement>) => void;
@@ -11,23 +13,36 @@ type UseLoadPluginReturnType = {
 
 export const useLoadPluginFromUrl = (): UseLoadPluginReturnType => {
   const [pluginUrl, setPluginUrl] = useState('');
+  const [isLoading, setIsLoading] = useState(false);
+  const activePlugins = useAppSelector(activePluginsDataSelector);
 
-  const isPending = !pluginUrl;
+  const isPending = useMemo(
+    () => !pluginUrl || isLoading || !URL.canParse(pluginUrl),
+    [pluginUrl, isLoading],
+  );
 
   const handleLoadPlugin = async (): Promise<void> => {
-    const response = await axios(pluginUrl);
-    const pluginScript = response.data;
-
-    /* eslint-disable no-new-func */
-    const loadPlugin = new Function(pluginScript);
-
-    PluginsManager.setHashedPlugin({
-      pluginUrl,
-      pluginScript,
-    });
-
-    loadPlugin();
-    setPluginUrl('');
+    try {
+      setIsLoading(true);
+      const response = await axios(pluginUrl);
+      const pluginScript = response.data;
+
+      /* eslint-disable no-new-func */
+      const loadPlugin = new Function(pluginScript);
+
+      const hash = PluginsManager.setHashedPlugin({
+        pluginUrl,
+        pluginScript,
+      });
+
+      if (!(hash in activePlugins)) {
+        loadPlugin();
+      }
+
+      setPluginUrl('');
+    } finally {
+      setIsLoading(false);
+    }
   };
   const handleChangePluginUrl = (event: ChangeEvent<HTMLInputElement>): void => {
     setPluginUrl(event.target.value);
diff --git a/src/services/pluginsManager/pluginsManager.ts b/src/services/pluginsManager/pluginsManager.ts
index 25df5404..a41a8ed5 100644
--- a/src/services/pluginsManager/pluginsManager.ts
+++ b/src/services/pluginsManager/pluginsManager.ts
@@ -10,6 +10,8 @@ export const PluginsManager: PluginsManagerType = {
     const hash = md5(pluginScript).toString();
 
     PluginsManager.hashedPlugins[pluginUrl] = hash;
+
+    return hash;
   },
   init() {
     window.minerva = {
diff --git a/src/services/pluginsManager/pluginsManager.types.ts b/src/services/pluginsManager/pluginsManager.types.ts
index 00e247a7..cedb9034 100644
--- a/src/services/pluginsManager/pluginsManager.types.ts
+++ b/src/services/pluginsManager/pluginsManager.types.ts
@@ -13,7 +13,7 @@ export type PluginsManagerType = {
   hashedPlugins: {
     [url: string]: string;
   };
-  setHashedPlugin({ pluginUrl, pluginScript }: { pluginUrl: string; pluginScript: string }): void;
+  setHashedPlugin({ pluginUrl, pluginScript }: { pluginUrl: string; pluginScript: string }): string;
   init(): Unsubscribe;
   registerPlugin({ pluginName, pluginVersion, pluginUrl }: RegisterPlugin): {
     element: HTMLDivElement;
-- 
GitLab