mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-28 18:35:07 +00:00
2011 lines
55 KiB
TypeScript
2011 lines
55 KiB
TypeScript
/*
|
|
* Copyright 2024 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, Page } from '@playwright/test';
|
|
import { isEmpty, lowerCase } from 'lodash';
|
|
import {
|
|
BIG_ENTITY_DELETE_TIMEOUT,
|
|
ENTITIES_WITHOUT_FOLLOWING_BUTTON,
|
|
LIST_OF_FIELDS_TO_EDIT_NOT_TO_BE_PRESENT,
|
|
LIST_OF_FIELDS_TO_EDIT_TO_BE_DISABLED,
|
|
} from '../constant/delete';
|
|
import { ES_RESERVED_CHARACTERS } from '../constant/entity';
|
|
import { SidebarItem } from '../constant/sidebar';
|
|
import { EntityTypeEndpoint } from '../support/entity/Entity.interface';
|
|
import { EntityClass } from '../support/entity/EntityClass';
|
|
import { TagClass } from '../support/tag/TagClass';
|
|
import {
|
|
clickOutside,
|
|
descriptionBox,
|
|
readElementInListWithScroll,
|
|
redirectToHomePage,
|
|
toastNotification,
|
|
uuid,
|
|
} from './common';
|
|
import {
|
|
customFormatDateTime,
|
|
getCurrentMillis,
|
|
getEpochMillisForFutureDays,
|
|
} from './dateTime';
|
|
import { searchAndClickOnOption } from './explore';
|
|
import { sidebarClick } from './sidebar';
|
|
|
|
export const waitForAllLoadersToDisappear = async (
|
|
page: Page,
|
|
dataTestId = 'loader'
|
|
) => {
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
const allLoaders = page.locator(`[data-testid="${dataTestId}"]`);
|
|
const count = await allLoaders.count();
|
|
|
|
let allLoadersGone = true;
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const loader = allLoaders.nth(i);
|
|
try {
|
|
if (await loader.isVisible()) {
|
|
await loader.waitFor({ state: 'detached', timeout: 1000 });
|
|
allLoadersGone = false;
|
|
}
|
|
} catch {
|
|
// Do nothing
|
|
}
|
|
}
|
|
|
|
if (allLoadersGone) {
|
|
break;
|
|
}
|
|
await page.waitForTimeout(100); // slight buffer before next retry
|
|
}
|
|
};
|
|
|
|
export const visitEntityPage = async (data: {
|
|
page: Page;
|
|
searchTerm: string;
|
|
dataTestId: string;
|
|
}) => {
|
|
const { page, searchTerm, dataTestId } = data;
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Unified loader handling
|
|
await waitForAllLoadersToDisappear(page);
|
|
|
|
const isWelcomeScreenVisible = await page
|
|
.getByTestId('welcome-screen')
|
|
.isVisible();
|
|
|
|
if (isWelcomeScreenVisible) {
|
|
await page.getByTestId('welcome-screen-close-btn').click();
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
|
|
const waitForSearchResponse = page.waitForResponse(
|
|
'/api/v1/search/query?q=*index=dataAsset*'
|
|
);
|
|
await page.getByTestId('searchBox').fill(searchTerm);
|
|
await waitForSearchResponse;
|
|
|
|
await page.getByTestId(dataTestId).getByTestId('data-name').click();
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForSelector('[data-testid="loader"]', {
|
|
state: 'detached',
|
|
});
|
|
await page.getByTestId('searchBox').clear();
|
|
};
|
|
|
|
export const addOwner = async ({
|
|
page,
|
|
owner,
|
|
endpoint,
|
|
type = 'Users',
|
|
dataTestId,
|
|
initiatorId = 'edit-owner',
|
|
}: {
|
|
page: Page;
|
|
owner: string;
|
|
endpoint: EntityTypeEndpoint;
|
|
type?: 'Teams' | 'Users';
|
|
dataTestId?: string;
|
|
initiatorId?: string;
|
|
}) => {
|
|
await page.getByTestId(initiatorId).click();
|
|
if (type === 'Users') {
|
|
const userListResponse = page.waitForResponse(
|
|
'/api/v1/search/query?q=*isBot:false*index=user_search_index*'
|
|
);
|
|
await page.getByRole('tab', { name: type }).click();
|
|
await userListResponse;
|
|
}
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
|
|
const ownerSearchBar = await page
|
|
.getByTestId(`owner-select-${lowerCase(type)}-search-bar`)
|
|
.isVisible();
|
|
|
|
if (!ownerSearchBar) {
|
|
await page.getByRole('tab', { name: type }).click();
|
|
}
|
|
|
|
const searchUser = page.waitForResponse(
|
|
`/api/v1/search/query?q=*${encodeURIComponent(owner)}*`
|
|
);
|
|
await page
|
|
.getByTestId(`owner-select-${lowerCase(type)}-search-bar`)
|
|
.fill(owner);
|
|
await searchUser;
|
|
|
|
if (type === 'Teams') {
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
|
await page.getByRole('listitem', { name: owner, exact: true }).click();
|
|
await patchRequest;
|
|
} else {
|
|
const ownerItem = page.getByRole('listitem', {
|
|
name: owner,
|
|
exact: true,
|
|
});
|
|
|
|
await ownerItem.waitFor({ state: 'visible' });
|
|
await ownerItem.click();
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
|
await page.getByTestId('selectable-list-update-btn').click();
|
|
await patchRequest;
|
|
}
|
|
|
|
await expect(
|
|
page.getByTestId(dataTestId ?? 'owner-link').getByTestId(`${owner}`)
|
|
).toBeVisible();
|
|
};
|
|
|
|
export const addOwnerWithoutValidation = async ({
|
|
page,
|
|
owner,
|
|
type = 'Users',
|
|
initiatorId = 'edit-owner',
|
|
}: {
|
|
page: Page;
|
|
owner: string;
|
|
type?: 'Teams' | 'Users';
|
|
initiatorId?: string;
|
|
}) => {
|
|
await page.getByTestId(initiatorId).click();
|
|
if (type === 'Users') {
|
|
const userListResponse = page.waitForResponse(
|
|
'/api/v1/search/query?q=*isBot:false*index=user_search_index*'
|
|
);
|
|
await page.getByRole('tab', { name: type }).click();
|
|
await userListResponse;
|
|
}
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
|
|
const ownerSearchBar = await page
|
|
.getByTestId(`owner-select-${lowerCase(type)}-search-bar`)
|
|
.isVisible();
|
|
|
|
if (!ownerSearchBar) {
|
|
await page.getByRole('tab', { name: type }).click();
|
|
}
|
|
|
|
const searchUser = page.waitForResponse(
|
|
`/api/v1/search/query?q=*${encodeURIComponent(owner)}*`
|
|
);
|
|
await page
|
|
.getByTestId(`owner-select-${lowerCase(type)}-search-bar`)
|
|
.fill(owner);
|
|
await searchUser;
|
|
|
|
if (type === 'Teams') {
|
|
await page.getByRole('listitem', { name: owner, exact: true }).click();
|
|
} else {
|
|
await page.getByRole('listitem', { name: owner, exact: true }).click();
|
|
await page.getByTestId('selectable-list-update-btn').click();
|
|
}
|
|
};
|
|
|
|
export const updateOwner = async ({
|
|
page,
|
|
owner,
|
|
endpoint,
|
|
type = 'Users',
|
|
dataTestId,
|
|
}: {
|
|
page: Page;
|
|
owner: string;
|
|
endpoint: EntityTypeEndpoint;
|
|
type?: 'Teams' | 'Users';
|
|
dataTestId?: string;
|
|
}) => {
|
|
await page.getByTestId('edit-owner').click();
|
|
await page.getByRole('tab', { name: type }).click();
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
|
|
const searchUser = page.waitForResponse(
|
|
`/api/v1/search/query?q=*${encodeURIComponent(owner)}*`
|
|
);
|
|
await page
|
|
.getByTestId(`owner-select-${lowerCase(type)}-search-bar`)
|
|
.fill(owner);
|
|
await searchUser;
|
|
|
|
if (type === 'Teams') {
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
|
await page.getByRole('listitem', { name: owner, exact: true }).click();
|
|
await patchRequest;
|
|
} else {
|
|
await page.getByRole('listitem', { name: owner, exact: true }).click();
|
|
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
|
await page.getByTestId('selectable-list-update-btn').click();
|
|
await patchRequest;
|
|
}
|
|
|
|
await expect(
|
|
page.getByTestId(dataTestId ?? 'owner-link').getByTestId(`${owner}`)
|
|
).toBeVisible();
|
|
};
|
|
|
|
export const removeOwnersFromList = async ({
|
|
page,
|
|
endpoint,
|
|
ownerNames,
|
|
dataTestId,
|
|
}: {
|
|
page: Page;
|
|
endpoint: EntityTypeEndpoint;
|
|
ownerNames: string[];
|
|
dataTestId?: string;
|
|
}) => {
|
|
await page.getByTestId('edit-owner').click();
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
|
|
for (const ownerName of ownerNames) {
|
|
const ownerItem = page.getByRole('listitem', {
|
|
name: ownerName,
|
|
exact: true,
|
|
});
|
|
|
|
await ownerItem.click();
|
|
}
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
|
await page.click('[data-testid="selectable-list-update-btn"]');
|
|
await patchRequest;
|
|
|
|
for (const ownerName of ownerNames) {
|
|
await expect(
|
|
page.getByTestId(dataTestId ?? 'owner-link').getByTestId(ownerName)
|
|
).not.toBeVisible();
|
|
}
|
|
};
|
|
|
|
// Removes All Owners
|
|
export const removeOwner = async ({
|
|
page,
|
|
endpoint,
|
|
ownerName,
|
|
type = 'Users',
|
|
dataTestId,
|
|
}: {
|
|
page: Page;
|
|
endpoint: EntityTypeEndpoint;
|
|
ownerName: string;
|
|
type?: 'Teams' | 'Users';
|
|
dataTestId?: string;
|
|
}) => {
|
|
await page.getByTestId('edit-owner').click();
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
|
if (type === 'Teams') {
|
|
await expect(page.getByTestId('remove-owner').locator('svg')).toBeVisible();
|
|
|
|
await page.getByTestId('remove-owner').locator('svg').click();
|
|
} else {
|
|
await page.click('[data-testid="clear-all-button"]');
|
|
await page.click('[data-testid="selectable-list-update-btn"]');
|
|
}
|
|
|
|
await patchRequest;
|
|
|
|
await page
|
|
.getByTestId(dataTestId ?? 'owner-link')
|
|
.getByTestId(ownerName)
|
|
.waitFor({ state: 'hidden' });
|
|
};
|
|
|
|
export const addMultiOwner = async (data: {
|
|
page: Page;
|
|
ownerNames: string | string[];
|
|
activatorBtnDataTestId: string;
|
|
endpoint: EntityTypeEndpoint;
|
|
resultTestId?: string;
|
|
isSelectableInsideForm?: boolean;
|
|
type: 'Teams' | 'Users';
|
|
clearAll?: boolean;
|
|
}) => {
|
|
const {
|
|
page,
|
|
ownerNames,
|
|
activatorBtnDataTestId,
|
|
resultTestId = 'owner-link',
|
|
isSelectableInsideForm = false,
|
|
endpoint,
|
|
type,
|
|
clearAll = true,
|
|
} = data;
|
|
const isMultipleOwners = Array.isArray(ownerNames);
|
|
const owners = isMultipleOwners ? ownerNames : [ownerNames];
|
|
|
|
await page.click(`[data-testid="${activatorBtnDataTestId}"]`);
|
|
|
|
await expect(page.locator("[data-testid='select-owner-tabs']")).toBeVisible();
|
|
|
|
await page.waitForSelector(
|
|
'[data-testid="select-owner-tabs"] [data-testid="loader"]',
|
|
{ state: 'detached' }
|
|
);
|
|
|
|
await page
|
|
.locator("[data-testid='select-owner-tabs']")
|
|
.getByRole('tab', { name: 'Users' })
|
|
.click();
|
|
|
|
await page.waitForSelector(
|
|
'[data-testid="select-owner-tabs"] [data-testid="loader"]',
|
|
{ state: 'detached' }
|
|
);
|
|
|
|
const isClearButtonVisible = await page
|
|
.locator("[data-testid='select-owner-tabs']")
|
|
.getByTestId('clear-all-button')
|
|
.isVisible();
|
|
|
|
// If the user is not in the Users tab, switch to it
|
|
if (!isClearButtonVisible) {
|
|
await page
|
|
.locator("[data-testid='select-owner-tabs']")
|
|
.getByRole('tab', { name: 'Users' })
|
|
.click();
|
|
|
|
await page.waitForSelector(
|
|
'[data-testid="select-owner-tabs"] [data-testid="loader"]',
|
|
{ state: 'detached' }
|
|
);
|
|
}
|
|
|
|
if (clearAll && isMultipleOwners) {
|
|
await page.click('[data-testid="clear-all-button"]');
|
|
}
|
|
|
|
for (const ownerName of owners) {
|
|
const searchOwner = page.waitForResponse(
|
|
'api/v1/search/query?q=*&index=user_search_index*'
|
|
);
|
|
await page.locator('[data-testid="owner-select-users-search-bar"]').clear();
|
|
await page.fill('[data-testid="owner-select-users-search-bar"]', ownerName);
|
|
await searchOwner;
|
|
await page.waitForSelector(
|
|
'[data-testid="select-owner-tabs"] [data-testid="loader"]',
|
|
{ state: 'detached' }
|
|
);
|
|
|
|
const ownerItem = page.getByRole('listitem', {
|
|
name: ownerName,
|
|
exact: true,
|
|
});
|
|
await ownerItem.waitFor({ state: 'visible' });
|
|
|
|
// Wait for the item to exist and be visible before clicking
|
|
|
|
if (type === 'Teams') {
|
|
if (isSelectableInsideForm) {
|
|
await ownerItem.click();
|
|
} else {
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
|
await ownerItem.click();
|
|
await patchRequest;
|
|
}
|
|
} else {
|
|
await ownerItem.click();
|
|
}
|
|
}
|
|
|
|
if (isMultipleOwners) {
|
|
const updateButton = page.getByTestId('selectable-list-update-btn');
|
|
|
|
if (isSelectableInsideForm) {
|
|
await updateButton.click();
|
|
} else {
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
|
await updateButton.click();
|
|
await patchRequest;
|
|
|
|
await page.waitForSelector('[data-testid="loader"]', {
|
|
state: 'detached',
|
|
});
|
|
|
|
await page.waitForSelector('[data-testid="select-owner-tabs"] ', {
|
|
state: 'detached',
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const name of owners) {
|
|
await expect(
|
|
page.locator(`[data-testid="${resultTestId}"]`).getByTestId(name)
|
|
).toBeVisible();
|
|
}
|
|
};
|
|
|
|
export const assignTier = async (
|
|
page: Page,
|
|
tier: string,
|
|
endpoint: string
|
|
) => {
|
|
await page.getByTestId('edit-tier').click();
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
|
await page.getByTestId(`radio-btn-${tier}`).click();
|
|
|
|
// Wait for the update button to be visible and clickable
|
|
await page.waitForSelector('[data-testid="update-tier-card"]', {
|
|
state: 'visible',
|
|
});
|
|
await page.click(`[data-testid="update-tier-card"]`);
|
|
|
|
await patchRequest;
|
|
await clickOutside(page);
|
|
|
|
await expect(page.getByTestId('Tier')).toContainText(tier);
|
|
};
|
|
|
|
export const removeTier = async (page: Page, endpoint: string) => {
|
|
await page.getByTestId('edit-tier').click();
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
const patchRequest = page.waitForResponse(
|
|
(response) =>
|
|
response.url().includes(`/api/v1/${endpoint}`) &&
|
|
response.request().method() === 'PATCH'
|
|
);
|
|
await page.getByTestId('clear-tier').click();
|
|
|
|
await patchRequest;
|
|
await clickOutside(page);
|
|
|
|
await expect(page.getByTestId('Tier')).toContainText('--');
|
|
};
|
|
|
|
export const assignCertification = async (
|
|
page: Page,
|
|
certification: TagClass,
|
|
endpoint: string
|
|
) => {
|
|
const certificationResponse = page.waitForResponse(
|
|
'/api/v1/tags?parent=Certification&limit=50'
|
|
);
|
|
await page.getByTestId('edit-certification').click();
|
|
await certificationResponse;
|
|
await page.waitForSelector('.certification-card-popover', {
|
|
state: 'visible',
|
|
});
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
|
|
await readElementInListWithScroll(
|
|
page,
|
|
page.getByTestId(
|
|
`radio-btn-${certification.responseData.fullyQualifiedName}`
|
|
),
|
|
page.locator('[data-testid="certification-cards"] .ant-radio-group')
|
|
);
|
|
|
|
await page
|
|
.getByTestId(`radio-btn-${certification.responseData.fullyQualifiedName}`)
|
|
.click();
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
|
await page.getByTestId('update-certification').click();
|
|
await patchRequest;
|
|
await clickOutside(page);
|
|
|
|
await expect(page.getByTestId('certification-label')).toContainText(
|
|
certification.responseData.displayName
|
|
);
|
|
};
|
|
|
|
export const removeCertification = async (page: Page, endpoint: string) => {
|
|
await page.getByTestId('edit-certification').click();
|
|
await page.waitForSelector('.certification-card-popover', {
|
|
state: 'visible',
|
|
});
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
|
await page.getByTestId('clear-certification').click();
|
|
await patchRequest;
|
|
await clickOutside(page);
|
|
|
|
await expect(page.getByTestId('certification-label')).toContainText('--');
|
|
};
|
|
|
|
export const updateDescription = async (
|
|
page: Page,
|
|
description: string,
|
|
isModal = false
|
|
) => {
|
|
await page.getByTestId('edit-description').click();
|
|
await page.locator(descriptionBox).first().click();
|
|
await page.locator(descriptionBox).first().clear();
|
|
await page.locator(descriptionBox).first().fill(description);
|
|
await page.getByTestId('save').click();
|
|
|
|
if (isModal) {
|
|
await page.waitForSelector('[role="dialog"].description-markdown-editor', {
|
|
state: 'hidden',
|
|
});
|
|
}
|
|
|
|
if (isEmpty(description)) {
|
|
// Check for either "No description" or handle potential UI duplication issue
|
|
const container = page.getByTestId('asset-description-container');
|
|
const text = await container.textContent();
|
|
|
|
expect(text).toMatch(/No description|Descriptiondescription/);
|
|
} else {
|
|
await expect(
|
|
page.getByTestId('asset-description-container').getByRole('paragraph')
|
|
).toContainText(description);
|
|
}
|
|
};
|
|
|
|
export const updateDescriptionForChildren = async (
|
|
page: Page,
|
|
description: string,
|
|
rowId: string,
|
|
rowSelector: string,
|
|
entityEndpoint: string
|
|
) => {
|
|
await page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('edit-button')
|
|
.click();
|
|
|
|
await page.waitForSelector('[role="dialog"]', { state: 'visible' });
|
|
|
|
await page.locator(descriptionBox).first().click();
|
|
await page.locator(descriptionBox).first().clear();
|
|
await page.locator(descriptionBox).first().fill(description);
|
|
let updateRequest;
|
|
if (
|
|
entityEndpoint === 'tables' ||
|
|
entityEndpoint === 'dashboard/datamodels'
|
|
) {
|
|
updateRequest = page.waitForResponse('/api/v1/columns/name/*');
|
|
} else {
|
|
updateRequest = page.waitForResponse(`/api/v1/${entityEndpoint}/*`);
|
|
}
|
|
await page.getByTestId('save').click();
|
|
await updateRequest;
|
|
|
|
await page.waitForSelector('[role="dialog"]', { state: 'hidden' });
|
|
|
|
isEmpty(description)
|
|
? await expect(
|
|
page.locator(`[${rowSelector}="${rowId}"]`).getByTestId('description')
|
|
).toContainText('No Description')
|
|
: await expect(
|
|
page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('viewer-container')
|
|
.getByRole('paragraph')
|
|
).toContainText(description);
|
|
};
|
|
|
|
export const assignTag = async (
|
|
page: Page,
|
|
tag: string,
|
|
action: 'Add' | 'Edit' = 'Add',
|
|
endpoint: string,
|
|
parentId = 'KnowledgePanel.Tags',
|
|
tagFqn?: string
|
|
) => {
|
|
await page
|
|
.getByTestId(parentId)
|
|
.getByTestId('tags-container')
|
|
.getByTestId(action === 'Add' ? 'add-tag' : 'edit-button')
|
|
.click();
|
|
|
|
// Wait for the form to be visible and stable
|
|
await page.locator('#tagsForm_tags').waitFor({
|
|
state: 'visible',
|
|
});
|
|
|
|
const searchTags = page.waitForResponse(
|
|
`/api/v1/search/query?q=*${encodeURIComponent(tag)}*`
|
|
);
|
|
|
|
await page.locator('#tagsForm_tags').fill(tag);
|
|
|
|
await searchTags;
|
|
|
|
await page
|
|
.getByTestId(`tag-${tagFqn ? `${tagFqn}` : tag}`)
|
|
.first()
|
|
.click();
|
|
|
|
await page.waitForSelector(
|
|
'.ant-select-dropdown [data-testid="saveAssociatedTag"]',
|
|
{ state: 'visible' }
|
|
);
|
|
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
|
|
|
await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled();
|
|
|
|
await page.getByTestId('saveAssociatedTag').click();
|
|
|
|
await patchRequest;
|
|
|
|
await expect(
|
|
page
|
|
.getByTestId(parentId)
|
|
.getByTestId('tags-container')
|
|
.getByTestId(`tag-${tagFqn ? `${tagFqn}` : tag}`)
|
|
).toBeVisible();
|
|
};
|
|
|
|
export const assignTagToChildren = async ({
|
|
page,
|
|
tag,
|
|
rowId,
|
|
action = 'Add',
|
|
rowSelector = 'data-row-key',
|
|
entityEndpoint,
|
|
}: {
|
|
page: Page;
|
|
tag: string;
|
|
rowId: string;
|
|
action?: 'Add' | 'Edit';
|
|
rowSelector?: string;
|
|
entityEndpoint: string;
|
|
}) => {
|
|
await page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('tags-container')
|
|
.getByTestId(action === 'Add' ? 'add-tag' : 'edit-button')
|
|
.click();
|
|
|
|
const searchTags = page.waitForResponse(
|
|
`/api/v1/search/query?q=*${encodeURIComponent(tag)}*`
|
|
);
|
|
|
|
await page.locator('#tagsForm_tags').fill(tag);
|
|
|
|
await searchTags;
|
|
|
|
await page.getByTestId(`tag-${tag}`).click();
|
|
let patchRequest;
|
|
if (
|
|
entityEndpoint === 'tables' ||
|
|
entityEndpoint === 'dashboard/datamodels'
|
|
) {
|
|
patchRequest = page.waitForResponse('/api/v1/columns/name/*');
|
|
} else {
|
|
patchRequest = page.waitForResponse(`/api/v1/${entityEndpoint}/*`);
|
|
}
|
|
|
|
await page.waitForSelector(
|
|
'.ant-select-dropdown [data-testid="saveAssociatedTag"]',
|
|
{ state: 'visible' }
|
|
);
|
|
|
|
await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled();
|
|
|
|
await page.getByTestId('saveAssociatedTag').click();
|
|
|
|
await patchRequest;
|
|
|
|
await expect(
|
|
page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('tags-container')
|
|
.getByTestId(`tag-${tag}`)
|
|
).toBeVisible();
|
|
};
|
|
|
|
export const removeTag = async (page: Page, tags: string[]) => {
|
|
for (const tag of tags) {
|
|
await page
|
|
.getByTestId('KnowledgePanel.Tags')
|
|
.getByTestId('tags-container')
|
|
.getByTestId('edit-button')
|
|
.click();
|
|
|
|
await page
|
|
.getByTestId(`selected-tag-${tag}`)
|
|
.getByTestId('remove-tags')
|
|
.locator('svg')
|
|
.click();
|
|
|
|
const patchRequest = page.waitForResponse(
|
|
(response) => response.request().method() === 'PATCH'
|
|
);
|
|
|
|
await page.waitForSelector(
|
|
'.ant-select-dropdown [data-testid="saveAssociatedTag"]',
|
|
{ state: 'visible' }
|
|
);
|
|
|
|
await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled();
|
|
|
|
await page.getByTestId('saveAssociatedTag').click();
|
|
await patchRequest;
|
|
|
|
await expect(
|
|
page
|
|
.getByTestId('KnowledgePanel.Tags')
|
|
.getByTestId('tags-container')
|
|
.getByTestId(`tag-${tag}`)
|
|
).not.toBeVisible();
|
|
}
|
|
};
|
|
|
|
export const removeTagsFromChildren = async ({
|
|
page,
|
|
rowId,
|
|
tags,
|
|
rowSelector = 'data-row-key',
|
|
entityEndpoint,
|
|
}: {
|
|
page: Page;
|
|
tags: string[];
|
|
rowId: string;
|
|
rowSelector?: string;
|
|
entityEndpoint: string;
|
|
}) => {
|
|
for (const tag of tags) {
|
|
await page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('tags-container')
|
|
.getByTestId('edit-button')
|
|
.click();
|
|
|
|
await page
|
|
.getByTestId('tag-selector')
|
|
.getByTestId(`selected-tag-${tag}`)
|
|
.getByTestId('remove-tags')
|
|
.click();
|
|
|
|
let patchRequest;
|
|
if (
|
|
entityEndpoint === 'tables' ||
|
|
entityEndpoint === 'dashboard/datamodels'
|
|
) {
|
|
patchRequest = page.waitForResponse('/api/v1/columns/name/*');
|
|
} else {
|
|
patchRequest = page.waitForResponse(`/api/v1/${entityEndpoint}/*`);
|
|
}
|
|
await page.waitForSelector(
|
|
'.ant-select-dropdown [data-testid="saveAssociatedTag"]',
|
|
{ state: 'visible' }
|
|
);
|
|
|
|
await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled();
|
|
|
|
await page.getByTestId('saveAssociatedTag').click();
|
|
|
|
await patchRequest;
|
|
|
|
await expect(
|
|
page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('tags-container')
|
|
.getByTestId(`tag-${tag}`)
|
|
).not.toBeVisible();
|
|
}
|
|
};
|
|
|
|
type GlossaryTermOption = {
|
|
displayName: string;
|
|
name: string;
|
|
fullyQualifiedName: string;
|
|
};
|
|
|
|
export const assignGlossaryTerm = async (
|
|
page: Page,
|
|
glossaryTerm: GlossaryTermOption,
|
|
action: 'Add' | 'Edit' = 'Add'
|
|
) => {
|
|
await page
|
|
.getByTestId('KnowledgePanel.GlossaryTerms')
|
|
.getByTestId('glossary-container')
|
|
.getByTestId(action === 'Add' ? 'add-tag' : 'edit-button')
|
|
.click();
|
|
const searchGlossaryTerm = page.waitForResponse(
|
|
`/api/v1/search/query?q=*${encodeURIComponent(glossaryTerm.displayName)}*`
|
|
);
|
|
// Wait for the form to be visible before proceeding
|
|
await page.locator('#tagsForm_tags').waitFor({ state: 'visible' });
|
|
|
|
// Fill the input first
|
|
await page.locator('#tagsForm_tags').fill(glossaryTerm.displayName);
|
|
await searchGlossaryTerm;
|
|
|
|
await page.getByTestId(`tag-${glossaryTerm.fullyQualifiedName}`).click();
|
|
|
|
await page.waitForSelector(
|
|
'.ant-select-dropdown [data-testid="saveAssociatedTag"]',
|
|
{ state: 'visible' }
|
|
);
|
|
|
|
await expect(
|
|
page.getByTestId('custom-drop-down-menu').getByTestId('saveAssociatedTag')
|
|
).toBeEnabled();
|
|
|
|
await page
|
|
.getByTestId('custom-drop-down-menu')
|
|
.getByTestId('saveAssociatedTag')
|
|
.click();
|
|
|
|
await expect(
|
|
page.getByTestId('custom-drop-down-menu').getByTestId('saveAssociatedTag')
|
|
).not.toBeVisible();
|
|
|
|
await expect(
|
|
page
|
|
.getByTestId('KnowledgePanel.GlossaryTerms')
|
|
.getByTestId('glossary-container')
|
|
.getByTestId(`tag-${glossaryTerm.fullyQualifiedName}`)
|
|
).toBeVisible();
|
|
};
|
|
|
|
export const assignGlossaryTermToChildren = async ({
|
|
page,
|
|
glossaryTerm,
|
|
action = 'Add',
|
|
rowId,
|
|
rowSelector = 'data-row-key',
|
|
entityEndpoint,
|
|
}: {
|
|
page: Page;
|
|
glossaryTerm: GlossaryTermOption;
|
|
rowId: string;
|
|
action?: 'Add' | 'Edit';
|
|
rowSelector?: string;
|
|
entityEndpoint: string;
|
|
}) => {
|
|
await page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('glossary-container')
|
|
.getByTestId(action === 'Add' ? 'add-tag' : 'edit-button')
|
|
.click();
|
|
|
|
const searchGlossaryTerm = page.waitForResponse(
|
|
`/api/v1/search/query?q=*${encodeURIComponent(glossaryTerm.displayName)}*`
|
|
);
|
|
await page.locator('#tagsForm_tags').fill(glossaryTerm.displayName);
|
|
await searchGlossaryTerm;
|
|
await page.getByTestId(`tag-${glossaryTerm.fullyQualifiedName}`).click();
|
|
|
|
const putRequest = page.waitForResponse(
|
|
(response) => response.request().method() === 'PUT'
|
|
);
|
|
|
|
await page.waitForSelector(
|
|
'.ant-select-dropdown [data-testid="saveAssociatedTag"]',
|
|
{ state: 'visible' }
|
|
);
|
|
|
|
let patchRequest;
|
|
if (
|
|
entityEndpoint === 'tables' ||
|
|
entityEndpoint === 'dashboard/datamodels'
|
|
) {
|
|
patchRequest = page.waitForResponse('/api/v1/columns/name/*');
|
|
} else {
|
|
patchRequest = page.waitForResponse(`/api/v1/${entityEndpoint}/*`);
|
|
}
|
|
|
|
await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled();
|
|
|
|
await page.getByTestId('saveAssociatedTag').click();
|
|
await patchRequest;
|
|
|
|
await expect(page.getByTestId('saveAssociatedTag')).not.toBeVisible();
|
|
|
|
await putRequest;
|
|
|
|
await expect(
|
|
page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('glossary-container')
|
|
.getByTestId(`tag-${glossaryTerm.fullyQualifiedName}`)
|
|
).toBeVisible();
|
|
};
|
|
|
|
export const removeGlossaryTerm = async (
|
|
page: Page,
|
|
glossaryTerms: GlossaryTermOption[]
|
|
) => {
|
|
for (const tag of glossaryTerms) {
|
|
await page
|
|
.getByTestId('KnowledgePanel.GlossaryTerms')
|
|
.getByTestId('glossary-container')
|
|
.getByTestId('edit-button')
|
|
.click();
|
|
|
|
await page
|
|
.getByTestId('glossary-container')
|
|
.getByTestId(new RegExp(tag.name))
|
|
.getByTestId('remove-tags')
|
|
.locator('svg')
|
|
.click();
|
|
|
|
const patchRequest = page.waitForResponse(
|
|
(response) => response.request().method() === 'PATCH'
|
|
);
|
|
|
|
await page.waitForSelector(
|
|
'.ant-select-dropdown [data-testid="saveAssociatedTag"]',
|
|
{ state: 'visible' }
|
|
);
|
|
|
|
await expect(
|
|
page.getByTestId('custom-drop-down-menu').getByTestId('saveAssociatedTag')
|
|
).toBeEnabled();
|
|
|
|
await page
|
|
.getByTestId('custom-drop-down-menu')
|
|
.getByTestId('saveAssociatedTag')
|
|
.click();
|
|
await patchRequest;
|
|
|
|
await expect(
|
|
page
|
|
.getByTestId('KnowledgePanel.GlossaryTerms')
|
|
.getByTestId('glossary-container')
|
|
.getByTestId(`tag-${tag.fullyQualifiedName}`)
|
|
).not.toBeVisible();
|
|
}
|
|
};
|
|
|
|
export const removeGlossaryTermFromChildren = async ({
|
|
page,
|
|
glossaryTerms,
|
|
rowId,
|
|
entityEndpoint,
|
|
rowSelector = 'data-row-key',
|
|
}: {
|
|
page: Page;
|
|
glossaryTerms: GlossaryTermOption[];
|
|
rowId: string;
|
|
entityEndpoint: string;
|
|
rowSelector?: string;
|
|
}) => {
|
|
for (const tag of glossaryTerms) {
|
|
await page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('glossary-container')
|
|
.getByTestId('edit-button')
|
|
.click();
|
|
|
|
await page
|
|
.getByTestId('glossary-container')
|
|
.getByTestId(new RegExp(tag.name))
|
|
.getByTestId('remove-tags')
|
|
.locator('svg')
|
|
.click();
|
|
|
|
let patchRequest;
|
|
if (
|
|
entityEndpoint === 'tables' ||
|
|
entityEndpoint === 'dashboard/datamodels'
|
|
) {
|
|
patchRequest = page.waitForResponse('/api/v1/columns/name/*');
|
|
} else {
|
|
patchRequest = page.waitForResponse(`/api/v1/${entityEndpoint}/*`);
|
|
}
|
|
|
|
await page.waitForSelector(
|
|
'.ant-select-dropdown [data-testid="saveAssociatedTag"]',
|
|
{ state: 'visible' }
|
|
);
|
|
|
|
await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled();
|
|
|
|
await page.getByTestId('saveAssociatedTag').click();
|
|
|
|
await patchRequest;
|
|
|
|
expect(
|
|
page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('glossary-container')
|
|
.getByTestId(`tag-${tag.fullyQualifiedName}`)
|
|
).not.toBeVisible();
|
|
}
|
|
};
|
|
|
|
export const upVote = async (page: Page, endPoint: string) => {
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endPoint}/*/vote`);
|
|
|
|
await page.getByTestId('up-vote-btn').click();
|
|
await patchRequest;
|
|
|
|
await expect(page.getByTestId('up-vote-count')).toContainText('1');
|
|
};
|
|
|
|
export const downVote = async (page: Page, endPoint: string) => {
|
|
const patchRequest = page.waitForResponse(`/api/v1/${endPoint}/*/vote`);
|
|
await page.getByTestId('down-vote-btn').click();
|
|
await patchRequest;
|
|
|
|
await expect(page.getByTestId('down-vote-count')).toContainText('1');
|
|
};
|
|
|
|
export const followEntity = async (
|
|
page: Page,
|
|
endpoint: EntityTypeEndpoint
|
|
) => {
|
|
const followResponse = page.waitForResponse(
|
|
`/api/v1/${endpoint}/*/followers`
|
|
);
|
|
await page.getByTestId('entity-follow-button').click();
|
|
await followResponse;
|
|
|
|
await expect(page.getByTestId('entity-follow-button')).toContainText(
|
|
'Unfollow'
|
|
);
|
|
};
|
|
|
|
export const unFollowEntity = async (
|
|
page: Page,
|
|
endpoint: EntityTypeEndpoint
|
|
) => {
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
const followButton = page.getByTestId('entity-follow-button');
|
|
|
|
await followButton.waitFor({ state: 'visible' });
|
|
|
|
await expect(followButton).toContainText('Unfollow');
|
|
|
|
const unFollowResponse = page.waitForResponse(
|
|
`/api/v1/${endpoint}/*/followers/*`
|
|
);
|
|
await followButton.click();
|
|
await unFollowResponse;
|
|
|
|
await expect(page.getByTestId('entity-follow-button')).toContainText(
|
|
'Follow'
|
|
);
|
|
};
|
|
|
|
export const validateFollowedEntityToWidget = async (
|
|
page: Page,
|
|
entity: string,
|
|
isFollowing: boolean
|
|
) => {
|
|
await redirectToHomePage(page);
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForSelector('[data-testid="loader"]', {
|
|
state: 'detached',
|
|
});
|
|
if (isFollowing) {
|
|
await page.getByTestId('following-widget').isVisible();
|
|
|
|
await page.getByTestId(`following-${entity}`).isVisible();
|
|
} else {
|
|
await page.getByTestId('following-widget').isVisible();
|
|
|
|
await expect(page.getByTestId(`following-${entity}`)).not.toBeVisible();
|
|
}
|
|
};
|
|
|
|
const announcementForm = async (
|
|
page: Page,
|
|
data: {
|
|
title: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
description: string;
|
|
}
|
|
) => {
|
|
await page.fill('#title', data.title);
|
|
|
|
await page.click('#startTime');
|
|
await page.fill('#startTime', `${data.startDate}`);
|
|
await page.press('#startTime', 'Enter');
|
|
|
|
await page.click('#endTime');
|
|
await page.fill('#endTime', `${data.endDate}`);
|
|
await page.press('#startTime', 'Enter');
|
|
|
|
await page.locator(descriptionBox).fill(data.description);
|
|
|
|
await page.locator('#announcement-submit').scrollIntoViewIfNeeded();
|
|
const announcementSubmit = page.waitForResponse(
|
|
'/api/v1/feed?entityLink=*type=Announcement*'
|
|
);
|
|
await page.click('#announcement-submit');
|
|
await announcementSubmit;
|
|
await page.click('[data-testid="announcement-close"]');
|
|
await page.click('[data-testid="alert-icon-close"]');
|
|
};
|
|
|
|
export const createAnnouncement = async (
|
|
page: Page,
|
|
data: { title: string; description: string }
|
|
) => {
|
|
await page.getByTestId('manage-button').click();
|
|
await page.getByTestId('announcement-button').click();
|
|
const startDate = customFormatDateTime(getCurrentMillis(), 'yyyy-MM-dd');
|
|
const endDate = customFormatDateTime(
|
|
getEpochMillisForFutureDays(5),
|
|
'yyyy-MM-dd'
|
|
);
|
|
|
|
await expect(page.getByTestId('announcement-error')).toContainText(
|
|
'No Announcements, Click on add announcement to add one.'
|
|
);
|
|
|
|
await page.getByTestId('add-announcement').click();
|
|
|
|
await expect(page.locator('.ant-modal-header')).toContainText(
|
|
'Make an announcement'
|
|
);
|
|
|
|
await announcementForm(page, { ...data, startDate, endDate });
|
|
await page.reload();
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForSelector('[data-testid="loader"]', {
|
|
state: 'detached',
|
|
});
|
|
|
|
await expect(page.getByTestId('announcement-card')).toBeVisible();
|
|
await expect(page.getByTestId('announcement-title')).toHaveText(data.title);
|
|
|
|
await expect(page.getByTestId('announcement-card')).toContainText(
|
|
data.description
|
|
);
|
|
};
|
|
|
|
export const replyAnnouncement = async (page: Page) => {
|
|
await page.click('[data-testid="announcement-card"]');
|
|
|
|
await page.hover(
|
|
'[data-testid="announcement-thread-body"] [data-testid="announcement-card"] [data-testid="main-message"]'
|
|
);
|
|
|
|
await page.waitForSelector('.ant-popover', { state: 'visible' });
|
|
|
|
await expect(page.getByTestId('add-reply').locator('svg')).toBeVisible();
|
|
|
|
await page.getByTestId('add-reply').locator('svg').click();
|
|
|
|
await expect(page.locator('.ql-editor')).toBeVisible();
|
|
|
|
const sendButtonIsDisabled = await page
|
|
.locator('[data-testid="send-button"]')
|
|
.isEnabled();
|
|
|
|
expect(sendButtonIsDisabled).toBe(false);
|
|
|
|
await page.fill('[data-testid="editor-wrapper"] .ql-editor', 'Reply message');
|
|
await page.click('[data-testid="send-button"]');
|
|
|
|
await expect(
|
|
page.locator('[data-testid="replies"] [data-testid="viewer-container"]')
|
|
).toHaveText('Reply message');
|
|
await expect(page.locator('[data-testid="show-reply-thread"]')).toHaveText(
|
|
'1 replies'
|
|
);
|
|
|
|
await page.hover('[data-testid="replies"] > [data-testid="main-message"]');
|
|
await page.waitForSelector('.ant-popover', { state: 'visible' });
|
|
await page.click('[data-testid="edit-message"]');
|
|
|
|
await page.fill(
|
|
'[data-testid="editor-wrapper"] .ql-editor',
|
|
'Reply message edited'
|
|
);
|
|
|
|
await page.click('[data-testid="save-button"]');
|
|
|
|
await expect(
|
|
page.locator('[data-testid="replies"] [data-testid="viewer-container"]')
|
|
).toHaveText('Reply message edited');
|
|
|
|
await page.reload();
|
|
};
|
|
|
|
export const deleteAnnouncement = async (page: Page) => {
|
|
await page.getByTestId('manage-button').click();
|
|
await page.getByTestId('announcement-button').click();
|
|
|
|
await page
|
|
.locator(
|
|
'[data-testid="announcement-thread-body"] [data-testid="announcement-card"]'
|
|
)
|
|
.isVisible();
|
|
|
|
await page.hover(
|
|
'[data-testid="announcement-thread-body"] [data-testid="announcement-card"] [data-testid="main-message"]'
|
|
);
|
|
|
|
await page.waitForSelector('.ant-popover', { state: 'visible' });
|
|
|
|
await page.click('[data-testid="delete-message"]');
|
|
const modalText = await page.textContent('.ant-modal-body');
|
|
|
|
expect(modalText).toContain(
|
|
'Are you sure you want to permanently delete this message?'
|
|
);
|
|
|
|
const getFeed = page.waitForResponse('/api/v1/feed/*');
|
|
await page.click('[data-testid="save-button"]');
|
|
await getFeed;
|
|
|
|
await page.reload();
|
|
await page.waitForLoadState('networkidle');
|
|
await page.getByTestId('manage-button').click();
|
|
await page.getByTestId('announcement-button').click();
|
|
|
|
await expect(page.getByTestId('announcement-error')).toContainText(
|
|
'No Announcements, Click on add announcement to add one.'
|
|
);
|
|
};
|
|
|
|
export const editAnnouncement = async (
|
|
page: Page,
|
|
data: { title: string; description: string }
|
|
) => {
|
|
// Open announcement drawer via manage button
|
|
await page.getByTestId('manage-button').click();
|
|
await page.getByTestId('announcement-button').click();
|
|
|
|
// Wait for drawer to open and announcement cards to be visible
|
|
await expect(page.getByTestId('announcement-drawer')).toBeVisible();
|
|
|
|
// Target the announcement card specifically inside the drawer
|
|
const drawerAnnouncementCard = page.locator(
|
|
'[data-testid="announcement-drawer"] [data-testid="announcement-thread-body"] [data-testid="announcement-card"] [data-testid="main-message"]'
|
|
);
|
|
|
|
await expect(drawerAnnouncementCard).toBeVisible();
|
|
|
|
// Hover over the announcement card inside the drawer to show the edit options popover
|
|
await drawerAnnouncementCard.hover();
|
|
|
|
// Wait for the popover to become visible
|
|
await page.waitForSelector('.ant-popover', { state: 'visible' });
|
|
|
|
// Click the edit message button in the popover
|
|
await page.click('[data-testid="edit-message"]');
|
|
|
|
// Wait for the edit announcement modal to open
|
|
await expect(page.locator('.ant-modal-header')).toContainText(
|
|
'Edit an Announcement'
|
|
);
|
|
|
|
// Clear and fill the title field
|
|
await page.fill('[data-testid="edit-announcement"] #title', '');
|
|
await page.fill('[data-testid="edit-announcement"] #title', data.title);
|
|
|
|
// Clear and fill the description field
|
|
await page
|
|
.locator('[data-testid="edit-announcement"]')
|
|
.locator(descriptionBox)
|
|
.fill('');
|
|
await page
|
|
.locator('[data-testid="edit-announcement"]')
|
|
.locator(descriptionBox)
|
|
.fill(data.description);
|
|
|
|
// Save the changes and wait for the API response
|
|
const updateResponse = page.waitForResponse('/api/v1/feed/*');
|
|
await page
|
|
.locator(
|
|
'[data-testid="edit-announcement"] .ant-modal-footer .ant-btn-primary'
|
|
)
|
|
.click();
|
|
await updateResponse;
|
|
|
|
// Wait for modal to close
|
|
await expect(
|
|
page.locator('[data-testid="edit-announcement"]')
|
|
).not.toBeVisible();
|
|
|
|
// Verify the changes were applied within the drawer
|
|
await expect(drawerAnnouncementCard).toContainText(data.title);
|
|
await expect(drawerAnnouncementCard).toContainText(data.description);
|
|
|
|
// Close the announcement drawer
|
|
await page.locator('[data-testid="announcement-close"]').click();
|
|
|
|
await expect(page.getByTestId('announcement-drawer')).not.toBeVisible();
|
|
};
|
|
|
|
export const createInactiveAnnouncement = async (
|
|
page: Page,
|
|
data: { title: string; description: string }
|
|
) => {
|
|
await page.getByTestId('manage-button').click();
|
|
await page.getByTestId('announcement-button').click();
|
|
const startDate = customFormatDateTime(
|
|
getEpochMillisForFutureDays(6),
|
|
'yyyy-MM-dd'
|
|
);
|
|
const endDate = customFormatDateTime(
|
|
getEpochMillisForFutureDays(11),
|
|
'yyyy-MM-dd'
|
|
);
|
|
|
|
await page.getByTestId('add-announcement').click();
|
|
|
|
await expect(page.locator('.ant-modal-header')).toContainText(
|
|
'Make an announcement'
|
|
);
|
|
|
|
await announcementForm(page, { ...data, startDate, endDate });
|
|
await page.getByTestId('inActive-announcements').isVisible();
|
|
await page.reload();
|
|
};
|
|
|
|
export const updateDisplayNameForEntity = async (
|
|
page: Page,
|
|
displayName: string,
|
|
endPoint: string
|
|
) => {
|
|
await page.click('[data-testid="manage-button"]');
|
|
await page.click('[data-testid="rename-button"]');
|
|
|
|
const nameInputIsDisabled = await page.locator('#name').isEnabled();
|
|
|
|
expect(nameInputIsDisabled).toBe(false);
|
|
|
|
await expect(page.locator('#displayName')).toBeVisible();
|
|
|
|
await page.locator('#displayName').clear();
|
|
|
|
await page.fill('#displayName', displayName);
|
|
const updateNameResponse = page.waitForResponse(`/api/v1/${endPoint}/*`);
|
|
await page.click('[data-testid="save-button"]');
|
|
await updateNameResponse;
|
|
|
|
await expect(
|
|
page.locator('[data-testid="entity-header-display-name"]')
|
|
).toHaveText(displayName);
|
|
};
|
|
|
|
export const updateDisplayNameForEntityChildren = async (
|
|
page: Page,
|
|
displayName: { oldDisplayName: string; newDisplayName: string },
|
|
rowId: string,
|
|
rowSelector = 'data-row-key'
|
|
) => {
|
|
await page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('edit-displayName-button')
|
|
.click();
|
|
|
|
const nameInputIsDisabled = await page.locator('#name').isEnabled();
|
|
|
|
expect(nameInputIsDisabled).toBe(false);
|
|
|
|
await expect(page.locator('#displayName')).toBeVisible();
|
|
|
|
expect(await page.locator('#displayName').inputValue()).toBe(
|
|
displayName.oldDisplayName
|
|
);
|
|
|
|
await page.locator('#displayName').fill(displayName.newDisplayName);
|
|
|
|
const updateRequest = page.waitForResponse((req) =>
|
|
['PUT', 'PATCH'].includes(req.request().method())
|
|
);
|
|
|
|
await page.click('[data-testid="save-button"]');
|
|
await updateRequest;
|
|
|
|
if (displayName.newDisplayName === '') {
|
|
await expect(
|
|
page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('column-display-name')
|
|
).not.toBeAttached();
|
|
} else {
|
|
await expect(
|
|
page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('column-display-name')
|
|
).toHaveText(displayName.newDisplayName);
|
|
}
|
|
};
|
|
|
|
export const removeDisplayNameForEntityChildren = async (
|
|
page: Page,
|
|
displayName: string,
|
|
rowId: string,
|
|
rowSelector = 'data-row-key'
|
|
) => {
|
|
await page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('edit-displayName-button')
|
|
.click();
|
|
|
|
const nameInputIsDisabled = await page.locator('#name').isEnabled();
|
|
|
|
expect(nameInputIsDisabled).toBe(false);
|
|
|
|
await expect(page.locator('#displayName')).toBeVisible();
|
|
|
|
expect(await page.locator('#displayName').inputValue()).toBe(displayName);
|
|
|
|
await page.locator('#displayName').fill('');
|
|
|
|
const updateRequest = page.waitForResponse((response) => {
|
|
const method = response.request().method();
|
|
const url = response.url();
|
|
|
|
// Check Analytics Api Does Not Intterupt With PUT CAll
|
|
return (
|
|
(method === 'PUT' || method === 'PATCH') &&
|
|
!url.includes('api/v1/analytics/web/events/collect')
|
|
);
|
|
});
|
|
await page.click('[data-testid="save-button"]');
|
|
await updateRequest;
|
|
|
|
await expect(
|
|
page
|
|
.locator(`[${rowSelector}="${rowId}"]`)
|
|
.getByTestId('column-display-name')
|
|
).not.toBeVisible();
|
|
};
|
|
|
|
export const checkForEditActions = async ({
|
|
page,
|
|
entityType,
|
|
deleted = false,
|
|
}: {
|
|
page: Page;
|
|
entityType: string;
|
|
deleted?: boolean;
|
|
}) => {
|
|
for (const {
|
|
containerSelector,
|
|
elementSelector,
|
|
} of LIST_OF_FIELDS_TO_EDIT_TO_BE_DISABLED) {
|
|
if (
|
|
elementSelector === '[data-testid="entity-follow-button"]' &&
|
|
ENTITIES_WITHOUT_FOLLOWING_BUTTON.includes(entityType)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (entityType.startsWith('services/')) {
|
|
await page.getByRole('tab').nth(1).click();
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
continue;
|
|
}
|
|
|
|
if (elementSelector === '[data-testid="entity-follow-button"]') {
|
|
deleted
|
|
? await expect(page.locator(elementSelector)).not.toBeVisible()
|
|
: await expect(page.locator(elementSelector)).toBeVisible();
|
|
|
|
continue;
|
|
}
|
|
|
|
const isDisabled = await page
|
|
.locator(`${containerSelector} ${elementSelector}`)
|
|
.isEnabled();
|
|
|
|
expect(isDisabled).toBe(!deleted);
|
|
}
|
|
|
|
for (const {
|
|
containerSelector,
|
|
elementSelector,
|
|
} of LIST_OF_FIELDS_TO_EDIT_NOT_TO_BE_PRESENT) {
|
|
if (!deleted) {
|
|
await expect(
|
|
page.locator(`${containerSelector} ${elementSelector}`)
|
|
).toBeVisible();
|
|
} else {
|
|
const exists = await page
|
|
.locator(`${containerSelector} ${elementSelector}`)
|
|
.isVisible();
|
|
|
|
expect(exists).toBe(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const checkLineageTabActions = async (page: Page, deleted?: boolean) => {
|
|
// Click the lineage tab
|
|
const lineageApi = page.waitForResponse('/api/v1/lineage/getLineage?fqn=*');
|
|
await page.click('[data-testid="lineage"]');
|
|
|
|
// Ensure the response has been received and check the status code
|
|
await lineageApi;
|
|
|
|
// Check the presence or absence of the edit-lineage element based on the deleted flag
|
|
if (deleted) {
|
|
await expect(
|
|
page.locator('[data-testid="edit-lineage"]')
|
|
).not.toBeVisible();
|
|
} else {
|
|
await expect(page.locator('[data-testid="edit-lineage"]')).toBeVisible();
|
|
}
|
|
};
|
|
|
|
export const checkForTableSpecificFields = async (
|
|
page: Page,
|
|
deleted?: boolean
|
|
) => {
|
|
const queryDataUrl = `/api/v1/search/query?q=*index=query_search_index*`;
|
|
|
|
const queryApi = page.waitForResponse(queryDataUrl);
|
|
// Click the table queries tab
|
|
await page.click('[data-testid="table_queries"]');
|
|
|
|
if (!deleted) {
|
|
await queryApi;
|
|
// Check if the add-query button is enabled
|
|
const addQueryButton = page.locator('[data-testid="add-query-btn"]');
|
|
|
|
await expect(addQueryButton).toBeEnabled();
|
|
} else {
|
|
// Check for the no data placeholder message
|
|
const noDataPlaceholder = page
|
|
.getByTestId('no-queries')
|
|
.getByTestId('no-data-placeholder');
|
|
|
|
await expect(noDataPlaceholder).toContainText(
|
|
'Queries data is not available for deleted entities.'
|
|
);
|
|
}
|
|
|
|
// Click the profiler tab
|
|
await page.click('[data-testid="profiler"]');
|
|
|
|
// Check the visibility of profiler buttons based on the deleted flag
|
|
const addTableTestButton = page.locator(
|
|
'[data-testid="profiler-add-table-test-btn"]'
|
|
);
|
|
const settingButton = page.locator('[data-testid="profiler-setting-btn"]');
|
|
|
|
if (!deleted) {
|
|
await expect(addTableTestButton).toBeVisible();
|
|
await expect(settingButton).toBeVisible();
|
|
} else {
|
|
await expect(addTableTestButton).not.toBeVisible();
|
|
await expect(settingButton).not.toBeVisible();
|
|
}
|
|
};
|
|
|
|
export const deletedEntityCommonChecks = async ({
|
|
page,
|
|
endPoint,
|
|
deleted,
|
|
}: {
|
|
page: Page;
|
|
endPoint: EntityTypeEndpoint;
|
|
deleted?: boolean;
|
|
}) => {
|
|
const isTableEntity = endPoint === EntityTypeEndpoint.Table;
|
|
|
|
// Check if all the edit actions are available for the entity
|
|
await checkForEditActions({
|
|
page,
|
|
entityType: endPoint,
|
|
deleted,
|
|
});
|
|
|
|
if (isTableEntity) {
|
|
await checkLineageTabActions(page, deleted);
|
|
}
|
|
|
|
if (isTableEntity) {
|
|
await checkForTableSpecificFields(page, deleted);
|
|
}
|
|
|
|
await page.click('[data-testid="manage-button"]');
|
|
|
|
if (deleted) {
|
|
// only two menu options (restore and delete) should be present
|
|
await expect(
|
|
page.locator(
|
|
'[data-testid="manage-dropdown-list-container"] [data-testid="announcement-button"]'
|
|
)
|
|
).toBeHidden();
|
|
await expect(
|
|
page.locator(
|
|
'[data-testid="manage-dropdown-list-container"] [data-testid="rename-button"]'
|
|
)
|
|
).toBeHidden();
|
|
await expect(
|
|
page.locator(
|
|
'[data-testid="manage-dropdown-list-container"] [data-testid="profiler-setting-button"]'
|
|
)
|
|
).toBeHidden();
|
|
await expect(
|
|
page.locator(
|
|
'[data-testid="manage-dropdown-list-container"] [data-testid="restore-button"]'
|
|
)
|
|
).toBeVisible();
|
|
await expect(
|
|
page.locator(
|
|
'[data-testid="manage-dropdown-list-container"] [data-testid="delete-button"]'
|
|
)
|
|
).toBeVisible();
|
|
} else {
|
|
await expect(
|
|
page.locator(
|
|
'[data-testid="manage-dropdown-list-container"] [data-testid="announcement-button"]'
|
|
)
|
|
).toBeVisible();
|
|
await expect(
|
|
page.locator(
|
|
'[data-testid="manage-dropdown-list-container"] [data-testid="rename-button"]'
|
|
)
|
|
).toBeVisible();
|
|
|
|
if (
|
|
[EntityTypeEndpoint.Database, EntityTypeEndpoint.DatabaseSchema].includes(
|
|
endPoint
|
|
)
|
|
) {
|
|
await expect(
|
|
page.locator(
|
|
'[data-testid="manage-dropdown-list-container"] [data-testid="profiler-setting-button"]'
|
|
)
|
|
).toBeVisible();
|
|
}
|
|
|
|
await expect(
|
|
page.locator(
|
|
'[data-testid="manage-dropdown-list-container"] [data-testid="delete-button"]'
|
|
)
|
|
).toBeVisible();
|
|
}
|
|
|
|
await clickOutside(page);
|
|
};
|
|
|
|
export const restoreEntity = async (page: Page) => {
|
|
await expect(page.locator('[data-testid="deleted-badge"]')).toBeVisible();
|
|
|
|
await page.click('[data-testid="manage-button"]');
|
|
await page.click('[data-testid="restore-button"]');
|
|
await page.click('button:has-text("Restore")');
|
|
|
|
await toastNotification(page, /restored successfully/);
|
|
|
|
const exists = await page
|
|
.locator('[data-testid="deleted-badge"]')
|
|
.isVisible();
|
|
|
|
expect(exists).toBe(false);
|
|
};
|
|
|
|
export const softDeleteEntity = async (
|
|
page: Page,
|
|
entityName: string,
|
|
endPoint: EntityTypeEndpoint,
|
|
displayName: string
|
|
) => {
|
|
await deletedEntityCommonChecks({
|
|
page,
|
|
endPoint,
|
|
deleted: false,
|
|
});
|
|
|
|
await clickOutside(page);
|
|
|
|
await page.click('[data-testid="manage-button"]');
|
|
await page.click('[data-testid="delete-button"]');
|
|
|
|
await page.waitForSelector('[role="dialog"].ant-modal');
|
|
|
|
await expect(page.locator('[role="dialog"].ant-modal')).toBeVisible();
|
|
await expect(page.locator('.ant-modal-title')).toContainText(displayName);
|
|
|
|
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
|
|
const deleteResponse = page.waitForResponse(
|
|
`/api/v1/${endPoint}/async/*?hardDelete=false&recursive=true`
|
|
);
|
|
await page.click('[data-testid="confirm-button"]');
|
|
|
|
await deleteResponse;
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await toastNotification(
|
|
page,
|
|
/(deleted successfully!|Delete operation initiated)/,
|
|
BIG_ENTITY_DELETE_TIMEOUT
|
|
);
|
|
|
|
await page.reload();
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
// Retry mechanism for checking deleted badge
|
|
let deletedBadge = page.locator('[data-testid="deleted-badge"]');
|
|
let attempts = 0;
|
|
const maxAttempts = 5;
|
|
|
|
while (attempts < maxAttempts) {
|
|
const isVisible = await deletedBadge.isVisible();
|
|
if (isVisible) {
|
|
break;
|
|
}
|
|
|
|
attempts++;
|
|
if (attempts < maxAttempts) {
|
|
await page.reload();
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForSelector('[data-testid="loader"]', {
|
|
state: 'detached',
|
|
});
|
|
deletedBadge = page.locator('[data-testid="deleted-badge"]');
|
|
}
|
|
}
|
|
|
|
await expect(deletedBadge).toHaveText('Deleted');
|
|
|
|
await deletedEntityCommonChecks({
|
|
page,
|
|
endPoint,
|
|
deleted: true,
|
|
});
|
|
|
|
await clickOutside(page);
|
|
|
|
if (endPoint === EntityTypeEndpoint.Table) {
|
|
await page.click('[data-testid="breadcrumb-link"]:last-child');
|
|
const deletedTableResponse = page.waitForResponse(
|
|
'/api/v1/tables?*databaseSchema=*'
|
|
);
|
|
await page.click('[data-testid="show-deleted"]');
|
|
await deletedTableResponse;
|
|
const tableCount = page.locator(
|
|
'[data-testid="table"] [data-testid="count"]'
|
|
);
|
|
|
|
await expect(tableCount).toContainText('1');
|
|
|
|
await page.click(`[data-testid="${entityName}"]`);
|
|
}
|
|
|
|
await restoreEntity(page);
|
|
await page.reload();
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
await deletedEntityCommonChecks({
|
|
page,
|
|
endPoint,
|
|
deleted: false,
|
|
});
|
|
};
|
|
|
|
export const hardDeleteEntity = async (
|
|
page: Page,
|
|
entityName: string,
|
|
endPoint: EntityTypeEndpoint
|
|
) => {
|
|
await clickOutside(page);
|
|
await page.click('[data-testid="manage-button"]');
|
|
await page.waitForSelector('[data-testid="delete-button"]');
|
|
await page.click('[data-testid="delete-button"]');
|
|
|
|
await page.waitForSelector('[role="dialog"].ant-modal');
|
|
|
|
await expect(page.locator('[role="dialog"].ant-modal')).toBeVisible();
|
|
|
|
await expect(
|
|
page.locator('[data-testid="delete-modal"] .ant-modal-title')
|
|
).toHaveText(new RegExp(entityName));
|
|
|
|
await page.click('[data-testid="hard-delete-option"]');
|
|
await page.check('[data-testid="hard-delete"]');
|
|
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
|
|
const deleteResponse = page.waitForResponse(
|
|
`/api/v1/${endPoint}/async/*?hardDelete=true&recursive=true`
|
|
);
|
|
await page.click('[data-testid="confirm-button"]');
|
|
await deleteResponse;
|
|
|
|
await expect(page.getByTestId('alert-bar')).toHaveText(
|
|
/(deleted successfully!|Delete operation initiated)/,
|
|
{ timeout: BIG_ENTITY_DELETE_TIMEOUT }
|
|
);
|
|
};
|
|
|
|
export const checkDataAssetWidget = async (page: Page, serviceType: string) => {
|
|
await clickOutside(page);
|
|
const quickFilterResponse = page.waitForResponse(
|
|
`/api/v1/search/query?q=&index=dataAsset*${serviceType}*`
|
|
);
|
|
|
|
await page
|
|
.locator(`[data-testid="data-asset-service-${serviceType}"]`)
|
|
.click();
|
|
|
|
await quickFilterResponse;
|
|
|
|
await expect(
|
|
page.locator('[data-testid="search-dropdown-Service Type"]')
|
|
).toContainText(serviceType);
|
|
|
|
await expect(
|
|
page
|
|
.getByTestId('explore-tree')
|
|
.locator('span')
|
|
.filter({ hasText: serviceType })
|
|
.first()
|
|
).toHaveClass(/ant-tree-node-selected/);
|
|
};
|
|
|
|
export const escapeESReservedCharacters = (text?: string) => {
|
|
const reUnescapedHtml = /[\\[\]#+=&|><!(){}^"~*?:/-]/g;
|
|
const reHasUnescapedHtml = RegExp(reUnescapedHtml.source);
|
|
|
|
const getReplacedChar = (char: string) => {
|
|
return ES_RESERVED_CHARACTERS[char] ?? char;
|
|
};
|
|
|
|
return text && reHasUnescapedHtml.test(text)
|
|
? text.replace(reUnescapedHtml, getReplacedChar)
|
|
: text ?? '';
|
|
};
|
|
|
|
export const getEncodedFqn = (fqn: string, spaceAsPlus = false) => {
|
|
let uri = encodeURIComponent(fqn);
|
|
|
|
if (spaceAsPlus) {
|
|
uri = uri.replaceAll('%20', '+');
|
|
}
|
|
|
|
return uri;
|
|
};
|
|
|
|
export const getEntityDisplayName = (entity?: {
|
|
name?: string;
|
|
displayName?: string;
|
|
}) => {
|
|
return entity?.displayName || entity?.name || '';
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param description HTML string
|
|
* @returns Text from HTML string
|
|
*/
|
|
export const getTextFromHtmlString = (description?: string): string => {
|
|
if (!description) {
|
|
return '';
|
|
}
|
|
|
|
return description.replace(/<[^>]*>/g, '').trim();
|
|
};
|
|
|
|
export const getFirstRowColumnLink = (page: Page) => {
|
|
return page
|
|
.getByTestId('databaseSchema-tables')
|
|
.locator('[data-testid="column-name"] a')
|
|
.first();
|
|
};
|
|
|
|
export const generateEntityChildren = (entityName: string, count = 25) => {
|
|
return Array.from({ length: count }, (_, i) => {
|
|
const id = uuid();
|
|
|
|
return {
|
|
name: `pw-${entityName}-${i + 1}-${id}`,
|
|
displayName: `pw-${entityName}-${i + 1}-${id}`,
|
|
};
|
|
});
|
|
};
|
|
|
|
export const checkItemNotExistsInQuickFilter = async (
|
|
page: Page,
|
|
filterLabel: string,
|
|
filterValue: string
|
|
) => {
|
|
await sidebarClick(page, SidebarItem.EXPLORE);
|
|
await page.waitForLoadState('networkidle');
|
|
await page.click(`[data-testid="search-dropdown-${filterLabel}"]`);
|
|
const testId = filterValue.toLowerCase();
|
|
|
|
// testId should not be present
|
|
await expect(page.getByTestId(testId)).toBeHidden();
|
|
|
|
await clickOutside(page);
|
|
};
|
|
|
|
export const checkExploreSearchFilter = async (
|
|
page: Page,
|
|
filterLabel: string,
|
|
filterKey: string,
|
|
filterValue: string,
|
|
entity?: EntityClass
|
|
) => {
|
|
await sidebarClick(page, SidebarItem.EXPLORE);
|
|
if (filterKey === 'tier.tagFQN') {
|
|
const tierList = page.waitForResponse(
|
|
`/api/v1/search/aggregate?index=dataAsset&field=tier.tagFQN**`
|
|
);
|
|
await page.click(`[data-testid="search-dropdown-${filterLabel}"]`);
|
|
await tierList;
|
|
} else {
|
|
await page.click(`[data-testid="search-dropdown-${filterLabel}"]`);
|
|
}
|
|
await searchAndClickOnOption(
|
|
page,
|
|
{
|
|
label: filterLabel,
|
|
key: filterKey,
|
|
value: filterValue,
|
|
},
|
|
true
|
|
);
|
|
|
|
const rawFilterValue = (filterValue ?? '').replace(/ /g, '+').toLowerCase();
|
|
|
|
// Escape double quotes before encoding
|
|
const escapedValue = rawFilterValue.replace(/"/g, '\\"');
|
|
|
|
const filterValueForSearchURL =
|
|
filterKey === 'tier.tagFQN'
|
|
? filterValue
|
|
: /["%]/.test(filterValue ?? '')
|
|
? encodeURIComponent(escapedValue)
|
|
: rawFilterValue;
|
|
|
|
const querySearchURL = `/api/v1/search/query?*index=dataAsset*query_filter=*should*${filterKey}*${filterValueForSearchURL}*`;
|
|
|
|
const queryRes = page.waitForResponse(querySearchURL);
|
|
await page.click('[data-testid="update-btn"]');
|
|
await queryRes;
|
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
|
|
|
await expect(
|
|
page.getByTestId(
|
|
`table-data-card_${
|
|
(entity as any)?.entityResponseData?.fullyQualifiedName
|
|
}`
|
|
)
|
|
).toBeVisible();
|
|
|
|
await page.click('[data-testid="clear-filters"]');
|
|
|
|
await entity?.visitEntityPage(page);
|
|
};
|
|
|
|
export const getEntityDataTypeDisplayPatch = (entity: EntityClass) => {
|
|
switch (entity.getType()) {
|
|
case 'Table':
|
|
case 'DashboardDataModel':
|
|
return '/columns/0/dataTypeDisplay';
|
|
case 'ApiEndpoint':
|
|
return '/requestSchema/schemaFields/0/dataTypeDisplay';
|
|
case 'Topic':
|
|
return '/messageSchema/schemaFields/0/dataTypeDisplay';
|
|
case 'Container':
|
|
return '/dataModel/columns/0/dataTypeDisplay';
|
|
case 'SearchIndex':
|
|
return '/fields/0/dataTypeDisplay';
|
|
|
|
default:
|
|
return undefined;
|
|
}
|
|
};
|