diff --git a/client/src/components/asset/AssetBoard.tsx b/client/src/components/asset/AssetBoard.tsx
index 741b45319..30d9cb44c 100644
--- a/client/src/components/asset/AssetBoard.tsx
+++ b/client/src/components/asset/AssetBoard.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import * as React from 'react';
import { Grid } from '@mui/material';
import { useDispatch, useSelector } from 'react-redux';
import { GitlabInstance } from 'util/gitlab';
diff --git a/client/src/components/asset/AssetCard.tsx b/client/src/components/asset/AssetCard.tsx
index 23a8fe359..d939b39ab 100644
--- a/client/src/components/asset/AssetCard.tsx
+++ b/client/src/components/asset/AssetCard.tsx
@@ -233,4 +233,4 @@ function AssetCardExecute({ asset }: AssetCardProps) {
);
}
-export { AssetCardManage, AssetCardExecute };
+export { AssetCardManage, AssetCardExecute, CardButtonsContainerManage, CardButtonsContainerExecute};
diff --git a/client/src/route/digitaltwins/DeleteDialog.tsx b/client/src/route/digitaltwins/DeleteDialog.tsx
index a8500f320..757a94d5c 100644
--- a/client/src/route/digitaltwins/DeleteDialog.tsx
+++ b/client/src/route/digitaltwins/DeleteDialog.tsx
@@ -1,3 +1,5 @@
+import * as React from 'react';
+import { Dispatch, SetStateAction } from 'react';
import {
Dialog,
DialogContent,
@@ -5,7 +7,6 @@ import {
Button,
Typography,
} from '@mui/material';
-import React, { Dispatch, SetStateAction } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { selectDigitalTwinByName } from 'store/digitalTwin.slice';
import DigitalTwin, { formatName } from 'util/gitlabDigitalTwin';
diff --git a/client/src/route/digitaltwins/DigitalTwinsPreview.tsx b/client/src/route/digitaltwins/DigitalTwinsPreview.tsx
index 31f93f1a9..9ccb37267 100644
--- a/client/src/route/digitaltwins/DigitalTwinsPreview.tsx
+++ b/client/src/route/digitaltwins/DigitalTwinsPreview.tsx
@@ -41,7 +41,7 @@ export const fetchSubfolders = async (
const subfolders = await gitlabInstance.getDTSubfolders(
gitlabInstance.projectId,
);
- dispatch(setAssets(subfolders)); // Dispatcha gli asset nel Redux store
+ dispatch(setAssets(subfolders));
} else {
dispatch(setAssets([]));
}
diff --git a/client/test/unit/components/asset/AssetBoard.test.tsx b/client/test/unit/components/asset/AssetBoard.test.tsx
index 78dc8b9bb..17209423d 100644
--- a/client/test/unit/components/asset/AssetBoard.test.tsx
+++ b/client/test/unit/components/asset/AssetBoard.test.tsx
@@ -1,11 +1,15 @@
import * as React from 'react';
-import { render, screen } from '@testing-library/react';
+import { render, screen, fireEvent } from '@testing-library/react';
import AssetBoard from 'components/asset/AssetBoard';
-import { Asset } from 'components/asset/Asset';
import { GitlabInstance } from 'util/gitlab';
import '@testing-library/jest-dom';
-import store from 'store/store';
import { Provider } from 'react-redux';
+import { createStore, combineReducers } from 'redux';
+import assetsReducer from 'store/assets.slice';
+import { Asset } from 'components/asset/Asset';
+import { RootState } from 'store/store';
+import { deleteAsset } from 'store/assets.slice';
+import * as ReactRedux from 'react-redux';
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
@@ -37,16 +41,31 @@ jest.mock('components/asset/AssetCard', () => ({
AssetCardExecute: jest.fn(({ asset }) => (
{`Execute ${asset.name}`}
)),
- AssetCardManage: jest.fn(({ asset }) => {`Manage ${asset.name}`}
),
+ AssetCardManage: jest.fn(({ asset, onDelete }) => (
+
+ {`Manage ${asset.name}`}
+
+
+ )),
}));
+const mockStore = createStore(
+ combineReducers({
+ assets: assetsReducer,
+ }),
+ {
+ assets: {
+ items: assetsMock,
+ },
+ } as RootState,
+);
+
describe('AssetBoard', () => {
it('renders AssetCardExecute components when tab is "Execute"', () => {
render(
-
+
@@ -59,10 +78,9 @@ describe('AssetBoard', () => {
it('renders AssetCardManage components when tab is not "Execute"', () => {
render(
-
+
@@ -76,10 +94,9 @@ describe('AssetBoard', () => {
it('displays an error message when error prop is provided', () => {
const errorMessage = 'Something went wrong!';
render(
-
+
@@ -90,11 +107,21 @@ describe('AssetBoard', () => {
});
it('renders correctly with no assets', () => {
+ const emptyStore = createStore(
+ combineReducers({
+ assets: assetsReducer,
+ }),
+ {
+ assets: {
+ items: [],
+ },
+ } as unknown as RootState,
+ );
+
render(
-
+
@@ -104,4 +131,24 @@ describe('AssetBoard', () => {
const manageCards = screen.queryAllByText(/Manage/);
expect(manageCards).toHaveLength(0);
});
+
+ it('dispatches deleteAsset action when delete button is clicked', () => {
+ const mockDispatch = jest.fn();
+ jest.spyOn(ReactRedux, 'useDispatch').mockReturnValue(mockDispatch);
+
+ render(
+
+
+ ,
+ );
+
+ const deleteButtons = screen.getAllByText('Delete');
+ fireEvent.click(deleteButtons[0]);
+
+ expect(mockDispatch).toHaveBeenCalledWith(deleteAsset('path1'));
+ });
});
diff --git a/client/test/unit/components/asset/AssetCard.test.tsx b/client/test/unit/components/asset/AssetCard.test.tsx
index 998d7a677..ceab3a7f4 100644
--- a/client/test/unit/components/asset/AssetCard.test.tsx
+++ b/client/test/unit/components/asset/AssetCard.test.tsx
@@ -1,65 +1,181 @@
import * as React from 'react';
-import { render, screen } from '@testing-library/react';
-import { AssetCardManage, AssetCardExecute } from 'components/asset/AssetCard';
-import { Provider } from 'react-redux';
-import store from 'store/store';
-import { enableFetchMocks } from 'jest-fetch-mock';
+import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
+import { Provider } from 'react-redux';
+import { createStore, combineReducers } from 'redux';
+import { AssetCardManage, AssetCardExecute, CardButtonsContainerManage, CardButtonsContainerExecute } from 'components/asset/AssetCard';
+import assetsReducer from 'store/assets.slice';
+import { Asset } from 'components/asset/Asset';
+import { RootState } from 'store/store';
+import { setDigitalTwin } from 'store/digitalTwin.slice';
+import DigitalTwin from 'util/gitlabDigitalTwin';
+
+jest.mock('route/digitaltwins/Snackbar', () => () => Snackbar
);
+jest.mock('route/digitaltwins/DetailsDialog', () => ({ showLog, setShowLog, name }: { showLog: boolean; setShowLog: (show: boolean) => void; name: string }) => (
+
+ DetailsDialog for {name}
+ {showLog && }
+
+));
+jest.mock('route/digitaltwins/DeleteDialog', () => ({ showLog, setShowLog, name, onDelete }: { showLog: boolean; setShowLog: (show: boolean) => void; name: string; onDelete: () => void }) => (
+
+ DeleteDialog for {name}
+ {showLog && }
+
+));
+jest.mock('route/digitaltwins/LogDialog', () => ({ showLog, setShowLog, name }: { showLog: boolean; setShowLog: (show: boolean) => void; name: string }) => (
+
+ LogDialog for {name}
+ {showLog && }
+
+));
-enableFetchMocks();
+const assetsMock: Asset[] = [
+ { name: 'Asset1', path: 'path1', description: 'Description1' },
+ { name: 'Asset2', path: 'path2', description: 'Description2' },
+];
+
+interface DigitalTwinState {
+ [key: string]: DigitalTwin;
+}
-if (!AbortSignal.timeout) {
- AbortSignal.timeout = (ms) => {
- const controller = new AbortController();
- setTimeout(() => controller.abort(new DOMException('TimeoutError')), ms);
- return controller.signal;
+interface SetDigitalTwinAction {
+ type: typeof setDigitalTwin.type;
+ payload: {
+ assetName: string;
+ digitalTwin: DigitalTwin;
};
}
-jest.deepUnmock('components/asset/AssetCard');
+type DigitalTwinActions = SetDigitalTwinAction;
-jest.mock('react-redux', () => ({
- ...jest.requireActual('react-redux'),
-}));
+const mockStore = createStore(
+ combineReducers({
+ assets: assetsReducer,
+ digitalTwin: (state: DigitalTwinState = {}, action: DigitalTwinActions) => {
+ switch (action.type) {
+ case setDigitalTwin.type:
+ return { ...state, [action.payload.assetName]: action.payload.digitalTwin };
+ default:
+ return state;
+ }
+ },
+ }),
+ {
+ assets: { items: assetsMock },
+ } as RootState,
+);
-jest.mock('react-oidc-context', () => ({
- ...jest.requireActual('react-oidc-context'),
- useAuth: jest.fn(),
-}));
+describe('AssetCard Components', () => {
+ it('renders AssetCard with asset and buttons', () => {
+ render(
+
+ {}}
+ />
+ ,
+ );
-jest.mock('util/envUtil', () => ({
- ...jest.requireActual('util/envUtil'),
- getAuthority: jest.fn(() => 'https://example.com'),
-}));
+ expect(screen.getByText(/Description1/)).toBeInTheDocument();
+ });
-jest.mock('');
+ it('renders CardButtonsContainerManage with buttons', () => {
+ const setShowDetailsLog = jest.fn();
+ const setShowDeleteLog = jest.fn();
-describe('AssetCard', () => {
- const assetMock = {
- name: 'TestName',
- path: 'testPath',
- description: 'testDescription',
- };
+ render(
+
+ );
+
+ expect(screen.getByText('DetailsButton')).toBeInTheDocument();
+ expect(screen.getByText('ReconfigureButton')).toBeInTheDocument();
+ expect(screen.getByText('DeleteButton')).toBeInTheDocument();
+ });
+
+ it('handles button clicks in CardButtonsContainerManage', () => {
+ const setShowDetailsLog = jest.fn();
+ const setShowDeleteLog = jest.fn();
- test('renders Asset Card Manage correctly', () => {
render(
-
-
- );
- ,
+
);
- expect(screen.getByText(assetMock.name)).toBeInTheDocument();
+ fireEvent.click(screen.getByText('DetailsButton'));
+ expect(setShowDetailsLog).toHaveBeenCalled();
+
+ fireEvent.click(screen.getByText('DeleteButton'));
+ expect(setShowDeleteLog).toHaveBeenCalled();
});
- test('renders Asset Card Execute correctly', () => {
+ it('renders CardButtonsContainerExecute with buttons', () => {
+ const setShowLog = jest.fn();
+
render(
-
-
- );
- ,
+
+ );
+
+ expect(screen.getByText('StartStopButton')).toBeInTheDocument();
+ expect(screen.getByText('LogButton')).toBeInTheDocument();
+ });
+
+ it('handles button clicks in CardButtonsContainerExecute', () => {
+ const setShowLog = jest.fn();
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText('StartStopButton'));
+
+ fireEvent.click(screen.getByText('LogButton'));
+ expect(setShowLog).toHaveBeenCalled();
+ });
+
+ it('renders AssetCardManage correctly and handles dialogs', () => {
+ const handleDelete = jest.fn();
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('Snackbar')).toBeInTheDocument();
+ fireEvent.click(screen.getByText('DetailsDialog for Asset1'));
+ fireEvent.click(screen.getByText('Close'));
+
+ fireEvent.click(screen.getByText('DeleteDialog for Asset1'));
+ fireEvent.click(screen.getByText('Delete'));
+ expect(handleDelete).toHaveBeenCalled();
+ });
+
+ it('renders AssetCardExecute correctly and handles dialogs', () => {
+ render(
+
+
+
);
- expect(screen.getByText(assetMock.name)).toBeInTheDocument();
+ expect(screen.getByText('Snackbar')).toBeInTheDocument();
+ fireEvent.click(screen.getByText('LogDialog for Asset1'));
+ fireEvent.click(screen.getByText('Close'));
});
});
diff --git a/client/test/unit/components/asset/DetailsButton.test.tsx b/client/test/unit/components/asset/DetailsButton.test.tsx
index 7e75f1668..40b6e1748 100644
--- a/client/test/unit/components/asset/DetailsButton.test.tsx
+++ b/client/test/unit/components/asset/DetailsButton.test.tsx
@@ -20,7 +20,6 @@ const digitalTwinMock = {
};
describe('DetailsButton', () => {
- const setFullDescriptionMock = jest.fn();
const setShowLogMock = jest.fn();
beforeEach(() => {
@@ -31,7 +30,7 @@ describe('DetailsButton', () => {
jest.clearAllMocks();
});
- test('renders the Details button correctly', () => {
+ it('renders the Details button correctly', () => {
render(
@@ -42,7 +41,7 @@ describe('DetailsButton', () => {
expect(button).toBeInTheDocument();
});
- test('calls handleToggleLog when button is clicked', async () => {
+ it('calls setShowLog with true after clicking the button and getting the description', async () => {
render(
@@ -50,14 +49,11 @@ describe('DetailsButton', () => {
);
const button = screen.getByRole('button', { name: /Details/i });
+
fireEvent.click(button);
- await waitFor(() => {
- expect(digitalTwinMock.getFullDescription).toHaveBeenCalled();
- expect(setFullDescriptionMock).toHaveBeenCalledWith(
- 'Full Description Mock',
- );
- expect(setShowLogMock).toHaveBeenCalledWith(true);
- });
+ await waitFor(() => expect(digitalTwinMock.getFullDescription).toHaveBeenCalled());
+
+ expect(setShowLogMock).toHaveBeenCalledWith(true);
});
});
diff --git a/client/test/unit/routes/digitaltwins/DeleteDialog.test.tsx b/client/test/unit/routes/digitaltwins/DeleteDialog.test.tsx
index 559abfacd..0019ea972 100644
--- a/client/test/unit/routes/digitaltwins/DeleteDialog.test.tsx
+++ b/client/test/unit/routes/digitaltwins/DeleteDialog.test.tsx
@@ -1,32 +1,30 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
-import configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
import DetailsDialog from 'route/digitaltwins/DetailsDialog';
import DigitalTwin from 'util/gitlabDigitalTwin';
import { selectDigitalTwinByName } from 'store/digitalTwin.slice';
+import { GitlabInstance } from 'util/gitlab';
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+}));
jest.mock('store/digitalTwin.slice', () => ({
selectDigitalTwinByName: jest.fn(),
}));
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-
describe('DetailsDialog', () => {
let store: any;
let setShowLogMock: jest.Mock;
beforeEach(() => {
- store = mockStore({
- digitalTwin: {},
- });
setShowLogMock = jest.fn();
});
it('should render the dialog and display the digital twin name', () => {
- const digitalTwin = new DigitalTwin('testDT', {});
+ const gitlabInstance = new GitlabInstance('user1', 'authority', 'token1');
+ const digitalTwin = new DigitalTwin('testDT', gitlabInstance);
(selectDigitalTwinByName as jest.Mock).mockReturnValue(digitalTwin);
render(
@@ -78,4 +76,4 @@ describe('DetailsDialog', () => {
expect(deleteMock).toHaveBeenCalled();
expect(setShowLogMock).toHaveBeenCalledWith(false);
});
-});
+});
\ No newline at end of file
diff --git a/client/test/unit/routes/digitaltwins/DigitalTwinsPreview.test.tsx b/client/test/unit/routes/digitaltwins/DigitalTwinsPreview.test.tsx
index 57f9ebcb0..8a133f74e 100644
--- a/client/test/unit/routes/digitaltwins/DigitalTwinsPreview.test.tsx
+++ b/client/test/unit/routes/digitaltwins/DigitalTwinsPreview.test.tsx
@@ -13,13 +13,8 @@ import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import '@testing-library/jest-dom';
import { GitlabInstance } from 'util/gitlab';
-import { renderHook, act } from '@testing-library/react';
-import { Asset } from 'components/asset/Asset';
-
-jest.mock('react-oidc-context', () => ({
- ...jest.requireActual('react-oidc-context'),
- useAuth: jest.fn(),
-}));
+import { setAssets } from 'store/assets.slice';
+import { useDispatch } from 'react-redux';
jest.mock('util/gitlab', () => ({
GitlabInstance: jest.fn().mockImplementation(() => ({
@@ -33,6 +28,11 @@ jest.mock('util/envUtil', () => ({
getAuthority: jest.fn(() => 'https://example.com'),
}));
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useDispatch: jest.fn(),
+}));
+
describe('Digital Twins Preview', () => {
const tabLabels: string[] = [];
tabs.forEach((tab) => tabLabels.push(tab.label));
@@ -49,26 +49,26 @@ describe('Digital Twins Preview', () => {
itHasCorrectExecuteTabNameInDTIframe(tabLabels);
- it('should call getDTSubfolders and update subfolders state', async () => {
+ it('should call getDTSubfolders and dispatch setAssets', async () => {
const mockGitlabInstance = new GitlabInstance(
'username',
'https://example.com',
'access_token',
);
- const { result } = renderHook(() => React.useState([]));
- const setSubfolders: React.Dispatch> =
- result.current[1];
+ const mockDispatch = jest.fn();
+ (useDispatch as jest.Mock).mockReturnValue(mockDispatch);
+
+ jest.spyOn(mockGitlabInstance, 'init').mockResolvedValue(undefined);
+ jest.spyOn(mockGitlabInstance, 'getDTSubfolders').mockResolvedValue([]);
- await act(async () => {
- await fetchSubfolders(mockGitlabInstance, setSubfolders, jest.fn());
- });
+ await fetchSubfolders(mockGitlabInstance, mockDispatch, jest.fn());
expect(mockGitlabInstance.init).toHaveBeenCalled();
expect(mockGitlabInstance.getDTSubfolders).toHaveBeenCalledWith(
- 'mockedProjectId',
+ 'mockedProjectId'
);
- expect(result.current[0]).toEqual([]);
+ expect(mockDispatch).toHaveBeenCalledWith(setAssets([]));
});
it('should handle empty projectId correctly', async () => {
@@ -77,23 +77,20 @@ describe('Digital Twins Preview', () => {
'https://example.com',
'access_token',
);
+
jest.spyOn(mockGitlabInstance, 'init').mockResolvedValue(undefined);
jest.spyOn(mockGitlabInstance, 'getDTSubfolders').mockResolvedValue([]);
mockGitlabInstance.projectId = null;
- const { result } = renderHook(() => React.useState([]));
-
- const setSubfolders: React.Dispatch> =
- result.current[1];
+ const mockDispatch = jest.fn();
+ (useDispatch as jest.Mock).mockReturnValue(mockDispatch);
- await act(async () => {
- await fetchSubfolders(mockGitlabInstance, setSubfolders, jest.fn());
- });
+ await fetchSubfolders(mockGitlabInstance, mockDispatch, jest.fn());
expect(mockGitlabInstance.init).toHaveBeenCalled();
expect(mockGitlabInstance.getDTSubfolders).not.toHaveBeenCalled();
- expect(result.current[0]).toEqual([]);
+ expect(mockDispatch).toHaveBeenCalledWith(setAssets([]));
});
it('should handle errors correctly', async () => {
@@ -102,29 +99,19 @@ describe('Digital Twins Preview', () => {
'https://example.com',
'access_token',
);
- jest
- .spyOn(mockGitlabInstance, 'init')
- .mockRejectedValue(new Error('Initialization failed'));
- const { result: subfoldersResult } = renderHook(() =>
- React.useState([]),
- );
- const { result: errorResult } = renderHook(() =>
- React.useState(null),
- );
+ jest.spyOn(mockGitlabInstance, 'init').mockRejectedValue(new Error('Initialization failed'));
+
+ const mockDispatch = jest.fn();
+ (useDispatch as jest.Mock).mockReturnValue(mockDispatch);
- const setSubfolders: React.Dispatch> =
- subfoldersResult.current[1];
- const setError: React.Dispatch> =
- errorResult.current[1];
+ const setError = jest.fn();
- await act(async () => {
- await fetchSubfolders(mockGitlabInstance, setSubfolders, setError);
- });
+ await fetchSubfolders(mockGitlabInstance, mockDispatch, setError);
expect(mockGitlabInstance.init).toHaveBeenCalled();
expect(mockGitlabInstance.getDTSubfolders).not.toHaveBeenCalled();
- expect(subfoldersResult.current[0]).toEqual([]);
- expect(errorResult.current[0]).toBe('An error occurred');
+ expect(mockDispatch).not.toHaveBeenCalled();
+ expect(setError).toHaveBeenCalledWith('An error occurred');
});
});
diff --git a/client/test/unit/routes/digitaltwins/Snackbar.test.tsx b/client/test/unit/routes/digitaltwins/Snackbar.test.tsx
index 632fdba7f..e775d5ce7 100644
--- a/client/test/unit/routes/digitaltwins/Snackbar.test.tsx
+++ b/client/test/unit/routes/digitaltwins/Snackbar.test.tsx
@@ -1,74 +1,103 @@
import * as React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom';
-import CustomSnackbar from 'route/digitaltwins/Snackbar'; // Adjust the import path accordingly
+import CustomSnackbar from 'route/digitaltwins/Snackbar';
+import { Provider } from 'react-redux';
+import { hideSnackbar } from 'store/snackbar.slice';
+
+const mockStore = (initialState: any) => {
+ return {
+ getState: () => initialState,
+ dispatch: jest.fn(),
+ subscribe: jest.fn(),
+ };
+};
describe('CustomSnackbar', () => {
it('renders the snackbar with the correct message and severity', () => {
- act(() => {
- render(
- ,
- );
+ const store = mockStore({
+ snackbar: {
+ open: true,
+ message: 'Test Message',
+ severity: 'success',
+ },
});
+ render(
+
+
+
+ );
+
expect(screen.getByText('Test Message')).toBeInTheDocument();
expect(screen.getByRole('alert')).toHaveClass('MuiAlert-standardSuccess');
});
- it('does not render the snackbar when snackbarOpen is false', () => {
- const { container } = render(
- ,
+ it('does not render the snackbar when open is false', () => {
+ const store = mockStore({
+ snackbar: {
+ open: false,
+ message: 'Test Message',
+ severity: 'success',
+ },
+ });
+
+ render(
+
+
+
);
- expect(container.querySelector('div')).toBeNull();
+ expect(screen.queryByText('Test Message')).toBeNull();
});
- it('calls handleCloseSnackbar when the snackbar is closed through the alert button', () => {
- const setSnackbarOpen = jest.fn();
- act(() => {
- render(
- ,
- );
+ it('dispatches hideSnackbar action when the snackbar is closed through the alert button', () => {
+ const mockDispatch = jest.fn();
+ const store = mockStore({
+ snackbar: {
+ open: true,
+ message: 'Test Message',
+ severity: 'error',
+ },
});
+ (store.dispatch as jest.Mock) = mockDispatch;
+
+ render(
+
+
+
+ );
+
fireEvent.click(screen.getByRole('button'));
- expect(setSnackbarOpen).toHaveBeenCalledWith(false);
+ expect(mockDispatch).toHaveBeenCalledWith(hideSnackbar());
});
- it('calls handleCloseSnackbar when the snackbar is closed via auto-hide duration', () => {
- jest.useFakeTimers(); // Mock timers
- const setSnackbarOpen = jest.fn();
- act(() => {
- render(
- ,
- );
+ it('dispatches hideSnackbar action when the snackbar is closed via auto-hide duration', () => {
+ jest.useFakeTimers();
+
+ const mockDispatch = jest.fn();
+ const store = mockStore({
+ snackbar: {
+ open: true,
+ message: 'Test Message',
+ severity: 'warning',
+ },
});
+ (store.dispatch as jest.Mock) = mockDispatch;
+
+ render(
+
+
+
+ );
+
act(() => {
jest.runAllTimers();
});
- expect(setSnackbarOpen).toHaveBeenCalledWith(false);
+ expect(mockDispatch).toHaveBeenCalledWith(hideSnackbar());
jest.useRealTimers();
});
});
diff --git a/client/test/unitTests/Util/gitlabDigitalTwin.test.ts b/client/test/unitTests/Util/gitlabDigitalTwin.test.ts
index 19a028699..0c17c25ed 100644
--- a/client/test/unitTests/Util/gitlabDigitalTwin.test.ts
+++ b/client/test/unitTests/Util/gitlabDigitalTwin.test.ts
@@ -1,25 +1,14 @@
-import { ProjectSchema, PipelineTriggerTokenSchema } from '@gitbeaker/rest';
-import DigitalTwin from 'util/gitlabDigitalTwin';
import { GitlabInstance } from 'util/gitlab';
-
-type LogEntry = { status: string; DTName: string; runnerTag: string };
+import DigitalTwin, { formatName } from 'util/gitlabDigitalTwin';
const mockApi = {
- Groups: {
+ RepositoryFiles: {
show: jest.fn(),
- allProjects: jest.fn(),
+ remove: jest.fn(),
},
PipelineTriggerTokens: {
- all: jest.fn(),
trigger: jest.fn(),
},
- Repositories: {
- allRepositoryTrees: jest.fn(),
- },
- RepositoryFiles: {
- show: jest.fn(),
- remove: jest.fn(),
- },
Pipelines: {
cancel: jest.fn(),
},
@@ -27,168 +16,153 @@ const mockApi = {
const mockGitlabInstance = {
api: mockApi as unknown as GitlabInstance['api'],
- executionLogs: jest.fn() as jest.Mock,
+ projectId: 1,
+ triggerToken: 'test-token',
+ logs: [] as { jobName: string; log: string }[],
getProjectId: jest.fn(),
getTriggerToken: jest.fn(),
- getDTSubfolders: jest.fn(),
- logs: [],
} as unknown as GitlabInstance;
describe('DigitalTwin', () => {
let dt: DigitalTwin;
beforeEach(() => {
+ mockGitlabInstance.projectId = 1;
dt = new DigitalTwin('test-DTName', mockGitlabInstance);
});
- it('should handle null project ID during pipeline execution', async () => {
- mockApi.Groups.show.mockResolvedValue({ id: 1, name: 'DTaaS' });
- mockApi.Groups.allProjects.mockResolvedValue([]);
- (mockGitlabInstance.getProjectId as jest.Mock).mockResolvedValue(null);
+ it('should return full description if projectId exists', async () => {
+ const mockContent = btoa('Test README content');
+ (mockApi.RepositoryFiles.show as jest.Mock).mockResolvedValue({
+ content: mockContent,
+ });
- const success = await dt.execute();
+ await dt.getFullDescription();
- expect(success).toBe(false);
- expect(dt.lastExecutionStatus).toBe('error');
- expect(mockApi.PipelineTriggerTokens.trigger).not.toHaveBeenCalled();
+ expect(dt.fullDescription).toBe('Test README content');
+ expect(mockApi.RepositoryFiles.show).toHaveBeenCalledWith(
+ 1,
+ 'digital_twins/test-DTName/README.md',
+ 'main',
+ );
});
- it('should handle null trigger token during pipeline execution', async () => {
- mockApi.Groups.show.mockResolvedValue({ id: 1, name: 'DTaaS' });
- mockApi.Groups.allProjects.mockResolvedValue([
- { id: 1, name: 'user1' } as ProjectSchema,
- ]);
- mockApi.PipelineTriggerTokens.all.mockResolvedValue([]);
- (mockGitlabInstance.getTriggerToken as jest.Mock).mockResolvedValue(null);
+ it('should return error message if no README.md file exists', async () => {
+ (mockApi.RepositoryFiles.show as jest.Mock).mockRejectedValue(
+ new Error('File not found'),
+ );
- const success = await dt.execute();
+ await dt.getFullDescription();
- expect(success).toBe(false);
- expect(dt.lastExecutionStatus).toBe('error');
- expect(mockApi.PipelineTriggerTokens.trigger).not.toHaveBeenCalled();
+ expect(dt.fullDescription).toBe(
+ 'There is no README.md file in the test-DTName GitLab folder',
+ );
+ });
+
+ it('should return error message when projectId is missing', async () => {
+ dt.gitlabInstance.projectId = null;
+ await dt.getFullDescription();
+ expect(dt.fullDescription).toBe('Error fetching description, retry.');
});
- it('should execute pipeline successfully', async () => {
- mockApi.Groups.show.mockResolvedValue({ id: 1, name: 'DTaaS' });
- mockApi.Groups.allProjects.mockResolvedValue([
- { id: 1, name: 'user1' } as ProjectSchema,
- ]);
- mockApi.PipelineTriggerTokens.all.mockResolvedValue([
- { token: 'test-token' } as PipelineTriggerTokenSchema,
- ]);
+ it('should execute pipeline and return the pipeline ID', async () => {
+ const mockResponse = { id: 123 };
+ (mockApi.PipelineTriggerTokens.trigger as jest.Mock).mockResolvedValue(
+ mockResponse,
+ );
(mockGitlabInstance.getProjectId as jest.Mock).mockResolvedValue(1);
(mockGitlabInstance.getTriggerToken as jest.Mock).mockResolvedValue(
'test-token',
);
- (mockApi.PipelineTriggerTokens.trigger as jest.Mock).mockResolvedValue(
- undefined,
- );
- const success = await dt.execute();
+ const pipelineId = await dt.execute();
- expect(success).toBe(true);
+ expect(pipelineId).toBe(123);
expect(dt.lastExecutionStatus).toBe('success');
expect(mockApi.PipelineTriggerTokens.trigger).toHaveBeenCalledWith(
1,
'main',
'test-token',
- { variables: { DTName: 'test-DTName', RunnerTag: 'test-runnerTag' } },
+ { variables: { DTName: 'test-DTName', RunnerTag: 'linux' } },
);
});
- it('should handle non-Error thrown during pipeline execution', async () => {
- mockApi.Groups.show.mockResolvedValue({ id: 1, name: 'DTaaS' });
- mockApi.Groups.allProjects.mockResolvedValue([
- { id: 1, name: 'user1' } as ProjectSchema,
- ]);
- mockApi.PipelineTriggerTokens.all.mockResolvedValue([
- { token: 'test-token' } as PipelineTriggerTokenSchema,
- ]);
- (mockGitlabInstance.getProjectId as jest.Mock).mockResolvedValue(1);
- (mockGitlabInstance.getTriggerToken as jest.Mock).mockResolvedValue(
- 'test-token',
- );
+ it('should log error and return null when projectId or triggerToken is missing', async () => {
+ dt.gitlabInstance.projectId = null;
+ dt.gitlabInstance.triggerToken = null;
+
+ jest.spyOn(dt, 'isValidInstance').mockReturnValue(false);
+
+ (mockApi.PipelineTriggerTokens.trigger as jest.Mock).mockReset();
+
+ const pipelineId = await dt.execute();
+
+ expect(pipelineId).toBeNull();
+ expect(dt.lastExecutionStatus).toBe('error');
+ expect(mockApi.PipelineTriggerTokens.trigger).not.toHaveBeenCalled();
+ });
+
+ it('should log success and update status', () => {
+ dt.logSuccess();
+
+ expect(dt.gitlabInstance.logs).toContainEqual({
+ status: 'success',
+ DTName: 'test-DTName',
+ runnerTag: 'linux',
+ });
+ expect(dt.lastExecutionStatus).toBe('success');
+ });
+
+ it('should log error when triggering pipeline fails', async () => {
+ jest.spyOn(dt, 'isValidInstance').mockReturnValue(true);
+ const errorMessage = 'Trigger failed';
(mockApi.PipelineTriggerTokens.trigger as jest.Mock).mockRejectedValue(
- 'String error message',
+ errorMessage,
);
- const success = await dt.execute();
+ const pipelineId = await dt.execute();
- expect(success).toBe(false);
+ expect(pipelineId).toBeNull();
expect(dt.lastExecutionStatus).toBe('error');
- expect(mockApi.PipelineTriggerTokens.trigger).toHaveBeenCalledWith(
- 1,
- 'main',
- 'test-token',
- { variables: { DTName: 'test-DTName', RunnerTag: 'test-runnerTag' } },
- );
});
- it('should handle Error thrown during pipeline execution', async () => {
- mockApi.Groups.show.mockResolvedValue({ id: 1, name: 'DTaaS' });
- mockApi.Groups.allProjects.mockResolvedValue([
- { id: 1, name: 'user1' } as ProjectSchema,
- ]);
- mockApi.PipelineTriggerTokens.all.mockResolvedValue([
- { token: 'test-token' } as PipelineTriggerTokenSchema,
- ]);
-
- mockApi.PipelineTriggerTokens.trigger.mockRejectedValue(
- new Error('Error instance message'),
+ it('should handle non-Error thrown during pipeline execution', async () => {
+ (mockApi.PipelineTriggerTokens.trigger as jest.Mock).mockRejectedValue(
+ 'String error message',
);
- const success = await dt.execute();
-
- expect(success).toBe(false);
+ const pipelineId = await dt.execute();
+ expect(pipelineId).toBeNull();
expect(dt.lastExecutionStatus).toBe('error');
});
- it('should return execution logs', async () => {
- mockApi.Groups.show.mockResolvedValue({ id: 1, name: 'DTaaS' });
- mockApi.Groups.allProjects.mockResolvedValue([
- { id: 1, name: 'user1' } as ProjectSchema,
- ]);
- mockApi.PipelineTriggerTokens.all.mockResolvedValue([
- { token: 'test-token' } as PipelineTriggerTokenSchema,
- ]);
- mockApi.PipelineTriggerTokens.trigger.mockResolvedValue(undefined);
-
- await dt.execute();
-
- (mockGitlabInstance.executionLogs as jest.Mock).mockReturnValue([
- { status: 'success', DTName: 'test-DTName', runnerTag: 'test-runnerTag' },
- ]);
-
- const logs = dt.gitlabInstance.executionLogs();
- expect(logs).toHaveLength(1);
- expect(logs[0].status).toBe('success');
- expect(logs[0].DTName).toBe('test-DTName');
- expect(logs[0].runnerTag).toBe('test-runnerTag');
- });
+ it('should stop the pipeline and update status', async () => {
+ const pipelineId = 456;
+ (mockApi.Pipelines.cancel as jest.Mock).mockResolvedValue({});
- it('should return an error message if README.md does not exist', async () => {
- mockGitlabInstance.projectId = 1;
- const errorMessage = `There is no README.md file in the test-DTName GitLab folder`;
- mockApi.RepositoryFiles.show.mockRejectedValue(new Error('File not found'));
+ await dt.stop(1, pipelineId);
- const description = await dt.getFullDescription();
+ expect(mockApi.Pipelines.cancel).toHaveBeenCalledWith(1, pipelineId);
+ expect(dt.lastExecutionStatus).toBe('canceled');
+ });
- expect(description).toBe(errorMessage);
- expect(mockApi.RepositoryFiles.show).toHaveBeenCalledWith(
- 1,
- 'digital_twins/test-DTName/README.md',
- 'main',
+ it('should handle stop error', async () => {
+ (mockApi.Pipelines.cancel as jest.Mock).mockRejectedValue(
+ new Error('Stop failed'),
);
+
+ await dt.stop(1, 456);
+
+ expect(dt.lastExecutionStatus).toBe('error');
});
- it('should delete the Digital Twin successfully', async () => {
- mockGitlabInstance.projectId = 1;
- mockApi.RepositoryFiles.remove.mockResolvedValue(undefined);
+ it('should delete the digital twin and return success message', async () => {
+ (mockApi.RepositoryFiles.remove as jest.Mock).mockResolvedValue({});
- const message = await dt.delete();
+ const result = await dt.delete();
- expect(message).toBe('test-DTName deleted successfully');
+ expect(result).toBe('test-DTName deleted successfully');
expect(mockApi.RepositoryFiles.remove).toHaveBeenCalledWith(
1,
'digital_twins/test-DTName',
@@ -197,60 +171,31 @@ describe('DigitalTwin', () => {
);
});
- it('should handle error during Digital Twin deletion', async () => {
- mockGitlabInstance.projectId = 1;
- mockApi.RepositoryFiles.remove.mockRejectedValue(
- new Error('Deletion error'),
+ it('should return error message when deletion fails', async () => {
+ (mockApi.RepositoryFiles.remove as jest.Mock).mockRejectedValue(
+ new Error('Delete failed'),
);
- const message = await dt.delete();
+ const result = await dt.delete();
- expect(message).toBe('Error deleting test-DTName digital twin');
- expect(mockApi.RepositoryFiles.remove).toHaveBeenCalledWith(
- 1,
- 'digital_twins/test-DTName',
- 'main',
- 'Removing test-DTName digital twin',
- );
+ expect(result).toBe('Error deleting test-DTName digital twin');
});
- it('should return an error if no project id is provided during deletion', async () => {
- mockGitlabInstance.projectId = null;
+ it('should return error message when projectId is missing during deletion', async () => {
+ dt.gitlabInstance.projectId = null;
- const message = await dt.delete();
+ const result = await dt.delete();
- expect(message).toBe(
+ expect(result).toBe(
'Error deleting test-DTName digital twin: no project id',
);
- expect(mockApi.RepositoryFiles.remove).not.toHaveBeenCalled();
- });
-
- it('should stop a pipeline successfully', async () => {
- mockApi.Pipelines.cancel.mockResolvedValue(undefined);
-
- await dt.stop(1, 123);
-
- expect(mockApi.Pipelines.cancel).toHaveBeenCalledWith(1, 123);
- expect(mockGitlabInstance.logs).toContainEqual({
- status: 'canceled',
- DTName: 'test-DTName',
- runnerTag: 'linux',
- });
- expect(dt.lastExecutionStatus).toBe('canceled');
});
- it('should handle error during pipeline stop', async () => {
- mockApi.Pipelines.cancel.mockRejectedValue(new Error('Stop error'));
-
- await dt.stop(1, 123);
+ it('should format the name correctly', () => {
+ const testCases = [{ input: 'digital-twin', expected: 'Digital twin' }];
- expect(mockApi.Pipelines.cancel).toHaveBeenCalledWith(1, 123);
- expect(mockGitlabInstance.logs).toContainEqual({
- status: 'error',
- error: new Error('Stop error'),
- DTName: 'test-DTName',
- runnerTag: 'linux',
+ testCases.forEach(({ input, expected }) => {
+ expect(formatName(input)).toBe(expected);
});
- expect(dt.lastExecutionStatus).toBe('error');
});
-});
+});
\ No newline at end of file