From 58ed12cf47c3a116b0ec8c6139d0a36dce2a0adf Mon Sep 17 00:00:00 2001 From: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:54:39 +0530 Subject: [PATCH] fix lineage playwright (#17889) * fix lineage playwright * remove in operator usage and use lodash get utility * fix metric playwright * fix lineage pw * update drag and drop * fix flakiness * minor sonar fix --------- Co-authored-by: Sachin Chaurasiya --- .../ui/playwright/e2e/Pages/Lineage.spec.ts | 303 +++++++++--------- .../resources/ui/playwright/utils/lineage.ts | 42 ++- .../DataAssetsHeader.component.tsx | 26 +- .../DataAssetsVersionHeader.tsx | 29 +- .../EntityInfoDrawer.component.tsx | 25 +- .../NodeSuggestions.component.tsx | 6 +- .../LineageProvider/LineageProvider.tsx | 8 +- .../ui/src/utils/EntityLineageUtils.tsx | 5 +- 8 files changed, 228 insertions(+), 216 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage.spec.ts index 73ad9fc00784..11f75bfb5f73 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage.spec.ts @@ -34,6 +34,7 @@ import { connectEdgeBetweenNodes, deleteEdge, deleteNode, + editLineage, performZoomOut, removeColumnLineage, setupEntitiesForLineage, @@ -52,6 +53,7 @@ const entities = [ MlModelClass, ContainerClass, SearchIndexClass, + ApiEndpointClass, MetricClass, ] as const; @@ -72,68 +74,66 @@ test.afterAll('Cleanup', async ({ browser }) => { for (const EntityClass of entities) { const defaultEntity = new EntityClass(); - test.fixme( - `Lineage creation from ${defaultEntity.getType()} entity`, - async ({ browser }) => { - test.slow(true); - - const { page } = await createNewPage(browser); - const { currentEntity, entities, cleanup } = - await setupEntitiesForLineage(page, defaultEntity); - - await test.step('Should create lineage for the entity', async () => { - await redirectToHomePage(page); - await currentEntity.visitEntityPage(page); - await visitLineageTab(page); - await verifyColumnLayerInactive(page); - await page.click('[data-testid="edit-lineage"]'); - await performZoomOut(page); - for (const entity of entities) { - await connectEdgeBetweenNodes(page, currentEntity, entity); - } - - await redirectToHomePage(page); - await currentEntity.visitEntityPage(page); - await visitLineageTab(page); - await page - .locator('.react-flow__controls-fitview') - .dispatchEvent('click'); - - for (const entity of entities) { - await verifyNodePresent(page, entity); - } - }); - - await test.step('Should create pipeline between entities', async () => { - await page.click('[data-testid="edit-lineage"]'); - await performZoomOut(page); - - for (const entity of entities) { - await applyPipelineFromModal(page, currentEntity, entity, pipeline); - } - }); - - await test.step( - 'Remove lineage between nodes for the entity', - async () => { - await redirectToHomePage(page); - await currentEntity.visitEntityPage(page); - await visitLineageTab(page); - await page.click('[data-testid="edit-lineage"]'); - await performZoomOut(page); - - for (const entity of entities) { - await deleteEdge(page, currentEntity, entity); - } - } - ); - - await cleanup(); - } - ); + test(`Lineage creation from ${defaultEntity.getType()} entity`, async ({ + browser, + }) => { + test.slow(true); + + const { page } = await createNewPage(browser); + const { currentEntity, entities, cleanup } = await setupEntitiesForLineage( + page, + defaultEntity + ); + + await test.step('Should create lineage for the entity', async () => { + await redirectToHomePage(page); + await currentEntity.visitEntityPage(page); + await visitLineageTab(page); + await verifyColumnLayerInactive(page); + await editLineage(page); + await performZoomOut(page); + for (const entity of entities) { + await connectEdgeBetweenNodes(page, currentEntity, entity); + } + + await redirectToHomePage(page); + await currentEntity.visitEntityPage(page); + await visitLineageTab(page); + await page + .locator('.react-flow__controls-fitview') + .dispatchEvent('click'); + + for (const entity of entities) { + await verifyNodePresent(page, entity); + } + }); + + await test.step('Should create pipeline between entities', async () => { + await editLineage(page); + await performZoomOut(page); + + for (const entity of entities) { + await applyPipelineFromModal(page, currentEntity, entity, pipeline); + } + }); + + await test.step('Remove lineage between nodes for the entity', async () => { + await redirectToHomePage(page); + await currentEntity.visitEntityPage(page); + await visitLineageTab(page); + await editLineage(page); + await performZoomOut(page); + + for (const entity of entities) { + await deleteEdge(page, currentEntity, entity); + } + }); + + await cleanup(); + }); } -test.fixme('Verify column lineage between tables', async ({ browser }) => { +test('Verify column lineage between tables', async ({ browser }) => { const { page } = await createNewPage(browser); const { apiContext, afterAction } = await getApiContext(page); const table1 = new TableClass(); @@ -171,117 +171,112 @@ test.fixme('Verify column lineage between tables', async ({ browser }) => { await afterAction(); }); -test.fixme( - 'Verify column lineage between table and topic', - async ({ browser }) => { - const { page } = await createNewPage(browser); - const { apiContext, afterAction } = await getApiContext(page); - const table = new TableClass(); - const topic = new TopicClass(); - await table.create(apiContext); - await topic.create(apiContext); - - const sourceTableFqn = get(table, 'entityResponseData.fullyQualifiedName'); - const sourceCol = `${sourceTableFqn}.${get( - table, - 'entityResponseData.columns[0].name' - )}`; - const targetCol = get( - topic, - 'entityResponseData.messageSchema.schemaFields[0].children[0].fullyQualifiedName' - ); +test('Verify column lineage between table and topic', async ({ browser }) => { + const { page } = await createNewPage(browser); + const { apiContext, afterAction } = await getApiContext(page); + const table = new TableClass(); + const topic = new TopicClass(); + await table.create(apiContext); + await topic.create(apiContext); - await addPipelineBetweenNodes(page, table, topic); - await activateColumnLayer(page); + const sourceTableFqn = get(table, 'entityResponseData.fullyQualifiedName'); + const sourceCol = `${sourceTableFqn}.${get( + table, + 'entityResponseData.columns[0].name' + )}`; + const targetCol = get( + topic, + 'entityResponseData.messageSchema.schemaFields[0].children[0].fullyQualifiedName' + ); - // Add column lineage - await addColumnLineage(page, sourceCol, targetCol); - await page.click('[data-testid="edit-lineage"]'); + await addPipelineBetweenNodes(page, table, topic); + await activateColumnLayer(page); - await removeColumnLineage(page, sourceCol, targetCol); - await page.click('[data-testid="edit-lineage"]'); + // Add column lineage + await addColumnLineage(page, sourceCol, targetCol); + await page.click('[data-testid="edit-lineage"]'); - await deleteNode(page, topic); - await table.delete(apiContext); - await topic.delete(apiContext); + await removeColumnLineage(page, sourceCol, targetCol); + await page.click('[data-testid="edit-lineage"]'); - await afterAction(); - } -); + await deleteNode(page, topic); + await table.delete(apiContext); + await topic.delete(apiContext); -test.fixme( - 'Verify column lineage between topic and api endpoint', - async ({ browser }) => { - const { page } = await createNewPage(browser); - const { apiContext, afterAction } = await getApiContext(page); - const topic = new TopicClass(); - const apiEndpoint = new ApiEndpointClass(); + await afterAction(); +}); - await topic.create(apiContext); - await apiEndpoint.create(apiContext); +test('Verify column lineage between topic and api endpoint', async ({ + browser, +}) => { + const { page } = await createNewPage(browser); + const { apiContext, afterAction } = await getApiContext(page); + const topic = new TopicClass(); + const apiEndpoint = new ApiEndpointClass(); - const sourceCol = get( - topic, - 'entityResponseData.messageSchema.schemaFields[0].children[0].fullyQualifiedName' - ); + await topic.create(apiContext); + await apiEndpoint.create(apiContext); - const targetCol = get( - apiEndpoint, - 'entityResponseData.responseSchema.schemaFields[0].children[1].fullyQualifiedName' - ); + const sourceCol = get( + topic, + 'entityResponseData.messageSchema.schemaFields[0].children[0].fullyQualifiedName' + ); - await addPipelineBetweenNodes(page, topic, apiEndpoint); - await activateColumnLayer(page); + const targetCol = get( + apiEndpoint, + 'entityResponseData.responseSchema.schemaFields[0].children[1].fullyQualifiedName' + ); - // Add column lineage - await addColumnLineage(page, sourceCol, targetCol); - await page.click('[data-testid="edit-lineage"]'); + await addPipelineBetweenNodes(page, topic, apiEndpoint); + await activateColumnLayer(page); - await removeColumnLineage(page, sourceCol, targetCol); - await page.click('[data-testid="edit-lineage"]'); + // Add column lineage + await addColumnLineage(page, sourceCol, targetCol); + await page.click('[data-testid="edit-lineage"]'); - await deleteNode(page, apiEndpoint); - await topic.delete(apiContext); - await apiEndpoint.delete(apiContext); + await removeColumnLineage(page, sourceCol, targetCol); + await page.click('[data-testid="edit-lineage"]'); - await afterAction(); - } -); + await deleteNode(page, apiEndpoint); + await topic.delete(apiContext); + await apiEndpoint.delete(apiContext); -test.fixme( - 'Verify column lineage between table and api endpoint', - async ({ browser }) => { - const { page } = await createNewPage(browser); - const { apiContext, afterAction } = await getApiContext(page); - const table = new TableClass(); - const apiEndpoint = new ApiEndpointClass(); - await table.create(apiContext); - await apiEndpoint.create(apiContext); - - const sourceTableFqn = get(table, 'entityResponseData.fullyQualifiedName'); - const sourceCol = `${sourceTableFqn}.${get( - table, - 'entityResponseData.columns[0].name' - )}`; - const targetCol = get( - apiEndpoint, - 'entityResponseData.responseSchema.schemaFields[0].children[0].fullyQualifiedName' - ); + await afterAction(); +}); - await addPipelineBetweenNodes(page, table, apiEndpoint); - await activateColumnLayer(page); +test('Verify column lineage between table and api endpoint', async ({ + browser, +}) => { + const { page } = await createNewPage(browser); + const { apiContext, afterAction } = await getApiContext(page); + const table = new TableClass(); + const apiEndpoint = new ApiEndpointClass(); + await table.create(apiContext); + await apiEndpoint.create(apiContext); - // Add column lineage - await addColumnLineage(page, sourceCol, targetCol); - await page.click('[data-testid="edit-lineage"]'); + const sourceTableFqn = get(table, 'entityResponseData.fullyQualifiedName'); + const sourceCol = `${sourceTableFqn}.${get( + table, + 'entityResponseData.columns[0].name' + )}`; + const targetCol = get( + apiEndpoint, + 'entityResponseData.responseSchema.schemaFields[0].children[0].fullyQualifiedName' + ); - await removeColumnLineage(page, sourceCol, targetCol); - await page.click('[data-testid="edit-lineage"]'); + await addPipelineBetweenNodes(page, table, apiEndpoint); + await activateColumnLayer(page); - await deleteNode(page, apiEndpoint); - await table.delete(apiContext); - await apiEndpoint.delete(apiContext); + // Add column lineage + await addColumnLineage(page, sourceCol, targetCol); + await page.click('[data-testid="edit-lineage"]'); - await afterAction(); - } -); + await removeColumnLineage(page, sourceCol, targetCol); + await page.click('[data-testid="edit-lineage"]'); + + await deleteNode(page, apiEndpoint); + await table.delete(apiContext); + await apiEndpoint.delete(apiContext); + + await afterAction(); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts index 3dfffaeb1f70..982ff7f664a7 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts @@ -37,12 +37,20 @@ export const activateColumnLayer = async (page: Page) => { await page.click('[data-testid="lineage-layer-column-btn"]'); }; +export const editLineage = async (page: Page) => { + await page.click('[data-testid="edit-lineage"]'); + + await expect( + page.getByTestId('table_search_index-draggable-icon') + ).toBeVisible(); +}; + export const performZoomOut = async (page: Page) => { - for (let i = 0; i < 5; i++) { - const zoomOutBtn = page.locator('.react-flow__controls-zoomout'); - const enabled = await zoomOutBtn.isEnabled(); - if (enabled) { - zoomOutBtn.dispatchEvent('click'); + const zoomOutBtn = page.locator('.react-flow__controls-zoomout'); + const enabled = await zoomOutBtn.isEnabled(); + if (enabled) { + for (const _ of Array.from({ length: 8 })) { + await zoomOutBtn.dispatchEvent('click'); } } }; @@ -78,6 +86,20 @@ export const deleteEdge = async ( await deleteRes; }; +export const dragAndDropNode = async ( + page: Page, + originSelector: string, + destinationSelector: string +) => { + const destinationElement = await page.waitForSelector(destinationSelector); + await page.hover(originSelector); + await page.mouse.down(); + const box = (await destinationElement.boundingBox())!; + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await destinationElement.hover(); + await page.mouse.up(); +}; + export const dragConnection = async ( page: Page, sourceId: string, @@ -109,10 +131,10 @@ export const connectEdgeBetweenNodes = async ( const toNodeName = get(toNode, 'entityResponseData.name'); const toNodeFqn = get(toNode, 'entityResponseData.fullyQualifiedName'); - await page.locator(`[data-testid="${type}-draggable-icon"]`).hover(); - await page.mouse.down(); - await page.locator('[data-testid="lineage-details"]').hover(); - await page.mouse.up(); + const source = `[data-testid="${type}-draggable-icon"]`; + const target = '[data-testid="lineage-details"]'; + + await dragAndDropNode(page, source, target); await page.locator('[data-testid="suggestion-node"]').dispatchEvent('click'); @@ -370,7 +392,7 @@ export const addPipelineBetweenNodes = async ( ) => { await sourceEntity.visitEntityPage(page); await page.click('[data-testid="lineage"]'); - await page.click('[data-testid="edit-lineage"]'); + await editLineage(page); await performZoomOut(page); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx index 9f10daa6a9a4..41903907db34 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx @@ -14,7 +14,7 @@ import Icon from '@ant-design/icons'; import { Button, Col, Divider, Row, Space, Tooltip, Typography } from 'antd'; import ButtonGroup from 'antd/lib/button/button-group'; import { AxiosError } from 'axios'; -import { capitalize, isEmpty } from 'lodash'; +import { capitalize, get, isEmpty } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; @@ -161,18 +161,18 @@ export const DataAssetsHeader = ({ const [isBreadcrumbLoading, setIsBreadcrumbLoading] = useState(false); const [isFollowingLoading, setIsFollowingLoading] = useState(false); const history = useHistory(); - const icon = useMemo( - () => - 'serviceType' in dataAsset ? ( - - ) : null, - [dataAsset] - ); + const icon = useMemo(() => { + const serviceType = get(dataAsset, 'serviceType', ''); + + return serviceType ? ( + + ) : null; + }, [dataAsset]); const [copyTooltip, setCopyTooltip] = useState(); const excludeEntityService = useMemo( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader.tsx index b2578cdd2f5d..8aa3322778fa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader.tsx @@ -13,7 +13,7 @@ import Icon from '@ant-design/icons/lib/components/Icon'; import { Button, Col, Divider, Row, Space, Tooltip, Typography } from 'antd'; -import { isEmpty } from 'lodash'; +import { get, isEmpty } from 'lodash'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as VersionIcon } from '../../../assets/svg/ic-version.svg'; @@ -89,13 +89,20 @@ function DataAssetsVersionHeader({ () => getDataAssetsVersionHeaderInfo(entityType, currentVersionData), [entityType, currentVersionData] ); - const logo = useMemo( - () => - serviceUtilClassBase.getServiceTypeLogo( - currentVersionData as SearchSourceAlias - ), - [currentVersionData] - ); + + const icon = useMemo(() => { + const serviceType = get(currentVersionData, 'serviceType', ''); + + return serviceType ? ( + service-icon + ) : null; + }, [currentVersionData]); return ( @@ -108,11 +115,7 @@ function DataAssetsVersionHeader({ - ) - } + icon={icon} name={currentVersionData?.name} serviceName={serviceName ?? ''} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityInfoDrawer/EntityInfoDrawer.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityInfoDrawer/EntityInfoDrawer.component.tsx index f2a79402800c..a41c7a8818c0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityInfoDrawer/EntityInfoDrawer.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityInfoDrawer/EntityInfoDrawer.component.tsx @@ -13,7 +13,7 @@ import { CloseOutlined } from '@ant-design/icons'; import { Col, Drawer, Row } from 'antd'; -import { cloneDeep } from 'lodash'; +import { cloneDeep, get } from 'lodash'; import { EntityDetailUnion } from 'Models'; import React, { useEffect, useMemo, useState } from 'react'; import { EntityType } from '../../../enums/entity.enum'; @@ -29,7 +29,6 @@ import { StoredProcedure } from '../../../generated/entity/data/storedProcedure' import { Table } from '../../../generated/entity/data/table'; import { Topic } from '../../../generated/entity/data/topic'; import { TagLabel } from '../../../generated/type/tagLabel'; -import { SearchSourceAlias } from '../../../interface/search.interface'; import entityUtilClassBase from '../../../utils/EntityUtilClassBase'; import { DRAWER_NAVIGATION_OPTIONS, @@ -72,18 +71,16 @@ const EntityInfoDrawer = ({ [selectedNode] ); - const icon = useMemo( - () => - 'serviceType' in selectedNode ? ( - - ) : null, - [selectedNode] - ); + const icon = useMemo(() => { + const serviceType = get(selectedNode, 'serviceType', ''); + + return serviceType ? ( + + ) : null; + }, [selectedNode]); const tags = useMemo( () => diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeSuggestions.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeSuggestions.component.tsx index 1c67494dbae5..bd48bb1c8bbb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeSuggestions.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeSuggestions.component.tsx @@ -13,7 +13,7 @@ import { Button, Select } from 'antd'; import { AxiosError } from 'axios'; -import { capitalize, debounce } from 'lodash'; +import { capitalize, debounce, get } from 'lodash'; import React, { FC, HTMLAttributes, @@ -125,9 +125,7 @@ const NodeSuggestions: FC = ({ }}>
{ { } else { setSelectedEdge(undefined); setActiveNode(node); - const sourceTypeNode = node.data.node as SourceType; - setSelectedNode({ - ...sourceTypeNode, - // we are getting deleted as a string instead of boolean from API so need to handle it like this - deleted: isDeleted(sourceTypeNode.deleted), - }); + setSelectedNode(node.data.node as SourceType); setIsDrawerOpen(true); handleLineageTracing(node); } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx index fd80600900b1..90c4e3c2d396 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx @@ -90,7 +90,7 @@ import { ColumnLineage, LineageDetails } from '../generated/type/entityLineage'; import { EntityReference } from '../generated/type/entityReference'; import { TagSource } from '../generated/type/tagLabel'; import { addLineage, deleteLineageEdge } from '../rest/miscAPI'; -import { getPartialNameFromTableFQN } from './CommonUtils'; +import { getPartialNameFromTableFQN, isDeleted } from './CommonUtils'; import { getEntityName, getEntityReferenceFromEntity } from './EntityUtils'; import Fqn from './Fqn'; import { jsonToCSV } from './StringsUtils'; @@ -728,6 +728,9 @@ export const createNodes = ( ? node.type : getNodeType(edgesData, node.id); + // we are getting deleted as a string instead of boolean from API so need to handle it like this + node.deleted = isDeleted(node.deleted); + return { id: `${node.id}`, sourcePosition: Position.Right,