playwright: fixed flakiness in AddTestCaseNewFlow, Tour & Domains (#23154)

* refactor: improve test case flows

* refactor: enhance test case flow by utilizing TableClass for table selection and management

* refactor: streamline domain verification by replacing viewer container check with locator for description

* refactor: update Playwright configuration and enhance Tour test flow

* refactor: rename waitForAllSkeletonLoadersToDisappear to waitForAllLoadersToDisappear and update usage across tests

* refactor: remove redundant afterAll cleanup and enhance loader visibility in AssetsTabs

* refactor: streamline test case flow by consolidating table selection and API context retrieval
This commit is contained in:
Shailesh Parmar 2025-08-31 12:46:39 +05:30 committed by GitHub
parent 527511ca50
commit d956087968
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 166 additions and 95 deletions

View File

@ -12,25 +12,22 @@
*/
import { expect, Page, Response } from '@playwright/test';
import { TableClass } from '../../../support/entity/TableClass';
import { performAdminLogin } from '../../../utils/admin';
import { toastNotification } from '../../../utils/common';
import { getApiContext, redirectToHomePage } from '../../../utils/common';
import { visitDataQualityTab } from '../../../utils/testCases';
import { test } from '../../fixtures/pages';
test.describe('Add TestCase New Flow', () => {
const table1 = new TableClass();
// Helper function to select table
const selectTable = async (page: Page, tableName: string) => {
const selectTable = async (page: Page, table: TableClass) => {
await page.click('#testCaseFormV1_selectedTable');
const tableResponse = page.waitForResponse(
'/api/v1/search/query?*index=table_search_index*'
);
await page.fill('#testCaseFormV1_selectedTable', tableName);
await page.fill('#testCaseFormV1_selectedTable', table.entity.name);
await tableResponse;
await page
.locator(
`.ant-select-dropdown [title="${table1.entityResponseData.fullyQualifiedName}"]`
`.ant-select-dropdown [title="${table.entityResponseData.fullyQualifiedName}"]`
)
.click();
};
@ -103,8 +100,6 @@ test.describe('Add TestCase New Flow', () => {
expect(response.status()).toBe(201);
expect(ingestionPipelineCalled).toBe(false);
}
await toastNotification(page, 'Test case created successfully.');
};
// Helper function to open test case form
@ -118,22 +113,19 @@ test.describe('Add TestCase New Flow', () => {
const visitDataQualityPage = async (page: Page) => {
await page.goto('/data-quality/test-cases');
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
};
test.beforeAll(async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await table1.create(apiContext);
await afterAction();
test.beforeEach(async ({ page }) => {
await redirectToHomePage(page);
});
test('Add Table & Column Test Case', async ({ page }) => {
test.slow(true);
test('Add Table Test Case', async ({ page }) => {
const table = new TableClass();
const { apiContext } = await getApiContext(page);
await table.create(apiContext);
const testCaseDetails = {
testType: 'table row count to equal',
@ -145,43 +137,7 @@ test.describe('Add TestCase New Flow', () => {
await test.step('Create table-level test case', async () => {
// Create table-level test case
await openTestCaseForm(page);
await selectTable(page, table1.entity.name);
await createTestCase({
page,
...testCaseDetails,
});
await expect(page.getByTestId('entity-header-name')).toHaveText(
`${testCaseDetails.testTypeId}_test_case`
);
});
await test.step('Create column-level test case', async () => {
const testCaseDetails = {
testType: 'Column Values To Be Unique',
testTypeId: 'columnValuesToBeUnique',
expectSchedulerCard: false,
};
await visitDataQualityPage(page);
// Create column-level test case
await openTestCaseForm(page);
await page
.getByTestId('select-table-card')
.getByText('Column Level')
.click();
await selectTable(page, table1.entity.name);
await page.click('#testCaseFormV1_selectedColumn');
await page.waitForLoadState('networkidle');
await page.waitForSelector(
`.ant-select-dropdown [title="${table1.entity.columns[0].name}"]`
);
await page
.locator(
`.ant-select-dropdown [title="${table1.entity.columns[0].name}"]`
)
.click();
await selectTable(page, table);
await createTestCase({
page,
...testCaseDetails,
@ -193,11 +149,8 @@ test.describe('Add TestCase New Flow', () => {
});
await test.step('Validate test case in Entity Page', async () => {
await visitDataQualityTab(page, table1);
await visitDataQualityTab(page, table);
await expect(
page.getByTestId('columnValuesToBeUnique_test_case')
).toBeVisible();
await expect(
page.getByTestId('tableRowCountToEqual_test_case')
).toBeVisible();
@ -212,7 +165,73 @@ test.describe('Add TestCase New Flow', () => {
page
.getByTestId('ingestion-list-table')
.locator(
`[data-row-key*="${table1.entityResponseData.fullyQualifiedName}.testSuite"]`
`[data-row-key*="${table.entityResponseData.fullyQualifiedName}.testSuite"]`
)
).toHaveCount(1);
});
});
test('Add Column Test Case', async ({ page }) => {
const table = new TableClass();
const { apiContext } = await getApiContext(page);
await table.create(apiContext);
await visitDataQualityPage(page);
await test.step('Create column-level test case', async () => {
const testCaseDetails = {
testType: 'Column Values To Be Unique',
testTypeId: 'columnValuesToBeUnique',
};
await visitDataQualityPage(page);
// Create column-level test case
await openTestCaseForm(page);
await page
.getByTestId('select-table-card')
.getByText('Column Level')
.click();
await selectTable(page, table);
await page.click('#testCaseFormV1_selectedColumn');
// appearing dropdown takes bit time and its not based on API call so adding manual wait to prevent flakiness.
await page.waitForTimeout(2000);
await page.waitForSelector(
`.ant-select-dropdown [title="${table.entity.columns[0].name}"]`
);
await page
.locator(
`.ant-select-dropdown [title="${table.entity.columns[0].name}"]`
)
.click();
await createTestCase({
page,
...testCaseDetails,
});
await expect(page.getByTestId('entity-header-name')).toHaveText(
`${testCaseDetails.testTypeId}_test_case`
);
});
await test.step('Validate test case in Entity Page', async () => {
await visitDataQualityTab(page, table);
await expect(
page.getByTestId('columnValuesToBeUnique_test_case')
).toBeVisible();
const pipelineApi = page.waitForResponse(
'/api/v1/services/ingestionPipelines?*'
);
await page.getByTestId('pipeline').click();
await pipelineApi;
await expect(
page
.getByTestId('ingestion-list-table')
.locator(
`[data-row-key*="${table.entityResponseData.fullyQualifiedName}.testSuite"]`
)
).toHaveCount(1);
});
@ -221,28 +240,26 @@ test.describe('Add TestCase New Flow', () => {
test('Non-owner user should not able to add test case', async ({
dataConsumerPage,
dataStewardPage,
page,
}) => {
await visitDataQualityPage(dataConsumerPage);
await visitDataQualityPage(dataStewardPage);
const table = new TableClass();
const { apiContext } = await getApiContext(page);
await table.create(apiContext);
await dataConsumerPage.getByTestId('add-test-case-btn').click();
await dataStewardPage.getByTestId('add-test-case-btn').click();
for (const page of [dataConsumerPage, dataStewardPage]) {
await visitDataQualityPage(page);
await selectTable(dataConsumerPage, table1.entity.name);
await selectTable(dataStewardPage, table1.entity.name);
await page.getByTestId('add-test-case-btn').click();
await dataConsumerPage.getByTestId('create-btn').click();
await dataStewardPage.getByTestId('create-btn').click();
await selectTable(page, table);
await expect(
dataConsumerPage.locator('#testCaseFormV1_selectedTable_help')
).toContainText(
'You do not have the necessary permissions to create a test case on this table.'
);
await expect(
dataStewardPage.locator('#testCaseFormV1_selectedTable_help')
).toContainText(
'You do not have the necessary permissions to create a test case on this table.'
);
await page.getByTestId('create-btn').click();
await expect(
page.locator('#testCaseFormV1_selectedTable_help')
).toContainText(
'You do not have the necessary permissions to create a test case on this table.'
);
}
});
});

View File

@ -17,9 +17,37 @@ import { waitForAllLoadersToDisappear } from '../../utils/entity';
const user = new UserClass();
const waitForTourBadgeWithRetry = async (
page: Page,
maxAttempts = 3,
timeout = 10000
) => {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await page.waitForSelector('[data-tour-elem="badge"]', {
state: 'visible',
timeout,
});
return; // Success
} catch (e) {
if (attempt < maxAttempts) {
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
await waitForAllLoadersToDisappear(page, 'entity-list-skeleton');
} else {
throw e;
}
}
}
};
const validateTourSteps = async (page: Page) => {
await page.waitForTimeout(1000);
await page.waitForSelector(`[data-tour-elem="badge"]`);
await waitForTourBadgeWithRetry(page);
await expect(page.locator(`[data-tour-elem="badge"]`)).toHaveText('1');
@ -34,7 +62,9 @@ const validateTourSteps = async (page: Page) => {
await expect(page.locator(`[data-tour-elem="badge"]`)).toHaveText('3');
await page.getByTestId('searchBox').fill('dim_a');
await page.getByTestId('searchBox').press('Enter');
await page.getByTestId('searchBox').press('Enter', { delay: 500 });
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await expect(page.locator(`[data-tour-elem="badge"]`)).toHaveText('4');
@ -129,9 +159,12 @@ test.describe('Tour should work properly', () => {
});
test('Tour should work from help section', async ({ page }) => {
test.slow();
await page.locator('[data-testid="help-icon"]').click();
await page.getByRole('link', { name: 'Tour', exact: true }).click();
await waitForAllLoadersToDisappear(page);
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await waitForAllLoadersToDisappear(page, 'entity-list-skeleton');
await page.waitForURL('**/tour');
await page.waitForSelector('#feedWidgetData');
@ -145,21 +178,34 @@ test.describe('Tour should work properly', () => {
.locator('.whats-new-alert-close')
.click();
await page.getByText('Take a product tour to get started!').click();
await waitForAllLoadersToDisappear(page);
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await waitForAllLoadersToDisappear(page, 'entity-list-skeleton');
await page.waitForURL('**/tour');
await page.waitForSelector('#feedWidgetData');
await validateTourSteps(page);
// Since the tour steps are already tested in the first test,
// here we only validate whether the tour is loading or not.
await waitForTourBadgeWithRetry(page);
});
test('Tour should work from URL directly', async ({ page }) => {
await page.goto('/tour');
await waitForAllLoadersToDisappear(page);
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
const isWelcomeScreenVisible = await page
.getByTestId('welcome-screen')
.isVisible();
if (isWelcomeScreenVisible) {
await page.getByTestId('welcome-screen-close-btn').click();
await page.waitForLoadState('networkidle');
}
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await waitForAllLoadersToDisappear(page, 'entity-list-skeleton');
await page.waitForURL('**/tour');
await page.waitForSelector('#feedWidgetData');
await validateTourSteps(page);
// Since the tour steps are already tested in the first test,
// here we only validate whether the tour is loading or not.
await waitForTourBadgeWithRetry(page);
});
});

View File

@ -35,7 +35,7 @@ import {
redirectToHomePage,
uuid,
} from './common';
import { addOwner } from './entity';
import { addOwner, waitForAllLoadersToDisappear } from './entity';
import { sidebarClick } from './sidebar';
export const assignDomain = async (page: Page, domain: Domain['data']) => {
@ -200,6 +200,7 @@ const goToAssetsTab = async (page: Page, domain: Domain['data']) => {
await selectDomain(page, domain);
await checkDomainDisplayName(page, domain.displayName);
await page.getByTestId('assets').click();
await waitForAllLoadersToDisappear(page);
};
const fillCommonFormItems = async (
@ -270,11 +271,11 @@ export const verifyDomain = async (
) => {
await checkDomainDisplayName(page, domain.displayName);
const viewerContainerText = await page.textContent(
'[data-testid="viewer-container"]'
);
await expect(page.getByText(domain.description)).toBeVisible();
await expect(viewerContainerText).toContain(domain.description);
expect(
await page.locator(`[id="KnowledgePanel\\.Description"]`).textContent()
).toContain(domain.description);
if (!isEmpty(domain.owners) && !isUndefined(domain.owners)) {
await expect(

View File

@ -38,9 +38,12 @@ import {
import { searchAndClickOnOption } from './explore';
import { sidebarClick } from './sidebar';
export const waitForAllLoadersToDisappear = async (page: Page) => {
export const waitForAllLoadersToDisappear = async (
page: Page,
dataTestId = 'loader'
) => {
for (let attempt = 0; attempt < 3; attempt++) {
const allLoaders = page.locator('[data-testid="loader"]');
const allLoaders = page.locator(`[data-testid="${dataTestId}"]`);
const count = await allLoaders.count();
let allLoadersGone = true;

View File

@ -863,7 +863,11 @@ const AssetsTabs = forwardRef(
)}
{isLoading ? (
<Col className="border-default border-radius-sm p-lg" span={24}>
<Space className="w-full" direction="vertical" size={16}>
<Space
className="w-full"
data-testid="loader"
direction="vertical"
size={16}>
<Skeleton />
<Skeleton />
<Skeleton />