From 56df47d623e6c32a56af61c8a85062370220ba03 Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Sun, 16 Mar 2025 20:54:57 +0530 Subject: [PATCH] improvement: added count support for test case and pipeline tab. (#20279) --- .../QualityTab/QualityTab.component.tsx | 43 ++++- .../QualityTab/QualityTab.test.tsx | 122 +++++++++++++- .../TestSuiteDetailsPage.component.tsx | 30 +++- .../TestSuiteDetailsPage.test.tsx | 152 +++++++++++++++++- 4 files changed, 331 insertions(+), 16 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.component.tsx index f864ff59c55..95adee00886 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.component.tsx @@ -23,7 +23,7 @@ import { Tooltip, } from 'antd'; import { isEmpty } from 'lodash'; -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; import { ReactComponent as SettingIcon } from '../../../../../assets/svg/ic-settings-primery.svg'; @@ -41,10 +41,12 @@ import { INITIAL_TEST_SUMMARY } from '../../../../../constants/TestSuite.constan import { useLimitStore } from '../../../../../context/LimitsProvider/useLimitsStore'; import { EntityTabs, EntityType } from '../../../../../enums/entity.enum'; import { ProfilerDashboardType } from '../../../../../enums/table.enum'; +import { PipelineType } from '../../../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { TestCaseStatus } from '../../../../../generated/tests/testCase'; import LimitWrapper from '../../../../../hoc/LimitWrapper'; import useCustomLocation from '../../../../../hooks/useCustomLocation/useCustomLocation'; import { useFqn } from '../../../../../hooks/useFqn'; +import { getIngestionPipelines } from '../../../../../rest/ingestionPipelineAPI'; import { ListTestCaseParamsBySearch, TestCaseType, @@ -107,6 +109,28 @@ export const QualityTab = () => { const [sortOptions, setSortOptions] = useState(DEFAULT_SORT_ORDER); const testSuite = useMemo(() => table?.testSuite, [table]); + const [ingestionPipelineCount, setIngestionPipelineCount] = + useState(0); + + const fetchIngestionPipelineCount = async () => { + try { + const { paging: ingestionPipelinePaging } = await getIngestionPipelines({ + arrQueryFields: [], + testSuite: testSuite?.fullyQualifiedName ?? '', + pipelineType: [PipelineType.TestSuite], + limit: 0, + }); + setIngestionPipelineCount(ingestionPipelinePaging.total); + } catch (error) { + // do nothing for count error + } + }; + + useEffect(() => { + if (testSuite?.fullyQualifiedName) { + fetchIngestionPipelineCount(); + } + }, [testSuite?.fullyQualifiedName]); const handleTestCasePageChange: NextPreviousProps['pagingHandler'] = ({ currentPage, @@ -161,7 +185,13 @@ export const QualityTab = () => { const tabs = useMemo( () => [ { - label: t('label.test-case-plural'), + label: ( + + ), key: EntityTabs.TEST_CASES, children: ( @@ -208,7 +238,13 @@ export const QualityTab = () => { ), }, { - label: t('label.pipeline'), + label: ( + + ), key: EntityTabs.PIPELINE, children: , }, @@ -222,6 +258,7 @@ export const QualityTab = () => { getResourceLimit, tableBreadcrumb, testCasePaging, + ingestionPipelineCount, ] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.test.tsx index 500d2e4d901..9c8190bcce1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.test.tsx @@ -14,6 +14,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; import LimitWrapper from '../../../../../hoc/LimitWrapper'; import { MOCK_TABLE } from '../../../../../mocks/TableData.mock'; +import { getIngestionPipelines } from '../../../../../rest/ingestionPipelineAPI'; import { useTableProfiler } from '../TableProfilerProvider'; import { QualityTab } from './QualityTab.component'; @@ -129,6 +130,21 @@ jest.mock('../../../../../hooks/useCustomLocation/useCustomLocation', () => ({ })), })); +jest.mock('../../../../common/TabsLabel/TabsLabel.component', () => { + return jest.fn().mockImplementation(({ id, name, count = 0 }) => ( +
+
{name}
+ {count} +
+ )); +}); + +jest.mock('../../../../../rest/ingestionPipelineAPI', () => ({ + getIngestionPipelines: jest.fn().mockResolvedValue({ + paging: { total: 0 }, + }), +})); + describe('QualityTab', () => { it('should render QualityTab', async () => { await act(async () => { @@ -157,7 +173,10 @@ describe('QualityTab', () => { expect( await screen.findByText('DataQualityTab.component') ).toBeInTheDocument(); - expect(await screen.findByText('label.pipeline')).toBeInTheDocument(); + expect( + await screen.findByText('label.pipeline-plural') + ).toBeInTheDocument(); + expect(await screen.findByTestId('pipeline-count')).toHaveTextContent('0'); }); it("Pagination should be called with 'handlePageChange'", async () => { @@ -248,12 +267,10 @@ describe('QualityTab', () => { render(); }); - expect( - await screen.findByRole('tab', { name: 'label.test-case-plural' }) - ).toHaveAttribute('aria-selected', 'true'); - expect( - await screen.findByRole('tab', { name: 'label.pipeline' }) - ).toHaveAttribute('aria-selected', 'false'); + const tabs = await screen.findAllByRole('tab'); + + expect(tabs[0]).toHaveAttribute('aria-selected', 'true'); + expect(tabs[1]).toHaveAttribute('aria-selected', 'false'); }); it('should display the initial summary data', async () => { @@ -290,4 +307,95 @@ describe('QualityTab', () => { expect(mockUseTableProfiler.onSettingButtonClick).toHaveBeenCalled(); }); + + it('should display correct test case count in tab', async () => { + (useTableProfiler as jest.Mock).mockReturnValue({ + ...mockUseTableProfiler, + testCasePaging: { + ...mockUseTableProfiler.testCasePaging, + paging: { total: 25, after: 'after' }, + }, + }); + + await act(async () => { + render(); + }); + + expect(await screen.findByTestId('test-cases-count')).toHaveTextContent( + '25' + ); + }); + + it('should display correct pipeline count in tab', async () => { + (getIngestionPipelines as jest.Mock).mockResolvedValueOnce({ + paging: { total: 5 }, + }); + + (useTableProfiler as jest.Mock).mockReturnValue({ + ...mockUseTableProfiler, + table: { + ...MOCK_TABLE, + testSuite: { + fullyQualifiedName: 'test.suite.name', + }, + }, + }); + + await act(async () => { + render(); + }); + + expect(await screen.findByTestId('pipeline-count')).toHaveTextContent('5'); + }); + + it('should show zero count when no test cases or pipelines exist', async () => { + (useTableProfiler as jest.Mock).mockReturnValue({ + ...mockUseTableProfiler, + testCasePaging: { + ...mockUseTableProfiler.testCasePaging, + paging: { total: 0, after: null }, + }, + table: { + ...MOCK_TABLE, + testSuite: { + fullyQualifiedName: 'test.suite.name', + }, + }, + }); + + await act(async () => { + render(); + }); + + expect(await screen.findByTestId('test-cases-count')).toHaveTextContent( + '0' + ); + expect(await screen.findByTestId('pipeline-count')).toHaveTextContent('0'); + }); + + it('should handle error in fetching pipeline count gracefully', async () => { + const mockGetIngestionPipelines = jest + .fn() + .mockRejectedValue(new Error('API Error')); + + jest.mock('../../../../../rest/ingestionPipelineAPI', () => ({ + getIngestionPipelines: mockGetIngestionPipelines, + })); + + (useTableProfiler as jest.Mock).mockReturnValue({ + ...mockUseTableProfiler, + table: { + ...MOCK_TABLE, + testSuite: { + fullyQualifiedName: 'test.suite.name', + }, + }, + }); + + await act(async () => { + render(); + }); + + expect(await screen.findByTestId('pipeline-count')).toHaveTextContent('0'); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.component.tsx index ad3e0da9146..27ce68aeda1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.component.tsx @@ -28,6 +28,7 @@ import { PagingHandlerParams, } from '../../components/common/NextPrevious/NextPrevious.interface'; import { OwnerLabel } from '../../components/common/OwnerLabel/OwnerLabel.component'; +import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; import TitleBreadcrumb from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.component'; import { TitleBreadcrumbProps } from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.interface'; import DataQualityTab from '../../components/Database/Profiler/DataQualityTab/DataQualityTab'; @@ -49,12 +50,14 @@ import { EntityType, TabSpecificField, } from '../../enums/entity.enum'; +import { PipelineType } from '../../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { TestCase } from '../../generated/tests/testCase'; import { EntityReference, TestSuite } from '../../generated/tests/testSuite'; import { Include } from '../../generated/type/include'; import { usePaging } from '../../hooks/paging/usePaging'; import { useFqn } from '../../hooks/useFqn'; import { DataQualityPageTabs } from '../../pages/DataQuality/DataQualityPage.interface'; +import { getIngestionPipelines } from '../../rest/ingestionPipelineAPI'; import { addTestCaseToLogicalTestSuite, getListTestCaseBySearch, @@ -101,6 +104,8 @@ const TestSuiteDetailsPage = () => { useState(false); const [sortOptions, setSortOptions] = useState(DEFAULT_SORT_ORDER); + const [ingestionPipelineCount, setIngestionPipelineCount] = + useState(0); const [slashedBreadCrumb, setSlashedBreadCrumb] = useState< TitleBreadcrumbProps['titleLinks'] @@ -176,7 +181,13 @@ const TestSuiteDetailsPage = () => { ...param, limit: pageSize, }); - + const { paging: ingestionPipelinePaging } = await getIngestionPipelines({ + arrQueryFields: [], + testSuite: testSuiteFQN, + pipelineType: [PipelineType.TestSuite], + limit: 0, + }); + setIngestionPipelineCount(ingestionPipelinePaging.total); setTestCaseResult(response.data); handlePagingChange(response.paging); } catch { @@ -363,7 +374,13 @@ const TestSuiteDetailsPage = () => { const tabs = useMemo( () => [ { - label: t('label.test-case-plural'), + label: ( + + ), key: EntityTabs.TEST_CASES, children: ( { ), }, { - label: t('label.pipeline-plural'), + label: ( + + ), key: EntityTabs.PIPELINE, children: ( @@ -399,6 +422,7 @@ const TestSuiteDetailsPage = () => { handleTestSuiteUpdate, handleSortTestCase, fetchTestCases, + ingestionPipelineCount, ] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.test.tsx index 1d7062dd7f3..163a5d954d6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.test.tsx @@ -14,7 +14,12 @@ import { act, render, screen } from '@testing-library/react'; import React from 'react'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { mockEntityPermissions } from '../../pages/DatabaseSchemaPage/mocks/DatabaseSchemaPage.mock'; -import { getTestSuiteByName } from '../../rest/testAPI'; +import { getIngestionPipelines } from '../../rest/ingestionPipelineAPI'; +import { + getListTestCaseBySearch, + getTestSuiteByName, + updateTestSuiteById, +} from '../../rest/testAPI'; import TestSuiteDetailsPage from './TestSuiteDetailsPage.component'; jest.mock('../../components/PageLayoutV1/PageLayoutV1', () => { @@ -90,10 +95,10 @@ jest.mock('../../rest/testAPI', () => { addTestCaseToLogicalTestSuite: jest .fn() .mockImplementation(() => Promise.resolve()), - getListTestCase: jest + getListTestCaseBySearch: jest .fn() .mockImplementation(() => Promise.resolve({ data: [] })), - ListTestCaseParams: jest + ListTestCaseParamsBySearch: jest .fn() .mockImplementation(() => Promise.resolve({ data: [] })), }; @@ -120,6 +125,28 @@ jest.mock('../../components/common/DomainLabel/DomainLabel.component', () => { .mockImplementation(() =>
DomainLabel.component
), }; }); +jest.mock('../../rest/ingestionPipelineAPI', () => ({ + getIngestionPipelines: jest.fn().mockImplementation(() => + Promise.resolve({ + data: [], + paging: { total: 0 }, + }) + ), +})); +jest.mock('../../components/common/OwnerLabel/OwnerLabel.component', () => ({ + OwnerLabel: jest + .fn() + .mockImplementation(() => ( +
OwnerLabel.component
+ )), +})); +jest.mock('../../components/common/TabsLabel/TabsLabel.component', () => { + return jest.fn().mockImplementation(({ id, name }) => ( +
+
{name}
+
+ )); +}); describe('TestSuiteDetailsPage component', () => { it('component should render', async () => { @@ -177,4 +204,123 @@ describe('TestSuiteDetailsPage component', () => { await screen.findByText('ErrorPlaceHolder.component') ).toBeInTheDocument(); }); + + it('should handle domain update', async () => { + const mockUpdateTestSuite = jest.fn().mockResolvedValue({ + id: '123', + name: 'test-suite', + domain: { id: 'domain-id', name: 'domain-name', type: 'domain' }, + }); + + (updateTestSuiteById as jest.Mock).mockImplementationOnce( + mockUpdateTestSuite + ); + + await act(async () => { + render(); + }); + + expect( + await screen.findByText('DomainLabel.component') + ).toBeInTheDocument(); + }); + + it('should handle description update', async () => { + const mockUpdateTestSuite = jest.fn().mockResolvedValue({ + id: '123', + name: 'test-suite', + description: 'Updated description', + }); + + (updateTestSuiteById as jest.Mock).mockImplementationOnce( + mockUpdateTestSuite + ); + + await act(async () => { + render(); + }); + + expect( + await screen.findByText('Description.component') + ).toBeInTheDocument(); + }); + + it('should handle test case pagination', async () => { + const mockGetListTestCase = jest.fn().mockResolvedValue({ + data: [], + paging: { total: 10 }, + }); + + (getListTestCaseBySearch as jest.Mock).mockImplementationOnce( + mockGetListTestCase + ); + + (getTestSuiteByName as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ + id: 'test-suite-id', + name: 'test-suite', + }) + ); + + await act(async () => { + render(); + }); + + await screen.findByTestId('test-cases'); + + expect(mockGetListTestCase).toHaveBeenCalledWith( + expect.objectContaining({ + fields: ['testCaseResult', 'testDefinition', 'testSuite', 'incidentId'], + testSuiteId: 'test-suite-id', + }) + ); + }); + + it('should handle add test case modal', async () => { + await act(async () => { + render(); + }); + + const addButton = await screen.findByTestId('add-test-case-btn'); + + expect(addButton).toBeInTheDocument(); + + await act(async () => { + addButton.click(); + }); + + // Modal should be visible after clicking add button + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + it('should handle ingestion pipeline count', async () => { + const mockGetIngestionPipelines = getIngestionPipelines as jest.Mock; + mockGetIngestionPipelines.mockImplementationOnce(() => + Promise.resolve({ + data: [], + paging: { total: 5 }, + }) + ); + + (getTestSuiteByName as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ + id: 'test-suite-id', + name: 'test-suite', + fullyQualifiedName: 'testSuiteFQN', + }) + ); + + await act(async () => { + render(); + }); + + expect(mockGetIngestionPipelines).toHaveBeenCalledWith( + expect.objectContaining({ + testSuite: 'testSuiteFQN', + pipelineType: ['TestSuite'], + arrQueryFields: [], + limit: 0, + }) + ); + }); });