mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-30 11:26:23 +00:00
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:
parent
527511ca50
commit
d956087968
@ -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.'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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 />
|
||||
|
Loading…
x
Reference in New Issue
Block a user