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 : ''}

+ + + + + +
+ ) + ) +); + +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';