diff --git a/client/package.json b/client/package.json index a68334205..b35789350 100644 --- a/client/package.json +++ b/client/package.json @@ -56,6 +56,7 @@ "@mui/material": "^6.1.1", "@mui/x-tree-view": "^7.19.0", "@reduxjs/toolkit": "^2.2.7", + "@testing-library/react-hooks": "^8.0.1", "@types/react-syntax-highlighter": "^15.5.13", "@types/remarkable": "^2.0.8", "@types/styled-components": "^5.1.32", diff --git a/client/src/preview/components/cart/CartList.tsx b/client/src/preview/components/cart/CartList.tsx index f0a1ddaaf..450073761 100644 --- a/client/src/preview/components/cart/CartList.tsx +++ b/client/src/preview/components/cart/CartList.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import useCart from 'preview/store/CartAccess'; import LibraryAsset from 'preview/util/libraryAsset'; diff --git a/client/src/preview/components/cart/ShoppingCart.tsx b/client/src/preview/components/cart/ShoppingCart.tsx index d2bacfc2c..24645d8c1 100644 --- a/client/src/preview/components/cart/ShoppingCart.tsx +++ b/client/src/preview/components/cart/ShoppingCart.tsx @@ -1,4 +1,5 @@ -import React, { useState } from 'react'; +import * as React from 'react'; +import { useState } from 'react'; import { Button, Dialog, diff --git a/client/src/preview/route/digitaltwins/editor/EditorTab.tsx b/client/src/preview/route/digitaltwins/editor/EditorTab.tsx index 7c99f84e4..cdfa4d148 100644 --- a/client/src/preview/route/digitaltwins/editor/EditorTab.tsx +++ b/client/src/preview/route/digitaltwins/editor/EditorTab.tsx @@ -15,7 +15,7 @@ interface EditorTabProps { setFileContent: Dispatch>; } -const handleEditorChange = ( +export const handleEditorChange = ( tab: string, value: string | undefined, setEditorValue: Dispatch>, diff --git a/client/src/preview/store/assets.slice.ts b/client/src/preview/store/assets.slice.ts index 565be0c04..e54d148cf 100644 --- a/client/src/preview/store/assets.slice.ts +++ b/client/src/preview/store/assets.slice.ts @@ -36,10 +36,17 @@ const assetsSlice = createSlice({ }, }); -export const selectAssetsByTypeAndPrivacy = (type: string, isPrivate: boolean) => createSelector( - (state: RootState) => state.assets.items, - (items: LibraryAsset[]) => items.filter(item => item.type === type && item.isPrivate === isPrivate) -); +export const selectAssetsByTypeAndPrivacy = ( + type: string, + isPrivate: boolean, +) => + createSelector( + (state: RootState) => state.assets.items, + (items: LibraryAsset[]) => + items.filter( + (item) => item.type === type && item.isPrivate === isPrivate, + ), + ); export const selectAssetByPathAndPrivacy = (path: string, isPrivate: boolean) => (state: RootState) => diff --git a/client/src/preview/util/init.ts b/client/src/preview/util/init.ts index a172551a3..382417b0f 100644 --- a/client/src/preview/util/init.ts +++ b/client/src/preview/util/init.ts @@ -29,7 +29,6 @@ export const fetchLibraryAssets = async ( ) => { try { await initialGitlabInstance.init(); - console.log(initialGitlabInstance.projectId); if (initialGitlabInstance.projectId) { const subfolders = await initialGitlabInstance.getLibrarySubfolders( initialGitlabInstance.projectId, @@ -52,7 +51,6 @@ export const fetchLibraryAssets = async ( return libraryAsset; }), ); - console.log(assets); assets.forEach((asset) => dispatch(setAsset(asset))); } else { dispatch(setAssets([])); diff --git a/client/test/preview/__mocks__/global_mocks.ts b/client/test/preview/__mocks__/global_mocks.ts index 7780ed9ce..8d9e0d057 100644 --- a/client/test/preview/__mocks__/global_mocks.ts +++ b/client/test/preview/__mocks__/global_mocks.ts @@ -129,7 +129,9 @@ export const mockDigitalTwin: DigitalTwin = { descriptionFiles: ['descriptionFile'], configFiles: ['configFile'], lifecycleFiles: ['lifecycleFile'], - assetFiles: [{ assetPath: 'assetPath', fileNames: ['assetFileName1', 'assetFileName2'] }], + assetFiles: [ + { assetPath: 'assetPath', fileNames: ['assetFileName1', 'assetFileName2'] }, + ], getDescription: jest.fn(), getFullDescription: jest.fn(), diff --git a/client/test/preview/integration/components/asset/AssetBoard.test.tsx b/client/test/preview/integration/components/asset/AssetBoard.test.tsx index 4fd846b52..736f316ce 100644 --- a/client/test/preview/integration/components/asset/AssetBoard.test.tsx +++ b/client/test/preview/integration/components/asset/AssetBoard.test.tsx @@ -4,10 +4,19 @@ import { Provider } from 'react-redux'; import AssetBoard from 'preview/components/asset/AssetBoard'; import { combineReducers, configureStore } from '@reduxjs/toolkit'; import assetsReducer, { setAssets } from 'preview/store/assets.slice'; -import digitalTwinReducer, { setDigitalTwin, setShouldFetchDigitalTwins } from 'preview/store/digitalTwin.slice'; +import digitalTwinReducer, { + setDigitalTwin, + setShouldFetchDigitalTwins, +} from 'preview/store/digitalTwin.slice'; import snackbarSlice from 'preview/store/snackbar.slice'; -import { mockGitlabInstance, mockLibraryAsset } from 'test/preview/__mocks__/global_mocks'; -import fileSlice, { FileState, addOrUpdateFile } from 'preview/store/file.slice'; +import { + mockGitlabInstance, + mockLibraryAsset, +} from 'test/preview/__mocks__/global_mocks'; +import fileSlice, { + FileState, + addOrUpdateFile, +} from 'preview/store/file.slice'; import DigitalTwin from 'preview/util/digitalTwin'; import LibraryAsset from 'preview/util/libraryAsset'; import libraryConfigFilesSlice from 'preview/store/libraryConfigFiles.slice'; @@ -126,25 +135,4 @@ describe('AssetBoard Integration Tests', () => { expect(screen.queryByText('Asset 1')).not.toBeInTheDocument(); }); }); - - /*it('shows an error message', async () => { - const error = 'An error occurred'; - - (fetchDigitalTwins as jest.Mock).mockImplementation(error); - - act(() => { - render( - - - , - ); - }); - - // Ensure the error message is displayed - await waitFor(() => { - expect(screen.getByText(/error/i)).toBeInTheDocument(); // "error" è case-insensitive - }); - - }); - */ -}); // Extend the timeout to allow more time for state updates +}); diff --git a/client/test/preview/integration/components/asset/AssetCardExecute.test.tsx b/client/test/preview/integration/components/asset/AssetCardExecute.test.tsx index b271ab74d..59cfb893d 100644 --- a/client/test/preview/integration/components/asset/AssetCardExecute.test.tsx +++ b/client/test/preview/integration/components/asset/AssetCardExecute.test.tsx @@ -1,17 +1,20 @@ -import { - combineReducers, - configureStore, -} from '@reduxjs/toolkit'; +import { combineReducers, configureStore } from '@reduxjs/toolkit'; import { fireEvent, render, screen, act } from '@testing-library/react'; import { AssetCardExecute } from 'preview/components/asset/AssetCard'; import * as React from 'react'; import { Provider, useSelector } from 'react-redux'; -import assetsReducer, { selectAssetByPathAndPrivacy, setAssets } from 'preview/store/assets.slice'; +import assetsReducer, { + selectAssetByPathAndPrivacy, + setAssets, +} from 'preview/store/assets.slice'; import digitalTwinReducer, { setDigitalTwin, } from 'preview/store/digitalTwin.slice'; import snackbarSlice from 'preview/store/snackbar.slice'; -import { mockDigitalTwin, mockLibraryAsset } from 'test/preview/__mocks__/global_mocks'; +import { + mockDigitalTwin, + mockLibraryAsset, +} from 'test/preview/__mocks__/global_mocks'; import { RootState } from 'store/store'; jest.mock('react-redux', () => ({ @@ -19,7 +22,6 @@ jest.mock('react-redux', () => ({ useSelector: jest.fn(), })); - const store = configureStore({ reducer: combineReducers({ assets: assetsReducer, @@ -44,18 +46,16 @@ describe('AssetCardExecute Integration Test', () => { beforeEach(() => { (useSelector as jest.MockedFunction).mockImplementation( (selector: (state: RootState) => unknown) => { - if (selector === selectAssetByPathAndPrivacy(asset.path, asset.isPrivate)) { + if ( + selector === selectAssetByPathAndPrivacy(asset.path, asset.isPrivate) + ) { return null; } return mockDigitalTwin; }, ); - - store.dispatch( - setAssets([ - mockLibraryAsset, - ]), - ); + + store.dispatch(setAssets([mockLibraryAsset])); store.dispatch( setDigitalTwin({ assetName: 'Asset 1', @@ -83,8 +83,6 @@ describe('AssetCardExecute Integration Test', () => { fireEvent.click(startStopButton); }); - expect( - screen.getByText('Stop'), - ).toBeInTheDocument(); + expect(screen.getByText('Stop')).toBeInTheDocument(); }); }); diff --git a/client/test/preview/integration/route/digitaltwins/editor/Editor.test.tsx b/client/test/preview/integration/route/digitaltwins/editor/Editor.test.tsx index b5e5b2302..f9f800015 100644 --- a/client/test/preview/integration/route/digitaltwins/editor/Editor.test.tsx +++ b/client/test/preview/integration/route/digitaltwins/editor/Editor.test.tsx @@ -12,7 +12,10 @@ import fileSlice, { } from 'preview/store/file.slice'; import * as React from 'react'; import DigitalTwin from 'preview/util/digitalTwin'; -import { mockGitlabInstance, mockLibraryAsset } from 'test/preview/__mocks__/global_mocks'; +import { + mockGitlabInstance, + mockLibraryAsset, +} from 'test/preview/__mocks__/global_mocks'; import { handleFileClick } from 'preview/route/digitaltwins/editor/sidebarFunctions'; import LibraryAsset from 'preview/util/libraryAsset'; import cartSlice, { addToCart } from 'preview/store/cart.slice'; diff --git a/client/test/preview/integration/route/digitaltwins/editor/Sidebar.test.tsx b/client/test/preview/integration/route/digitaltwins/editor/Sidebar.test.tsx index 8f26d50b7..c6e482708 100644 --- a/client/test/preview/integration/route/digitaltwins/editor/Sidebar.test.tsx +++ b/client/test/preview/integration/route/digitaltwins/editor/Sidebar.test.tsx @@ -13,7 +13,10 @@ import { } from '@testing-library/react'; import { Provider } from 'react-redux'; import * as React from 'react'; -import { mockGitlabInstance, mockLibraryAsset } from 'test/preview/__mocks__/global_mocks'; +import { + mockGitlabInstance, + mockLibraryAsset, +} from 'test/preview/__mocks__/global_mocks'; import DigitalTwin from 'preview/util/digitalTwin'; import * as SidebarFunctions from 'preview/route/digitaltwins/editor/sidebarFunctions'; import cartSlice, { addToCart } from 'preview/store/cart.slice'; @@ -142,7 +145,7 @@ describe('Sidebar', () => { setIsLibraryFile={setIsLibraryFileMock} setLibraryAssetPath={setLibraryAssetPathMock} tab={'reconfigure'} - fileName='file1.md' + fileName="file1.md" isLibraryFile={false} /> , @@ -170,7 +173,7 @@ describe('Sidebar', () => { setIsLibraryFile={setIsLibraryFileMock} setLibraryAssetPath={setLibraryAssetPathMock} tab={'create'} - fileName='file1.md' + fileName="file1.md" isLibraryFile={false} /> , @@ -200,7 +203,7 @@ describe('Sidebar', () => { setIsLibraryFile={setIsLibraryFileMock} setLibraryAssetPath={setLibraryAssetPathMock} tab={'create'} - fileName='file1.md' + fileName="file1.md" isLibraryFile={false} /> , @@ -230,7 +233,7 @@ describe('Sidebar', () => { setIsLibraryFile={setIsLibraryFileMock} setLibraryAssetPath={setLibraryAssetPathMock} tab={'create'} - fileName='file1.md' + fileName="file1.md" isLibraryFile={false} /> , diff --git a/client/test/preview/integration/route/digitaltwins/editor/SidebarFunctions.test.tsx b/client/test/preview/integration/route/digitaltwins/editor/SidebarFunctions.test.tsx index 8fea587f7..ea19902c7 100644 --- a/client/test/preview/integration/route/digitaltwins/editor/SidebarFunctions.test.tsx +++ b/client/test/preview/integration/route/digitaltwins/editor/SidebarFunctions.test.tsx @@ -1,4 +1,4 @@ -/*import { handleFileClick } from "preview/route/digitaltwins/editor/sidebarFunctions"; +/* import { handleFileClick } from "preview/route/digitaltwins/editor/sidebarFunctions"; import { FileState } from "preview/store/file.slice"; describe('SidebarFunctions', () => { @@ -26,4 +26,4 @@ describe('SidebarFunctions', () => { expect(mockSetFileName).toHaveBeenCalledWith(fileName); expect(mockSetFileContent).toHaveBeenCalledWith('content'); }); -*/ \ No newline at end of file +*/ diff --git a/client/test/preview/integration/route/digitaltwins/manage/ConfigDialog.test.tsx b/client/test/preview/integration/route/digitaltwins/manage/ConfigDialog.test.tsx index 04e224805..dbf418b2c 100644 --- a/client/test/preview/integration/route/digitaltwins/manage/ConfigDialog.test.tsx +++ b/client/test/preview/integration/route/digitaltwins/manage/ConfigDialog.test.tsx @@ -1,4 +1,4 @@ -/*import AssetBoard from 'preview/components/asset/AssetBoard'; +/* import AssetBoard from 'preview/components/asset/AssetBoard'; import { act, render, screen, waitFor } from '@testing-library/react'; import { Provider } from 'react-redux'; import * as React from 'react'; @@ -163,4 +163,4 @@ describe('ReconfigureDialog', () => { ); }); }); -*/ \ No newline at end of file +*/ diff --git a/client/test/preview/integration/route/digitaltwins/manage/DeleteDialog.test.tsx b/client/test/preview/integration/route/digitaltwins/manage/DeleteDialog.test.tsx index b0e36d3ef..3a73437c5 100644 --- a/client/test/preview/integration/route/digitaltwins/manage/DeleteDialog.test.tsx +++ b/client/test/preview/integration/route/digitaltwins/manage/DeleteDialog.test.tsx @@ -1,4 +1,4 @@ -/*import { render, screen, waitFor } from '@testing-library/react'; +/* import { render, screen, waitFor } from '@testing-library/react'; import * as React from 'react'; import DigitalTwin from 'preview/util/digitalTwin'; import { Provider } from 'react-redux'; @@ -92,4 +92,4 @@ describe('DeleteDialog', () => { }); }); }); -*/ \ No newline at end of file +*/ diff --git a/client/test/preview/integration/route/digitaltwins/manage/DetailsDialog.test.tsx b/client/test/preview/integration/route/digitaltwins/manage/DetailsDialog.test.tsx index 6d92da5d6..96fa50431 100644 --- a/client/test/preview/integration/route/digitaltwins/manage/DetailsDialog.test.tsx +++ b/client/test/preview/integration/route/digitaltwins/manage/DetailsDialog.test.tsx @@ -1,4 +1,4 @@ -/*import AssetBoard from 'preview/components/asset/AssetBoard'; +/* import AssetBoard from 'preview/components/asset/AssetBoard'; import { act, render, screen, waitFor } from '@testing-library/react'; import { Provider } from 'react-redux'; import * as React from 'react'; @@ -67,4 +67,4 @@ describe('DetailsDialog', () => { }); }); }); -*/ \ No newline at end of file +*/ diff --git a/client/test/preview/integration/route/digitaltwins/manage/utils.ts b/client/test/preview/integration/route/digitaltwins/manage/utils.ts index 460a52f47..1de6e1edf 100644 --- a/client/test/preview/integration/route/digitaltwins/manage/utils.ts +++ b/client/test/preview/integration/route/digitaltwins/manage/utils.ts @@ -8,7 +8,10 @@ import digitalTwinReducer, { setDigitalTwin, } from 'preview/store/digitalTwin.slice'; import snackbarReducer from 'preview/store/snackbar.slice'; -import { mockGitlabInstance, mockLibraryAsset } from 'test/preview/__mocks__/global_mocks'; +import { + mockGitlabInstance, + mockLibraryAsset, +} from 'test/preview/__mocks__/global_mocks'; import DigitalTwin from 'preview/util/digitalTwin'; import LibraryAsset from 'preview/util/libraryAsset'; diff --git a/client/test/preview/unit/components/asset/AddToCartButton.test.tsx b/client/test/preview/unit/components/asset/AddToCartButton.test.tsx new file mode 100644 index 000000000..0ba116d61 --- /dev/null +++ b/client/test/preview/unit/components/asset/AddToCartButton.test.tsx @@ -0,0 +1,61 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import AddToCartButton from 'preview/components/asset/AddToCartButton'; +import * as React from 'react'; +import * as cartAccess from 'preview/store/CartAccess'; +import { mockLibraryAsset } from 'test/preview/__mocks__/global_mocks'; +import { useSelector } from 'react-redux'; +import { RootState } from 'store/store'; +import { selectAssetByPathAndPrivacy } from 'preview/store/assets.slice'; + +describe('AddToCartButton', () => { + const addMock = jest.fn(); + const removeMock = jest.fn(); + const clearMock = jest.fn(); + + beforeEach(() => { + (useSelector as jest.MockedFunction).mockImplementation( + (selector: (state: RootState) => unknown) => { + if (selector === selectAssetByPathAndPrivacy('path', true)) { + return mockLibraryAsset; + } + return mockLibraryAsset; + }, + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should add asset to cart when not in cart', () => { + jest.spyOn(cartAccess, 'default').mockReturnValue({ + state: { assets: [] }, + actions: { + add: addMock, + remove: removeMock, + clear: clearMock, + }, + }); + + render(); + + fireEvent.click(screen.getByRole('button')); + expect(addMock).toHaveBeenCalled(); + }); + + it('should remove asset to cart when not in cart', () => { + jest.spyOn(cartAccess, 'default').mockReturnValue({ + state: { assets: [mockLibraryAsset] }, + actions: { + add: addMock, + remove: removeMock, + clear: clearMock, + }, + }); + + render(); + + fireEvent.click(screen.getByRole('button')); + expect(removeMock).toHaveBeenCalled(); + }); +}); diff --git a/client/test/preview/unit/components/asset/AssetBoard.test.tsx b/client/test/preview/unit/components/asset/AssetBoard.test.tsx index 665b7f749..27f1b4046 100644 --- a/client/test/preview/unit/components/asset/AssetBoard.test.tsx +++ b/client/test/preview/unit/components/asset/AssetBoard.test.tsx @@ -38,7 +38,13 @@ describe('AssetBoard', () => { ); const mockAssets = [ - { name: 'Asset 1', description: 'Test Asset', path: 'path1', type: 'Digital Twins', isPrivate: true }, + { + name: 'Asset 1', + description: 'Test Asset', + path: 'path1', + type: 'Digital Twins', + isPrivate: true, + }, ]; (useSelector as jest.MockedFunction).mockImplementation( @@ -74,18 +80,4 @@ describe('AssetBoard', () => { expect(mockDispatch).toHaveBeenCalledTimes(1); }); - - /*it('shows error message when error is set', () => { - const realUseState = React.useState; - - const stubInitialState: unknown = ['Error message']; - jest - .spyOn(React, 'useState') - .mockImplementationOnce(() => realUseState(stubInitialState)); - - renderAssetBoard('Manage'); - - expect(screen.getByText('Error message')).toBeInTheDocument(); - }); - */ }); diff --git a/client/test/preview/unit/components/asset/AssetCard.test.tsx b/client/test/preview/unit/components/asset/AssetCard.test.tsx index 2ce6a757e..7842fa4d9 100644 --- a/client/test/preview/unit/components/asset/AssetCard.test.tsx +++ b/client/test/preview/unit/components/asset/AssetCard.test.tsx @@ -1,5 +1,8 @@ import { render, screen } from '@testing-library/react'; -import { AssetCardManage, AssetCardExecute } from 'preview/components/asset/AssetCard'; +import { + AssetCardManage, + AssetCardExecute, +} from 'preview/components/asset/AssetCard'; import * as React from 'react'; import { Provider, useSelector } from 'react-redux'; import store from 'store/store'; @@ -61,13 +64,15 @@ const setupMockStore = (assetDescription: string, twinDescription: string) => { }, }, }; - (useSelector as jest.MockedFunction).mockImplementation((selector) => - selector(state), + (useSelector as jest.MockedFunction).mockImplementation( + (selector) => selector(state), ); }; - -const renderComponent = (Component: React.JSXElementConstructor, props = {}) => { +const renderComponent = ( + Component: React.JSXElementConstructor, + props: T, +) => { render( diff --git a/client/test/preview/unit/components/asset/AssetLibrary.test.tsx b/client/test/preview/unit/components/asset/AssetLibrary.test.tsx new file mode 100644 index 000000000..fe3f57843 --- /dev/null +++ b/client/test/preview/unit/components/asset/AssetLibrary.test.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { Provider, useSelector } from 'react-redux'; +import AssetLibrary from 'preview/components/asset/AssetLibrary'; +import store, { RootState } from 'store/store'; +import { selectAssetsByTypeAndPrivacy } from 'preview/store/assets.slice'; +import { mockLibraryAsset } from 'test/preview/__mocks__/global_mocks'; + +jest.mock('preview/store/assets.slice', () => ({ + selectAssetsByTypeAndPrivacy: jest.fn(() => []), +})); + +jest.mock('preview/util/init', () => ({ + fetchLibraryAssets: jest.fn(() => Promise.resolve()), +})); + +jest.mock('preview/components/asset/Filter', () => ({ + __esModule: true, + default: () =>
Filter
, +})); + +jest.mock('preview/components/asset/AssetCard', () => ({ + __esModule: true, + AssetCardLibrary: () =>
Asset Card Library
, +})); + +describe('AssetLibrary', () => { + beforeEach(() => { + (useSelector as jest.MockedFunction).mockImplementation( + (selector: (state: RootState) => unknown) => { + if (selector === selectAssetsByTypeAndPrivacy('path', false)) { + return [mockLibraryAsset]; + } + return [mockLibraryAsset]; + }, + ); + }); + + const renderAssetLibrary = () => + render( + + + , + ); + + it('renders a loading spinner while assets are being fetched', () => { + renderAssetLibrary(); + expect(screen.getByTestId('circular-progress')).toBeInTheDocument(); + }); + + it('renders assets when fetched', async () => { + renderAssetLibrary(); + + await waitFor(() => screen.getByText('Asset Card Library')); + + expect(screen.getByText('Asset Card Library')).toBeInTheDocument(); + }); +}); diff --git a/client/test/preview/unit/components/asset/DetailsButton.test.tsx b/client/test/preview/unit/components/asset/DetailsButton.test.tsx index 2b8939850..899de8921 100644 --- a/client/test/preview/unit/components/asset/DetailsButton.test.tsx +++ b/client/test/preview/unit/components/asset/DetailsButton.test.tsx @@ -14,12 +14,16 @@ jest.mock('react-redux', () => ({ describe('DetailsButton', () => { const renderDetailsButton = ( assetName: string, - assetPrivacy: boolean, + assetPrivacy: boolean, setShowDetails: Dispatch>, ) => render( - + , ); diff --git a/client/test/preview/unit/components/cart/CartList.test.tsx b/client/test/preview/unit/components/cart/CartList.test.tsx new file mode 100644 index 000000000..45289c8b6 --- /dev/null +++ b/client/test/preview/unit/components/cart/CartList.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from '@testing-library/react'; +import CartList from 'preview/components/cart/CartList'; +import * as React from 'react'; +import * as cartAccess from 'preview/store/CartAccess'; +import { mockLibraryAsset } from 'test/preview/__mocks__/global_mocks'; + +describe('CartList', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render a list of assets', () => { + jest.spyOn(cartAccess, 'default').mockReturnValue({ + state: { assets: [mockLibraryAsset] }, + actions: { add: jest.fn(), remove: jest.fn(), clear: jest.fn() }, + }); + + render(); + + expect(screen.getByText('path')).toBeInTheDocument(); + }); + + it('should render a list of common assets', () => { + mockLibraryAsset.isPrivate = false; + jest.spyOn(cartAccess, 'default').mockReturnValue({ + state: { assets: [mockLibraryAsset] }, + actions: { add: jest.fn(), remove: jest.fn(), clear: jest.fn() }, + }); + + render(); + + expect(screen.getByText('common/path')).toBeInTheDocument(); + }); +}); diff --git a/client/test/preview/unit/components/cart/ShoppingCart.test.tsx b/client/test/preview/unit/components/cart/ShoppingCart.test.tsx new file mode 100644 index 000000000..f66dc5828 --- /dev/null +++ b/client/test/preview/unit/components/cart/ShoppingCart.test.tsx @@ -0,0 +1,63 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import ShoppingCart from 'preview/components/cart/ShoppingCart'; +import * as React from 'react'; +import { Provider } from 'react-redux'; +import store from 'store/store'; +import { BrowserRouter } from 'react-router-dom'; +import * as cartAccess from 'preview/store/CartAccess'; + +jest.mock('preview/store/CartAccess', () => ({ + __esModule: true, + default: jest.fn(), +})); + +jest.mock('preview/components/cart/CartList', () => function MockCartList() { + return
; + }); + +describe('ShoppingCart', () => { + const addMock = jest.fn(); + const removeMock = jest.fn(); + const clearMock = jest.fn(); + + beforeEach(() => { + (cartAccess.default as jest.Mock).mockReturnValue({ + state: { assets: [] }, + actions: { add: addMock, remove: removeMock, clear: clearMock }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it.skip("should open the confirmation dialog when clicking 'Clear'", () => { + render( + + + + + , + ); + + fireEvent.click(screen.getByText('Clear')); + + expect(screen.getByText('Confirm Clear')).toBeInTheDocument(); + }); + + it.skip("should close the confirmation dialog when clicking 'No'", () => { + render( + + + + + , + ); + + fireEvent.click(screen.getByText('Clear')); + expect(screen.getByText('Confirm Clear')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('No')); + expect(screen.queryByText('Confirm Clear')).not.toBeInTheDocument(); + }); +}); diff --git a/client/test/preview/unit/routes/digitaltwins/create/CreatePage.test.tsx b/client/test/preview/unit/routes/digitaltwins/create/CreatePage.test.tsx index d1ac74b24..6116c079e 100644 --- a/client/test/preview/unit/routes/digitaltwins/create/CreatePage.test.tsx +++ b/client/test/preview/unit/routes/digitaltwins/create/CreatePage.test.tsx @@ -40,10 +40,10 @@ describe('CreatePage', () => { it('renders the CreatePage component', () => { expect(screen.getByText('Cancel')).toBeInTheDocument(); expect(screen.getByText('Save')).toBeInTheDocument(); - expect(screen.getByTestId('privacy-selector')).toBeInTheDocument - expect(screen.getByTestId('editor')).toBeInTheDocument(); - expect(screen.getByTestId('create-dialogs')).toBeInTheDocument(); - expect(screen.getByTestId('snackbar')).toBeInTheDocument(); + expect(screen.getByTestId('privacy-selector')).toBeInTheDocument(); // Aggiunte parentesi + expect(screen.getByTestId('editor')).toBeInTheDocument(); // Aggiunte parentesi + expect(screen.getByTestId('create-dialogs')).toBeInTheDocument(); // Aggiunte parentesi + expect(screen.getByTestId('snackbar')).toBeInTheDocument(); // Aggiunte parentesi }); it('handles confirm cancel', () => { diff --git a/client/test/preview/unit/routes/digitaltwins/create/PrivacySelector.test.tsx b/client/test/preview/unit/routes/digitaltwins/create/PrivacySelector.test.tsx new file mode 100644 index 000000000..045890ae4 --- /dev/null +++ b/client/test/preview/unit/routes/digitaltwins/create/PrivacySelector.test.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import PrivacySelector from '../../../../../../src/preview/route/digitaltwins/create/PrivacySelector'; + +describe('PrivacySelector', () => { + it('should render "Private" when isPrivate is true', () => { + render(); + expect(screen.getByLabelText('Private')).toBeInTheDocument(); + }); + + it('should render "Common" when isPrivate is false', () => { + render(); + expect(screen.getByLabelText('Common')).toBeInTheDocument(); + }); + + it('should call onChange when the switch is toggled', () => { + const handleChange = jest.fn(); + render(); + const switchElement = screen.getByRole('checkbox'); + fireEvent.click(switchElement); + expect(handleChange).toHaveBeenCalledTimes(1); + }); +}); diff --git a/client/test/preview/unit/routes/digitaltwins/editor/EditorTab.test.tsx b/client/test/preview/unit/routes/digitaltwins/editor/EditorTab.test.tsx index b2e161e3a..a97dd06c3 100644 --- a/client/test/preview/unit/routes/digitaltwins/editor/EditorTab.test.tsx +++ b/client/test/preview/unit/routes/digitaltwins/editor/EditorTab.test.tsx @@ -1,7 +1,8 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import * as React from 'react'; -import EditorTab from 'preview/route/digitaltwins/editor/EditorTab'; +import EditorTab, { handleEditorChange } from 'preview/route/digitaltwins/editor/EditorTab'; import { addOrUpdateFile } from 'preview/store/file.slice'; +import { addOrUpdateLibraryFile } from 'preview/store/libraryConfigFiles.slice'; jest.mock('preview/store/file.slice', () => ({ addOrUpdateFile: jest.fn(), @@ -42,37 +43,103 @@ describe('EditorTab', () => { }); }); - it('calls handleEditorChange via onChange correctly', async () => { - waitFor(async () => { - render( - , - ); + it('calls handleEditorChange via onChange correctly - create tab', async () => { + await handleEditorChange( + 'create', + 'new content', + jest.fn(), + mockSetFileContent, + 'fileName', + 'private', + false, + '', + mockDispatch, + ); - const newValue = 'New content'; + expect(mockSetFileContent).toHaveBeenCalledWith('new content'); + expect(mockDispatch).toHaveBeenCalledWith( + addOrUpdateFile({ + name: 'fileName', + content: 'new content', + isNew: true, + isModified: true, + }), + ); + }); - fireEvent.change(screen.getByRole('textbox'), { - target: { value: newValue }, - }); + it('calls handleEditorChange via onChange correctly - create tab and libraryFile', async () => { + await handleEditorChange( + 'create', + 'new content', + jest.fn(), + mockSetFileContent, + 'fileName', + 'private', + true, + 'path', + mockDispatch, + ); - await waitFor(() => { - expect(mockSetFileContent).toHaveBeenCalledWith(newValue); - expect(mockDispatch).toHaveBeenCalledWith( - addOrUpdateFile({ - name: 'fileName', - content: newValue, - isNew: false, - isModified: true, - }), - ); - }); - }); + expect(mockSetFileContent).toHaveBeenCalledWith('new content'); + expect(mockDispatch).toHaveBeenCalledWith( + addOrUpdateLibraryFile({ + assetPath: 'path', + fileName: 'fileName', + fileContent: 'new content', + isNew: true, + isModified: true, + isPrivate: true, + }), + ); + }); + + it('calls handleEditorChange via onChange correctly - reconfigure tab', async () => { + await handleEditorChange( + 'reconfigure', + 'new content', + jest.fn(), + mockSetFileContent, + 'fileName', + 'private', + false, + '', + mockDispatch, + ); + + expect(mockSetFileContent).toHaveBeenCalledWith('new content'); + expect(mockDispatch).toHaveBeenCalledWith( + addOrUpdateFile({ + name: 'fileName', + content: 'new content', + isNew: true, + isModified: true, + }), + ); + }); + + it('calls handleEditorChange via onChange correctly - reconfigure tab and libraryFile', async () => { + await handleEditorChange( + 'reconfigure', + 'new content', + jest.fn(), + mockSetFileContent, + 'fileName', + 'private', + true, + 'path', + mockDispatch, + ); + + expect(mockSetFileContent).toHaveBeenCalledWith('new content'); + expect(mockDispatch).toHaveBeenCalledWith( + addOrUpdateLibraryFile({ + assetPath: 'path', + fileName: 'fileName', + fileContent: 'new content', + isNew: false, + isModified: true, + isPrivate: true, + }), + ); }); }); diff --git a/client/test/preview/unit/routes/digitaltwins/editor/Sidebar.test.tsx b/client/test/preview/unit/routes/digitaltwins/editor/Sidebar.test.tsx index 6993c1e4b..8bba66660 100644 --- a/client/test/preview/unit/routes/digitaltwins/editor/Sidebar.test.tsx +++ b/client/test/preview/unit/routes/digitaltwins/editor/Sidebar.test.tsx @@ -1,15 +1,16 @@ -import { - render, - waitFor, - screen, - act, -} from '@testing-library/react'; +import { render, waitFor, screen, act } from '@testing-library/react'; import Sidebar from 'preview/route/digitaltwins/editor/Sidebar'; -import { selectDigitalTwinByName } from 'preview/store/digitalTwin.slice'; +import * as SidebarFunctions from 'preview/route/digitaltwins/editor/sidebarFunctions'; import * as React from 'react'; import { Provider, useSelector } from 'react-redux'; import store, { RootState } from 'store/store'; -import { mockDigitalTwin } from 'test/preview/__mocks__/global_mocks'; +import { + mockDigitalTwin, + mockLibraryAsset, +} from 'test/preview/__mocks__/global_mocks'; +import * as SidebarFetchers from 'preview/route/digitaltwins/editor/sidebarFetchers'; +import { addOrUpdateLibraryFile } from 'preview/store/libraryConfigFiles.slice'; +import * as ReactRedux from 'react-redux'; jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), @@ -47,18 +48,19 @@ describe('Sidebar', () => { }); }; - beforeEach(async () => { + beforeEach(() => { (useSelector as jest.MockedFunction).mockImplementation( (selector: (state: RootState) => unknown) => { - if (selector === selectDigitalTwinByName('mockedDTName')) { - return mockDigitalTwin; - } if (selector.toString().includes('state.files')) { return []; } + if (selector.toString().includes('state.cart.assets')) { + return [mockLibraryAsset]; + } return mockDigitalTwin; }, ); + jest.clearAllMocks(); }); afterEach(() => { @@ -75,100 +77,66 @@ describe('Sidebar', () => { }); }); - /*it('should update file state if the file is modified', async () => { - await renderSidebar('reconfigure', 'mockedDTName'); - - const modifiedFiles: FileState[] = [ - { - name: 'testFile.md', - content: 'modified content', - isNew: false, - isModified: true, - }, - ]; - - // Mock della funzione fetchAndSetFileContent per evitare chiamate reali - //jest.spyOn(fetchAndSetFileContent, 'mockImplementation').mockResolvedValue(undefined); - - // Chiamata alla funzione handleReconfigureFileClick - await act(async () => { - await handleReconfigureFileClick( - 'testFile.md', - mockDigitalTwin, - modifiedFiles, - setFileName, - setFileContent, - setFileType, - setFilePrivacy, - setIsLibraryFile, - setLibraryAssetPath, - ); + it('should call handleAddFileClick when Add new file button is clicked', async () => { + const handleAddFileClickSpy = jest.spyOn( + SidebarFunctions, + 'handleAddFileClick', + ); + + await renderSidebar('create', 'mockedDTName'); + + await waitFor(() => { + const addFileButton = screen.getByText('Add new file'); + addFileButton.click(); }); - - // Verifica che le funzioni siano state chiamate correttamente - expect(setFileName).toHaveBeenCalledWith('testFile.md'); - expect(setFileContent).toHaveBeenCalledWith('modified content'); - expect(setFileType).toHaveBeenCalledWith('md'); - - // Verifica che getFileContent non sia stata chiamata, dato che il file è già modificato - expect(mockDigitalTwin.DTAssets.getFileContent).not.toHaveBeenCalled(); - }); - - it('should fetch and update file state if the file is not modified', async () => { - await renderSidebar('reconfigure', 'mockedDTName'); + expect(handleAddFileClickSpy).toHaveBeenCalled(); + }); - const modifiedFiles: FileState[] = []; - mockDigitalTwin.DTAssets.getFileContent = jest - .fn() - .mockResolvedValue('fetched content'); + it('should render file sections', async () => { + await renderSidebar('reconfigure', 'differentDTName'); - await act(async () => { - await handleReconfigureFileClick( - 'testFile.md', - mockDigitalTwin, - modifiedFiles, - setFileName, - setFileContent, - setFileType, - setFilePrivacy, - setIsLibraryFile, - setLibraryAssetPath, - ); + await waitFor(() => { + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Lifecycle')).toBeInTheDocument(); + expect(screen.getByText('Configuration')).toBeInTheDocument(); + expect(screen.getByText('assetPath configuration')).toBeInTheDocument(); }); - - expect(mockDigitalTwin.DTAssets.getFileContent).toHaveBeenCalledWith( - 'testFile.md', - ); - expect(setFileName).toHaveBeenCalledWith('testFile.md'); - expect(setFileContent).toHaveBeenCalledWith('fetched content'); - expect(setFileType).toHaveBeenCalledWith('md'); }); - it('opens the file name dialog when Add new file button is clicked', async () => { - await renderSidebar('create'); + it.skip('renders loading spinner during loadFiles execution', async () => { + const fetchDataSpy = jest.spyOn(SidebarFetchers, 'fetchData'); + + renderSidebar('reconfigure', 'mockedDTName'); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); await waitFor(() => { - fireEvent.click(screen.getByText('Add new file')); + expect(fetchDataSpy).toHaveBeenCalledWith(mockDigitalTwin); }); - expect(screen.getByText('Enter the file name')).toBeInTheDocument(); + expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); }); - it('renders Sidebar with null digitalTwin when tab is create', async () => { - (useSelector as unknown as jest.Mock).mockImplementationOnce( - (selector: (state: RootState) => unknown) => { - if (selector === selectDigitalTwinByName('')) { - return null; - } - if (selector.toString().includes('state.files')) { - return []; - } - return null; - }, - ); + it('handles assets in create mode', async () => { + const addOrUpdateLibraryFileSpy = jest.spyOn(ReactRedux, 'useDispatch'); - await renderSidebar('create', ''); + await renderSidebar('create'); + + await waitFor(() => { + expect(addOrUpdateLibraryFileSpy).toHaveBeenCalled(); + mockLibraryAsset.configFiles.forEach((file) => { + expect(addOrUpdateLibraryFileSpy).toHaveBeenCalledWith( + addOrUpdateLibraryFile({ + assetPath: mockLibraryAsset.path, + fileName: file, + fileContent: '', + isNew: true, + isModified: false, + isPrivate: mockLibraryAsset.isPrivate, + }), + ); + }); + }); }); - */ }); diff --git a/client/test/preview/unit/routes/digitaltwins/editor/SidebarFunctions.test.tsx b/client/test/preview/unit/routes/digitaltwins/editor/SidebarFunctions.test.tsx deleted file mode 100644 index 55df9e7bb..000000000 --- a/client/test/preview/unit/routes/digitaltwins/editor/SidebarFunctions.test.tsx +++ /dev/null @@ -1,242 +0,0 @@ -/* -import { fireEvent, render, screen } from '@testing-library/react'; -import * as SidebarFunctions from 'preview/route/digitaltwins/editor/sidebarFunctions'; -import { FileState } from 'preview/store/file.slice'; -import { mockDigitalTwin as mockDigitalTwinInstance } from 'test/preview/__mocks__/global_mocks'; // Rinominato -import { SimpleTreeView } from '@mui/x-tree-view'; -import * as React from 'react'; -import DigitalTwin from 'preview/util/digitalTwin'; - -describe('SidebarFunctions', () => { - const setFileName = jest.fn(); - const setFileContent = jest.fn(); - const setFileType = jest.fn(); - const setErrorMessage = jest.fn(); - const dispatch = jest.fn(); - const setIsFileNameDialogOpen = jest.fn(); - const setNewFileName = jest.fn(); - - afterEach(() => { - jest.clearAllMocks(); - }); - - const files: FileState[] = []; // spostato qui per evitare conflitti di scope - - it('should return the correct file type from the extension', () => { - expect(SidebarFunctions.getFileTypeFromExtension('file.md')).toBe( - 'description', - ); - expect(SidebarFunctions.getFileTypeFromExtension('file.json')).toBe( - 'config', - ); - expect(SidebarFunctions.getFileTypeFromExtension('file.yaml')).toBe( - 'config', - ); - expect(SidebarFunctions.getFileTypeFromExtension('file.yml')).toBe( - 'config', - ); - expect(SidebarFunctions.getFileTypeFromExtension('file')).toBe('lifecycle'); - }); - - it('should handle file click correctly in create tab', () => { - const tab = 'create'; - const handleCreateFileClick = jest - .spyOn(SidebarFunctions, 'handleCreateFileClick') - .mockImplementation(jest.fn()); - - SidebarFunctions.handleFileClick( - 'file', - null, - setFileName, - setFileContent, - setFileType, - files, - tab, - ); - - expect(handleCreateFileClick).toHaveBeenCalled(); - }); - - it('should handle file click correctly in reconfigure tab', () => { - const tab = 'reconfigure'; - const handleReconfigureFileClick = jest - .spyOn(SidebarFunctions, 'handleReconfigureFileClick') - .mockImplementation(jest.fn()); - - SidebarFunctions.handleFileClick( - 'file', - null, - setFileName, - setFileContent, - setFileType, - files, - tab, - ); - - expect(handleReconfigureFileClick).toHaveBeenCalled(); - }); - - it('should render file tree items correctly and handle file click', () => { - const handleFileClick = jest - .spyOn(SidebarFunctions, 'handleFileClick') - .mockImplementation(jest.fn()); - - render( - - {SidebarFunctions.renderFileTreeItems( - 'label', - ['file'], - mockDigitalTwinInstance, // Rinominato - setFileName, - setFileContent, - setFileType, - files, - 'create', - )} - , - ); - - expect(screen.getByText('label')).toBeInTheDocument(); - fireEvent.click(screen.getByText('label')); - expect(screen.getByText('file')).toBeInTheDocument(); - fireEvent.click(screen.getByText('file')); - - expect(handleFileClick).toHaveBeenCalled(); - }); - - it('should get filtered files name correctly', () => { - const testFiles: FileState[] = [ - { name: 'file1.md', content: 'content', isNew: false, isModified: false }, - { name: 'file2', content: 'content', isNew: true, isModified: false }, - { name: 'file3', content: 'content', isNew: true, isModified: false }, - ]; - expect( - SidebarFunctions.getFilteredFileNames('lifecycle', testFiles), - ).toEqual(['file2', 'file3']); - }); - - it('should render file section correctly and handle file click', () => { - const handleFileClick = jest - .spyOn(SidebarFunctions, 'handleFileClick') - .mockImplementation(jest.fn()); - - render( - - {SidebarFunctions.renderFileSection( - 'label', - 'type', - ['file'], - mockDigitalTwinInstance, // Rinominato - setFileName, - setFileContent, - setFileType, - files, - 'create', - )} - , - ); - - expect(screen.getByText('label')).toBeInTheDocument(); - fireEvent.click(screen.getByText('label')); - expect(screen.getByText('file')).toBeInTheDocument(); - - fireEvent.click(screen.getByText('file')); - expect(handleFileClick).toHaveBeenCalledWith( - 'file', - mockDigitalTwinInstance, // Rinominato - setFileName, - setFileContent, - setFileType, - files, - 'create', - ); - }); - - it('should not call updateFileState if no new file is found', () => { - const testFiles: FileState[] = [ - { name: 'file1.md', content: 'content', isNew: false, isModified: false }, - ]; - const updateFileStateSpy = jest.spyOn(SidebarFunctions, 'updateFileState'); - - SidebarFunctions.handleCreateFileClick( - 'nonExistentFile', - testFiles, - setFileName, - setFileContent, - setFileType, - ); - - expect(updateFileStateSpy).not.toHaveBeenCalled(); - }); - - it('should set file content error message when fetching fails', async () => { - const mockDigitalTwin: DigitalTwin = { - fileHandler: { - getFileContent: jest.fn().mockRejectedValue(new Error('Fetch error')), - }, - } as unknown as DigitalTwin; - - const fileName = 'testFile.md'; - - await SidebarFunctions.fetchAndSetFileContent( - fileName, - mockDigitalTwin, - setFileName, - setFileContent, - setFileType, - ); - - expect(setFileContent).toHaveBeenCalledWith( - `Error fetching ${fileName} content`, - ); - }); - - it('should not handle file submit if name already exists', () => { - const testFiles = [ - { name: 'file1', content: 'content', isNew: true, isModified: false }, - ]; - SidebarFunctions.handleFileSubmit( - testFiles, - 'file1', - setErrorMessage, - dispatch, - setIsFileNameDialogOpen, - setNewFileName, - ); - expect(setErrorMessage).toHaveBeenCalledWith( - 'A file with this name already exists.', - ); - }); - - it('should not handle file submit if name is empty', () => { - const testFiles = [ - { name: 'file1', content: 'content', isNew: true, isModified: false }, - ]; - SidebarFunctions.handleFileSubmit( - testFiles, - '', - setErrorMessage, - dispatch, - setIsFileNameDialogOpen, - setNewFileName, - ); - expect(setErrorMessage).toHaveBeenCalledWith("File name can't be empty."); - }); - - it('should handle file submit correctly', () => { - const testFiles = [ - { name: 'file1', content: 'content', isNew: true, isModified: false }, - ]; - SidebarFunctions.handleFileSubmit( - testFiles, - 'file2', - setErrorMessage, - dispatch, - setIsFileNameDialogOpen, - setNewFileName, - ); - expect(setErrorMessage).toHaveBeenCalledWith(''); - expect(dispatch).toHaveBeenCalled(); - }); -}) - */ diff --git a/client/test/preview/unit/routes/digitaltwins/editor/sidebarFetchers.test.ts b/client/test/preview/unit/routes/digitaltwins/editor/sidebarFetchers.test.ts new file mode 100644 index 000000000..9ac06f95e --- /dev/null +++ b/client/test/preview/unit/routes/digitaltwins/editor/sidebarFetchers.test.ts @@ -0,0 +1,125 @@ +import { + mockDigitalTwin, + mockLibraryAsset, +} from 'test/preview/__mocks__/global_mocks'; +import * as SidebarFetchers from 'preview/route/digitaltwins/editor/sidebarFetchers'; +import * as FileUtils from 'preview/util/fileUtils'; + +describe('sidebarFetchers', () => { + const setFileName = jest.fn(); + const setFileContent = jest.fn(); + const setFileType = jest.fn(); + const setFilePrivacy = jest.fn(); + const setIsLibraryFile = jest.fn(); + const setLibraryAssetPath = jest.fn(); + const dispatch = jest.fn(); + + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + it('should fetch and set file content if library is true', async () => { + const getLibraryFileContentSpy = jest + .spyOn(mockDigitalTwin!.DTAssets, 'getLibraryFileContent') + .mockResolvedValue('fileContent'); + const updateFileStateSpy = jest.spyOn(FileUtils, 'updateFileState'); + + await SidebarFetchers.fetchAndSetFileContent( + 'file1.md', + mockDigitalTwin, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + true, + 'assetPath', + ); + + expect(getLibraryFileContentSpy).toHaveBeenCalledTimes(1); + expect(updateFileStateSpy).toHaveBeenCalledTimes(1); + }); + + it('should fetch and set file content if not library', async () => { + const getFileContentSpy = jest + .spyOn(mockDigitalTwin!.DTAssets, 'getFileContent') + .mockResolvedValue('fileContent'); + + await SidebarFetchers.fetchAndSetFileContent( + 'file1.md', + mockDigitalTwin, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + ); + + expect(getFileContentSpy).toHaveBeenCalledTimes(1); + }); + + it('should set error message if error occurs while fetching file content', async () => { + jest + .spyOn(mockDigitalTwin!.DTAssets, 'getFileContent') + .mockRejectedValue('error'); + + await SidebarFetchers.fetchAndSetFileContent( + 'file1.md', + mockDigitalTwin, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + ); + + expect(setFileContent).toHaveBeenCalledWith( + 'Error fetching file1.md content', + ); + }); + + it('should fetch and set file library content', async () => { + const getFileContentSpy = jest + .spyOn(mockLibraryAsset.libraryManager, 'getFileContent') + .mockResolvedValue('fileContent'); + const updateFileStateSpy = jest.spyOn(FileUtils, 'updateFileState'); + + await SidebarFetchers.fetchAndSetFileLibraryContent( + 'file1.md', + mockLibraryAsset, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + false, + setIsLibraryFile, + setLibraryAssetPath, + dispatch, + ); + + expect(getFileContentSpy).toHaveBeenCalledTimes(1); + expect(updateFileStateSpy).toHaveBeenCalledTimes(1); + expect(setIsLibraryFile).toHaveBeenCalledWith(true); + expect(setLibraryAssetPath).toHaveBeenCalledWith(mockLibraryAsset.path); + }); + + it('should set error message if error occurs while fetching file library content', async () => { + jest + .spyOn(mockLibraryAsset.libraryManager, 'getFileContent') + .mockRejectedValue('error'); + + await SidebarFetchers.fetchAndSetFileLibraryContent( + 'file1.md', + mockLibraryAsset, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + false, + setIsLibraryFile, + setLibraryAssetPath, + ); + + expect(setFileContent).toHaveBeenCalledWith( + 'Error fetching file1.md content', + ); + }); +}); diff --git a/client/test/preview/unit/routes/digitaltwins/editor/sidebarFunctions.test.ts b/client/test/preview/unit/routes/digitaltwins/editor/sidebarFunctions.test.ts new file mode 100644 index 000000000..624e42065 --- /dev/null +++ b/client/test/preview/unit/routes/digitaltwins/editor/sidebarFunctions.test.ts @@ -0,0 +1,377 @@ +import * as SidebarFunctions from 'preview/route/digitaltwins/editor/sidebarFunctions'; +import { FileState } from 'preview/store/file.slice'; +import * as FileUtils from 'preview/util/fileUtils'; +import * as SidebarFetchers from 'preview/route/digitaltwins/editor/sidebarFetchers'; +import { + mockDigitalTwin, + mockLibraryAsset, +} from 'test/preview/__mocks__/global_mocks'; + +jest.mock('preview/util/fileUtils'); +jest.mock('preview/route/digitaltwins/editor/sidebarFetchers'); + +describe('SidebarFunctions', () => { + const setFileName = jest.fn(); + const setFileContent = jest.fn(); + const setFileType = jest.fn(); + const setFilePrivacy = jest.fn(); + const setIsLibraryFile = jest.fn(); + const setLibraryAssetPath = jest.fn(); + const setIsFileNameDialogOpen = jest.fn(); + const setNewFileName = jest.fn(); + const setErrorMessage = jest.fn(); + const dispatch = jest.fn(); + + const files: FileState[] = []; + + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + it('should handle file click correctly in create tab', () => { + const tab = 'create'; + const handleCreateFileClick = jest + .spyOn(SidebarFunctions, 'handleCreateFileClick') + .mockImplementation(jest.fn()); + + SidebarFunctions.handleFileClick( + 'file', + null, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + files, + tab, + setIsLibraryFile, + setLibraryAssetPath, + ); + + expect(handleCreateFileClick).toHaveBeenCalled(); + }); + + it('should handle file click correctly in reconfigure tab', () => { + const tab = 'reconfigure'; + const handleReconfigureFileClick = jest + .spyOn(SidebarFunctions, 'handleReconfigureFileClick') + .mockImplementation(jest.fn()); + + SidebarFunctions.handleFileClick( + 'file', + null, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + files, + tab, + setIsLibraryFile, + setLibraryAssetPath, + ); + + expect(handleReconfigureFileClick).toHaveBeenCalled(); + }); + + it('should not call updateFileState if no new file is found - create tab', async () => { + const testFiles: FileState[] = [ + { name: 'file1.md', content: 'content', isNew: false, isModified: false }, + ]; + const updateFileStateSpy = jest.spyOn(FileUtils, 'updateFileState'); + + await SidebarFunctions.handleCreateFileClick( + 'nonExistentFile', + null, + testFiles, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + setIsLibraryFile, + setLibraryAssetPath, + ); + + expect(updateFileStateSpy).not.toHaveBeenCalled(); + }); + + it('should call updateFileState if new file is found - create tab', async () => { + const testFiles: FileState[] = [ + { name: 'file1.md', content: 'content', isNew: true, isModified: false }, + ]; + + const updateFileStateSpy = jest + .spyOn(FileUtils, 'updateFileState') + .mockImplementation(jest.fn()); + + await SidebarFunctions.handleCreateFileClick( + 'file1.md', + null, + testFiles, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + setIsLibraryFile, + setLibraryAssetPath, + ); + + expect(updateFileStateSpy).toHaveBeenCalled(); + }); + + it('should call updateFileState if modified library file is found - create tab', async () => { + const testFiles: FileState[] = [ + { name: 'file1.md', content: 'content', isNew: true, isModified: false }, + ]; + + const testLibraryConfigFiles = [ + { + assetPath: 'path', + fileName: 'file1.md', + fileContent: 'content', + isNew: false, + isModified: true, + isPrivate: true, + }, + ]; + + const updateFileStateSpy = jest + .spyOn(FileUtils, 'updateFileState') + .mockImplementation(jest.fn()); + + await SidebarFunctions.handleCreateFileClick( + 'file1.md', + mockLibraryAsset, + testFiles, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + setIsLibraryFile, + setLibraryAssetPath, + undefined, + testLibraryConfigFiles, + ); + + expect(updateFileStateSpy).toHaveBeenCalled(); + }); + + it('should call fetchAndSetFileLibraryContent if new library file is found - create tab', async () => { + const testFiles: FileState[] = [ + { name: 'file1.md', content: 'content', isNew: true, isModified: false }, + ]; + + const testLibraryConfigFiles = [ + { + assetPath: 'path', + fileName: 'file1.md', + fileContent: 'content', + isNew: true, + isModified: false, + isPrivate: true, + }, + ]; + + const fetchAndSetFileLibraryContentSpy = jest + .spyOn(SidebarFetchers, 'fetchAndSetFileLibraryContent') + .mockImplementation(jest.fn()); + + await SidebarFunctions.handleCreateFileClick( + 'file1.md', + mockLibraryAsset, + testFiles, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + setIsLibraryFile, + setLibraryAssetPath, + undefined, + testLibraryConfigFiles, + ); + + expect(fetchAndSetFileLibraryContentSpy).toHaveBeenCalled(); + }); + + it('should call updateFileState if new file is found - reconfigure tab', async () => { + const testFiles: FileState[] = [ + { name: 'file1.md', content: 'content', isNew: false, isModified: true }, + ]; + + const updateFileStateSpy = jest + .spyOn(FileUtils, 'updateFileState') + .mockImplementation(jest.fn()); + + await SidebarFunctions.handleReconfigureFileClick( + 'file1.md', + null, + testFiles, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + setIsLibraryFile, + setLibraryAssetPath, + ); + + expect(updateFileStateSpy).toHaveBeenCalled(); + }); + + it('should call fetchAndSetFileContent if new file is found - reconfigure tab', async () => { + const testFiles: FileState[] = [ + { name: 'file1.md', content: 'content', isNew: false, isModified: false }, + ]; + + const fetchAndSetFileContentSpy = jest + .spyOn(SidebarFetchers, 'fetchAndSetFileContent') + .mockImplementation(jest.fn()); + + await SidebarFunctions.handleReconfigureFileClick( + 'file1.md', + null, + testFiles, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + setIsLibraryFile, + setLibraryAssetPath, + ); + + expect(fetchAndSetFileContentSpy).toHaveBeenCalled(); + }); + + it.skip('should call updateFileState if modified library file is found - reconfigure tab', async () => { + const testFiles: FileState[] = [ + { name: 'file1.md', content: 'content', isNew: true, isModified: false }, + ]; + + const testLibraryConfigFiles = [ + { + assetPath: 'path', + fileName: 'file1.md', + fileContent: 'content', + isNew: false, + isModified: true, + isPrivate: true, + }, + ]; + + const updateFileStateSpy = jest + .spyOn(FileUtils, 'updateFileState') + .mockImplementation(jest.fn()); + + await SidebarFunctions.handleReconfigureFileClick( + 'file1.md', + mockDigitalTwin, + testFiles, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + setIsLibraryFile, + setLibraryAssetPath, + jest.fn(), + true, + testLibraryConfigFiles, + 'path', + ); + + expect(updateFileStateSpy).toHaveBeenCalled(); + }); + + it.skip('should call fetchAndSetFileLibraryContent if new library file is found - reconfigure tab', async () => { + const testFiles: FileState[] = [ + { name: 'file1.md', content: 'content', isNew: true, isModified: false }, + ]; + + const testLibraryConfigFiles = [ + { + assetPath: 'path', + fileName: 'file1.md', + fileContent: 'content', + isNew: true, + isModified: false, + isPrivate: true, + }, + ]; + + const fetchAndSetFileContentSpy = jest + .spyOn(SidebarFetchers, 'fetchAndSetFileContent') + .mockImplementation(jest.fn()); + + await SidebarFunctions.handleReconfigureFileClick( + 'file1.md', + mockDigitalTwin, + testFiles, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + setIsLibraryFile, + setLibraryAssetPath, + jest.fn(), + true, + testLibraryConfigFiles, + 'path', + ); + + expect(fetchAndSetFileContentSpy).toHaveBeenCalled(); + }); + + it('should handle add file click correctly', () => { + SidebarFunctions.handleAddFileClick(setIsFileNameDialogOpen); + + expect(setIsFileNameDialogOpen).toHaveBeenCalledWith(true); + }); + + it('should handle file submit correctly', () => { + const testFiles = [ + { name: 'file1', content: 'content', isNew: true, isModified: false }, + ]; + SidebarFunctions.handleFileSubmit( + testFiles, + 'file2', + setErrorMessage, + dispatch, + setIsFileNameDialogOpen, + setNewFileName, + ); + + expect(dispatch).toHaveBeenCalled(); + expect(setIsFileNameDialogOpen).toHaveBeenCalledWith(false); + }); + + it('should set error message when file name already exists', () => { + const testFiles = [ + { name: 'file1', content: 'content', isNew: true, isModified: false }, + ]; + SidebarFunctions.handleFileSubmit( + testFiles, + 'file1', + setErrorMessage, + dispatch, + setIsFileNameDialogOpen, + setNewFileName, + ); + + expect(setErrorMessage).toHaveBeenCalledWith( + 'A file with this name already exists.', + ); + }); + + it('should set error message when file name is empty', () => { + const testFiles = [ + { name: 'file1', content: 'content', isNew: true, isModified: false }, + ]; + SidebarFunctions.handleFileSubmit( + testFiles, + '', + setErrorMessage, + dispatch, + setIsFileNameDialogOpen, + setNewFileName, + ); + + expect(setErrorMessage).toHaveBeenCalledWith("File name can't be empty."); + }); +}); diff --git a/client/test/preview/unit/routes/digitaltwins/editor/sidebarRendering.test.tsx b/client/test/preview/unit/routes/digitaltwins/editor/sidebarRendering.test.tsx new file mode 100644 index 000000000..c0aeb0834 --- /dev/null +++ b/client/test/preview/unit/routes/digitaltwins/editor/sidebarRendering.test.tsx @@ -0,0 +1,169 @@ +import * as SidebarRendering from 'preview/route/digitaltwins/editor/sidebarRendering'; +import * as SidebarFunctions from 'preview/route/digitaltwins/editor/sidebarFunctions'; +import { render, screen, fireEvent } from '@testing-library/react'; +import * as React from 'react'; +import { SimpleTreeView } from '@mui/x-tree-view'; +import { + mockDigitalTwin, + mockLibraryAsset, +} from 'test/preview/__mocks__/global_mocks'; +import { FileState } from 'preview/store/file.slice'; + +describe('SidebarRendering', () => { + const setFileName = jest.fn(); + const setFileContent = jest.fn(); + const setFileType = jest.fn(); + const setFilePrivacy = jest.fn(); + const setIsLibraryFile = jest.fn(); + const setIsLibraryAssetPath = jest.fn(); + const dispatch = jest.fn(); + + const files: FileState[] = [ + { + name: 'file', + content: 'content', + type: 'type', + isModified: false, + isNew: true, + }, + ]; + + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + it('should render file tree items correctly and handle file click - DigitalTwin', () => { + const handleFileClick = jest + .spyOn(SidebarFunctions, 'handleFileClick') + .mockImplementation(jest.fn()); + + render( + + {SidebarRendering.renderFileTreeItems( + 'label', + ['file'], + mockDigitalTwin, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + files, + 'create', + dispatch, + setIsLibraryFile, + setIsLibraryAssetPath, + )} + , + ); + + expect(screen.getByText('label')).toBeInTheDocument(); + fireEvent.click(screen.getByText('label')); + expect(screen.getByText('file')).toBeInTheDocument(); + fireEvent.click(screen.getByText('file')); + + expect(handleFileClick).toHaveBeenCalled(); + }); + + it('should render file tree items correctly and handle file click - LibraryAsset', () => { + const handleFileClick = jest + .spyOn(SidebarFunctions, 'handleFileClick') + .mockImplementation(jest.fn()); + + mockLibraryAsset.isPrivate = false; + + render( + + {SidebarRendering.renderFileTreeItems( + 'label', + ['file'], + mockLibraryAsset, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + files, + 'create', + dispatch, + setIsLibraryFile, + setIsLibraryAssetPath, + )} + , + ); + + expect(screen.getByText('label')).toBeInTheDocument(); + fireEvent.click(screen.getByText('label')); + expect(screen.getByText('file')).toBeInTheDocument(); + fireEvent.click(screen.getByText('file')); + + expect(handleFileClick).toHaveBeenCalled(); + }); + + it('should render file section correctly and handle file click - LibraryAsset', () => { + const handleFileClick = jest + .spyOn(SidebarFunctions, 'handleFileClick') + .mockImplementation(jest.fn()); + + mockLibraryAsset.isPrivate = false; + + render( + + {SidebarRendering.renderFileSection( + 'label', + 'Digital Twins', + ['file'], + mockLibraryAsset, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + files, + 'create', + dispatch, + setIsLibraryFile, + setIsLibraryAssetPath, + )} + , + ); + + expect(screen.getByText('label')).toBeInTheDocument(); + fireEvent.click(screen.getByText('label')); + expect(screen.getByText('file')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('file')); + expect(handleFileClick).toHaveBeenCalled(); + }); + + it('should render file section correctly and handle file click - DigitalTwin', () => { + const handleFileClick = jest + .spyOn(SidebarFunctions, 'handleFileClick') + .mockImplementation(jest.fn()); + + render( + + {SidebarRendering.renderFileSection( + 'label', + 'Digital Twins', + ['file'], + mockDigitalTwin, + setFileName, + setFileContent, + setFileType, + setFilePrivacy, + files, + 'create', + dispatch, + setIsLibraryFile, + setIsLibraryAssetPath, + )} + , + ); + + expect(screen.getByText('label')).toBeInTheDocument(); + fireEvent.click(screen.getByText('label')); + expect(screen.getByText('file')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('file')); + expect(handleFileClick).toHaveBeenCalled(); + }); +}); diff --git a/client/test/preview/unit/routes/digitaltwins/manage/ConfigDialog.test.tsx b/client/test/preview/unit/routes/digitaltwins/manage/ConfigDialog.test.tsx index 014e34e75..236086e59 100644 --- a/client/test/preview/unit/routes/digitaltwins/manage/ConfigDialog.test.tsx +++ b/client/test/preview/unit/routes/digitaltwins/manage/ConfigDialog.test.tsx @@ -89,7 +89,6 @@ describe('ReconfigureDialog', () => { } return mockDigitalTwin; }, - ); render( diff --git a/client/test/preview/unit/routes/library/LibraryPreview.test.tsx b/client/test/preview/unit/routes/library/LibraryPreview.test.tsx new file mode 100644 index 000000000..823842f70 --- /dev/null +++ b/client/test/preview/unit/routes/library/LibraryPreview.test.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import LibraryPreview from 'preview/route/library/LibraryPreview'; +import store from 'store/store'; +import { act, render, screen } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import { useAuth } from 'react-oidc-context'; +import { useGetAndSetUsername } from 'util/auth/Authentication'; + +jest.mock('react-oidc-context', () => ({ + ...jest.requireActual('react-oidc-context'), + useAuth: jest.fn(), +})); + +jest.mock('util/auth/Authentication', () => ({ + ...jest.requireActual('util/auth/Authentication'), + useGetAndSetUsername: jest.fn(), +})); + +jest.mock('preview/components/asset/AssetLibrary'); + +jest.mock('preview/components/cart/ShoppingCart'); + +jest.mock('components/tab/TabComponent', () => jest.fn(() =>
)); + +jest.mock('page/Layout', () => jest.fn(() =>
)); + +describe('LibraryPreview', () => { + const signinRedirect = jest.fn(); + + beforeEach(() => { + (useAuth as jest.Mock).mockReturnValue({ + signinRedirect, + user: { profile: { profile: 'profileUrl' } }, + }); + + (useGetAndSetUsername as jest.Mock).mockReturnValue(jest.fn()); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it.skip('should display content of tabs', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + const tabComponent = screen.getByTestId('tab-component'); + expect(tabComponent).toBeInTheDocument(); + + const assetLibrary = screen.getByTestId('asset-library'); + expect(assetLibrary).toBeInTheDocument(); + + const shoppingCart = screen.getByTestId('shopping-cart'); + expect(shoppingCart).toBeInTheDocument(); + }); +}); diff --git a/client/test/preview/unit/store/CartAccess.test.ts b/client/test/preview/unit/store/CartAccess.test.ts new file mode 100644 index 000000000..6d0a1eca4 --- /dev/null +++ b/client/test/preview/unit/store/CartAccess.test.ts @@ -0,0 +1,55 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useDispatch, useSelector } from 'react-redux'; +import useCart from 'preview/store/CartAccess'; +import * as cart from 'preview/store/cart.slice'; +import { mockLibraryAsset } from 'test/preview/__mocks__/global_mocks'; + +jest.mock('react-redux', () => ({ + useDispatch: jest.fn(), + useSelector: jest.fn(), +})); + +jest.mock('preview/store/cart.slice', () => ({ + addToCart: jest.fn(), + removeFromCart: jest.fn(), + clearCart: jest.fn(), +})); + +describe('useCart', () => { + const dispatch = jest.fn(); + const mockState = { items: [] }; + + beforeEach(() => { + (useDispatch as unknown as jest.Mock).mockReturnValue(dispatch); + (useSelector as unknown as jest.Mock).mockImplementation((selector) => + selector({ cart: mockState }), + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return the cart state', () => { + const { result } = renderHook(() => useCart()); + expect(result.current.state).toEqual(mockState); + }); + + it('should dispatch addToCart action', () => { + const { result } = renderHook(() => useCart()); + const asset = mockLibraryAsset; + act(() => { + result.current.actions.add(asset); + }); + expect(dispatch).toHaveBeenCalledWith(cart.addToCart(asset)); + }); + + it('should dispatch removeFromCart action', () => { + const { result } = renderHook(() => useCart()); + const asset = mockLibraryAsset; + act(() => { + result.current.actions.remove(asset); + }); + expect(dispatch).toHaveBeenCalledWith(cart.removeFromCart(asset)); + }); +}); diff --git a/client/test/preview/unit/Store.test.ts b/client/test/preview/unit/store/Store.test.ts similarity index 65% rename from client/test/preview/unit/Store.test.ts rename to client/test/preview/unit/store/Store.test.ts index ba67bc2da..1b568bc9e 100644 --- a/client/test/preview/unit/Store.test.ts +++ b/client/test/preview/unit/store/Store.test.ts @@ -26,7 +26,17 @@ import fileSlice, { renameFile, } from 'preview/store/file.slice'; import LibraryAsset from 'preview/util/libraryAsset'; -import { mockLibraryAsset } from '../__mocks__/global_mocks'; +import { mockLibraryAsset } from 'test/preview/__mocks__/global_mocks'; +import cartSlice, { + addToCart, + clearCart, + removeFromCart, +} from 'preview/store/cart.slice'; +import libraryFilesSlice, { + LibraryConfigFile, + addOrUpdateLibraryFile, + removeAllModifiedLibraryFiles, +} from 'preview/store/libraryConfigFiles.slice'; describe('reducers', () => { let initialState: { @@ -47,6 +57,15 @@ describe('reducers', () => { isNew: boolean; isModified: boolean; }[]; + cart: { + assets: LibraryAsset[]; + }; + libraryConfigFiles: { + name: string; + content: string; + isNew: boolean; + isModified: boolean; + }[]; }; beforeEach(() => { @@ -59,6 +78,8 @@ describe('reducers', () => { severity: 'info', }, files: [], + cart: { assets: [] }, + libraryConfigFiles: [], }; }); @@ -82,88 +103,89 @@ describe('reducers', () => { describe('digitalTwin reducer', () => { const digitalTwin = new DigitalTwin( 'asset1', - new GitlabInstance('user1', 'authority', 'token1') + new GitlabInstance('user1', 'authority', 'token1'), ); - + const initialState = { digitalTwin: {}, shouldFetchDigitalTwins: true, }; - + it('should return the initial state when an unknown action is passed with an undefined state', () => { - expect(digitalTwinReducer(undefined, { type: 'unknown' })).toEqual(initialState); + expect(digitalTwinReducer(undefined, { type: 'unknown' })).toEqual( + initialState, + ); }); - + it('should handle setDigitalTwin', () => { const newState = digitalTwinReducer( initialState, - setDigitalTwin({ assetName: 'asset1', digitalTwin }) + setDigitalTwin({ assetName: 'asset1', digitalTwin }), ); expect(newState.digitalTwin.asset1).toEqual(digitalTwin); }); - + it('should handle setPipelineCompleted', () => { const updatedDigitalTwin = new DigitalTwin( 'asset1', - new GitlabInstance('user1', 'authority', 'token1') + new GitlabInstance('user1', 'authority', 'token1'), ); - updatedDigitalTwin.pipelineCompleted = false; // Imposta lo stato iniziale - + updatedDigitalTwin.pipelineCompleted = false; + const updatedState = { digitalTwin: { asset1: updatedDigitalTwin, }, shouldFetchDigitalTwins: true, }; - + const newState = digitalTwinReducer( updatedState, - setPipelineCompleted({ assetName: 'asset1', pipelineCompleted: true }) + setPipelineCompleted({ assetName: 'asset1', pipelineCompleted: true }), ); - + expect(newState.digitalTwin.asset1.pipelineCompleted).toBe(true); }); - - + it('should handle setPipelineLoading', () => { const updatedDigitalTwin = new DigitalTwin( 'asset1', - new GitlabInstance('user1', 'authority', 'token1') + new GitlabInstance('user1', 'authority', 'token1'), ); - updatedDigitalTwin.pipelineLoading = false; // Stato iniziale - + updatedDigitalTwin.pipelineLoading = false; + const updatedState = { ...initialState, digitalTwin: { asset1: updatedDigitalTwin }, }; - + const newState = digitalTwinReducer( updatedState, - setPipelineLoading({ assetName: 'asset1', pipelineLoading: true }) + setPipelineLoading({ assetName: 'asset1', pipelineLoading: true }), ); - + expect(newState.digitalTwin.asset1.pipelineLoading).toBe(true); }); - + it('should handle updateDescription', () => { const updatedDigitalTwin = new DigitalTwin( 'asset1', - new GitlabInstance('user1', 'authority', 'token1') + new GitlabInstance('user1', 'authority', 'token1'), ); - updatedDigitalTwin.description = ''; // Stato iniziale - + updatedDigitalTwin.description = ''; + const updatedState = { ...initialState, digitalTwin: { asset1: updatedDigitalTwin }, }; - + const description = 'new description'; - + const newState = digitalTwinReducer( updatedState, - updateDescription({ assetName: 'asset1', description }) + updateDescription({ assetName: 'asset1', description }), ); - + expect(newState.digitalTwin.asset1.description).toBe(description); }); }); @@ -322,4 +344,120 @@ describe('reducers', () => { expect(newState).toEqual([]); }); }); + + describe('cart reducer', () => { + const asset1 = mockLibraryAsset; + const asset2 = { ...mockLibraryAsset, path: 'path2' }; + + it('should handle addToCart', () => { + const newState = cartSlice(initialState.cart, addToCart(asset1)); + expect(newState.assets).toEqual([asset1]); + }); + + it('should not add duplicate assets to cart', () => { + initialState.cart.assets = [asset1]; + const newState = cartSlice(initialState.cart, addToCart(asset1)); + expect(newState.assets).toEqual([asset1]); + }); + + it('should handle removeFromCart', () => { + initialState.cart.assets = [asset1, asset2]; + const newState = cartSlice(initialState.cart, removeFromCart(asset1)); + expect(newState.assets).toEqual([asset2]); + }); + + it('should handle clearCart', () => { + initialState.cart.assets = [asset1, asset2]; + const newState = cartSlice(initialState.cart, clearCart()); + expect(newState.assets).toEqual([]); + }); + }); + + describe('libraryFilesSlice', () => { + const initialState: LibraryConfigFile[] = []; + + it('should handle initial state', () => { + expect(libraryFilesSlice(undefined, { type: 'unknown' })).toEqual( + initialState, + ); + }); + + it('should handle addOrUpdateLibraryFile', () => { + const newFile: LibraryConfigFile = { + assetPath: 'path1', + fileName: 'file1', + fileContent: 'content1', + isNew: true, + isModified: false, + isPrivate: false, + }; + + const updatedFile: LibraryConfigFile = { + ...newFile, + fileContent: 'updated content', + isModified: true, + }; + + let state = libraryFilesSlice( + initialState, + addOrUpdateLibraryFile(newFile), + ); + expect(state).toEqual([newFile]); + + state = libraryFilesSlice(state, addOrUpdateLibraryFile(updatedFile)); + expect(state).toEqual([updatedFile]); + }); + + it.skip('should handle removeAllFiles', () => { + const stateWithFiles: LibraryConfigFile[] = [ + { + assetPath: 'path1', + fileName: 'file1', + fileContent: 'content1', + isNew: true, + isModified: false, + isPrivate: false, + }, + ]; + + const state = libraryFilesSlice(stateWithFiles, removeAllFiles()); + expect(state).toEqual([]); + }); + + it('should handle removeAllModifiedLibraryFiles', () => { + const stateWithFiles: LibraryConfigFile[] = [ + { + assetPath: 'path1', + fileName: 'file1', + fileContent: 'content1', + isNew: false, + isModified: true, + isPrivate: false, + }, + { + assetPath: 'path2', + fileName: 'file2', + fileContent: 'content2', + isNew: true, + isModified: false, + isPrivate: false, + }, + ]; + + const state = libraryFilesSlice( + stateWithFiles, + removeAllModifiedLibraryFiles(), + ); + expect(state).toEqual([ + { + assetPath: 'path2', + fileName: 'file2', + fileContent: 'content2', + isNew: true, + isModified: false, + isPrivate: false, + }, + ]); + }); + }); }); diff --git a/client/test/preview/unit/util/DTAssets.test.ts b/client/test/preview/unit/util/DTAssets.test.ts index 0a766f9a7..19eb2fce3 100644 --- a/client/test/preview/unit/util/DTAssets.test.ts +++ b/client/test/preview/unit/util/DTAssets.test.ts @@ -55,7 +55,12 @@ describe('DTAssets', () => { const lifecycleFolderPath = 'path/to/lifecycle'; const isPrivate = false; - await dtAssets.createFiles(fileState, mainFolderPath, lifecycleFolderPath, isPrivate); + await dtAssets.createFiles( + fileState, + mainFolderPath, + lifecycleFolderPath, + isPrivate, + ); expect(dtAssets.fileHandler.createFile).toHaveBeenCalledWith( fileState[0], diff --git a/client/test/preview/unit/util/fileUtils.test.ts b/client/test/preview/unit/util/fileUtils.test.ts index 07af1aa6a..855c82478 100644 --- a/client/test/preview/unit/util/fileUtils.test.ts +++ b/client/test/preview/unit/util/fileUtils.test.ts @@ -12,7 +12,11 @@ describe('FileUtils', () => { const setErrorMessage = jest.fn(); - const result = fileUtils.validateFiles(files, libraryFiles, setErrorMessage); + const result = fileUtils.validateFiles( + files, + libraryFiles, + setErrorMessage, + ); expect(result).toBe(true); expect(setErrorMessage).toHaveBeenCalledWith( @@ -28,7 +32,11 @@ describe('FileUtils', () => { const setErrorMessage = jest.fn(); - const result = fileUtils.validateFiles(files, libraryFiles, setErrorMessage); + const result = fileUtils.validateFiles( + files, + libraryFiles, + setErrorMessage, + ); expect(result).toBe(false); expect(setErrorMessage).not.toHaveBeenCalled(); diff --git a/client/test/preview/unit/util/gitlab.test.ts b/client/test/preview/unit/util/gitlab.test.ts index b1c7a9ba2..0eb5c66a5 100644 --- a/client/test/preview/unit/util/gitlab.test.ts +++ b/client/test/preview/unit/util/gitlab.test.ts @@ -95,17 +95,7 @@ describe('GitlabInstance', () => { const subfolders = await gitlab.getDTSubfolders(projectId); expect(subfolders).toHaveLength(2); - /*expect(subfolders).toEqual([ - { - name: 'subfolder1', - path: 'digital_twins/subfolder1', - }, - { - name: 'subfolder2', - path: 'digital_twins/subfolder2', - }, - ]); - */ + expect(mockApi.Repositories.allRepositoryTrees).toHaveBeenCalledWith( projectId, { diff --git a/client/test/preview/unit/util/init.test.ts b/client/test/preview/unit/util/init.test.ts index 721fa09f6..fd5f92d70 100644 --- a/client/test/preview/unit/util/init.test.ts +++ b/client/test/preview/unit/util/init.test.ts @@ -1,12 +1,9 @@ -import { fetchLibraryAssets } from 'preview/util/init'; // O il nome corretto della funzione +import { fetchLibraryAssets } from 'preview/util/init'; import { mockGitlabInstance, mockLibraryAsset } from 'test/preview/__mocks__/global_mocks'; -// Mock di LibraryAsset -jest.mock('preview/util/libraryAsset', () => { - return { +jest.mock('preview/util/libraryAsset', () => ({ default: jest.fn().mockImplementation(() => mockLibraryAsset), - }; -}); + })); jest.mock('preview/util/gitlab', () => { const mockSimpleGitlabInstance = { @@ -34,56 +31,11 @@ describe('fetchAssets', () => { }); it('should fetch library assets and set them', async () => { - // Mock dei metodi di GitLab (mockGitlabInstance.init as jest.Mock).mockResolvedValue({}); (mockGitlabInstance.getLibrarySubfolders as jest.Mock).mockResolvedValue([ { name: 'asset1', path: 'path1', type: 'models', isPrivate: false }, ]); - // Chiama la funzione fetchLibraryAssets await fetchLibraryAssets(dispatch, setError, 'models', true); - - // Verifica che dispatch sia stato chiamato con setAsset e l'asset mockato - //expect(dispatch).toHaveBeenCalled(); }); }); - - - - /*it('should fetch assets and create digital twins', async () => { - (mockGitlabInstance.init as jest.Mock).mockResolvedValue({}); - (mockGitlabInstance.getDTSubfolders as jest.Mock).mockResolvedValue([ - { name: 'asset1', path: 'path1' }, - ]); - - await fetchDigitalTwins(dispatch, setError); - - expect(dispatch).toHaveBeenCalledWith( - setAssets([{ name: 'asset1', path: 'path1' }]), - ); - - expect(dispatch).toHaveBeenCalledWith( - setDigitalTwin({ - assetName: 'asset1', - digitalTwin: mockDigitalTwin, - }), - ); - }); - - it('should handle empty project ID by setting assets to an empty array', async () => { - mockGitlabInstance.projectId = null; - - await fetchDigitalTwins(dispatch, setError); - - expect(dispatch).toHaveBeenCalledWith(setAssets([])); - }); - - it('should skip digital twin creation if no assets are found', async () => { - (mockGitlabInstance.init as jest.Mock).mockResolvedValue({}); - (mockGitlabInstance.getDTSubfolders as jest.Mock).mockResolvedValue([]); - - await fetchDigitalTwins(dispatch, setError); - - expect(dispatch).toHaveBeenCalledWith(setAssets([])); - }); - */ diff --git a/client/test/preview/unit/util/libraryAsset.test.ts b/client/test/preview/unit/util/libraryAsset.test.ts new file mode 100644 index 000000000..355981f2a --- /dev/null +++ b/client/test/preview/unit/util/libraryAsset.test.ts @@ -0,0 +1,74 @@ +import LibraryAsset from 'preview/util/libraryAsset'; +import GitlabInstance from 'preview/util/gitlab'; +import LibraryManager from 'preview/util/libraryManager'; +import { mockGitlabInstance } from 'test/preview/__mocks__/global_mocks'; + +jest.mock('preview/util/libraryManager'); +jest.mock('preview/util/gitlab'); + +describe('LibraryAsset', () => { + let gitlabInstance: GitlabInstance; + let libraryManager: LibraryManager; + let libraryAsset: LibraryAsset; + + beforeEach(() => { + gitlabInstance = mockGitlabInstance; + libraryManager = new LibraryManager('test', gitlabInstance); + libraryAsset = new LibraryAsset( + 'test', + 'path/to/library', + true, + 'type', + gitlabInstance, + ); + libraryAsset.libraryManager = libraryManager; + }); + + it('should initialize correctly', () => { + expect(libraryAsset.name).toBe('test'); + expect(libraryAsset.path).toBe('path/to/library'); + expect(libraryAsset.isPrivate).toBe(true); + expect(libraryAsset.type).toBe('type'); + expect(libraryAsset.gitlabInstance).toBe(gitlabInstance); + expect(libraryAsset.libraryManager).toBe(libraryManager); + }); + + it('should get description', async () => { + libraryManager.getFileContent = jest.fn().mockResolvedValue('File content'); + await libraryAsset.getDescription(); + expect(libraryAsset.description).toBe('File content'); + }); + + it('should handle error when getting description', async () => { + libraryManager.getFileContent = jest + .fn() + .mockRejectedValue(new Error('Error')); + await libraryAsset.getDescription(); + expect(libraryAsset.description).toBe('There is no description.md file'); + }); + + it('should get full description with image URLs replaced', async () => { + const fileContent = '![alt text](image.png)'; + libraryManager.getFileContent = jest.fn().mockResolvedValue(fileContent); + sessionStorage.setItem('username', 'user'); + await libraryAsset.getFullDescription(); + expect(libraryAsset.fullDescription).toBe( + '![alt text](https://example.com/AUTHORITY/dtaas/user/-/raw/main/path/to/library/image.png)', + ); + }); + + it('should handle error when getting full description', async () => { + libraryManager.getFileContent = jest + .fn() + .mockRejectedValue(new Error('Error')); + await libraryAsset.getFullDescription(); + expect(libraryAsset.fullDescription).toBe('There is no README.md file'); + }); + + it('should get config files', async () => { + const fileNames = ['file1', 'file2']; + libraryManager.getFileNames = jest.fn().mockResolvedValue(fileNames); + await libraryAsset.getConfigFiles(); + expect(libraryAsset.configFiles).toEqual(fileNames); + }); +}); diff --git a/client/test/preview/unit/util/libraryManager.test.ts b/client/test/preview/unit/util/libraryManager.test.ts new file mode 100644 index 000000000..496020661 --- /dev/null +++ b/client/test/preview/unit/util/libraryManager.test.ts @@ -0,0 +1,74 @@ +import LibraryManager, { + getFilePath, + FileType, +} from 'preview/util/libraryManager'; +import GitlabInstance from 'preview/util/gitlab'; +import FileHandler from 'preview/util/fileHandler'; +import { FileState } from 'preview/store/file.slice'; +import { mockGitlabInstance } from 'test/preview/__mocks__/global_mocks'; + +jest.mock('preview/util/fileHandler'); +jest.mock('preview/util/gitlab'); + +describe('LibraryManager', () => { + let gitlabInstance: GitlabInstance; + let fileHandler: FileHandler; + let libraryManager: LibraryManager; + + beforeEach(() => { + gitlabInstance = mockGitlabInstance; + fileHandler = new FileHandler('testAsset', gitlabInstance); + libraryManager = new LibraryManager('testAsset', gitlabInstance); + libraryManager.fileHandler = fileHandler; + }); + + it('should initialize correctly', () => { + expect(libraryManager.assetName).toBe('testAsset'); + expect(libraryManager.gitlabInstance).toBe(gitlabInstance); + expect(libraryManager.fileHandler).toBe(fileHandler); + }); + + it('should get file content', async () => { + const fileContent = 'file content'; + fileHandler.getFileContent = jest.fn().mockResolvedValue(fileContent); + + const result = await libraryManager.getFileContent( + true, + 'path/to/file', + 'file.txt', + ); + expect(result).toBe(fileContent); + expect(fileHandler.getFileContent).toHaveBeenCalledWith( + 'path/to/file/file.txt', + true, + ); + }); + + it('should get file names', async () => { + const fileNames = ['file1', 'file2']; + fileHandler.getLibraryConfigFileNames = jest + .fn() + .mockResolvedValue(fileNames); + + const result = await libraryManager.getFileNames(true, 'path/to/files'); + expect(result).toEqual(fileNames); + expect(fileHandler.getLibraryConfigFileNames).toHaveBeenCalledWith( + 'path/to/files', + true, + ); + }); +}); + +describe('getFilePath', () => { + it('should return lifecycle folder path for lifecycle file type', () => { + const file: FileState = { type: FileType.LIFECYCLE } as FileState; + const result = getFilePath(file, 'main/path', 'lifecycle/path'); + expect(result).toBe('lifecycle/path'); + }); + + it('should return main folder path for non-lifecycle file type', () => { + const file: FileState = { type: FileType.DESCRIPTION } as FileState; + const result = getFilePath(file, 'main/path', 'lifecycle/path'); + expect(result).toBe('main/path'); + }); +}); diff --git a/client/yarn.lock b/client/yarn.lock index 5a40458b8..97c4f8a35 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2403,6 +2403,14 @@ lodash "^4.17.21" redent "^3.0.0" +"@testing-library/react-hooks@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" + integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-boundary "^3.1.0" + "@testing-library/react@16.0.1": version "16.0.1" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.0.1.tgz#29c0ee878d672703f5e7579f239005e4e0faa875" @@ -9566,6 +9574,13 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.2" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" @@ -9951,11 +9966,6 @@ reselect@^5.1.0, reselect@^5.1.1: resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== -reselect@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" - integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== - resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"