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