diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.test.tsx new file mode 100644 index 00000000000..e8d4495cf6e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.test.tsx @@ -0,0 +1,218 @@ +/* + * 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, waitFor } from '@testing-library/react'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { EntityType, TabSpecificField } from '../../enums/entity.enum'; +import { Include } from '../../generated/type/include'; +import { useFqn } from '../../hooks/useFqn'; +import { getApiCollectionByFQN } from '../../rest/apiCollectionsAPI'; +import { getApiEndPoints } from '../../rest/apiEndpointsAPI'; +import { getFeedCounts } from '../../utils/CommonUtils'; +import APICollectionPage from './APICollectionPage'; + +jest.mock('../../rest/apiCollectionsAPI', () => ({ + getApiCollectionByFQN: jest.fn().mockResolvedValue({}), + restoreApiCollection: jest.fn().mockResolvedValue({ version: 1 }), + patchApiCollection: jest.fn().mockResolvedValue({}), + updateApiCollectionVote: jest.fn().mockResolvedValue({}), +})); + +jest.mock('../../rest/apiEndpointsAPI', () => ({ + getApiEndPoints: jest.fn().mockResolvedValue({ paging: { total: 0 } }), +})); + +jest.mock('../../utils/CommonUtils', () => ({ + getFeedCounts: jest.fn(), + getEntityMissingError: jest.fn(), + showErrorToast: jest.fn(), + showSuccessToast: jest.fn(), + getCountBadge: jest.fn().mockImplementation((count) => {count}), +})); + +jest.mock('../../hooks/useFqn', () => ({ + useFqn: jest.fn().mockReturnValue({ fqn: 'api.collection.v1' }), +})); + +jest.mock('../../hooks/useCustomPages', () => ({ + useCustomPages: jest.fn().mockReturnValue({ + customizedPage: null, + isLoading: false, + }), +})); + +jest.mock('../../hooks/useTableFilters', () => ({ + useTableFilters: jest.fn().mockReturnValue({ + filters: { showDeletedEndpoints: false }, + setFilters: jest.fn(), + }), +})); + +jest.mock('../../context/PermissionProvider/PermissionProvider', () => ({ + usePermissionProvider: jest.fn().mockReturnValue({ + getEntityPermissionByFqn: jest.fn().mockResolvedValue({ + ViewAll: true, + EditAll: true, + }), + }), +})); + +jest.mock('react-router-dom', () => ({ + useHistory: jest.fn().mockReturnValue({ push: jest.fn() }), + useParams: jest + .fn() + .mockReturnValue({ fqn: 'api.collection.v1', tab: 'api_endpoint' }), + useLocation: jest.fn().mockReturnValue({ pathname: '/test' }), +})); + +jest.mock('../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder', () => + jest.fn().mockImplementation(() =>
ErrorPlaceHolder
) +); + +jest.mock('../../components/common/Loader/Loader', () => + jest.fn().mockImplementation(() =>
Loader
) +); + +jest.mock('../../components/AppRouter/withActivityFeed', () => ({ + withActivityFeed: jest.fn().mockImplementation((Component) => Component), +})); + +jest.mock('../../components/common/DocumentTitle/DocumentTitle', () => + jest.fn().mockImplementation(() =>
DocumentTitle
) +); + +jest.mock( + '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component', + () => ({ + DataAssetsHeader: jest + .fn() + .mockImplementation(() =>
DataAssetsHeader
), + }) +); + +jest.mock( + '../../components/Customization/GenericProvider/GenericProvider', + () => ({ + GenericProvider: jest + .fn() + .mockImplementation(({ children }) =>
{children}
), + }) +); + +jest.mock('../../utils/AdvancedSearchClassBase', () => { + const mockAutocomplete = () => async () => ({ + data: [], + paging: { total: 0 }, + }); + + const AdvancedSearchClassBase = Object.assign( + jest.fn().mockImplementation(() => ({ + baseConfig: { + types: { + multiselect: { + widgets: {}, + }, + select: { + widgets: { + text: { + operators: ['like', 'not_like', 'regexp'], + }, + }, + }, + }, + }, + })), + { + autocomplete: mockAutocomplete, + } + ); + + return { + AdvancedSearchClassBase, + __esModule: true, + default: AdvancedSearchClassBase, + }; +}); + +describe('APICollectionPage', () => { + const renderComponent = () => { + return render(); + }; + + it('should call APIs with updated FQN when FQN changes', async () => { + // Set initial FQN + (useParams as jest.Mock).mockReturnValue({ + fqn: 'api.collection.v1', + tab: 'api_endpoint', + }); + + const { rerender } = renderComponent(); + + // Verify initial API calls + await waitFor(() => { + expect(getApiCollectionByFQN).toHaveBeenCalledWith('api.collection.v1', { + fields: `${TabSpecificField.OWNERS},${TabSpecificField.TAGS},${TabSpecificField.DOMAIN},${TabSpecificField.VOTES},${TabSpecificField.EXTENSION},${TabSpecificField.DATA_PRODUCTS}`, + include: Include.All, + }); + expect(getApiEndPoints).toHaveBeenCalledWith({ + apiCollection: 'api.collection.v1', + service: '', + paging: { limit: 0 }, + include: Include.NonDeleted, + }); + expect(getFeedCounts).toHaveBeenCalledWith( + EntityType.API_COLLECTION, + 'api.collection.v1', + expect.any(Function) + ); + }); + + // Clear mocks to track new calls + jest.clearAllMocks(); + + // Change FQN + (useParams as jest.Mock).mockReturnValue({ + fqn: 'api.collection.v2', + tab: 'api_endpoint', + }); + (useFqn as jest.Mock).mockReturnValue({ fqn: 'api.collection.v2' }); + + // Rerender with new FQN + rerender(); + + // Verify APIs are called with new FQN + await waitFor(() => { + expect(getApiCollectionByFQN).toHaveBeenCalledWith('api.collection.v2', { + fields: `${TabSpecificField.OWNERS},${TabSpecificField.TAGS},${TabSpecificField.DOMAIN},${TabSpecificField.VOTES},${TabSpecificField.EXTENSION},${TabSpecificField.DATA_PRODUCTS}`, + include: Include.All, + }); + expect(getApiEndPoints).toHaveBeenCalledWith({ + apiCollection: 'api.collection.v2', + service: '', + paging: { limit: 0 }, + include: Include.NonDeleted, + }); + expect(getFeedCounts).toHaveBeenCalledWith( + EntityType.API_COLLECTION, + 'api.collection.v2', + expect.any(Function) + ); + }); + + // Verify each API was called exactly once with new FQN + expect(getApiCollectionByFQN).toHaveBeenCalledTimes(1); + expect(getApiEndPoints).toHaveBeenCalledTimes(1); + expect(getFeedCounts).toHaveBeenCalledTimes(1); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx index d0fd21704a2..6698f74b89b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx @@ -144,13 +144,13 @@ const APICollectionPage: FunctionComponent = () => { setFeedCount(data); }, []); - const getEntityFeedCount = () => { + const getEntityFeedCount = useCallback(() => { getFeedCounts( EntityType.API_COLLECTION, decodedAPICollectionFQN, handleFeedCount ); - }; + }, [handleFeedCount, decodedAPICollectionFQN]); const fetchAPICollectionDetails = useCallback(async () => { try { @@ -350,7 +350,11 @@ const APICollectionPage: FunctionComponent = () => { fetchAPICollectionDetails(); getEntityFeedCount(); } - }, [viewAPICollectionPermission]); + }, [ + viewAPICollectionPermission, + fetchAPICollectionDetails, + getEntityFeedCount, + ]); useEffect(() => { if (viewAPICollectionPermission && decodedAPICollectionFQN) { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx index 2c4e1bf6649..a7d31e29b9c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx @@ -171,13 +171,13 @@ const DatabaseSchemaPage: FunctionComponent = () => { setFeedCount(data); }, []); - const getEntityFeedCount = () => { + const getEntityFeedCount = useCallback(() => { getFeedCounts( EntityType.DATABASE_SCHEMA, decodedDatabaseSchemaFQN, handleFeedCount ); - }; + }, [decodedDatabaseSchemaFQN, handleFeedCount]); const fetchDatabaseSchemaDetails = useCallback(async () => { try { @@ -403,10 +403,14 @@ const DatabaseSchemaPage: FunctionComponent = () => { if (viewDatabaseSchemaPermission) { fetchDatabaseSchemaDetails(); fetchStoreProcedureCount(); - getEntityFeedCount(); } - }, [viewDatabaseSchemaPermission]); + }, [ + viewDatabaseSchemaPermission, + fetchDatabaseSchemaDetails, + fetchStoreProcedureCount, + getEntityFeedCount, + ]); useEffect(() => { fetchTableCount(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.test.tsx index 9dda0b9265d..543f78db4cd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.test.tsx @@ -11,12 +11,13 @@ * limitations under the License. */ -import { act, render, screen } from '@testing-library/react'; +import { act, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { getDatabaseSchemaDetailsByFQN } from '../../rest/databaseAPI'; import { getStoredProceduresList } from '../../rest/storedProceduresAPI'; +import { getFeedCounts } from '../../utils/CommonUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import DatabaseSchemaPageComponent from './DatabaseSchemaPage.component'; import { @@ -322,4 +323,64 @@ describe('Tests for DatabaseSchemaPage', () => { expect(await screen.findByText('testSchemaTablesTab')).toBeInTheDocument(); }); + + it('should refetch data when decodedDatabaseSchemaFQN changes', async () => { + const mockUseParams = jest.requireMock('react-router-dom').useParams; + mockUseParams.mockReturnValue({ + fqn: 'sample_data.ecommerce_db.shopify', + tab: 'table', + }); + + (usePermissionProvider as jest.Mock).mockImplementation(() => ({ + getEntityPermissionByFqn: jest.fn().mockResolvedValue({ + ViewBasic: true, + }), + })); + + const { rerender } = render(); + + // Wait for initial API calls + await waitFor(() => { + expect(getDatabaseSchemaDetailsByFQN).toHaveBeenCalledWith( + 'sample_data.ecommerce_db.shopify', + expect.any(Object) + ); + expect(getStoredProceduresList).toHaveBeenCalledWith({ + databaseSchema: 'sample_data.ecommerce_db.shopify', + limit: 0, + }); + expect(getFeedCounts).toHaveBeenCalledWith( + 'databaseSchema', + 'sample_data.ecommerce_db.shopify', + expect.any(Function) + ); + }); + + jest.clearAllMocks(); + + mockUseParams.mockReturnValue({ + fqn: 'Glue.default.information_schema', + tab: 'table', + }); + + // Rerender with new FQN + rerender(); + + // API calls should be made again with new FQN + await waitFor(() => { + expect(getDatabaseSchemaDetailsByFQN).toHaveBeenCalledWith( + 'Glue.default.information_schema', + expect.any(Object) + ); + expect(getStoredProceduresList).toHaveBeenCalledWith({ + databaseSchema: 'Glue.default.information_schema', + limit: 0, + }); + expect(getFeedCounts).toHaveBeenCalledWith( + 'databaseSchema', + 'Glue.default.information_schema', + expect.any(Function) + ); + }); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.test.tsx new file mode 100644 index 00000000000..b4b923c6c3f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.test.tsx @@ -0,0 +1,185 @@ +/* + * 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, waitFor } from '@testing-library/react'; +import React from 'react'; +import { useFqn } from '../../hooks/useFqn'; +import { searchData } from '../../rest/miscAPI'; +import { getTagByFqn } from '../../rest/tagAPI'; +import TagPage from './TagPage'; + +jest.mock('../../rest/tagAPI', () => ({ + getTagByFqn: jest.fn().mockResolvedValue({ + name: 'NonSensitive', + fullyQualifiedName: 'PII.NonSensitive', + }), +})); + +jest.mock('../../rest/miscAPI', () => ({ + searchData: jest.fn().mockResolvedValue({ + data: { + hits: { + total: { value: 0 }, + }, + }, + }), +})); + +jest.mock('../../hooks/useFqn', () => ({ + useFqn: jest.fn(), +})); + +jest.mock('react-router-dom', () => ({ + useHistory: jest.fn().mockReturnValue({ push: jest.fn() }), + useParams: jest.fn().mockReturnValue({ fqn: 'PII.NonSensitive' }), + useLocation: jest + .fn() + .mockReturnValue({ pathname: '/tags/PII.NonSensitive' }), +})); + +jest.mock('../../context/PermissionProvider/PermissionProvider', () => ({ + usePermissionProvider: jest.fn().mockReturnValue({ + getEntityPermission: jest.fn().mockResolvedValue({ + Create: true, + Delete: true, + ViewAll: true, + EditAll: true, + EditDescription: true, + EditDisplayName: true, + EditCustomFields: true, + }), + }), +})); + +jest.mock( + '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider', + () => ({ + useActivityFeedProvider: jest.fn().mockReturnValue({ + postFeed: jest.fn(), + deleteFeed: jest.fn(), + updateFeed: jest.fn(), + }), + __esModule: true, + default: 'ActivityFeedProvider', + }) +); + +jest.mock( + '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component', + () => ({ + ActivityFeedTab: jest.fn().mockImplementation(() => <>ActivityFeedTab), + }) +); + +jest.mock('../../components/PageLayoutV1/PageLayoutV1', () => { + return jest.fn().mockImplementation(({ children }) =>
{children}
); +}); + +jest.mock( + '../../components/common/TitleBreadcrumb/TitleBreadcrumb.component', + () => { + return jest.fn().mockImplementation(() =>
TitleBreadcrumb
); + } +); + +jest.mock('../../components/common/EntityDescription/DescriptionV1', () => { + return jest.fn().mockImplementation(() =>
DescriptionV1
); +}); + +jest.mock('../../components/common/DomainLabel/DomainLabel.component', () => ({ + DomainLabel: jest.fn().mockImplementation(() =>
DomainLabel
), +})); + +jest.mock('../../components/common/ResizablePanels/ResizablePanels', () => { + return jest.fn().mockImplementation(({ children }) =>
{children}
); +}); + +jest.mock( + '../../components/Entity/EntityHeader/EntityHeader.component', + () => ({ + EntityHeader: jest.fn().mockImplementation(() =>
EntityHeader
), + }) +); + +jest.mock( + '../../components/Explore/EntitySummaryPanel/EntitySummaryPanel.component', + () => { + return jest.fn().mockImplementation(() =>
EntitySummaryPanel
); + } +); + +jest.mock( + '../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component', + () => { + return jest.fn().mockImplementation(() =>
AssetsTabs
); + } +); + +jest.mock('../../components/Modals/EntityDeleteModal/EntityDeleteModal', () => { + return jest.fn().mockImplementation(() =>
EntityDeleteModal
); +}); + +jest.mock( + '../../components/Modals/EntityNameModal/EntityNameModal.component', + () => { + return jest.fn().mockImplementation(() =>
EntityNameModal
); + } +); + +jest.mock('../../components/Modals/StyleModal/StyleModal.component', () => { + return jest.fn().mockImplementation(() =>
StyleModal
); +}); + +jest.mock( + '../../components/DataAssets/AssetsSelectionModal/AssetSelectionModal', + () => ({ + AssetSelectionModal: jest + .fn() + .mockImplementation(() =>
AssetSelectionModal
), + }) +); + +describe('TagPage', () => { + it('should call getTagData and fetchClassificationTagAssets when tagFqn changes', async () => { + (useFqn as jest.Mock).mockReturnValue({ fqn: 'PII.NonSensitive' }); + + const { rerender } = render(); + + // Verify initial API calls + await waitFor(() => { + expect(getTagByFqn).toHaveBeenCalledWith('PII.NonSensitive', { + fields: 'domain', + }); + expect(searchData).toHaveBeenCalled(); + }); + + jest.clearAllMocks(); + + // Change FQN + (useFqn as jest.Mock).mockReturnValue({ fqn: 'Certification.Gold' }); + + (getTagByFqn as jest.Mock).mockResolvedValueOnce({ + name: 'Gold', + fullyQualifiedName: 'Certification.Gold', + }); + + rerender(); + + await waitFor(() => { + expect(getTagByFqn).toHaveBeenCalledWith('Certification.Gold', { + fields: 'domain', + }); + expect(searchData).toHaveBeenCalled(); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx index 280e642cb94..3958b678358 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx @@ -566,7 +566,7 @@ const TagPage = () => { useEffect(() => { getTagData(); fetchClassificationTagAssets(); - }, []); + }, [tagFqn]); useEffect(() => { if (tagItem) {