From 7ef792db590bc75f5b7165fae2223b69b9ac7e7f Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Wed, 17 Sep 2025 16:42:43 +0530 Subject: [PATCH] fix: Search Filter removal doesn't restore full test case list in edit page of test suite pipeline (#23419) * fix: Search Filter removal doesn't restore full test case list in edit page of test suite pipeline Fixes #23405 * fix: enhance search query to include wildcard when filters are applied --- .../components/AddTestSuitePipeline.test.tsx | 547 +++++++++++++++- .../AddTestCaseList.component.test.tsx | 603 +++++++++++++++++- .../AddTestCaseList.component.tsx | 4 +- 3 files changed, 1106 insertions(+), 48 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.test.tsx index 9822479c589..456433d491e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.test.tsx @@ -12,45 +12,45 @@ */ import { act, fireEvent, render, screen } from '@testing-library/react'; import { Form } from 'antd'; +import React from 'react'; +import { TestCase } from '../../../../generated/tests/testCase'; +import { TestSuite } from '../../../../generated/tests/testSuite'; import { AddTestSuitePipelineProps } from '../AddDataQualityTest.interface'; import AddTestSuitePipeline from './AddTestSuitePipeline'; const mockNavigate = jest.fn(); +const mockUseCustomLocation = jest.fn(); +const mockUseFqn = jest.fn(); +const mockScheduleInterval = jest.fn(); + +jest.mock('react-router-dom', () => ({ + useNavigate: () => mockNavigate, +})); + +jest.mock('../../../../hooks/useCustomLocation/useCustomLocation', () => + jest.fn().mockImplementation(() => mockUseCustomLocation()) +); -jest.mock('../../../../hooks/useCustomLocation/useCustomLocation', () => { - return jest.fn().mockImplementation(() => ({ - search: `?testSuiteId=test-suite-id`, - })); -}); jest.mock('../../../../hooks/useFqn', () => ({ - useFqn: jest.fn().mockReturnValue({ fqn: 'test-suite-fqn' }), + useFqn: jest.fn().mockImplementation(() => mockUseFqn()), })); + jest.mock('../../AddTestCaseList/AddTestCaseList.component', () => ({ - AddTestCaseList: jest - .fn() - .mockImplementation(() =>
AddTestCaseList.component
), + AddTestCaseList: () =>
AddTestCaseList.component
, })); + jest.mock( '../../../Settings/Services/AddIngestion/Steps/ScheduleInterval', - () => - jest - .fn() - .mockImplementation(({ children, topChildren, onDeploy, onBack }) => ( -
- ScheduleInterval - {topChildren} - {children} -
submit
-
cancel
-
- )) + () => jest.fn().mockImplementation((props) => mockScheduleInterval(props)) ); -jest.mock('react-router-dom', () => ({ - useNavigate: jest.fn().mockImplementation(() => mockNavigate), -})); jest.mock('../../../../utils/SchedularUtils', () => ({ - getRaiseOnErrorFormField: jest.fn().mockReturnValue({}), + getRaiseOnErrorFormField: () => ({ + name: 'raiseOnError', + label: 'Raise On Error', + type: 'switch', + required: false, + }), })); const mockProps: AddTestSuitePipelineProps = { @@ -59,6 +59,25 @@ const mockProps: AddTestSuitePipelineProps = { }; describe('AddTestSuitePipeline', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUseCustomLocation.mockReturnValue({ + search: '?testSuiteId=test-suite-id', + }); + mockUseFqn.mockReturnValue({ ingestionFQN: '' }); + mockScheduleInterval.mockImplementation( + ({ children, topChildren, onDeploy, onBack }) => ( +
+ ScheduleInterval + {topChildren} + {children} +
submit
+
cancel
+
+ ) + ); + }); + it('renders form fields', () => { render(
@@ -66,7 +85,6 @@ describe('AddTestSuitePipeline', () => {
); - // Assert that the form fields are rendered expect(screen.getByTestId('pipeline-name')).toBeInTheDocument(); expect(screen.getByTestId('select-all-test-cases')).toBeInTheDocument(); expect(screen.getByText('submit')).toBeInTheDocument(); @@ -90,7 +108,6 @@ describe('AddTestSuitePipeline', () => { fireEvent.click(screen.getByText('submit')); }); - // Assert that onSubmit is called with the correct values expect(mockProps.onSubmit).toHaveBeenCalled(); }); @@ -130,7 +147,7 @@ describe('AddTestSuitePipeline', () => { onClick={() => onFormChange('', { forms: { - ['schedular-form']: { + 'schedular-form': { getFieldValue: jest.fn().mockImplementation(() => true), setFieldsValue: jest.fn(), }, @@ -147,15 +164,483 @@ describe('AddTestSuitePipeline', () => { ); - // Assert that AddTestCaseList.component is now visible expect(screen.getByText('AddTestCaseList.component')).toBeInTheDocument(); - // Click on the select-all-test-cases switch await act(async () => { fireEvent.click(screen.getByTestId('select-all-test-cases')); }); - // Assert that AddTestCaseList.component is not initially visible expect(screen.queryByText('AddTestCaseList.component')).toBeNull(); }); + + describe('raiseOnError functionality', () => { + it('includes raiseOnError field in form submission', async () => { + const mockOnSubmit = jest.fn(); + render( +
+ + + ); + + await act(async () => { + fireEvent.click(screen.getByText('submit')); + }); + + expect(mockOnSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + raiseOnError: undefined, + }) + ); + }); + + it('passes raiseOnError value from form to onSubmit', async () => { + const mockOnSubmit = jest.fn(); + const initialData = { + raiseOnError: true, + selectAllTestCases: true, + }; + + mockScheduleInterval.mockImplementationOnce( + ({ + children, + onDeploy, + }: { + children: React.ReactNode; + onDeploy: (values: unknown) => void; + }) => ( +
+ {children} +
+ onDeploy({ + raiseOnError: true, + selectAllTestCases: true, + }) + }> + submit +
+
+ ) + ); + + render( +
+ + + ); + + await act(async () => { + fireEvent.click(screen.getByText('submit')); + }); + + expect(mockOnSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + raiseOnError: true, + }) + ); + }); + }); + + describe('testCase mapping logic', () => { + it('maps TestCase objects to string names', async () => { + const mockOnSubmit = jest.fn(); + const testCaseObject: TestCase = { + name: 'test-case-object', + id: '123', + fullyQualifiedName: 'test.case.object', + } as TestCase; + + mockScheduleInterval.mockImplementationOnce( + ({ + children, + onDeploy, + }: { + children: React.ReactNode; + onDeploy: (values: unknown) => void; + }) => ( +
+ {children} +
+ onDeploy({ + testCases: [testCaseObject, 'test-case-string'], + }) + }> + submit +
+
+ ) + ); + + render( +
+ + + ); + + await act(async () => { + fireEvent.click(screen.getByText('submit')); + }); + + expect(mockOnSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + testCases: ['test-case-object', 'test-case-string'], + }) + ); + }); + + it('handles undefined testCases array', async () => { + const mockOnSubmit = jest.fn(); + + mockScheduleInterval.mockImplementationOnce( + ({ + children, + onDeploy, + }: { + children: React.ReactNode; + onDeploy: (values: unknown) => void; + }) => ( +
+ {children} +
+ onDeploy({ + testCases: undefined, + selectAllTestCases: true, + }) + }> + submit +
+
+ ) + ); + + render( +
+ + + ); + + await act(async () => { + fireEvent.click(screen.getByText('submit')); + }); + + expect(mockOnSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + testCases: undefined, + selectAllTestCases: true, + }) + ); + }); + + it('handles mixed array of TestCase objects and strings', async () => { + const mockOnSubmit = jest.fn(); + const testCase1: TestCase = { + name: 'test-case-1', + id: '1', + fullyQualifiedName: 'test.case.1', + } as TestCase; + const testCase2: TestCase = { + name: 'test-case-2', + id: '2', + fullyQualifiedName: 'test.case.2', + } as TestCase; + + mockScheduleInterval.mockImplementationOnce( + ({ + children, + onDeploy, + }: { + children: React.ReactNode; + onDeploy: (values: unknown) => void; + }) => ( +
+ {children} +
+ onDeploy({ + testCases: [testCase1, 'string-test', testCase2], + }) + }> + submit +
+
+ ) + ); + + render( +
+ + + ); + + await act(async () => { + fireEvent.click(screen.getByText('submit')); + }); + + expect(mockOnSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + testCases: ['test-case-1', 'string-test', 'test-case-2'], + }) + ); + }); + }); + + describe('testSuiteId extraction', () => { + it('uses testSuiteId from testSuite prop when available', () => { + const testSuite = { id: 'prop-test-suite-id' } as TestSuite; + + render( +
+ + + ); + + expect(screen.getByText('AddTestCaseList.component')).toBeInTheDocument(); + }); + + it('extracts testSuiteId from URL search params when testSuite prop is not provided', () => { + mockUseCustomLocation.mockReturnValueOnce({ + search: '?testSuiteId=url-test-suite-id', + }); + + render( +
+ + + ); + + expect(screen.getByText('AddTestCaseList.component')).toBeInTheDocument(); + }); + + it('handles URL search params without question mark', () => { + mockUseCustomLocation.mockReturnValueOnce({ + search: 'testSuiteId=no-question-mark-id', + }); + + render( +
+ + + ); + + expect(screen.getByText('AddTestCaseList.component')).toBeInTheDocument(); + }); + + it('prioritizes testSuite prop over URL params', () => { + mockUseCustomLocation.mockReturnValueOnce({ + search: '?testSuiteId=url-id', + }); + + const testSuite = { id: 'prop-id' } as TestSuite; + + render( +
+ + + ); + + expect(screen.getByText('AddTestCaseList.component')).toBeInTheDocument(); + }); + }); + + describe('Form state management', () => { + it('clears testCases field when selectAllTestCases is enabled', async () => { + const mockSetFieldsValue = jest.fn(); + const mockGetFieldValue = jest.fn().mockReturnValue(true); + + jest.spyOn(Form, 'Provider').mockImplementation( + jest.fn().mockImplementation(({ onFormChange, children }) => ( +
+ {children} + +
+ )) + ); + + render( +
+ + + ); + + await act(async () => { + fireEvent.click(screen.getByTestId('trigger-form-change')); + }); + + expect(mockGetFieldValue).toHaveBeenCalledWith('selectAllTestCases'); + expect(mockSetFieldsValue).toHaveBeenCalledWith({ testCases: undefined }); + }); + + it('does not clear testCases when selectAllTestCases is false', async () => { + const mockSetFieldsValue = jest.fn(); + const mockGetFieldValue = jest.fn().mockReturnValue(false); + + jest.spyOn(Form, 'Provider').mockImplementation( + jest.fn().mockImplementation(({ onFormChange, children }) => ( +
+ {children} + +
+ )) + ); + + render( +
+ + + ); + + await act(async () => { + fireEvent.click(screen.getByTestId('trigger-form-change')); + }); + + expect(mockGetFieldValue).toHaveBeenCalledWith('selectAllTestCases'); + expect(mockSetFieldsValue).not.toHaveBeenCalled(); + }); + + it('updates selectAllTestCases state when form changes', async () => { + const { rerender } = render( +
+ + + ); + + expect(screen.getByText('AddTestCaseList.component')).toBeInTheDocument(); + + const propsWithInitialData = { + ...mockProps, + initialData: { selectAllTestCases: true }, + }; + + rerender( +
+ + + ); + + await act(async () => { + // Form state should reflect the initial data + }); + }); + }); + + describe('Edit mode behavior', () => { + it('displays Save button in edit mode', () => { + mockUseFqn.mockReturnValueOnce({ ingestionFQN: 'test-ingestion-fqn' }); + + render( +
+ + + ); + + expect(screen.getByText('ScheduleInterval')).toBeInTheDocument(); + }); + + it('displays Create button when not in edit mode', () => { + mockUseFqn.mockReturnValueOnce({ ingestionFQN: '' }); + + render( +
+ + + ); + + expect(screen.getByText('ScheduleInterval')).toBeInTheDocument(); + }); + }); + + describe('Form submission with all fields', () => { + it('submits form with all populated fields', async () => { + const mockOnSubmit = jest.fn(); + const initialData = { + name: 'Test Pipeline', + cron: '0 0 * * *', + enableDebugLog: true, + selectAllTestCases: false, + raiseOnError: true, + }; + + mockScheduleInterval.mockImplementationOnce( + ({ + children, + onDeploy, + }: { + children: React.ReactNode; + onDeploy: (values: unknown) => void; + }) => ( +
+ {children} +
+ onDeploy({ + ...initialData, + testCases: ['test-1', 'test-2'], + }) + }> + submit +
+
+ ) + ); + + render( +
+ + + ); + + await act(async () => { + fireEvent.click(screen.getByText('submit')); + }); + + expect(mockOnSubmit).toHaveBeenCalledWith({ + name: 'Test Pipeline', + cron: '0 0 * * *', + enableDebugLog: true, + selectAllTestCases: false, + testCases: ['test-1', 'test-2'], + raiseOnError: true, + }); + }); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseList.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseList.component.test.tsx index f00e3f07507..862781eaa47 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseList.component.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseList.component.test.tsx @@ -11,14 +11,16 @@ * limitations under the License. */ import { - act, fireEvent, queryByAttribute, render, screen, waitFor, } from '@testing-library/react'; -import { EntityReference } from '../../../generated/tests/testCase'; +import { act } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { EntityReference, TestCase } from '../../../generated/tests/testCase'; +import { getListTestCaseBySearch } from '../../../rest/testAPI'; import { AddTestCaseList } from './AddTestCaseList.component'; import { AddTestCaseModalProps } from './AddTestCaseList.interface'; @@ -31,7 +33,15 @@ jest.mock('../../common/Loader/Loader', () => { }); jest.mock('../../common/SearchBarComponent/SearchBar.component', () => { - return jest.fn().mockImplementation(() =>
Search Bar Mock
); + return jest.fn().mockImplementation(({ onSearch, searchValue }) => ( +
+ onSearch(e.target.value)} + /> +
+ )); }); jest.mock('../../../utils/StringsUtils', () => { return { @@ -60,12 +70,7 @@ jest.mock('../../../utils/CommonUtils', () => { }); jest.mock('../../../rest/testAPI', () => { return { - getListTestCaseBySearch: jest.fn().mockResolvedValue({ - data: [], - paging: { - total: 0, - }, - }), + getListTestCaseBySearch: jest.fn(), }; }); @@ -85,23 +90,116 @@ const mockProps: AddTestCaseModalProps = { }; jest.mock('../../../utils/RouterUtils', () => ({ - getEntityDetailsPath: jest.fn(), + getEntityDetailsPath: jest.fn().mockReturnValue('/path/to/entity'), })); +const mockGetListTestCaseBySearch = + getListTestCaseBySearch as jest.MockedFunction< + typeof getListTestCaseBySearch + >; + +const mockTestCases: TestCase[] = [ + { + id: 'test-case-1', + name: 'test_case_1', + displayName: 'Test Case 1', + entityLink: '<#E::table::sample.table>', + testDefinition: { + id: 'test-def-1', + name: 'table_column_count_to_equal', + displayName: 'Table Column Count To Equal', + }, + } as TestCase, + { + id: 'test-case-2', + name: 'test_case_2', + displayName: 'Test Case 2', + entityLink: '<#E::table::sample.table::columns::id>', + testDefinition: { + id: 'test-def-2', + name: 'column_values_to_be_unique', + displayName: 'Column Values To Be Unique', + }, + } as TestCase, + { + id: 'test-case-3', + name: 'test_case_3', + displayName: 'Test Case 3', + entityLink: '<#E::table::another.table>', + testDefinition: { + id: 'test-def-3', + name: 'table_row_count_to_be_between', + displayName: 'Table Row Count To Be Between', + }, + } as TestCase, +]; + +const renderWithRouter = (props: AddTestCaseModalProps) => { + return render( + + + + ); +}; + describe('AddTestCaseList', () => { - it('renders the component', async () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetListTestCaseBySearch.mockResolvedValue({ + data: [], + paging: { + total: 0, + }, + }); + }); + + it('renders the component with initial state', async () => { await act(async () => { - render(); + renderWithRouter(mockProps); }); - expect(screen.getByText('Search Bar Mock')).toBeInTheDocument(); + expect(screen.getByTestId('search-bar')).toBeInTheDocument(); expect(screen.getByTestId('cancel')).toBeInTheDocument(); expect(screen.getByTestId('submit')).toBeInTheDocument(); + expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({ + q: '*', + limit: 25, + offset: 0, + }); + }); + + it('renders empty state when no test cases are found', async () => { + await act(async () => { + renderWithRouter(mockProps); + }); + + await waitFor(() => { + expect(screen.getByText('Error Placeholder Mock')).toBeInTheDocument(); + }); + }); + + it('renders test cases when data is available', async () => { + mockGetListTestCaseBySearch.mockResolvedValue({ + data: mockTestCases, + paging: { + total: 3, + }, + }); + + await act(async () => { + renderWithRouter(mockProps); + }); + + await waitFor(() => { + expect(screen.getByTestId('test_case_1')).toBeInTheDocument(); + expect(screen.getByTestId('test_case_2')).toBeInTheDocument(); + expect(screen.getByTestId('test_case_3')).toBeInTheDocument(); + }); }); it('calls onCancel when cancel button is clicked', async () => { await act(async () => { - render(); + renderWithRouter(mockProps); }); fireEvent.click(screen.getByTestId('cancel')); @@ -110,7 +208,7 @@ describe('AddTestCaseList', () => { it('calls onSubmit when submit button is clicked', async () => { await act(async () => { - render(); + renderWithRouter(mockProps); }); const submitBtn = screen.getByTestId('submit'); fireEvent.click(submitBtn); @@ -125,10 +223,483 @@ describe('AddTestCaseList', () => { it('does not render submit and cancel buttons when showButton is false', async () => { await act(async () => { - render(); + renderWithRouter({ ...mockProps, showButton: false }); }); expect(screen.queryByTestId('cancel')).toBeNull(); expect(screen.queryByTestId('submit')).toBeNull(); }); + + describe('Search functionality', () => { + it('triggers search when search term is entered', async () => { + await act(async () => { + renderWithRouter(mockProps); + }); + + const searchBar = screen.getByTestId('search-bar'); + + await act(async () => { + fireEvent.change(searchBar, { target: { value: 'test_search' } }); + }); + + await waitFor(() => { + expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({ + q: 'test_search', + limit: 25, + offset: 0, + }); + }); + }); + + it('applies filters when provided', async () => { + const filters = 'testSuiteFullyQualifiedName:sample.test.suite'; + + await act(async () => { + renderWithRouter({ ...mockProps, filters }); + }); + + await waitFor(() => { + expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({ + q: `* && ${filters}`, + limit: 25, + offset: 0, + }); + }); + }); + + it('combines search term with filters', async () => { + const filters = 'testSuiteFullyQualifiedName:sample.test.suite'; + + await act(async () => { + renderWithRouter({ ...mockProps, filters }); + }); + + const searchBar = screen.getByTestId('search-bar'); + + await act(async () => { + fireEvent.change(searchBar, { target: { value: 'column_test' } }); + }); + + await waitFor(() => { + expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({ + q: `column_test && ${filters}`, + limit: 25, + offset: 0, + }); + }); + }); + + it('passes testCaseParams to API call', async () => { + const testCaseParams = { + testSuiteId: 'test-suite-123', + includeFields: ['testDefinition', 'testSuite'], + }; + + await act(async () => { + render( + + ); + }); + + await waitFor(() => { + expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({ + q: '*', + limit: 25, + offset: 0, + ...testCaseParams, + }); + }); + }); + }); + + describe('Test case selection', () => { + beforeEach(() => { + mockGetListTestCaseBySearch.mockResolvedValue({ + data: mockTestCases, + paging: { + total: 3, + }, + }); + }); + + it('selects a test case when clicked', async () => { + const onChange = jest.fn(); + + await act(async () => { + renderWithRouter({ ...mockProps, onChange }); + }); + + await waitFor(() => { + expect(screen.getByTestId('test_case_1')).toBeInTheDocument(); + }); + + const testCaseCard = screen + .getByTestId('test_case_1') + .closest('.cursor-pointer'); + + expect(testCaseCard).not.toBeNull(); + + await act(async () => { + fireEvent.click(testCaseCard as Element); + }); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith([mockTestCases[0]]); + }); + + const checkbox = screen.getByTestId('checkbox-test_case_1'); + + expect(checkbox).toHaveProperty('checked', true); + }); + + it('deselects a test case when clicked again', async () => { + const onChange = jest.fn(); + + await act(async () => { + renderWithRouter({ ...mockProps, onChange }); + }); + + await waitFor(() => { + expect(screen.getByTestId('test_case_1')).toBeInTheDocument(); + }); + + const testCaseCard = screen + .getByTestId('test_case_1') + .closest('.cursor-pointer'); + + expect(testCaseCard).not.toBeNull(); + + await act(async () => { + fireEvent.click(testCaseCard as Element); + }); + + await act(async () => { + fireEvent.click(testCaseCard as Element); + }); + + await waitFor(() => { + expect(onChange).toHaveBeenLastCalledWith([]); + }); + + const checkbox = screen.getByTestId('checkbox-test_case_1'); + + expect(checkbox).toHaveProperty('checked', false); + }); + + it('handles multiple test case selections', async () => { + const onChange = jest.fn(); + + await act(async () => { + renderWithRouter({ ...mockProps, onChange }); + }); + + await waitFor(() => { + expect(screen.getByTestId('test_case_1')).toBeInTheDocument(); + }); + + const testCaseCard1 = screen + .getByTestId('test_case_1') + .closest('.cursor-pointer'); + const testCaseCard2 = screen + .getByTestId('test_case_2') + .closest('.cursor-pointer'); + + expect(testCaseCard1).not.toBeNull(); + expect(testCaseCard2).not.toBeNull(); + + await act(async () => { + fireEvent.click(testCaseCard1 as Element); + }); + + await act(async () => { + fireEvent.click(testCaseCard2 as Element); + }); + + await waitFor(() => { + expect(onChange).toHaveBeenLastCalledWith([ + mockTestCases[0], + mockTestCases[1], + ]); + }); + }); + + it('pre-selects test cases when selectedTest prop is provided', async () => { + mockGetListTestCaseBySearch.mockResolvedValue({ + data: [mockTestCases[0], mockTestCases[1]], + paging: { + total: 2, + }, + }); + + await act(async () => { + renderWithRouter({ + ...mockProps, + selectedTest: ['test_case_1', 'test_case_2'], + }); + }); + + await waitFor(() => { + const checkbox1 = screen.getByTestId('checkbox-test_case_1'); + const checkbox2 = screen.getByTestId('checkbox-test_case_2'); + + expect(checkbox1).toHaveProperty('checked', true); + expect(checkbox2).toHaveProperty('checked', true); + }); + }); + + it('handles test cases without id gracefully', async () => { + const testCasesWithoutId = [{ ...mockTestCases[0], id: undefined }]; + + mockGetListTestCaseBySearch.mockResolvedValue({ + data: testCasesWithoutId, + paging: { + total: 1, + }, + }); + + const onChange = jest.fn(); + + await act(async () => { + renderWithRouter({ ...mockProps, onChange }); + }); + + await waitFor(() => { + expect(screen.getByTestId('test_case_1')).toBeInTheDocument(); + }); + + const testCaseCard = screen + .getByTestId('test_case_1') + .closest('.cursor-pointer'); + + expect(testCaseCard).not.toBeNull(); + + await act(async () => { + fireEvent.click(testCaseCard as Element); + }); + + expect(onChange).not.toHaveBeenCalled(); + }); + }); + + describe('Pagination and virtual list', () => { + it('fetches data with correct pagination parameters', async () => { + mockGetListTestCaseBySearch.mockResolvedValue({ + data: mockTestCases.slice(0, 2), + paging: { + total: 3, + }, + }); + + renderWithRouter(mockProps); + + await waitFor(() => { + expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({ + q: '*', + limit: 25, + offset: 0, + }); + }); + + await waitFor(() => { + expect(screen.getByTestId('test_case_1')).toBeInTheDocument(); + expect(screen.getByTestId('test_case_2')).toBeInTheDocument(); + }); + }); + + it('maintains search term with API calls', async () => { + mockGetListTestCaseBySearch.mockResolvedValue({ + data: [mockTestCases[0]], + paging: { + total: 2, + }, + }); + + renderWithRouter(mockProps); + + const searchBar = screen.getByTestId('search-bar'); + + await act(async () => { + fireEvent.change(searchBar, { target: { value: 'specific_test' } }); + }); + + await waitFor(() => { + expect(mockGetListTestCaseBySearch).toHaveBeenCalledWith({ + q: 'specific_test', + limit: 25, + offset: 0, + }); + }); + }); + + it('uses virtual list for performance optimization', async () => { + mockGetListTestCaseBySearch.mockResolvedValue({ + data: mockTestCases, + paging: { + total: 100, + }, + }); + + const { container } = renderWithRouter(mockProps); + + await waitFor(() => { + expect(screen.getByTestId('test_case_1')).toBeInTheDocument(); + }); + + const virtualList = container.querySelector('.rc-virtual-list-holder'); + + expect(virtualList).toBeInTheDocument(); + }); + }); + + describe('Submit functionality', () => { + it('submits selected test cases', async () => { + mockGetListTestCaseBySearch.mockResolvedValue({ + data: mockTestCases, + paging: { + total: 3, + }, + }); + + const onSubmit = jest.fn(); + + await act(async () => { + renderWithRouter({ ...mockProps, onSubmit }); + }); + + await waitFor(() => { + expect(screen.getByTestId('test_case_1')).toBeInTheDocument(); + }); + + const testCaseCard1 = screen + .getByTestId('test_case_1') + .closest('.cursor-pointer'); + const testCaseCard2 = screen + .getByTestId('test_case_2') + .closest('.cursor-pointer'); + + expect(testCaseCard1).not.toBeNull(); + expect(testCaseCard2).not.toBeNull(); + + await act(async () => { + fireEvent.click(testCaseCard1 as Element); + fireEvent.click(testCaseCard2 as Element); + }); + + const submitBtn = screen.getByTestId('submit'); + + await act(async () => { + fireEvent.click(submitBtn); + }); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledWith([ + mockTestCases[0], + mockTestCases[1], + ]); + }); + }); + + it('handles async submit operations', async () => { + mockGetListTestCaseBySearch.mockResolvedValue({ + data: mockTestCases, + paging: { + total: 3, + }, + }); + + const onSubmit = jest + .fn() + .mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 100)) + ); + + await act(async () => { + renderWithRouter({ ...mockProps, onSubmit }); + }); + + await waitFor(() => { + expect(screen.getByTestId('test_case_1')).toBeInTheDocument(); + }); + + const submitBtn = screen.getByTestId('submit'); + + await act(async () => { + fireEvent.click(submitBtn); + }); + + await waitFor(() => { + const loader = queryByAttribute('aria-label', submitBtn, 'loading'); + + expect(loader).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalled(); + }); + }); + }); + + describe('Column test cases', () => { + it('displays column information for column test cases', async () => { + const columnTestCase: TestCase = { + id: 'column-test', + name: 'column_test', + displayName: 'Column Test', + entityLink: '<#E::table::sample.table::columns::user_id>', + testDefinition: { + id: 'test-def', + name: 'column_values_to_be_unique', + displayName: 'Column Values To Be Unique', + }, + } as TestCase; + + mockGetListTestCaseBySearch.mockResolvedValue({ + data: [columnTestCase], + paging: { + total: 1, + }, + }); + + await act(async () => { + renderWithRouter(mockProps); + }); + + await waitFor(() => { + expect(screen.getByTestId('column_test')).toBeInTheDocument(); + }); + + expect(screen.getByText('label.column:')).toBeInTheDocument(); + }); + + it('does not display column information for table test cases', async () => { + const tableTestCase: TestCase = { + id: 'table-test', + name: 'table_test', + displayName: 'Table Test', + entityLink: '<#E::table::sample.table>', + testDefinition: { + id: 'test-def', + name: 'table_row_count_to_be_between', + displayName: 'Table Row Count To Be Between', + }, + } as TestCase; + + mockGetListTestCaseBySearch.mockResolvedValue({ + data: [tableTestCase], + paging: { + total: 1, + }, + }); + + await act(async () => { + renderWithRouter(mockProps); + }); + + await waitFor(() => { + expect(screen.getByTestId('table_test')).toBeInTheDocument(); + }); + + expect(screen.queryByText('label.column:')).not.toBeInTheDocument(); + }); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseList.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseList.component.tsx index b1ff5ead48b..3d78fb0cade 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseList.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseList.component.tsx @@ -70,7 +70,9 @@ export const AddTestCaseList = ({ setIsLoading(true); const testCaseResponse = await getListTestCaseBySearch({ - q: filters ? `${searchText} && ${filters}` : searchText, + q: filters + ? `${searchText || WILD_CARD_CHAR} && ${filters}` + : searchText, limit: PAGE_SIZE_MEDIUM, offset: (page - 1) * PAGE_SIZE_MEDIUM, ...(testCaseParams ?? {}),