mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-01 03:46:24 +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 { expect, Page, Response } from '@playwright/test';
|
||||||
import { TableClass } from '../../../support/entity/TableClass';
|
import { TableClass } from '../../../support/entity/TableClass';
|
||||||
import { performAdminLogin } from '../../../utils/admin';
|
import { getApiContext, redirectToHomePage } from '../../../utils/common';
|
||||||
import { toastNotification } from '../../../utils/common';
|
|
||||||
import { visitDataQualityTab } from '../../../utils/testCases';
|
import { visitDataQualityTab } from '../../../utils/testCases';
|
||||||
import { test } from '../../fixtures/pages';
|
import { test } from '../../fixtures/pages';
|
||||||
|
|
||||||
test.describe('Add TestCase New Flow', () => {
|
test.describe('Add TestCase New Flow', () => {
|
||||||
const table1 = new TableClass();
|
|
||||||
|
|
||||||
// Helper function to select table
|
// Helper function to select table
|
||||||
const selectTable = async (page: Page, tableName: string) => {
|
const selectTable = async (page: Page, table: TableClass) => {
|
||||||
await page.click('#testCaseFormV1_selectedTable');
|
await page.click('#testCaseFormV1_selectedTable');
|
||||||
const tableResponse = page.waitForResponse(
|
const tableResponse = page.waitForResponse(
|
||||||
'/api/v1/search/query?*index=table_search_index*'
|
'/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 tableResponse;
|
||||||
await page
|
await page
|
||||||
.locator(
|
.locator(
|
||||||
`.ant-select-dropdown [title="${table1.entityResponseData.fullyQualifiedName}"]`
|
`.ant-select-dropdown [title="${table.entityResponseData.fullyQualifiedName}"]`
|
||||||
)
|
)
|
||||||
.click();
|
.click();
|
||||||
};
|
};
|
||||||
@ -103,8 +100,6 @@ test.describe('Add TestCase New Flow', () => {
|
|||||||
expect(response.status()).toBe(201);
|
expect(response.status()).toBe(201);
|
||||||
expect(ingestionPipelineCalled).toBe(false);
|
expect(ingestionPipelineCalled).toBe(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await toastNotification(page, 'Test case created successfully.');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to open test case form
|
// Helper function to open test case form
|
||||||
@ -118,22 +113,19 @@ test.describe('Add TestCase New Flow', () => {
|
|||||||
|
|
||||||
const visitDataQualityPage = async (page: Page) => {
|
const visitDataQualityPage = async (page: Page) => {
|
||||||
await page.goto('/data-quality/test-cases');
|
await page.goto('/data-quality/test-cases');
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForSelector('[data-testid="loader"]', {
|
await page.waitForSelector('[data-testid="loader"]', {
|
||||||
state: 'detached',
|
state: 'detached',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
test.beforeAll(async ({ browser }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
const { apiContext, afterAction } = await performAdminLogin(browser);
|
await redirectToHomePage(page);
|
||||||
|
|
||||||
await table1.create(apiContext);
|
|
||||||
|
|
||||||
await afterAction();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Add Table & Column Test Case', async ({ page }) => {
|
test('Add Table Test Case', async ({ page }) => {
|
||||||
test.slow(true);
|
const table = new TableClass();
|
||||||
|
const { apiContext } = await getApiContext(page);
|
||||||
|
await table.create(apiContext);
|
||||||
|
|
||||||
const testCaseDetails = {
|
const testCaseDetails = {
|
||||||
testType: 'table row count to equal',
|
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 () => {
|
await test.step('Create table-level test case', async () => {
|
||||||
// Create table-level test case
|
// Create table-level test case
|
||||||
await openTestCaseForm(page);
|
await openTestCaseForm(page);
|
||||||
await selectTable(page, table1.entity.name);
|
await selectTable(page, table);
|
||||||
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 createTestCase({
|
await createTestCase({
|
||||||
page,
|
page,
|
||||||
...testCaseDetails,
|
...testCaseDetails,
|
||||||
@ -193,11 +149,8 @@ test.describe('Add TestCase New Flow', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Validate test case in Entity Page', async () => {
|
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(
|
await expect(
|
||||||
page.getByTestId('tableRowCountToEqual_test_case')
|
page.getByTestId('tableRowCountToEqual_test_case')
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@ -212,7 +165,73 @@ test.describe('Add TestCase New Flow', () => {
|
|||||||
page
|
page
|
||||||
.getByTestId('ingestion-list-table')
|
.getByTestId('ingestion-list-table')
|
||||||
.locator(
|
.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);
|
).toHaveCount(1);
|
||||||
});
|
});
|
||||||
@ -221,28 +240,26 @@ test.describe('Add TestCase New Flow', () => {
|
|||||||
test('Non-owner user should not able to add test case', async ({
|
test('Non-owner user should not able to add test case', async ({
|
||||||
dataConsumerPage,
|
dataConsumerPage,
|
||||||
dataStewardPage,
|
dataStewardPage,
|
||||||
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await visitDataQualityPage(dataConsumerPage);
|
const table = new TableClass();
|
||||||
await visitDataQualityPage(dataStewardPage);
|
const { apiContext } = await getApiContext(page);
|
||||||
|
await table.create(apiContext);
|
||||||
|
|
||||||
await dataConsumerPage.getByTestId('add-test-case-btn').click();
|
for (const page of [dataConsumerPage, dataStewardPage]) {
|
||||||
await dataStewardPage.getByTestId('add-test-case-btn').click();
|
await visitDataQualityPage(page);
|
||||||
|
|
||||||
await selectTable(dataConsumerPage, table1.entity.name);
|
await page.getByTestId('add-test-case-btn').click();
|
||||||
await selectTable(dataStewardPage, table1.entity.name);
|
|
||||||
|
|
||||||
await dataConsumerPage.getByTestId('create-btn').click();
|
await selectTable(page, table);
|
||||||
await dataStewardPage.getByTestId('create-btn').click();
|
|
||||||
|
await page.getByTestId('create-btn').click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
dataConsumerPage.locator('#testCaseFormV1_selectedTable_help')
|
page.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(
|
).toContainText(
|
||||||
'You do not have the necessary permissions to create a test case on this table.'
|
'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 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) => {
|
const validateTourSteps = async (page: Page) => {
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await page.waitForSelector(`[data-tour-elem="badge"]`);
|
await waitForTourBadgeWithRetry(page);
|
||||||
|
|
||||||
await expect(page.locator(`[data-tour-elem="badge"]`)).toHaveText('1');
|
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 expect(page.locator(`[data-tour-elem="badge"]`)).toHaveText('3');
|
||||||
|
|
||||||
await page.getByTestId('searchBox').fill('dim_a');
|
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');
|
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('Tour should work from help section', async ({ page }) => {
|
||||||
|
test.slow();
|
||||||
|
|
||||||
await page.locator('[data-testid="help-icon"]').click();
|
await page.locator('[data-testid="help-icon"]').click();
|
||||||
await page.getByRole('link', { name: 'Tour', exact: true }).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.waitForURL('**/tour');
|
||||||
|
|
||||||
await page.waitForSelector('#feedWidgetData');
|
await page.waitForSelector('#feedWidgetData');
|
||||||
@ -145,21 +178,34 @@ test.describe('Tour should work properly', () => {
|
|||||||
.locator('.whats-new-alert-close')
|
.locator('.whats-new-alert-close')
|
||||||
.click();
|
.click();
|
||||||
await page.getByText('Take a product tour to get started!').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.waitForURL('**/tour');
|
||||||
|
|
||||||
await page.waitForSelector('#feedWidgetData');
|
await page.waitForSelector('#feedWidgetData');
|
||||||
|
// Since the tour steps are already tested in the first test,
|
||||||
await validateTourSteps(page);
|
// here we only validate whether the tour is loading or not.
|
||||||
|
await waitForTourBadgeWithRetry(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Tour should work from URL directly', async ({ page }) => {
|
test('Tour should work from URL directly', async ({ page }) => {
|
||||||
await page.goto('/tour');
|
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.waitForURL('**/tour');
|
||||||
|
|
||||||
await page.waitForSelector('#feedWidgetData');
|
await page.waitForSelector('#feedWidgetData');
|
||||||
|
// Since the tour steps are already tested in the first test,
|
||||||
await validateTourSteps(page);
|
// here we only validate whether the tour is loading or not.
|
||||||
|
await waitForTourBadgeWithRetry(page);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -35,7 +35,7 @@ import {
|
|||||||
redirectToHomePage,
|
redirectToHomePage,
|
||||||
uuid,
|
uuid,
|
||||||
} from './common';
|
} from './common';
|
||||||
import { addOwner } from './entity';
|
import { addOwner, waitForAllLoadersToDisappear } from './entity';
|
||||||
import { sidebarClick } from './sidebar';
|
import { sidebarClick } from './sidebar';
|
||||||
|
|
||||||
export const assignDomain = async (page: Page, domain: Domain['data']) => {
|
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 selectDomain(page, domain);
|
||||||
await checkDomainDisplayName(page, domain.displayName);
|
await checkDomainDisplayName(page, domain.displayName);
|
||||||
await page.getByTestId('assets').click();
|
await page.getByTestId('assets').click();
|
||||||
|
await waitForAllLoadersToDisappear(page);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fillCommonFormItems = async (
|
const fillCommonFormItems = async (
|
||||||
@ -270,11 +271,11 @@ export const verifyDomain = async (
|
|||||||
) => {
|
) => {
|
||||||
await checkDomainDisplayName(page, domain.displayName);
|
await checkDomainDisplayName(page, domain.displayName);
|
||||||
|
|
||||||
const viewerContainerText = await page.textContent(
|
await expect(page.getByText(domain.description)).toBeVisible();
|
||||||
'[data-testid="viewer-container"]'
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(viewerContainerText).toContain(domain.description);
|
expect(
|
||||||
|
await page.locator(`[id="KnowledgePanel\\.Description"]`).textContent()
|
||||||
|
).toContain(domain.description);
|
||||||
|
|
||||||
if (!isEmpty(domain.owners) && !isUndefined(domain.owners)) {
|
if (!isEmpty(domain.owners) && !isUndefined(domain.owners)) {
|
||||||
await expect(
|
await expect(
|
||||||
|
@ -38,9 +38,12 @@ import {
|
|||||||
import { searchAndClickOnOption } from './explore';
|
import { searchAndClickOnOption } from './explore';
|
||||||
import { sidebarClick } from './sidebar';
|
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++) {
|
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();
|
const count = await allLoaders.count();
|
||||||
|
|
||||||
let allLoadersGone = true;
|
let allLoadersGone = true;
|
||||||
|
@ -863,7 +863,11 @@ const AssetsTabs = forwardRef(
|
|||||||
)}
|
)}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Col className="border-default border-radius-sm p-lg" span={24}>
|
<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 />
|
<Skeleton />
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user