diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts index 689d395c030c..f6731edac01a 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseResultPermissions.spec.ts @@ -218,6 +218,59 @@ test.describe( } }); + test('User with TEST_CASE.VIEW_ALL can view library test SQL expression in UI', async ({ + viewResultsPage, + }) => { + const sqlExpression = 'SELECT COUNT(*) FROM {table}'; + let testDefinitionRequestUrl = ''; + + await viewResultsPage.route( + '**/api/v1/dataQuality/testDefinitions/*', + async (route) => { + testDefinitionRequestUrl = route.request().url(); + + await route.fulfill({ + status: 200, + contentType: 'application/json', + json: { + id: 'test-definition-id', + name: 'tableRowCountToBeBetween', + parameterDefinition: [], + sqlExpression, + }, + }); + } + ); + + const testDefinitionResponse = viewResultsPage.waitForResponse( + (response) => + response.url().includes('/api/v1/dataQuality/testDefinitions/') && + response.request().method() === 'GET' && + response.ok() + ); + + await visitTestCaseDetailsPage(viewResultsPage); + await testDefinitionResponse; + await waitForAllLoadersToDisappear(viewResultsPage); + + await expect( + viewResultsPage.getByTestId('test-case-result-tab-container') + ).toBeVisible(); + expect(decodeURIComponent(testDefinitionRequestUrl)).toContain( + 'sqlExpression' + ); + const sqlExpressionContainer = viewResultsPage.getByTestId( + 'sql-expression-container' + ); + await expect(sqlExpressionContainer).toBeVisible({ timeout: 15000 }); + await expect(sqlExpressionContainer).toContainText('SQL Expression'); + await expect(sqlExpressionContainer).toContainText(sqlExpression); + + await viewResultsPage.unroute( + '**/api/v1/dataQuality/testDefinitions/*' + ); + }); + test('User with TABLE.VIEW_TESTS can view test case and results in UI (alternative)', async ({ tableEditResultsPage, }) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx index 5a5b599e1c8a..c66749c8208c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx @@ -18,7 +18,6 @@ import { compare } from 'fast-json-patch'; import chunk from 'lodash/chunk'; import isEmpty from 'lodash/isEmpty'; import isUndefined from 'lodash/isUndefined'; -import startCase from 'lodash/startCase'; import toString from 'lodash/toString'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -102,7 +101,10 @@ const TestCaseResultTab = () => { if (testCaseData?.testDefinition?.id) { try { const definition = await getTestDefinitionById( - testCaseData.testDefinition.id + testCaseData.testDefinition.id, + { + fields: ['parameterDefinition', 'sqlExpression'], + } ); setTestDefinition(definition); } catch (error) { @@ -181,6 +183,30 @@ const TestCaseResultTab = () => { ); }, [testCaseData?.parameterValues]); + const sqlExpressionItems = useMemo(() => { + if (!isEmpty(withSqlParams)) { + return withSqlParams.map((param) => ({ + isEditable: true, + key: param.name ?? 'sqlExpression', + label: t('label.sql-expression'), + value: param.value ?? '', + })); + } + + if (!isEmpty(testDefinition?.sqlExpression)) { + return [ + { + isEditable: false, + key: 'test-definition-sql-expression', + label: t('label.sql-expression'), + value: testDefinition?.sqlExpression ?? '', + }, + ]; + } + + return []; + }, [testDefinition?.sqlExpression, t, withSqlParams]); + const handleTagSelection = async (selectedTags: EntityTags[]) => { if (!testCaseData) { return; @@ -489,21 +515,21 @@ const TestCaseResultTab = () => { - {!isUndefined(withSqlParams) && !isVersionPage ? ( + {!isEmpty(sqlExpressionItems) && !isVersionPage ? (
- {withSqlParams.map((param) => ( + {sqlExpressionItems.map((param) => (
+ key={param.key}>
- {startCase(param.name)} + {param.label} - {hasEditPermission && ( + {hasEditPermission && param.isEditable && ( { styleActiveLine: false, readOnly: true, }} - value={param.value ?? ''} + value={param.value} />
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.test.tsx index c65288135ef1..d67c778673a0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.test.tsx @@ -103,11 +103,36 @@ jest.mock('../../../common/EntityDescription/DescriptionV1', () => { return jest.fn().mockImplementation(() =>
DescriptionV1
); }); jest.mock('../../../Database/SchemaEditor/SchemaEditor', () => { - return jest.fn().mockImplementation(() =>
SchemaEditor
); + return jest + .fn() + .mockImplementation(({ value }) => ( +
SchemaEditor: {value}
+ )); }); jest.mock('../../../Database/Profiler/TestSummary/TestSummary', () => { return jest.fn().mockImplementation(() =>
TestSummary
); }); +jest.mock('../../../../utils/EntityVersionUtils', () => ({ + getComputeRowCountDiffDisplay: jest.fn(), + getEntityVersionByField: jest.fn(), + getEntityVersionTags: jest.fn().mockReturnValue([]), + getParameterValueDiffDisplay: jest.fn(), +})); +jest.mock('../../../../utils/TableUtils', () => ({ + getTagsWithoutTier: jest + .fn() + .mockImplementation((tags = []) => + tags.filter((tag: TagLabel) => !tag.tagFQN?.startsWith('Tier.')) + ), + getTierTags: jest + .fn() + .mockImplementation((tags = []) => + tags.find((tag: TagLabel) => tag.tagFQN?.startsWith('Tier.')) + ), +})); +jest.mock('../../../../utils/TagsUtils', () => ({ + createTagObject: jest.fn().mockImplementation((tags) => tags), +})); jest.mock('../../AddDataQualityTest/EditTestCaseModal', () => { return jest.fn().mockImplementation(({ onUpdate, testCase, onCancel }) => (
@@ -127,10 +152,10 @@ const mockGetTestDefinitionById = jest.fn(); jest.mock('../../../../rest/testAPI', () => ({ updateTestCaseById: jest .fn() - .mockImplementation(() => mockUpdateTestCaseById()), + .mockImplementation((...args) => mockUpdateTestCaseById(...args)), getTestDefinitionById: jest .fn() - .mockImplementation(() => mockGetTestDefinitionById()), + .mockImplementation((...args) => mockGetTestDefinitionById(...args)), TestCaseType: { all: 'all', table: 'table', @@ -163,6 +188,8 @@ describe('TestCaseResultTab', () => { ); mockUseTestCaseStore.testCase.useDynamicAssertion = undefined; mockUseTestCaseStore.testCase.computePassedFailedRowCount = undefined; + mockGetTestDefinitionById.mockReset(); + mockUpdateTestCaseById.mockReset(); }); it('Should render component', async () => { @@ -199,6 +226,67 @@ describe('TestCaseResultTab', () => { expect(await screen.findByText('EditTestCaseModal')).toBeInTheDocument(); }); + it('Should fetch test definition with sqlExpression field', async () => { + mockGetTestDefinitionById.mockResolvedValue({ + id: '48063740-ac35-4854-9ab3-b1b542c820fe', + name: 'tableColumnCountToEqual', + }); + + render(); + + await screen.findByTestId('test-case-result-tab-container'); + + expect(mockGetTestDefinitionById).toHaveBeenCalledWith( + '48063740-ac35-4854-9ab3-b1b542c820fe', + { + fields: ['parameterDefinition', 'sqlExpression'], + } + ); + }); + + it('Should render SQL expression from test definition for library based test', async () => { + mockUseTestCaseStore.testCase.parameterValues = [ + { + name: 'columnCount', + value: '10', + }, + ]; + mockGetTestDefinitionById.mockResolvedValue({ + id: '48063740-ac35-4854-9ab3-b1b542c820fe', + name: 'tableColumnCountToEqual', + sqlExpression: 'SELECT COUNT(*) FROM {table}', + }); + + render(); + + expect(await screen.findByText('label.sql-expression')).toBeInTheDocument(); + expect( + await screen.findByText('SchemaEditor: SELECT COUNT(*) FROM {table}') + ).toBeInTheDocument(); + expect(screen.queryByTestId('edit-sql-param-icon')).not.toBeInTheDocument(); + }); + + it('Should prefer test case SQL expression over test definition SQL expression', async () => { + mockUseTestCaseStore.testCase.parameterValues = [ + { name: 'sqlExpression', value: 'select * from dim_address' }, + ]; + mockGetTestDefinitionById.mockResolvedValue({ + id: '48063740-ac35-4854-9ab3-b1b542c820fe', + name: 'tableColumnCountToEqual', + sqlExpression: 'SELECT COUNT(*) FROM {table}', + }); + + render(); + + expect( + await screen.findByText('SchemaEditor: select * from dim_address') + ).toBeInTheDocument(); + expect( + screen.queryByText('SchemaEditor: SELECT COUNT(*) FROM {table}') + ).not.toBeInTheDocument(); + expect(screen.getByTestId('edit-sql-param-icon')).toBeInTheDocument(); + }); + it('EditTestCaseModal should be removed on cancel click', async () => { const { container } = render();