improvement: added count support for test case and pipeline tab. (#20279)

This commit is contained in:
Shailesh Parmar 2025-03-16 20:54:57 +05:30 committed by GitHub
parent 4edce20955
commit 56df47d623
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 331 additions and 16 deletions

View File

@ -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<ListTestCaseParamsBySearch>(DEFAULT_SORT_ORDER);
const testSuite = useMemo(() => table?.testSuite, [table]);
const [ingestionPipelineCount, setIngestionPipelineCount] =
useState<number>(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: (
<TabsLabel
count={paging.total}
id={EntityTabs.TEST_CASES}
name={t('label.test-case-plural')}
/>
),
key: EntityTabs.TEST_CASES,
children: (
<Row className="p-t-md">
@ -208,7 +238,13 @@ export const QualityTab = () => {
),
},
{
label: t('label.pipeline'),
label: (
<TabsLabel
count={ingestionPipelineCount}
id={EntityTabs.PIPELINE}
name={t('label.pipeline-plural')}
/>
),
key: EntityTabs.PIPELINE,
children: <TestSuitePipelineTab testSuite={testSuite} />,
},
@ -222,6 +258,7 @@ export const QualityTab = () => {
getResourceLimit,
tableBreadcrumb,
testCasePaging,
ingestionPipelineCount,
]
);

View File

@ -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 }) => (
<div data-testid={id}>
<div>{name}</div>
<span data-testid={`${id}-count`}>{count}</span>
</div>
));
});
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(<QualityTab />);
});
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(<QualityTab />);
});
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(<QualityTab />);
});
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(<QualityTab />);
});
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(<QualityTab />);
});
expect(await screen.findByTestId('pipeline-count')).toHaveTextContent('0');
});
});

View File

@ -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<boolean>(false);
const [sortOptions, setSortOptions] =
useState<ListTestCaseParamsBySearch>(DEFAULT_SORT_ORDER);
const [ingestionPipelineCount, setIngestionPipelineCount] =
useState<number>(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: (
<TabsLabel
count={pagingData.paging.total}
id={EntityTabs.TEST_CASES}
name={t('label.test-case-plural')}
/>
),
key: EntityTabs.TEST_CASES,
children: (
<DataQualityTab
@ -381,7 +398,13 @@ const TestSuiteDetailsPage = () => {
),
},
{
label: t('label.pipeline-plural'),
label: (
<TabsLabel
count={ingestionPipelineCount}
id={EntityTabs.PIPELINE}
name={t('label.pipeline-plural')}
/>
),
key: EntityTabs.PIPELINE,
children: (
<TestSuitePipelineTab isLogicalTestSuite testSuite={testSuite} />
@ -399,6 +422,7 @@ const TestSuiteDetailsPage = () => {
handleTestSuiteUpdate,
handleSortTestCase,
fetchTestCases,
ingestionPipelineCount,
]
);

View File

@ -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(() => <div>DomainLabel.component</div>),
};
});
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(() => (
<div data-testid="owner-label">OwnerLabel.component</div>
)),
}));
jest.mock('../../components/common/TabsLabel/TabsLabel.component', () => {
return jest.fn().mockImplementation(({ id, name }) => (
<div className="w-full tabs-label-container" data-testid={id}>
<div className="d-flex justify-between gap-2">{name}</div>
</div>
));
});
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(<TestSuiteDetailsPage />);
});
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(<TestSuiteDetailsPage />);
});
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(<TestSuiteDetailsPage />);
});
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(<TestSuiteDetailsPage />);
});
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(<TestSuiteDetailsPage />);
});
expect(mockGetIngestionPipelines).toHaveBeenCalledWith(
expect.objectContaining({
testSuite: 'testSuiteFQN',
pipelineType: ['TestSuite'],
arrQueryFields: [],
limit: 0,
})
);
});
});