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 <sachinchaurasiyachotey87@gmail.com>
This commit is contained in:
Karan Hotchandani 2024-09-30 10:54:39 +05:30 committed by GitHub
parent 41d94ac068
commit 58ed12cf47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 217 additions and 205 deletions

View File

@ -34,6 +34,7 @@ import {
connectEdgeBetweenNodes, connectEdgeBetweenNodes,
deleteEdge, deleteEdge,
deleteNode, deleteNode,
editLineage,
performZoomOut, performZoomOut,
removeColumnLineage, removeColumnLineage,
setupEntitiesForLineage, setupEntitiesForLineage,
@ -52,6 +53,7 @@ const entities = [
MlModelClass, MlModelClass,
ContainerClass, ContainerClass,
SearchIndexClass, SearchIndexClass,
ApiEndpointClass,
MetricClass, MetricClass,
] as const; ] as const;
@ -72,68 +74,66 @@ test.afterAll('Cleanup', async ({ browser }) => {
for (const EntityClass of entities) { for (const EntityClass of entities) {
const defaultEntity = new EntityClass(); const defaultEntity = new EntityClass();
test.fixme( test(`Lineage creation from ${defaultEntity.getType()} entity`, async ({
`Lineage creation from ${defaultEntity.getType()} entity`, browser,
async ({ browser }) => { }) => {
test.slow(true); test.slow(true);
const { page } = await createNewPage(browser); const { page } = await createNewPage(browser);
const { currentEntity, entities, cleanup } = const { currentEntity, entities, cleanup } = await setupEntitiesForLineage(
await setupEntitiesForLineage(page, defaultEntity); page,
defaultEntity
);
await test.step('Should create lineage for the entity', async () => { await test.step('Should create lineage for the entity', async () => {
await redirectToHomePage(page); await redirectToHomePage(page);
await currentEntity.visitEntityPage(page); await currentEntity.visitEntityPage(page);
await visitLineageTab(page); await visitLineageTab(page);
await verifyColumnLayerInactive(page); await verifyColumnLayerInactive(page);
await page.click('[data-testid="edit-lineage"]'); await editLineage(page);
await performZoomOut(page); await performZoomOut(page);
for (const entity of entities) { for (const entity of entities) {
await connectEdgeBetweenNodes(page, currentEntity, entity); await connectEdgeBetweenNodes(page, currentEntity, entity);
} }
await redirectToHomePage(page); await redirectToHomePage(page);
await currentEntity.visitEntityPage(page); await currentEntity.visitEntityPage(page);
await visitLineageTab(page); await visitLineageTab(page);
await page await page
.locator('.react-flow__controls-fitview') .locator('.react-flow__controls-fitview')
.dispatchEvent('click'); .dispatchEvent('click');
for (const entity of entities) { for (const entity of entities) {
await verifyNodePresent(page, entity); await verifyNodePresent(page, entity);
} }
}); });
await test.step('Should create pipeline between entities', async () => { await test.step('Should create pipeline between entities', async () => {
await page.click('[data-testid="edit-lineage"]'); await editLineage(page);
await performZoomOut(page); await performZoomOut(page);
for (const entity of entities) { for (const entity of entities) {
await applyPipelineFromModal(page, currentEntity, entity, pipeline); await applyPipelineFromModal(page, currentEntity, entity, pipeline);
} }
}); });
await test.step( await test.step('Remove lineage between nodes for the entity', async () => {
'Remove lineage between nodes for the entity', await redirectToHomePage(page);
async () => { await currentEntity.visitEntityPage(page);
await redirectToHomePage(page); await visitLineageTab(page);
await currentEntity.visitEntityPage(page); await editLineage(page);
await visitLineageTab(page); await performZoomOut(page);
await page.click('[data-testid="edit-lineage"]');
await performZoomOut(page);
for (const entity of entities) { for (const entity of entities) {
await deleteEdge(page, currentEntity, entity); await deleteEdge(page, currentEntity, entity);
} }
} });
);
await cleanup(); 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 { page } = await createNewPage(browser);
const { apiContext, afterAction } = await getApiContext(page); const { apiContext, afterAction } = await getApiContext(page);
const table1 = new TableClass(); const table1 = new TableClass();
@ -171,117 +171,112 @@ test.fixme('Verify column lineage between tables', async ({ browser }) => {
await afterAction(); await afterAction();
}); });
test.fixme( test('Verify column lineage between table and topic', async ({ browser }) => {
'Verify column lineage between table and topic', const { page } = await createNewPage(browser);
async ({ browser }) => { const { apiContext, afterAction } = await getApiContext(page);
const { page } = await createNewPage(browser); const table = new TableClass();
const { apiContext, afterAction } = await getApiContext(page); const topic = new TopicClass();
const table = new TableClass(); await table.create(apiContext);
const topic = new TopicClass(); await topic.create(apiContext);
await table.create(apiContext);
await topic.create(apiContext);
const sourceTableFqn = get(table, 'entityResponseData.fullyQualifiedName'); const sourceTableFqn = get(table, 'entityResponseData.fullyQualifiedName');
const sourceCol = `${sourceTableFqn}.${get( const sourceCol = `${sourceTableFqn}.${get(
table, table,
'entityResponseData.columns[0].name' 'entityResponseData.columns[0].name'
)}`; )}`;
const targetCol = get( const targetCol = get(
topic, topic,
'entityResponseData.messageSchema.schemaFields[0].children[0].fullyQualifiedName' 'entityResponseData.messageSchema.schemaFields[0].children[0].fullyQualifiedName'
); );
await addPipelineBetweenNodes(page, table, topic); await addPipelineBetweenNodes(page, table, topic);
await activateColumnLayer(page); await activateColumnLayer(page);
// Add column lineage // Add column lineage
await addColumnLineage(page, sourceCol, targetCol); await addColumnLineage(page, sourceCol, targetCol);
await page.click('[data-testid="edit-lineage"]'); await page.click('[data-testid="edit-lineage"]');
await removeColumnLineage(page, sourceCol, targetCol); await removeColumnLineage(page, sourceCol, targetCol);
await page.click('[data-testid="edit-lineage"]'); await page.click('[data-testid="edit-lineage"]');
await deleteNode(page, topic); await deleteNode(page, topic);
await table.delete(apiContext); await table.delete(apiContext);
await topic.delete(apiContext); await topic.delete(apiContext);
await afterAction(); await afterAction();
} });
);
test.fixme( test('Verify column lineage between topic and api endpoint', async ({
'Verify column lineage between topic and api endpoint', browser,
async ({ browser }) => { }) => {
const { page } = await createNewPage(browser); const { page } = await createNewPage(browser);
const { apiContext, afterAction } = await getApiContext(page); const { apiContext, afterAction } = await getApiContext(page);
const topic = new TopicClass(); const topic = new TopicClass();
const apiEndpoint = new ApiEndpointClass(); const apiEndpoint = new ApiEndpointClass();
await topic.create(apiContext); await topic.create(apiContext);
await apiEndpoint.create(apiContext); await apiEndpoint.create(apiContext);
const sourceCol = get( const sourceCol = get(
topic, topic,
'entityResponseData.messageSchema.schemaFields[0].children[0].fullyQualifiedName' 'entityResponseData.messageSchema.schemaFields[0].children[0].fullyQualifiedName'
); );
const targetCol = get( const targetCol = get(
apiEndpoint, apiEndpoint,
'entityResponseData.responseSchema.schemaFields[0].children[1].fullyQualifiedName' 'entityResponseData.responseSchema.schemaFields[0].children[1].fullyQualifiedName'
); );
await addPipelineBetweenNodes(page, topic, apiEndpoint); await addPipelineBetweenNodes(page, topic, apiEndpoint);
await activateColumnLayer(page); await activateColumnLayer(page);
// Add column lineage // Add column lineage
await addColumnLineage(page, sourceCol, targetCol); await addColumnLineage(page, sourceCol, targetCol);
await page.click('[data-testid="edit-lineage"]'); await page.click('[data-testid="edit-lineage"]');
await removeColumnLineage(page, sourceCol, targetCol); await removeColumnLineage(page, sourceCol, targetCol);
await page.click('[data-testid="edit-lineage"]'); await page.click('[data-testid="edit-lineage"]');
await deleteNode(page, apiEndpoint); await deleteNode(page, apiEndpoint);
await topic.delete(apiContext); await topic.delete(apiContext);
await apiEndpoint.delete(apiContext); await apiEndpoint.delete(apiContext);
await afterAction(); await afterAction();
} });
);
test.fixme( test('Verify column lineage between table and api endpoint', async ({
'Verify column lineage between table and api endpoint', browser,
async ({ browser }) => { }) => {
const { page } = await createNewPage(browser); const { page } = await createNewPage(browser);
const { apiContext, afterAction } = await getApiContext(page); const { apiContext, afterAction } = await getApiContext(page);
const table = new TableClass(); const table = new TableClass();
const apiEndpoint = new ApiEndpointClass(); const apiEndpoint = new ApiEndpointClass();
await table.create(apiContext); await table.create(apiContext);
await apiEndpoint.create(apiContext); await apiEndpoint.create(apiContext);
const sourceTableFqn = get(table, 'entityResponseData.fullyQualifiedName'); const sourceTableFqn = get(table, 'entityResponseData.fullyQualifiedName');
const sourceCol = `${sourceTableFqn}.${get( const sourceCol = `${sourceTableFqn}.${get(
table, table,
'entityResponseData.columns[0].name' 'entityResponseData.columns[0].name'
)}`; )}`;
const targetCol = get( const targetCol = get(
apiEndpoint, apiEndpoint,
'entityResponseData.responseSchema.schemaFields[0].children[0].fullyQualifiedName' 'entityResponseData.responseSchema.schemaFields[0].children[0].fullyQualifiedName'
); );
await addPipelineBetweenNodes(page, table, apiEndpoint); await addPipelineBetweenNodes(page, table, apiEndpoint);
await activateColumnLayer(page); await activateColumnLayer(page);
// Add column lineage // Add column lineage
await addColumnLineage(page, sourceCol, targetCol); await addColumnLineage(page, sourceCol, targetCol);
await page.click('[data-testid="edit-lineage"]'); await page.click('[data-testid="edit-lineage"]');
await removeColumnLineage(page, sourceCol, targetCol); await removeColumnLineage(page, sourceCol, targetCol);
await page.click('[data-testid="edit-lineage"]'); await page.click('[data-testid="edit-lineage"]');
await deleteNode(page, apiEndpoint); await deleteNode(page, apiEndpoint);
await table.delete(apiContext); await table.delete(apiContext);
await apiEndpoint.delete(apiContext); await apiEndpoint.delete(apiContext);
await afterAction(); await afterAction();
} });
);

View File

@ -37,12 +37,20 @@ export const activateColumnLayer = async (page: Page) => {
await page.click('[data-testid="lineage-layer-column-btn"]'); 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) => { export const performZoomOut = async (page: Page) => {
for (let i = 0; i < 5; i++) { const zoomOutBtn = page.locator('.react-flow__controls-zoomout');
const zoomOutBtn = page.locator('.react-flow__controls-zoomout'); const enabled = await zoomOutBtn.isEnabled();
const enabled = await zoomOutBtn.isEnabled(); if (enabled) {
if (enabled) { for (const _ of Array.from({ length: 8 })) {
zoomOutBtn.dispatchEvent('click'); await zoomOutBtn.dispatchEvent('click');
} }
} }
}; };
@ -78,6 +86,20 @@ export const deleteEdge = async (
await deleteRes; 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 ( export const dragConnection = async (
page: Page, page: Page,
sourceId: string, sourceId: string,
@ -109,10 +131,10 @@ export const connectEdgeBetweenNodes = async (
const toNodeName = get(toNode, 'entityResponseData.name'); const toNodeName = get(toNode, 'entityResponseData.name');
const toNodeFqn = get(toNode, 'entityResponseData.fullyQualifiedName'); const toNodeFqn = get(toNode, 'entityResponseData.fullyQualifiedName');
await page.locator(`[data-testid="${type}-draggable-icon"]`).hover(); const source = `[data-testid="${type}-draggable-icon"]`;
await page.mouse.down(); const target = '[data-testid="lineage-details"]';
await page.locator('[data-testid="lineage-details"]').hover();
await page.mouse.up(); await dragAndDropNode(page, source, target);
await page.locator('[data-testid="suggestion-node"]').dispatchEvent('click'); await page.locator('[data-testid="suggestion-node"]').dispatchEvent('click');
@ -370,7 +392,7 @@ export const addPipelineBetweenNodes = async (
) => { ) => {
await sourceEntity.visitEntityPage(page); await sourceEntity.visitEntityPage(page);
await page.click('[data-testid="lineage"]'); await page.click('[data-testid="lineage"]');
await page.click('[data-testid="edit-lineage"]'); await editLineage(page);
await performZoomOut(page); await performZoomOut(page);

View File

@ -14,7 +14,7 @@ import Icon from '@ant-design/icons';
import { Button, Col, Divider, Row, Space, Tooltip, Typography } from 'antd'; import { Button, Col, Divider, Row, Space, Tooltip, Typography } from 'antd';
import ButtonGroup from 'antd/lib/button/button-group'; import ButtonGroup from 'antd/lib/button/button-group';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { capitalize, isEmpty } from 'lodash'; import { capitalize, get, isEmpty } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
@ -161,18 +161,18 @@ export const DataAssetsHeader = ({
const [isBreadcrumbLoading, setIsBreadcrumbLoading] = useState(false); const [isBreadcrumbLoading, setIsBreadcrumbLoading] = useState(false);
const [isFollowingLoading, setIsFollowingLoading] = useState(false); const [isFollowingLoading, setIsFollowingLoading] = useState(false);
const history = useHistory(); const history = useHistory();
const icon = useMemo( const icon = useMemo(() => {
() => const serviceType = get(dataAsset, 'serviceType', '');
'serviceType' in dataAsset ? (
<img return serviceType ? (
className="h-9" <img
src={serviceUtilClassBase.getServiceTypeLogo( className="h-9"
dataAsset as SearchSourceAlias src={serviceUtilClassBase.getServiceTypeLogo(
)} dataAsset as SearchSourceAlias
/> )}
) : null, />
[dataAsset] ) : null;
); }, [dataAsset]);
const [copyTooltip, setCopyTooltip] = useState<string>(); const [copyTooltip, setCopyTooltip] = useState<string>();
const excludeEntityService = useMemo( const excludeEntityService = useMemo(

View File

@ -13,7 +13,7 @@
import Icon from '@ant-design/icons/lib/components/Icon'; import Icon from '@ant-design/icons/lib/components/Icon';
import { Button, Col, Divider, Row, Space, Tooltip, Typography } from 'antd'; 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 React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ReactComponent as VersionIcon } from '../../../assets/svg/ic-version.svg'; import { ReactComponent as VersionIcon } from '../../../assets/svg/ic-version.svg';
@ -89,13 +89,20 @@ function DataAssetsVersionHeader({
() => getDataAssetsVersionHeaderInfo(entityType, currentVersionData), () => getDataAssetsVersionHeaderInfo(entityType, currentVersionData),
[entityType, currentVersionData] [entityType, currentVersionData]
); );
const logo = useMemo(
() => const icon = useMemo(() => {
serviceUtilClassBase.getServiceTypeLogo( const serviceType = get(currentVersionData, 'serviceType', '');
currentVersionData as SearchSourceAlias
), return serviceType ? (
[currentVersionData] <img
); alt="service-icon"
className="h-9"
src={serviceUtilClassBase.getServiceTypeLogo(
currentVersionData as SearchSourceAlias
)}
/>
) : null;
}, [currentVersionData]);
return ( return (
<Row className="p-x-lg" gutter={[8, 12]} justify="space-between"> <Row className="p-x-lg" gutter={[8, 12]} justify="space-between">
@ -108,11 +115,7 @@ function DataAssetsVersionHeader({
<EntityHeaderTitle <EntityHeaderTitle
deleted={deleted} deleted={deleted}
displayName={displayName} displayName={displayName}
icon={ icon={icon}
'serviceType' in currentVersionData && (
<img className="h-9" src={logo} />
)
}
name={currentVersionData?.name} name={currentVersionData?.name}
serviceName={serviceName ?? ''} serviceName={serviceName ?? ''}
/> />

View File

@ -13,7 +13,7 @@
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { Col, Drawer, Row } from 'antd'; import { Col, Drawer, Row } from 'antd';
import { cloneDeep } from 'lodash'; import { cloneDeep, get } from 'lodash';
import { EntityDetailUnion } from 'Models'; import { EntityDetailUnion } from 'Models';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { EntityType } from '../../../enums/entity.enum'; 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 { Table } from '../../../generated/entity/data/table';
import { Topic } from '../../../generated/entity/data/topic'; import { Topic } from '../../../generated/entity/data/topic';
import { TagLabel } from '../../../generated/type/tagLabel'; import { TagLabel } from '../../../generated/type/tagLabel';
import { SearchSourceAlias } from '../../../interface/search.interface';
import entityUtilClassBase from '../../../utils/EntityUtilClassBase'; import entityUtilClassBase from '../../../utils/EntityUtilClassBase';
import { import {
DRAWER_NAVIGATION_OPTIONS, DRAWER_NAVIGATION_OPTIONS,
@ -72,18 +71,16 @@ const EntityInfoDrawer = ({
[selectedNode] [selectedNode]
); );
const icon = useMemo( const icon = useMemo(() => {
() => const serviceType = get(selectedNode, 'serviceType', '');
'serviceType' in selectedNode ? (
<img return serviceType ? (
className="h-9" <img
src={serviceUtilClassBase.getServiceTypeLogo( className="h-9"
selectedNode as SearchSourceAlias src={serviceUtilClassBase.getServiceTypeLogo(selectedNode)}
)} />
/> ) : null;
) : null, }, [selectedNode]);
[selectedNode]
);
const tags = useMemo( const tags = useMemo(
() => () =>

View File

@ -13,7 +13,7 @@
import { Button, Select } from 'antd'; import { Button, Select } from 'antd';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { capitalize, debounce } from 'lodash'; import { capitalize, debounce, get } from 'lodash';
import React, { import React, {
FC, FC,
HTMLAttributes, HTMLAttributes,
@ -125,9 +125,7 @@ const NodeSuggestions: FC<EntitySuggestionProps> = ({
}}> }}>
<div className="d-flex items-center w-full overflow-hidden"> <div className="d-flex items-center w-full overflow-hidden">
<img <img
alt={ alt={get(entity, 'serviceType', '') || entity.name}
'serviceType' in entity ? entity.serviceType : entity.name
}
className="m-r-xs" className="m-r-xs"
height="16px" height="16px"
src={serviceUtilClassBase.getServiceTypeLogo(entity)} src={serviceUtilClassBase.getServiceTypeLogo(entity)}

View File

@ -75,7 +75,6 @@ import {
} from '../../generated/type/entityLineage'; } from '../../generated/type/entityLineage';
import { useFqn } from '../../hooks/useFqn'; import { useFqn } from '../../hooks/useFqn';
import { getLineageDataByFQN, updateLineageEdge } from '../../rest/lineageAPI'; import { getLineageDataByFQN, updateLineageEdge } from '../../rest/lineageAPI';
import { isDeleted } from '../../utils/CommonUtils';
import { import {
addLineageHandler, addLineageHandler,
centerNodePosition, centerNodePosition,
@ -650,12 +649,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
} else { } else {
setSelectedEdge(undefined); setSelectedEdge(undefined);
setActiveNode(node); setActiveNode(node);
const sourceTypeNode = node.data.node as SourceType; setSelectedNode(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),
});
setIsDrawerOpen(true); setIsDrawerOpen(true);
handleLineageTracing(node); handleLineageTracing(node);
} }

View File

@ -90,7 +90,7 @@ import { ColumnLineage, LineageDetails } from '../generated/type/entityLineage';
import { EntityReference } from '../generated/type/entityReference'; import { EntityReference } from '../generated/type/entityReference';
import { TagSource } from '../generated/type/tagLabel'; import { TagSource } from '../generated/type/tagLabel';
import { addLineage, deleteLineageEdge } from '../rest/miscAPI'; import { addLineage, deleteLineageEdge } from '../rest/miscAPI';
import { getPartialNameFromTableFQN } from './CommonUtils'; import { getPartialNameFromTableFQN, isDeleted } from './CommonUtils';
import { getEntityName, getEntityReferenceFromEntity } from './EntityUtils'; import { getEntityName, getEntityReferenceFromEntity } from './EntityUtils';
import Fqn from './Fqn'; import Fqn from './Fqn';
import { jsonToCSV } from './StringsUtils'; import { jsonToCSV } from './StringsUtils';
@ -728,6 +728,9 @@ export const createNodes = (
? node.type ? node.type
: getNodeType(edgesData, node.id); : 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 { return {
id: `${node.id}`, id: `${node.id}`,
sourcePosition: Position.Right, sourcePosition: Position.Right,