diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightHeader/DataInsightHeader.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightHeader/DataInsightHeader.test.tsx
new file mode 100644
index 000000000000..b5cee374e11a
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/DataInsightHeader/DataInsightHeader.test.tsx
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import { ROUTES } from '../../../constants/constants';
+import DataInsightHeader from './DataInsightHeader.component';
+
+const mockPush = jest.fn();
+
+jest.mock('react-router-dom', () => ({
+ useHistory: jest.fn().mockImplementation(() => ({
+ push: mockPush,
+ })),
+ useParams: jest.fn(() => ({ tab: 'tab' })),
+}));
+
+jest.mock('../../../components/DataInsightDetail/DataInsightSummary', () =>
+ jest.fn(() =>
DataInsightSummary
)
+);
+
+jest.mock('../../../components/DataInsightDetail/KPIChart', () =>
+ jest.fn(() => KPIChart
)
+);
+
+jest.mock('../../../components/DatePickerMenu/DatePickerMenu.component', () =>
+ jest.fn(() => DatePickerMenu
)
+);
+
+jest.mock('../../../components/PermissionProvider/PermissionProvider', () => ({
+ usePermissionProvider: jest.fn(() => ({
+ permissions: {},
+ })),
+}));
+
+jest.mock('../../../components/SearchDropdown/SearchDropdown', () =>
+ jest.fn(() => SearchDropdown
)
+);
+
+jest.mock('../../../utils/DataInsightUtils', () => ({
+ getOptionalDataInsightTabFlag: jest.fn(() => ({
+ showDataInsightSummary: true,
+ showKpiChart: true,
+ })),
+}));
+
+jest.mock('../../../utils/date-time/DateTimeUtils', () => ({
+ formatDate: jest.fn().mockReturnValue('formattedDate'),
+}));
+
+jest.mock('../../../utils/PermissionsUtils', () => ({
+ checkPermission: jest.fn().mockReturnValue(true),
+}));
+
+jest.mock('../DataInsightProvider', () => ({
+ useDataInsightProvider: jest
+ .fn()
+ .mockReturnValue({ chartFilter: {}, kpi: {} }),
+}));
+
+const mockProps = {
+ onScrollToChart: jest.fn(),
+};
+
+describe('DataInsightHeader component', () => {
+ it('should render all necessary elements', () => {
+ render( );
+
+ expect(screen.getByText('label.data-insight-plural')).toBeInTheDocument();
+ expect(
+ screen.getByText('message.data-insight-subtitle')
+ ).toBeInTheDocument();
+
+ userEvent.click(
+ screen.getByRole('button', {
+ name: 'label.add-entity',
+ })
+ );
+
+ expect(mockPush).toHaveBeenCalledWith(ROUTES.ADD_KPI);
+
+ expect(screen.getAllByText('SearchDropdown')).toHaveLength(2);
+ expect(
+ screen.getByText('formattedDate - formattedDate')
+ ).toBeInTheDocument();
+ expect(screen.getByText('DatePickerMenu')).toBeInTheDocument();
+ expect(screen.getByText('DataInsightSummary')).toBeInTheDocument();
+ expect(screen.getByText('KPIChart')).toBeInTheDocument();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.test.tsx
new file mode 100644
index 000000000000..9d4ec9a02c49
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.test.tsx
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ act,
+ render,
+ screen,
+ waitForElementToBeRemoved,
+} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import { mockUserData } from '../../components/Users/mocks/User.mocks';
+import DataModelsPage from './DataModelPage.component';
+import {
+ CREATE_THREAD,
+ DATA_MODEL_DELETED,
+ ERROR,
+ ERROR_PLACEHOLDER,
+ FETCH_ENTITY_PERMISSION_ERROR,
+ FOLLOW_DATA_MODEL,
+ TOGGLE_DELETE,
+ UPDATE_DATA_MODEL,
+ UPDATE_VOTE,
+} from './mocks/DataModelPage.mock';
+
+const mockAddDataModelFollower = jest.fn().mockResolvedValue({});
+const mockGetDataModelByFqn = jest.fn().mockResolvedValue({});
+const mockPatchDataModelDetails = jest.fn().mockResolvedValue({});
+const mockRemoveDataModelFollower = jest.fn().mockResolvedValue({});
+const mockUpdateDataModelVotes = jest.fn().mockResolvedValue({});
+const mockPostThread = jest.fn().mockResolvedValue({});
+const mockGetEntityPermissionByFqn = jest.fn().mockResolvedValue({
+ ViewAll: true,
+ ViewBasic: true,
+});
+const mockUpdateTierTag = jest.fn();
+const mockShowErrorToast = jest.fn();
+const ENTITY_MISSING_ERROR = 'Entity missing error.';
+
+jest.mock('../../components/Auth/AuthProviders/AuthProvider', () => ({
+ useAuthContext: jest.fn(() => ({
+ currentUser: mockUserData,
+ })),
+}));
+
+jest.mock('../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder', () =>
+ jest.fn().mockImplementation(() => {ERROR_PLACEHOLDER}
)
+);
+
+jest.mock('../../components/DataModels/DataModelDetails.component', () =>
+ jest
+ .fn()
+ .mockImplementation(
+ ({
+ createThread,
+ dataModelData,
+ handleColumnUpdateDataModel,
+ handleFollowDataModel,
+ handleToggleDelete,
+ handleUpdateDescription,
+ handleUpdateOwner,
+ handleUpdateTags,
+ handleUpdateTier,
+ onUpdateDataModel,
+ onUpdateVote,
+ }) => (
+
+ DataModelDetails
+
{dataModelData.deleted ? DATA_MODEL_DELETED : ''}
+
{CREATE_THREAD}
+
{
+ handleColumnUpdateDataModel();
+ handleUpdateDescription();
+ handleUpdateOwner();
+ handleUpdateTags();
+ handleUpdateTier();
+ onUpdateDataModel();
+ }}>
+ {UPDATE_DATA_MODEL}
+
+
{FOLLOW_DATA_MODEL}
+
{UPDATE_VOTE}
+
{TOGGLE_DELETE}
+
+ )
+ )
+);
+
+jest.mock('../../components/Loader/Loader', () =>
+ jest.fn().mockImplementation(() => Loader
)
+);
+
+jest.mock('../../components/PermissionProvider/PermissionProvider', () => ({
+ usePermissionProvider: jest.fn().mockReturnValue({
+ getEntityPermissionByFqn: jest.fn(() => mockGetEntityPermissionByFqn()),
+ }),
+}));
+
+jest.mock('../../hooks/useFqn', () => ({
+ useFqn: jest.fn().mockImplementation(() => ({ fqn: 'testFqn' })),
+}));
+
+jest.mock('../../rest/dataModelsAPI', () => ({
+ addDataModelFollower: () => mockAddDataModelFollower(),
+ getDataModelByFqn: () => mockGetDataModelByFqn(),
+ patchDataModelDetails: () => mockPatchDataModelDetails(),
+ removeDataModelFollower: () => mockRemoveDataModelFollower(),
+ updateDataModelVotes: () => mockUpdateDataModelVotes(),
+}));
+
+jest.mock('../../rest/feedsAPI', () => ({
+ postThread: () => mockPostThread(),
+}));
+
+jest.mock('../../utils/CommonUtils', () => ({
+ getEntityMissingError: jest.fn(() => ENTITY_MISSING_ERROR),
+ sortTagsCaseInsensitive: jest.fn((tags) => tags),
+}));
+
+jest.mock('../../utils/DataModelsUtils', () => ({
+ getSortedDataModelColumnTags: jest.fn().mockImplementation((tags) => tags),
+}));
+
+jest.mock('../../utils/TableUtils', () => {
+ return {
+ getTierTags: jest.fn().mockImplementation((tags) => tags),
+ };
+});
+
+jest.mock('../../utils/TagsUtils', () => ({
+ updateTierTag: () => mockUpdateTierTag(),
+}));
+
+jest.mock('../../utils/ToastUtils', () => ({
+ showErrorToast: jest.fn((...args) => mockShowErrorToast(...args)),
+}));
+
+jest.mock('fast-json-patch', () => ({
+ ...jest.requireActual('fast-json-patch'),
+ compare: jest.fn(),
+}));
+
+describe('DataModelPage component', () => {
+ it('should render necessary elements', async () => {
+ render( );
+ await waitForElementToBeRemoved(() => screen.getByText('Loader'));
+
+ expect(mockGetDataModelByFqn).toHaveBeenCalled();
+ expect(screen.getByText('DataModelDetails')).toBeInTheDocument();
+ });
+
+ it('toggle delete action check', async () => {
+ render( );
+ await waitForElementToBeRemoved(() => screen.getByText('Loader'));
+
+ // toggle delete
+ await act(async () => {
+ userEvent.click(
+ screen.getByRole('button', {
+ name: TOGGLE_DELETE,
+ })
+ );
+ });
+
+ expect(screen.getByText(DATA_MODEL_DELETED)).toBeInTheDocument();
+ });
+
+ it('follow data model action check', async () => {
+ render( );
+ await waitForElementToBeRemoved(() => screen.getByText('Loader'));
+
+ // follow data model
+ act(() => {
+ userEvent.click(
+ screen.getByRole('button', {
+ name: FOLLOW_DATA_MODEL,
+ })
+ );
+ });
+
+ expect(mockAddDataModelFollower).toHaveBeenCalled();
+ });
+
+ it('unfollow data model action check', async () => {
+ mockGetDataModelByFqn.mockResolvedValueOnce({
+ followers: [
+ {
+ id: mockUserData.id,
+ },
+ ],
+ });
+
+ render( );
+ await waitForElementToBeRemoved(() => screen.getByText('Loader'));
+
+ // unfollow data model
+ await act(async () => {
+ userEvent.click(
+ screen.getByRole('button', {
+ name: FOLLOW_DATA_MODEL,
+ })
+ );
+ });
+
+ expect(mockRemoveDataModelFollower).toHaveBeenCalled();
+ });
+
+ it('create thread action checks', async () => {
+ render( );
+ await waitForElementToBeRemoved(() => screen.getByText('Loader'));
+
+ // create thread
+ userEvent.click(
+ screen.getByRole('button', {
+ name: CREATE_THREAD,
+ })
+ );
+
+ expect(mockPostThread).toHaveBeenCalled();
+ });
+
+ it('update data model action check', async () => {
+ render( );
+ await waitForElementToBeRemoved(() => screen.getByText('Loader'));
+
+ // update data model
+ await act(async () => {
+ userEvent.click(
+ screen.getByRole('button', {
+ name: UPDATE_DATA_MODEL,
+ })
+ );
+ });
+
+ expect(mockPatchDataModelDetails).toHaveBeenCalledTimes(6);
+ });
+
+ it('update vote action check', async () => {
+ render( );
+ await waitForElementToBeRemoved(() => screen.getByText('Loader'));
+
+ // update vote
+ await act(async () => {
+ userEvent.click(
+ screen.getByRole('button', {
+ name: UPDATE_VOTE,
+ })
+ );
+ });
+
+ expect(mockUpdateDataModelVotes).toHaveBeenCalled();
+ });
+
+ it('errors check', async () => {
+ mockPostThread.mockRejectedValueOnce(ERROR);
+ mockPatchDataModelDetails.mockRejectedValue(ERROR);
+ mockAddDataModelFollower.mockRejectedValueOnce(ERROR);
+ mockUpdateDataModelVotes.mockRejectedValueOnce(ERROR);
+
+ await act(async () => {
+ render( );
+ });
+
+ // create thread
+ userEvent.click(
+ screen.getByRole('button', {
+ name: CREATE_THREAD,
+ })
+ );
+
+ await act(async () => {
+ // update data model
+ userEvent.click(
+ screen.getByRole('button', {
+ name: UPDATE_DATA_MODEL,
+ })
+ );
+
+ // follow data model
+ userEvent.click(
+ screen.getByRole('button', {
+ name: FOLLOW_DATA_MODEL,
+ })
+ );
+
+ // update vote
+ userEvent.click(
+ screen.getByRole('button', {
+ name: UPDATE_VOTE,
+ })
+ );
+ });
+
+ expect(mockShowErrorToast).toHaveBeenCalledTimes(9);
+
+ mockPatchDataModelDetails.mockResolvedValue({});
+ });
+
+ it('error when rendering component', async () => {
+ mockGetEntityPermissionByFqn.mockRejectedValueOnce(ERROR);
+
+ await act(async () => {
+ render( );
+ });
+
+ expect(screen.getByText(ERROR_PLACEHOLDER)).toBeInTheDocument();
+ expect(mockShowErrorToast).toHaveBeenCalledWith(
+ FETCH_ENTITY_PERMISSION_ERROR
+ );
+ });
+
+ it('error while fetching data model data', async () => {
+ mockGetDataModelByFqn.mockRejectedValueOnce(ERROR);
+
+ await act(async () => {
+ render( );
+ });
+
+ expect(screen.getByText(ERROR_PLACEHOLDER)).toBeInTheDocument();
+ expect(mockShowErrorToast).toHaveBeenCalledWith(ERROR);
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/mocks/DataModelPage.mock.ts b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/mocks/DataModelPage.mock.ts
new file mode 100644
index 000000000000..304ca4adc17a
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/mocks/DataModelPage.mock.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export const ERROR = 'error';
+export const ERROR_PLACEHOLDER = 'ErrorPlaceHolder';
+export const FETCH_ENTITY_PERMISSION_ERROR =
+ 'server.fetch-entity-permissions-error';
+export const ENTITY_MISSING_ERROR = 'entity missing error';
+export const CREATE_THREAD = 'Create Thread';
+export const UPDATE_DATA_MODEL = 'Update Data Model';
+export const FOLLOW_DATA_MODEL = 'Follow Data Model';
+export const TOGGLE_DELETE = 'Toggle Delete';
+export const DATA_MODEL_DELETED = 'Data Model is deleted';
+export const UPDATE_VOTE = 'Update Vote';