From 0686070a50105dd030fb5d4f9ef2a4a9bfd4c71f Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Sat, 15 Aug 2026 14:28:31 +0530 Subject: [PATCH] test(ui): de-flake Data Quality toast assertions against cross-worker toast leakage Toasts render as a stacking queue where every entry carries data-testid="alert-bar", and the backend fans async-delete/job notifications out to *every* socket of the logged-in user (WebSocketManager.sendToOne). Playwright runs fullyParallel with all workers on the same admin storage state, so one worker's cleanup pops a '"pw-xxx" deleted successfully!' toast into another worker's page. That broke DataQuality.spec.ts "Table test case": an unfiltered getByTestId('alert-bar').waitFor({state:'detached'}) hit a strict mode violation on both attempts, each time against a different foreign entity. - add waitForToastToDisappear() and expectNoErrorToast() to playwright utils/common.ts, both scoped so background toasts cannot interfere - expose the toast variant as data-variant so tests can assert on error toasts specifically instead of "no toast at all" - DataQuality: wait only for the toast the step produced - FailedTestCaseSampleData: assert no *error* toast for the 404 empty state - IncidentManagerAfterSoftDelete/AfterOwnerChange: their /deleted|owners/i filters matched the background delete notification; scope to error variant Co-Authored-By: Claude Opus 5 --- .../components/application/toast/toast.tsx | 1 + .../Features/DataQuality/DataQuality.spec.ts | 8 ++-- .../IncidentManagerAfterOwnerChange.spec.ts | 11 ++--- .../IncidentManagerAfterSoftDelete.spec.ts | 16 ++++--- .../Features/FailedTestCaseSampleData.spec.ts | 9 +++- .../resources/ui/playwright/utils/common.ts | 43 +++++++++++++++++++ 6 files changed, 70 insertions(+), 18 deletions(-) diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/toast/toast.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/toast/toast.tsx index 9a910badcb76..ce210c9a2811 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/toast/toast.tsx +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/toast/toast.tsx @@ -83,6 +83,7 @@ export const Toast = ({ toast }: ToastProps) => { 'tw:animate-in tw:fade-in tw:slide-in-from-bottom-2 tw:duration-150' )} data-testid="alert-bar" + data-variant={variant} toast={toast}> undefined); if (owner.responseData.id) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/IncidentManagerAfterSoftDelete.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/IncidentManagerAfterSoftDelete.spec.ts index dda94572e660..aa5fccecc687 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/IncidentManagerAfterSoftDelete.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/IncidentManagerAfterSoftDelete.spec.ts @@ -28,7 +28,11 @@ import test, { expect } from '@playwright/test'; import { SidebarItem } from '../../../constant/sidebar'; import { TableClass } from '../../../support/entity/TableClass'; -import { createNewPage, redirectToHomePage } from '../../../utils/common'; +import { + createNewPage, + expectNoErrorToast, + redirectToHomePage, +} from '../../../utils/common'; import { sidebarClick } from '../../../utils/sidebar'; test.use({ storageState: 'playwright/.auth/admin.json' }); @@ -105,12 +109,10 @@ test('Incident Manager renders without Jackson error after a test case is soft-d expect(response.status()).toBe(200); // And the toast bar must not surface a Jackson "Unrecognized field"/"deleted" error. - // Scope to the toast container so we don't false-positive on legitimate page text - // (e.g. a table cell that happens to contain the word "deleted"). - const errorToast = page - .locator('[data-testid="alert-bar"]') - .filter({ hasText: /Unrecognized field|deleted/i }); - await expect(errorToast).toHaveCount(0); + // Scoped to error-variant toasts so we don't false-positive on legitimate page text + // (e.g. a table cell containing "deleted") nor on the background + // '"" deleted successfully!' notification a parallel worker can trigger. + await expectNoErrorToast(page, /Unrecognized field|deleted/i); } finally { await table.delete(apiContext); await afterAction(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/FailedTestCaseSampleData.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/FailedTestCaseSampleData.spec.ts index e264c94db204..7eb23b9d7235 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/FailedTestCaseSampleData.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/FailedTestCaseSampleData.spec.ts @@ -15,7 +15,12 @@ import { expect, test } from '@playwright/test'; import { PLAYWRIGHT_INGESTION_TAG_OBJ } from '../../constant/config'; import { TableClass } from '../../support/entity/TableClass'; import { performAdminLogin } from '../../utils/admin'; -import { getApiContext, redirectToHomePage, uuid } from '../../utils/common'; +import { + expectNoErrorToast, + getApiContext, + redirectToHomePage, + uuid, +} from '../../utils/common'; import { fillDeleteConfirmationIfPresent } from '../../utils/entity'; import { getFailedRowsData, @@ -249,7 +254,7 @@ test.describe('Failed rows sample fetch gating', () => { // The 404 is the expected "no sample stored" empty state — it must not // surface as an error toast. - await expect(page.getByTestId('alert-bar')).not.toBeVisible(); + await expectNoErrorToast(page); }); } ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 9a85d13bff5b..fb21d09f0540 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -343,6 +343,49 @@ export const toastNotification = async ( await expect(toast.getByTestId('alert-icon')).toBeVisible(); }; +/** + * Waits until the toast carrying `message` is gone. + * + * Always filter by message instead of waiting on a bare `alert-bar` locator: toasts + * are a stacking queue, and the backend fans async-delete/job notifications out to + * every socket of the logged-in user — so a parallel worker's cleanup can pop an + * unrelated toast into this page and turn an unfiltered locator into a strict-mode + * violation. + */ +export const waitForToastToDisappear = async ( + page: Page, + message: string | RegExp, + timeout?: number +) => { + await page + .getByTestId('alert-bar') + .filter({ hasText: message }) + .first() + .waitFor({ state: 'detached', timeout }); +}; + +/** + * Asserts that the page is showing no error toast, optionally narrowed to the + * ones carrying `message`. + * + * Scoped to the error variant on purpose — a bare `alert-bar` assertion also + * catches the background success notifications the backend fans out to every + * socket of the logged-in user (async delete, export jobs), which a parallel + * worker can trigger at any moment. + */ +export const expectNoErrorToast = async ( + page: Page, + message?: string | RegExp +) => { + const errorToast = page.locator( + '[data-testid="alert-bar"][data-variant="error"]' + ); + + await expect( + message ? errorToast.filter({ hasText: message }) : errorToast + ).toHaveCount(0); +}; + export const clickOutside = async (page: Page) => { await page.locator('body').click({ position: {