Fix: Playwright AUTs flakiness (#22128)

* Fix the flak AUT tests

* Add license
This commit is contained in:
Aniket Katkar 2025-07-04 11:47:31 +05:30 committed by GitHub
parent f524219292
commit 7e39510ddb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 92 additions and 26 deletions

View File

@ -1,7 +1,20 @@
/*
* Copyright 2025 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 test, { expect } from '@playwright/test';
import { redirectToHomePage } from '../../utils/common';
const DESCRIPTION_SEARCH =
// eslint-disable-next-line max-len
'The dimension table contains data about your customers. The customers table contains one row per customer. It includes historical metrics (such as the total amount that each customer has spent in your store) as well as forward-looking metrics (such as the predicted number of days between future orders and the expected order value in the next 30 days). This table also includes columns that segment customers into various categories (such as new, returning, promising, at risk, dormant, and loyal), which you can use to target marketing activities.The dimension table contains data about your customers. The customers table contains one row per customer. It includes historical metrics (such as the total amount that each customer has spent in your store) as well as forward-looking metrics (such as the predicted number of days between future orders and the expected order value in the next 30 days). This table also includes columns that segment customers into various categories (such as new, returning, promising, at risk, dormant, and loyal), which you can use to target marketing activities.';
// use the admin user to login

View File

@ -15,7 +15,7 @@ import test from '@playwright/test';
import { PLAYWRIGHT_INGESTION_TAG_OBJ } from '../../constant/config';
import MysqlIngestionClass from '../../support/entity/ingestion/MySqlIngestionClass';
import { addAndTriggerAutoClassificationPipeline } from '../../utils/autoClassification';
import { redirectToHomePage } from '../../utils/common';
import { getApiContext, redirectToHomePage } from '../../utils/common';
import { settingClick, SettingOptionsType } from '../../utils/sidebar';
const mysqlService = new MysqlIngestionClass({
@ -102,10 +102,7 @@ test.describe('Auto Classification', PLAYWRIGHT_INGESTION_TAG_OBJ, async () => {
.toBeAttached();
// Delete the created service
await settingClick(
page,
mysqlService.category as unknown as SettingOptionsType
);
await mysqlService.deleteService(page);
const { apiContext } = await getApiContext(page);
await mysqlService.deleteServiceByAPI(apiContext);
});
});

View File

@ -22,6 +22,7 @@ import {
import { MAX_CONSECUTIVE_ERRORS } from '../../../constant/service';
import {
descriptionBox,
executeWithRetry,
getApiContext,
INVALID_NAMES,
NAME_VALIDATION_ERROR,
@ -642,11 +643,13 @@ class ServiceBaseClass {
async deleteServiceByAPI(apiContext: APIRequestContext) {
if (this.serviceResponseData.fullyQualifiedName) {
await apiContext.delete(
`/api/v1/services/dashboardServices/name/${encodeURIComponent(
this.serviceResponseData.fullyQualifiedName
)}?recursive=true&hardDelete=true`
);
await executeWithRetry(async () => {
await apiContext.delete(
`/api/v1/services/dashboardServices/name/${encodeURIComponent(
this.serviceResponseData.fullyQualifiedName
)}?recursive=true&hardDelete=true`
);
}, 'delete service');
}
}
}

View File

@ -352,7 +352,62 @@ export const closeFirstPopupAlert = async (page: Page) => {
export const reloadAndWaitForNetworkIdle = async (page: Page) => {
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
};
/**
* Utility function to handle API calls with retry logic for connection-related errors.
* This is particularly useful for cleanup operations that might fail due to network issues.
*
* @param apiCall - The API call function to execute
* @param operationName - Name of the operation for logging purposes
* @param maxRetries - Maximum number of retry attempts (default: 3)
* @param baseDelay - Base delay in milliseconds for exponential backoff (default: 1000)
* @returns The result of the API call if successful
*/
export const executeWithRetry = async <T>(
apiCall: () => Promise<T>,
operationName: string,
maxRetries = 3,
baseDelay = 1000
): Promise<T | void> => {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await apiCall();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
// Check if it's a retriable error (connection-related issues)
const isRetriableError =
errorMessage.includes('socket hang up') ||
errorMessage.includes('ECONNRESET') ||
errorMessage.includes('ENOTFOUND') ||
errorMessage.includes('ETIMEDOUT') ||
errorMessage.includes('Connection refused') ||
errorMessage.includes('ECONNREFUSED');
if (isRetriableError && attempt < maxRetries - 1) {
// Exponential backoff: 1s, 2s, 4s
const delay = baseDelay * Math.pow(2, attempt);
// eslint-disable-next-line no-console
console.log(
`${operationName} attempt ${
attempt + 1
} failed with retriable error: ${errorMessage}. Retrying in ${delay}ms...`
);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
} else {
// eslint-disable-next-line no-console
console.error(
`Failed to ${operationName} after ${attempt + 1} attempts:`,
errorMessage
);
// Don't throw the error to prevent test failures - just log it
break;
}
}
}
};

View File

@ -38,6 +38,9 @@ export const visitServiceDetailsPage = async (
// Click on created service
await page.click(`[data-testid="service-name-${service.name}"]`);
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', { state: 'hidden' });
if (visitChildrenTab) {
// Click on children tab Ex. DatabaseService -> Databases
await page.getByRole('tab').nth(1).click();

View File

@ -16,7 +16,7 @@ import { BIG_ENTITY_DELETE_TIMEOUT } from '../constant/delete';
import { GlobalSettingOptions } from '../constant/settings';
import { EntityTypeEndpoint } from '../support/entity/Entity.interface';
import { getApiContext, toastNotification } from './common';
import { escapeESReservedCharacters } from './entity';
import { escapeESReservedCharacters, getEncodedFqn } from './entity';
export enum Services {
Database = GlobalSettingOptions.DATABASES,
@ -80,18 +80,13 @@ export const deleteService = async (
serviceName: string,
page: Page
) => {
const serviceResponse = page.waitForResponse(
`/api/v1/search/query?q=*${encodeURIComponent(
escapeESReservedCharacters(serviceName)
)}*`
await page.goto(
`/service/${getServiceCategoryFromService(typeOfService)}s/${getEncodedFqn(
serviceName
)}?currentPage=1`
);
await page.fill('[data-testid="searchbar"]', serviceName);
await serviceResponse;
// click on created service
await page.click(`[data-testid="service-name-${serviceName}"]`);
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await expect(page.getByTestId('entity-header-name')).toHaveText(serviceName);