Chore(UI): Fix flaky playwright tests (#23156)

* Fix playwright tests
- AdvancedSearch.spec.ts
- Autopilot.spec.ts
- DescriptionSuggestion.spec.ts
- ObservabilityAlert.spec.ts
- EntityVersionPages.spec.ts

* Fix the flakiness while logging in

* Update config to run only required tests

* Revert description suggestion spec changes

* Revert playwright config changes

* Update playwright config

* improve observability alert page navigation

* Fix Description suggestion

* Standardize left nav bar to close for all tests

* Fix failing playwright tests
This commit is contained in:
Aniket Katkar 2025-08-30 18:37:45 +05:30
parent 83ba3633ae
commit 4cd9f13d97
18 changed files with 196 additions and 116 deletions

View File

@ -98,20 +98,23 @@ test.describe('Advanced Search Custom Property', () => {
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
'Custom Properties'
'Custom Properties',
true
);
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
'Table'
'Table',
true
);
// Perform click on custom property type to filter
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
durationPropertyName
durationPropertyName,
true
);
// Perform click on operator

View File

@ -27,7 +27,6 @@ import {
verifyAllConditions,
} from '../../utils/advancedSearch';
import { createNewPage, redirectToHomePage } from '../../utils/common';
import { assignTier } from '../../utils/entity';
import { sidebarClick } from '../../utils/sidebar';
const creationConfig: EntityDataClassCreationConfig = {
@ -74,7 +73,6 @@ test.describe('Advanced Search', { tag: '@advanced-search' }, () => {
await table.create(apiContext);
// Add Owner & Tag to the table
await EntityDataClass.table1.visitEntityPage(page);
await EntityDataClass.table1.patch({
apiContext,
patchData: [
@ -106,7 +104,6 @@ test.describe('Advanced Search', { tag: '@advanced-search' }, () => {
],
});
await EntityDataClass.table2.visitEntityPage(page);
await EntityDataClass.table2.patch({
apiContext,
patchData: [
@ -139,20 +136,38 @@ test.describe('Advanced Search', { tag: '@advanced-search' }, () => {
});
// Add Tier To the topic 1
await EntityDataClass.topic1.visitEntityPage(page);
await assignTier(
page,
COMMON_TIER_TAG[0].name,
EntityDataClass.topic1.endpoint
);
await EntityDataClass.topic1.patch({
apiContext,
patchData: [
{
op: 'add',
path: '/tags/0',
value: {
name: COMMON_TIER_TAG[0].name,
tagFQN: COMMON_TIER_TAG[0].fullyQualifiedName,
labelType: 'Manual',
state: 'Confirmed',
},
},
],
});
// Add Tier To the topic 2
await EntityDataClass.topic2.visitEntityPage(page);
await assignTier(
page,
COMMON_TIER_TAG[1].name,
EntityDataClass.topic2.endpoint
);
await EntityDataClass.topic2.patch({
apiContext,
patchData: [
{
op: 'add',
path: '/tags/0',
value: {
name: COMMON_TIER_TAG[1].name,
tagFQN: COMMON_TIER_TAG[1].fullyQualifiedName,
labelType: 'Manual',
state: 'Confirmed',
},
},
],
});
// Update Search Criteria here
searchCriteria = {

View File

@ -58,7 +58,6 @@ test.describe('Curated Assets', () => {
test.slow(true);
await redirectToHomePage(page);
await page.getByTestId('sidebar-toggle').click();
await setUserDefaultPersona(page, persona.responseData.displayName);
});
@ -91,7 +90,8 @@ test.describe('Curated Assets', () => {
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
'Owners'
'Owners',
true
);
await selectOption(
@ -103,7 +103,8 @@ test.describe('Curated Assets', () => {
await selectOption(
page,
ruleLocator.locator('.rule--value .ant-select'),
'admin'
'admin',
true
);
await page.getByRole('button', { name: 'Add Condition' }).click();
@ -112,7 +113,8 @@ test.describe('Curated Assets', () => {
await selectOption(
page,
ruleLocator2.locator('.rule--field .ant-select'),
'Display Name'
'Display Name',
true
);
await selectOption(
@ -124,7 +126,8 @@ test.describe('Curated Assets', () => {
await selectOption(
page,
ruleLocator2.locator('.rule--value .ant-select'),
'arcs'
'arcs',
true
);
await expect(page.locator('[data-testid="saveButton"]')).toBeEnabled();

View File

@ -25,7 +25,7 @@ const user2 = new UserClass();
const user3 = new UserClass();
let entityLinkList: string[];
test.describe('Description Suggestions Table Entity', () => {
test.describe.serial('Description Suggestions Table Entity', () => {
test.slow(true);
test.beforeAll('Setup pre-requests', async ({ browser }) => {
@ -49,16 +49,6 @@ test.describe('Description Suggestions Table Entity', () => {
await afterAction();
});
test.afterAll('Cleanup', async ({ browser }) => {
const { afterAction, apiContext } = await performAdminLogin(browser);
await table.delete(apiContext);
await table2.delete(apiContext);
await user1.delete(apiContext);
await user2.delete(apiContext);
await user3.delete(apiContext);
await afterAction();
});
test('View, Close, Reject and Accept the Suggestions', async ({
browser,
}) => {
@ -233,6 +223,12 @@ test.describe('Description Suggestions Table Entity', () => {
test('Reject All Suggestions', async ({ browser }) => {
const { page, afterAction } = await performAdminLogin(browser);
const { afterAction: afterAction2, apiContext: apiContext2 } =
await performUserLogin(browser, user1);
for (const entityLink of entityLinkList) {
await createTableDescriptionSuggestions(apiContext2, entityLink);
}
await redirectToHomePage(page);
await table.visitEntityPage(page);
@ -244,13 +240,13 @@ test.describe('Description Suggestions Table Entity', () => {
// Click the first avatar
await allAvatarSuggestion.nth(0).click();
const acceptResponse = page.waitForResponse(
const rejectResponse = page.waitForResponse(
'/api/v1/suggestions/reject-all?userId=*&entityFQN=*&suggestionType=SuggestDescription'
);
await page.click(`[data-testid="reject-all-suggestions"]`);
await acceptResponse;
await rejectResponse;
// check the last column description
await expect(
@ -265,6 +261,7 @@ test.describe('Description Suggestions Table Entity', () => {
await expect(page.getByTestId('close-suggestion')).not.toBeVisible();
await afterAction();
await afterAction2();
});
test('Fetch on avatar click and then all Pending Suggestions button click', async ({
@ -311,7 +308,7 @@ test.describe('Description Suggestions Table Entity', () => {
.getByTestId('profile-avatar');
// Click the first avatar
await expect(allAvatarSuggestion).toHaveCount(4);
await expect(allAvatarSuggestion).toHaveCount(3);
await afterAction();
await afterAction2();
@ -375,7 +372,7 @@ test.describe('Description Suggestions Table Entity', () => {
page.getByTestId('more-suggestion-button')
).not.toBeVisible();
await expect(allAvatarSuggestion).toHaveCount(1);
await expect(allAvatarSuggestion).toHaveCount(0);
}
}

View File

@ -78,7 +78,11 @@ test.describe('Navigation Blocker Tests', () => {
).toBeEnabled();
// Try to navigate to another page by clicking a sidebar link
await adminPage.locator('[data-testid="app-bar-item-settings"]').click();
await adminPage
.locator(
'[data-menu-id*="settings"] [data-testid="app-bar-item-settings"]'
)
.click();
// Navigation blocker modal should appear
await expect(adminPage.locator('.ant-modal')).toBeVisible();
@ -114,7 +118,11 @@ test.describe('Navigation Blocker Tests', () => {
});
// Try to navigate away
await adminPage.locator('[data-testid="app-bar-item-settings"]').click();
await adminPage
.locator(
'[data-menu-id*="settings"] [data-testid="app-bar-item-settings"]'
)
.click();
// Modal should appear
await expect(adminPage.locator('.ant-modal')).toBeVisible();
@ -153,7 +161,11 @@ test.describe('Navigation Blocker Tests', () => {
});
// Try to navigate to settings page
await adminPage.locator('[data-testid="app-bar-item-settings"]').click();
await adminPage
.locator(
'[data-menu-id*="settings"] [data-testid="app-bar-item-settings"]'
)
.click();
// Modal should appear
await expect(adminPage.locator('.ant-modal')).toBeVisible();
@ -204,7 +216,11 @@ test.describe('Navigation Blocker Tests', () => {
).toBeDisabled();
// Try to navigate away after saving
await adminPage.locator('[data-testid="app-bar-item-settings"]').click();
await adminPage
.locator(
'[data-menu-id*="settings"] [data-testid="app-bar-item-settings"]'
)
.click();
// Navigation should happen immediately without modal
await adminPage.waitForLoadState('networkidle');

View File

@ -129,13 +129,15 @@ test('CustomProperty Dashboard Filter', async ({ page }) => {
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
'Custom Properties'
'Custom Properties',
true
);
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
'Dashboard'
'Dashboard',
true
);
// Select Custom Property Field when we want filter

View File

@ -57,7 +57,6 @@ base.afterAll('Cleanup', async ({ browser }) => {
test.describe('Customize Landing Page Flow', () => {
test('Check all default widget present', async ({ adminPage }) => {
await redirectToHomePage(adminPage);
await adminPage.getByTestId('welcome-screen-close-btn').click();
await checkAllDefaultWidgets(adminPage);
});

View File

@ -40,7 +40,7 @@ const test = base.extend<{ page: Page }>({
},
});
base.beforeAll('Setup pre-requests', async ({ browser, page }) => {
base.beforeAll('Setup pre-requests', async ({ browser }) => {
const { afterAction, apiContext } = await performAdminLogin(browser);
await adminUser.create(apiContext);
await adminUser.setAdminRole(apiContext);
@ -60,16 +60,16 @@ test.describe('Widgets', () => {
test.slow(true);
await redirectToHomePage(page);
await page.getByTestId('sidebar-toggle').click();
await setUserDefaultPersona(page, persona.responseData.displayName);
});
test.beforeEach(async ({ page }) => {
await redirectToHomePage(page);
});
test('Activity Feed', async ({ page }) => {
test.slow(true);
await redirectToHomePage(page);
await page.getByTestId('welcome-screen-close-btn').click();
await expect(page.getByTestId('KnowledgePanel.ActivityFeed')).toBeVisible();
await verifyActivityFeedFilters(page, 'KnowledgePanel.ActivityFeed');
@ -90,9 +90,6 @@ test.describe('Widgets', () => {
test('Data Assets', async ({ page }) => {
test.slow(true);
await redirectToHomePage(page);
await page.getByTestId('welcome-screen-close-btn').click();
await expect(page.getByTestId('KnowledgePanel.DataAssets')).toBeVisible();
await removeAndVerifyWidget(
@ -111,9 +108,6 @@ test.describe('Widgets', () => {
test('My Data', async ({ page }) => {
test.slow(true);
await redirectToHomePage(page);
await page.getByTestId('welcome-screen-close-btn').click();
await expect(page.getByTestId('KnowledgePanel.MyData')).toBeVisible();
await verifyDataFilters(page, 'KnowledgePanel.MyData');
@ -134,9 +128,6 @@ test.describe('Widgets', () => {
test('KPI', async ({ page }) => {
test.slow(true);
await redirectToHomePage(page);
await page.getByTestId('welcome-screen-close-btn').click();
await expect(page.getByTestId('KnowledgePanel.KPI')).toBeVisible();
await removeAndVerifyWidget(
@ -155,9 +146,6 @@ test.describe('Widgets', () => {
test('Total Data Assets', async ({ page }) => {
test.slow(true);
await redirectToHomePage(page);
await page.getByTestId('welcome-screen-close-btn').click();
await expect(page.getByTestId('KnowledgePanel.TotalAssets')).toBeVisible();
await verifyTotalDataAssetsFilters(page, 'KnowledgePanel.TotalAssets');
@ -178,9 +166,6 @@ test.describe('Widgets', () => {
test('Following Assets', async ({ page }) => {
test.slow(true);
await redirectToHomePage(page);
await page.getByTestId('welcome-screen-close-btn').click();
await expect(page.getByTestId('KnowledgePanel.Following')).toBeVisible();
await verifyDataFilters(page, 'KnowledgePanel.Following');
@ -201,9 +186,6 @@ test.describe('Widgets', () => {
test('Domains', async ({ page }) => {
test.slow(true);
await redirectToHomePage(page);
await page.getByTestId('welcome-screen-close-btn').click();
await expect(page.getByTestId('KnowledgePanel.Domains')).not.toBeVisible();
await addAndVerifyWidget(
@ -224,9 +206,6 @@ test.describe('Widgets', () => {
test('My Tasks', async ({ page }) => {
test.slow(true);
await redirectToHomePage(page);
await page.getByTestId('welcome-screen-close-btn').click();
await expect(page.getByTestId('KnowledgePanel.MyTask')).not.toBeVisible();
await addAndVerifyWidget(

View File

@ -13,7 +13,6 @@
import { expect, Page, test } from '@playwright/test';
import { UserClass } from '../../support/user/UserClass';
import { performAdminLogin } from '../../utils/admin';
import { redirectToHomePage } from '../../utils/common';
import { waitForAllLoadersToDisappear } from '../../utils/entity';
const user = new UserClass();
@ -157,7 +156,6 @@ test.describe('Tour should work properly', () => {
test.beforeEach('Visit entity details page', async ({ page }) => {
await user.login(page);
await redirectToHomePage(page);
});
test('Tour should work from help section', async ({ page }) => {

View File

@ -183,7 +183,8 @@ test.describe('Data Contracts', () => {
await selectOption(
page,
ruleLocator.locator('.group--field .ant-select'),
DATA_CONTRACT_SEMANTICS1.rules[0].field
DATA_CONTRACT_SEMANTICS1.rules[0].field,
true
);
await selectOption(
page,
@ -193,7 +194,8 @@ test.describe('Data Contracts', () => {
await selectOption(
page,
ruleLocator.locator('.rule--value .ant-select'),
'admin'
'admin',
true
);
await page.getByRole('button', { name: 'Add New Rule' }).click();
@ -203,7 +205,8 @@ test.describe('Data Contracts', () => {
await selectOption(
page,
ruleLocator2.locator('.rule--field .ant-select'),
DATA_CONTRACT_SEMANTICS1.rules[1].field
DATA_CONTRACT_SEMANTICS1.rules[1].field,
true
);
await selectOption(
page,
@ -241,7 +244,8 @@ test.describe('Data Contracts', () => {
await selectOption(
page,
ruleLocator3.locator('.group--field .ant-select'),
DATA_CONTRACT_SEMANTICS2.rules[0].field
DATA_CONTRACT_SEMANTICS2.rules[0].field,
true
);
await selectOption(
page,
@ -596,7 +600,8 @@ test.describe('Data Contracts', () => {
await selectOption(
page,
ruleLocator.locator('.group--field .ant-select'),
DATA_CONTRACT_SEMANTICS1.rules[0].field
DATA_CONTRACT_SEMANTICS1.rules[0].field,
true
);
await selectOption(
page,
@ -606,7 +611,8 @@ test.describe('Data Contracts', () => {
await selectOption(
page,
ruleLocator.locator('.rule--value .ant-select'),
'admin'
'admin',
true
);
await page.getByTestId('save-semantic-button').click();

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { expect, Page, test as base } from '@playwright/test';
import { COMMON_TIER_TAG } from '../../constant/common';
import { BIG_ENTITY_DELETE_TIMEOUT } from '../../constant/delete';
import { EntityDataClass } from '../../support/entity/EntityDataClass';
import { EntityDataClassCreationConfig } from '../../support/entity/EntityDataClass.interface';
@ -19,14 +20,12 @@ import { UserClass } from '../../support/user/UserClass';
import { performAdminLogin } from '../../utils/admin';
import {
descriptionBoxReadOnly,
getApiContext,
redirectToHomePage,
reloadAndWaitForNetworkIdle,
toastNotification,
} from '../../utils/common';
import {
addMultiOwner,
assignTier,
getEntityDataTypeDisplayPatch,
} from '../../utils/entity';
import { getEntityDataTypeDisplayPatch } from '../../utils/entity';
const entityCreationConfig: EntityDataClassCreationConfig = {
apiEndpoint: true,
@ -158,6 +157,7 @@ test.describe('Entity Version pages', () => {
test(`${entity.getType()}`, async ({ page }) => {
test.slow();
const { apiContext } = await getApiContext(page);
await entity.visitEntityPage(page);
const versionDetailResponse = page.waitForResponse(`**/versions/0.2`);
await page.locator('[data-testid="version-button"]').click();
@ -194,17 +194,24 @@ test.describe('Entity Version pages', () => {
await test.step('should show owner changes', async () => {
await page.locator('[data-testid="version-button"]').click();
const OWNER1 = EntityDataClass.user1.getUserName();
const OWNER1 = EntityDataClass.user1.responseData;
await addMultiOwner({
page,
ownerNames: [OWNER1],
activatorBtnDataTestId: 'edit-owner',
resultTestId: 'data-assets-header',
endpoint: entity.endpoint,
type: 'Users',
await entity.patch({
apiContext,
patchData: [
{
op: 'add',
path: '/owners/0',
value: {
id: OWNER1.id,
type: 'user',
},
},
],
});
await reloadAndWaitForNetworkIdle(page);
const versionDetailResponse = page.waitForResponse(`**/versions/0.3`);
await page.locator('[data-testid="version-button"]').click();
await versionDetailResponse;
@ -255,7 +262,23 @@ test.describe('Entity Version pages', () => {
await test.step('should show tier changes', async () => {
await page.locator('[data-testid="version-button"]').click();
await assignTier(page, 'Tier1', entity.endpoint);
await entity.patch({
apiContext,
patchData: [
{
op: 'add',
path: '/tags/0',
value: {
name: COMMON_TIER_TAG[0].name,
tagFQN: COMMON_TIER_TAG[0].fullyQualifiedName,
labelType: 'Manual',
state: 'Confirmed',
},
},
],
});
await reloadAndWaitForNetworkIdle(page);
const versionDetailResponse = page.waitForResponse(`**/versions/0.3`);
await page.locator('[data-testid="version-button"]').click();

View File

@ -101,9 +101,6 @@ setup('authenticate all users', async ({ browser }) => {
const newAdminPage = await browser.newPage();
await admin.login(newAdminPage);
// Close the leftside bar to run tests smoothly
await newAdminPage.getByTestId('sidebar-toggle').click();
await newAdminPage.waitForURL('**/my-data');
const { apiContext, afterAction } = await getApiContext(adminPage);

View File

@ -126,20 +126,15 @@ class ServiceBaseClass {
await page.click('[data-testid="next-button"]');
await page.waitForSelector('#name_help');
const nameHelp = await page.$eval('#name_help', (el) => el.textContent);
expect(nameHelp).toContain('Name is required');
await expect(page.locator('#name_help')).toHaveText('Name is required');
// invalid name validation should work
await page
.locator('[data-testid="service-name"]')
.fill(INVALID_NAMES.WITH_SPECIAL_CHARS);
const nameHelpError = await page.$eval(
'#name_help',
(el) => el.textContent
);
expect(nameHelpError).toContain(NAME_VALIDATION_ERROR);
await expect(page.locator('#name_help')).toHaveText(NAME_VALIDATION_ERROR);
await page.fill('[data-testid="service-name"]', serviceName);

View File

@ -203,6 +203,8 @@ export class UserClass {
password = this.data.password
) {
await page.goto('/');
await page.waitForURL('**/signin');
await page.waitForLoadState('networkidle');
await page.fill('input[id="email"]', userName);
await page.locator('#email').press('Tab');
await page.fill('input[id="password"]', password);
@ -220,6 +222,17 @@ export class UserClass {
if (modal) {
await page.getByRole('dialog').getByRole('img').first().click();
}
// Collapse the left side bar after logging in if it's open
const leftNavBar = page.locator('[data-testid="left-sidebar"]');
const hasOpenClass = await leftNavBar.evaluate((el) =>
el.classList.contains('sidebar-open')
);
if (hasOpenClass) {
await page.getByTestId('sidebar-toggle').click();
}
}
async logout(page: Page) {

View File

@ -197,13 +197,29 @@ export const showAdvancedSearchDialog = async (page: Page) => {
export const selectOption = async (
page: Page,
dropdownLocator: Locator,
optionTitle: string
optionTitle: string,
isSearchable = false
) => {
await dropdownLocator.click();
await page.keyboard.type(optionTitle);
if (isSearchable) {
// Force click on the selector to ensure it opens even if there's an existing selection
await dropdownLocator
.locator('.ant-select-selector')
.click({ force: true });
// Clear any existing input and type the new value
const combobox = dropdownLocator.getByRole('combobox');
await combobox.clear();
await combobox.fill(optionTitle);
} else {
await dropdownLocator.click();
}
await expect(dropdownLocator).toHaveClass(/(^|\s)ant-select-focused(\s|$)/);
await page.waitForSelector(`.ant-select-dropdown:visible`, {
state: 'visible',
});
await page.click(`.ant-select-dropdown:visible [title="${optionTitle}"]`);
};
@ -227,7 +243,8 @@ export const fillRule = async (
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
field.id
field.id,
true
);
// Perform click on operator
@ -595,7 +612,8 @@ export const runRuleGroupTestsWithNonExistingValue = async (page: Page) => {
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
'Database'
'Database',
true
);
await selectOption(
page,

View File

@ -835,19 +835,22 @@ export const verifyCustomPropertyInAdvancedSearch = async (
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
'Custom Properties'
'Custom Properties',
true
);
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
entityType
entityType,
true
);
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
propertyName
propertyName,
true
);
await page.getByTestId('cancel-btn').click();

View File

@ -45,12 +45,21 @@ export const visitObservabilityAlertPage = async (page: Page) => {
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
// Set up the response promise before navigation
const getAlerts = page.waitForResponse(
'/api/v1/events/subscriptions?*alertType=Observability*'
);
// Set up navigation promise before clicking
const navigationPromise = page.waitForURL('**/observability/alerts', {
waitUntil: 'networkidle',
});
await sidebarClick(page, SidebarItem.OBSERVABILITY_ALERT);
await page.waitForURL('**/observability/alerts');
await getAlerts;
// Wait for both navigation and API response
await Promise.all([navigationPromise, getAlerts]);
};
export const addExternalDestination = async ({

View File

@ -29,9 +29,13 @@ export const clickOnLogo = async (page: Page) => {
export const sidebarClick = async (page: Page, id: string) => {
const items = SIDEBAR_LIST_ITEMS[id as keyof typeof SIDEBAR_LIST_ITEMS];
if (items) {
await page.hover('[data-testid="left-sidebar"]');
await page.waitForTimeout(300);
await page.click(`[data-testid="${items[0]}"]`);
await page.hover(
`[data-testid="left-sidebar"] [data-menu-id*="${items[0]}"]`
);
await page.waitForSelector(`[data-testid="app-bar-item-${items[1]}"]`, {
state: 'visible',
});
await page.click(`[data-testid="app-bar-item-${items[1]}"]`);
} else {
await page.click(`[data-testid="app-bar-item-${id}"]`);