diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/TestCaseVersionPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/TestCaseVersionPage.spec.ts new file mode 100644 index 00000000000..3d7bf3f601b --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/TestCaseVersionPage.spec.ts @@ -0,0 +1,154 @@ +/* + * Copyright 2025 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 { expect, test } from '@playwright/test'; +import { TableClass } from '../../support/entity/TableClass'; +import { + createNewPage, + descriptionBox, + redirectToHomePage, +} from '../../utils/common'; + +// use the admin user to login +test.use({ storageState: 'playwright/.auth/admin.json' }); + +test.describe('TestCase Version Page', () => { + const table1 = new TableClass(); + + test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + + await table1.create(apiContext); + await table1.createTestCase(apiContext); + + await afterAction(); + }); + + test.afterAll('Cleanup', async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + + await table1.delete(apiContext); + + await afterAction(); + }); + + test('should show the test case version page', async ({ page }) => { + const testCase = table1.testCasesResponseData[0]; + + await redirectToHomePage(page); + await page.goto(`/test-case/${testCase.fullyQualifiedName}`); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + await test.step('Display name change', async () => { + await expect(page.getByTestId('entity-header-name')).toHaveText( + testCase.name + ); + + await expect(page.getByTestId('version-button')).toBeVisible(); + await expect(page.getByTestId('version-button')).toHaveText('0.1'); + + await page.getByTestId('manage-button').click(); + await page.getByTestId('rename-button').click(); + + await page.waitForSelector('#displayName'); + await page.fill('#displayName', 'test-case-version-changed'); + const updateNameRes = page.waitForResponse( + '/api/v1/dataQuality/testCases/*' + ); + await page.getByTestId('save-button').click(); + await updateNameRes; + + await expect(page.getByTestId('version-button')).toHaveText('0.2'); + + await page.getByTestId('version-button').click(); + await page.waitForLoadState('networkidle'); + + await expect( + page.getByTestId('entity-header-display-name').getByTestId('diff-added') + ).toHaveText('test-case-version-changed'); + + await page.getByTestId('version-button').click(); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + }); + + await test.step('Description change', async () => { + await page.getByTestId('edit-description').click(); + await page.waitForSelector('[data-testid="editor"]'); + + await page.fill(descriptionBox, 'test case description changed'); + const updateDescriptionRes = page.waitForResponse( + '/api/v1/dataQuality/testCases/*' + ); + await page.getByTestId('save').click(); + await updateDescriptionRes; + + await expect(page.getByTestId('version-button')).toHaveText('0.3'); + + await page.getByTestId('version-button').click(); + await page.waitForLoadState('networkidle'); + + await expect( + page + .getByTestId('asset-description-container') + .getByTestId('markdown-parser') + .getByTestId('diff-added') + ).toHaveText('test case description changed'); + + await page.getByTestId('version-button').click(); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + }); + + await test.step('Parameter change', async () => { + await page.getByTestId('edit-parameter-icon').click(); + await page.waitForSelector('#tableTestForm'); + + await page.locator('#tableTestForm_params_minValue').clear(); + await page.locator('#tableTestForm_params_minValue').fill('20'); + await page.locator('#tableTestForm_params_maxValue').clear(); + await page.locator('#tableTestForm_params_maxValue').fill('40'); + + const updateParameterRes = page.waitForResponse( + '/api/v1/dataQuality/testCases/*' + ); + await page.getByRole('button', { name: 'Submit' }).click(); + await updateParameterRes; + + await expect(page.getByTestId('version-button')).toHaveText('0.4'); + + await page.getByTestId('version-button').click(); + await page.waitForLoadState('networkidle'); + + await expect( + page.getByTestId('minValue').getByTestId('diff-removed') + ).toHaveText('12'); + await expect( + page.getByTestId('minValue').getByTestId('diff-added') + ).toHaveText('20'); + + await expect( + page.getByTestId('maxValue').getByTestId('diff-removed') + ).toHaveText('34'); + await expect( + page.getByTestId('maxValue').getByTestId('diff-added') + ).toHaveText('40'); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx index a434549671a..f8e7cb6e6a2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx @@ -240,6 +240,12 @@ const IncidentManagerDetailPage = withSuspenseFallback( ) ); +const TestCaseVersionPage = withSuspenseFallback( + React.lazy( + () => import('../../pages/TestCaseVersionPage/TestCaseVersionPage') + ) +); + const ObservabilityAlertsPage = withSuspenseFallback( React.lazy( () => import('../../pages/ObservabilityAlertsPage/ObservabilityAlertsPage') @@ -461,9 +467,18 @@ const AuthenticatedAppRouter: FunctionComponent = () => { ResourceEntity.TEST_CASE, permissions )} + path={[ROUTES.TEST_CASE_DETAILS, ROUTES.TEST_CASE_DETAILS_WITH_TAB]} + /> + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader.interface.ts index cab9c740105..227287797af 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader.interface.ts @@ -16,6 +16,7 @@ import { APICollection } from '../../../generated/entity/data/apiCollection'; import { Database } from '../../../generated/entity/data/database'; import { DatabaseSchema } from '../../../generated/entity/data/databaseSchema'; import { EntityReference } from '../../../generated/entity/type'; +import { TestCase } from '../../../generated/tests/testCase'; import { ServicesType } from '../../../interface/service.interface'; import { VersionData } from '../../../pages/EntityVersionPage/EntityVersionPage.component'; import { TitleBreadcrumbProps } from '../../common/TitleBreadcrumb/TitleBreadcrumb.interface'; @@ -31,7 +32,8 @@ export interface DataAssetsVersionHeaderProps { | ServicesType | Database | DatabaseSchema - | APICollection; + | APICollection + | TestCase; ownerDisplayName: React.ReactNode[]; domainDisplayName?: React.ReactNode; tierDisplayName: React.ReactNode; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/EditTestCaseModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/EditTestCaseModal.tsx index 4ec2ec22f2c..1d45bc0014a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/EditTestCaseModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/EditTestCaseModal.tsx @@ -112,7 +112,9 @@ const EditTestCaseModal: React.FC = ({ : isEmpty(value.description) ? undefined : value.description, - displayName: value.displayName, + displayName: showOnlyParameter + ? testCase?.displayName + : value.displayName, computePassedFailedRowCount: isComputeRowCountFieldVisible ? value.computePassedFailedRowCount : testCase?.computePassedFailedRowCount, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component.tsx index 0ad7bfc439b..25c9cf0e523 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component.tsx @@ -25,7 +25,10 @@ import { Thread, ThreadTaskStatus, } from '../../../../generated/entity/feed/thread'; -import { EntityReference } from '../../../../generated/tests/testCase'; +import { + ChangeDescription, + EntityReference, +} from '../../../../generated/tests/testCase'; import { Severities, TestCaseResolutionStatus, @@ -55,11 +58,13 @@ import { IncidentManagerPageHeaderProps } from './IncidentManagerPageHeader.inte import { ReactComponent as InternalLinkIcon } from '../../../../assets/svg/InternalIcons.svg'; +import { getCommonExtraInfoForVersionDetails } from '../../../../utils/EntityVersionUtils'; import { getTaskDetailPath } from '../../../../utils/TasksUtils'; import './incident-manager.less'; const IncidentManagerPageHeader = ({ onOwnerUpdate, fetchTaskCount, + isVersionPage = false, }: IncidentManagerPageHeaderProps) => { const { t } = useTranslation(); const [activeTask, setActiveTask] = useState(); @@ -78,6 +83,13 @@ const IncidentManagerPageHeader = ({ updateTestCaseIncidentStatus, } = useActivityFeedProvider(); + const { ownerDisplayName, ownerRef } = useMemo(() => { + return getCommonExtraInfoForVersionDetails( + testCaseData?.changeDescription as ChangeDescription, + testCaseData?.owners + ); + }, [testCaseData?.changeDescription, testCaseData?.owners]); + const columnName = useMemo(() => { const isColumn = testCaseData?.entityLink.includes('::columns::'); if (isColumn) { @@ -161,7 +173,7 @@ const IncidentManagerPageHeader = ({ const { data } = await getListTestCaseIncidentByStateId(id); setTestCaseStatusData(first(data)); - } catch (error) { + } catch { setTestCaseStatusData(undefined); } }; @@ -212,13 +224,18 @@ const IncidentManagerPageHeader = ({ }, [testCaseData]); const { hasEditStatusPermission, hasEditOwnerPermission } = useMemo(() => { - return { - hasEditStatusPermission: - testCasePermission?.EditAll || testCasePermission?.EditStatus, - hasEditOwnerPermission: - testCasePermission?.EditAll || testCasePermission?.EditOwners, - }; - }, [testCasePermission]); + return isVersionPage + ? { + hasEditStatusPermission: false, + hasEditOwnerPermission: false, + } + : { + hasEditStatusPermission: + testCasePermission?.EditAll || testCasePermission?.EditStatus, + hasEditOwnerPermission: + testCasePermission?.EditAll || testCasePermission?.EditOwners, + }; + }, [testCasePermission, isVersionPage]); const statusDetails = useMemo(() => { if (isLoading) { @@ -310,10 +327,11 @@ const IncidentManagerPageHeader = ({ - {statusDetails} + {!isVersionPage && statusDetails} {tableFqn && ( <> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.interface.ts index eda647b61cd..44a46a0708d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.interface.ts @@ -17,4 +17,5 @@ export interface IncidentManagerPageHeaderProps { onOwnerUpdate: (owner?: EntityReference[]) => Promise; testCaseData?: TestCase; fetchTaskCount: () => void; + isVersionPage?: boolean; } 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 a8cf02ab192..36eeb0c7e4b 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 @@ -12,26 +12,31 @@ */ import Icon from '@ant-design/icons/lib/components/Icon'; -import { Col, Divider, Row, Space, Tooltip, Typography } from 'antd'; +import { Col, Divider, Row, Space, Typography } from 'antd'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; import { isEmpty, isUndefined, startCase } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg'; -import { - DE_ACTIVE_COLOR, - ICON_DIMENSION, -} from '../../../../constants/constants'; import { CSMode } from '../../../../enums/codemirror.enum'; import { EntityType } from '../../../../enums/entity.enum'; +import { useParams } from 'react-router-dom'; import { ReactComponent as StarIcon } from '../../../../assets/svg/ic-suggestions.svg'; -import { TestCaseParameterValue } from '../../../../generated/tests/testCase'; +import { EntityField } from '../../../../constants/Feeds.constants'; +import { + ChangeDescription, + TestCaseParameterValue, +} from '../../../../generated/tests/testCase'; import { useTestCaseStore } from '../../../../pages/IncidentManager/IncidentManagerDetailPage/useTestCase.store'; import { updateTestCaseById } from '../../../../rest/testAPI'; +import { + getEntityVersionByField, + getParameterValueDiffDisplay, +} from '../../../../utils/EntityVersionUtils'; import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils'; import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; +import { EditIconButton } from '../../../common/IconButtons/EditIconButton'; import TestSummary from '../../../Database/Profiler/TestSummary/TestSummary'; import SchemaEditor from '../../../Database/SchemaEditor/SchemaEditor'; import EditTestCaseModal from '../../AddDataQualityTest/EditTestCaseModal'; @@ -47,17 +52,24 @@ const TestCaseResultTab = () => { showAILearningBanner, testCasePermission, } = useTestCaseStore(); + const { version } = useParams<{ version: string }>(); + const isVersionPage = !isUndefined(version); const additionalComponent = testCaseResultTabClassBase.getAdditionalComponents(); const [isParameterEdit, setIsParameterEdit] = useState(false); const { hasEditPermission, hasEditDescriptionPermission } = useMemo(() => { - return { - hasEditPermission: testCasePermission?.EditAll, - hasEditDescriptionPermission: - testCasePermission?.EditAll || testCasePermission?.EditDescription, - }; - }, [testCasePermission]); + return isVersionPage + ? { + hasEditPermission: false, + hasEditDescriptionPermission: false, + } + : { + hasEditPermission: testCasePermission?.EditAll, + hasEditDescriptionPermission: + testCasePermission?.EditAll || testCasePermission?.EditDescription, + }; + }, [testCasePermission, isVersionPage]); const { withSqlParams, withoutSqlParams } = useMemo(() => { const params = testCaseData?.parameterValues ?? []; @@ -119,7 +131,28 @@ const TestCaseResultTab = () => { [] ); + const description = useMemo(() => { + return isVersionPage + ? getEntityVersionByField( + testCaseData?.changeDescription as ChangeDescription, + EntityField.DESCRIPTION, + testCaseData?.description + ) + : testCaseData?.description; + }, [ + testCaseData?.changeDescription, + testCaseData?.description, + isVersionPage, + ]); + const testCaseParams = useMemo(() => { + if (isVersionPage) { + return getParameterValueDiffDisplay( + testCaseData?.changeDescription as ChangeDescription, + testCaseData?.parameterValues + ); + } + if (testCaseData?.useDynamicAssertion) { return (