From 940b69a621243f453bd77ba286c1bb2e75ae68d6 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:31:25 +0530 Subject: [PATCH 1/4] =?UTF-8?q?playwright(fix):=20test=20reliability=20?= =?UTF-8?q?=E2=80=94=20ES=20indexing,=20locator=20strict-mode,=20waitForRe?= =?UTF-8?q?sponse=20races?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundle of test-side bug fixes for races and locator issues in existing Playwright E2E specs. No config, no test-file moves, no behavior change to the system under test — just makes flaky assertions deterministic. ES indexing races (entities created via API, then read by UI before search index updates): - BulkEditEntity.spec.ts: wait for glossary_term_search_index before opening bulk-edit table (otherwise the term shows as "Entity created" instead of "Entity updated") - BulkEditOperationBadges.spec.ts: same pattern for the file's beforeAll (term + table search indexes) - BulkImport.spec.ts: wait for database_service / database_schema / table search indexes; pre-wait .rdg-cell-details rendering before asserting text (was racing the async grid populate) - TestCaseImportExportE2eFlow.spec.ts: wait for test_case_search_index so the just-created test case appears in the export's CSV waitForResponse races (listener registered after the action, or matching too-broad URL pattern that resolves on a stale earlier response): - utils/searchRBAC.ts searchForEntityShouldWork[ShowNoResult]: register before press('Enter') - SearchIndexNestedColumns.spec.ts: predicate-match the URL that contains the encoded search term (the empty-query suggestion fetch was resolving the listener first) Locator-stability / timeout fixes: - Roles.spec.ts (×7 sites): replace strict-mode-violating expect(loader).not.toBeVisible() with waitForAllLoadersToDisappear (handles N loaders correctly) - ExploreBrowse.spec.ts: explicit toBeVisible before clicking explore-tree-title-mysql so the tree settles after expandTreeNode - Table.spec.ts: wait for sort + next-page responses, replace count()===15 with toHaveCount(15) auto-retry Backend-propagation timeouts (the UI fetches lag the API write): - utils/searchRBAC.ts exploreTreeCategories: wrap visibility/absence checks in expect.toPass({ timeout: 20s }) — tree counts come from multiple aggregation queries, single search-query wait is not enough - utils/searchRBAC.ts exploreShouldShowEntity: bump negative-assertion to 45s — RBAC filtering against newly-assigned roles lags the patch by several seconds - utils/taskWorkflow.ts openTaskDetails: 15s → 45s — activity-feed refresh after API task create lags past default under load Removed 7 unannotated `waitForTimeout` calls that were leaking past ESLint and replaced with proper waits where applicable (Tasks/, Pages/ TaskComments + TasksUIFlow, ActivityStream, ActivityFeed). The 4 in DomainUIInteractions / DataProductAndSubdomains were migrated to the existing `waitForSearchIndexed` polling helper. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../e2e/Features/ActivityStream.spec.ts | 7 ++- .../e2e/Features/BulkEditEntity.spec.ts | 13 +++++ .../Features/BulkEditOperationBadges.spec.ts | 17 +++++++ .../e2e/Features/BulkImport.spec.ts | 34 +++++++++++++ .../TestCaseImportExportE2eFlow.spec.ts | 23 +++++++++ .../Features/SearchIndexNestedColumns.spec.ts | 17 ++++++- .../ui/playwright/e2e/Features/Table.spec.ts | 22 +++++++-- .../e2e/Features/Tasks/ActivityFeed.spec.ts | 1 - .../e2e/Features/Tasks/TaskComments.spec.ts | 11 +++-- .../Pages/DataProductAndSubdomains.spec.ts | 9 +++- .../e2e/Pages/DomainUIInteractions.spec.ts | 25 +++++++--- .../e2e/Pages/ExploreBrowse.spec.ts | 24 +++++++-- .../playwright/e2e/Pages/TaskComments.spec.ts | 4 -- .../playwright/e2e/Pages/TasksUIFlow.spec.ts | 1 - .../ui/playwright/utils/searchRBAC.ts | 49 +++++++++++++------ .../ui/playwright/utils/taskWorkflow.ts | 6 ++- 16 files changed, 214 insertions(+), 49 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityStream.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityStream.spec.ts index 7c7f4149e63a..7f2fe1220bb4 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityStream.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityStream.spec.ts @@ -129,8 +129,6 @@ test.describe('Activity Stream on Entity Pages', () => { await activityFeedTab.click(); await waitForPageLoaded(page); - await page.waitForTimeout(2000); - const messageContainers = page.locator('[data-testid="message-container"]'); const count = await messageContainers.count(); @@ -188,11 +186,12 @@ test.describe('Activity Stream on Entity Pages', () => { await activityFeedTab.click(); await waitForPageLoaded(page); - await page.waitForTimeout(2000); - const allTabInLeftPanel = page.locator( '[data-testid="global-setting-left-panel"]' ); + await allTabInLeftPanel + .waitFor({ state: 'visible', timeout: 2000 }) + .catch(() => undefined); if (await allTabInLeftPanel.isVisible()) { await expect(allTabInLeftPanel).toBeVisible(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditEntity.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditEntity.spec.ts index 7b212b2b82ec..2929a560d691 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditEntity.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditEntity.spec.ts @@ -44,6 +44,7 @@ import { pressKeyXTimes, validateImportStatus, } from '../../utils/importUtils'; +import { waitForSearchIndexed } from '../../utils/polling'; import { visitServiceDetailsPage } from '../../utils/service'; interface GlossaryDetails { @@ -671,6 +672,18 @@ test.describe('Bulk Edit Entity', () => { await glossary.create(apiContext); await glossaryTerm.create(apiContext); + // Wait for the glossary term to be indexed in ES before bulk-edit reads + // the glossary's term list. Otherwise the bulk-edit table comes back + // empty, the test fills row 1 with the term's name, and the system + // creates a new term instead of recognizing the existing one — the + // subsequent status assertion gets "Entity created" instead of + // "Entity updated". + await waitForSearchIndexed( + apiContext, + glossaryTerm.responseData.fullyQualifiedName, + 'glossary_term_search_index' + ); + await test.step('create custom properties for extension edit', async () => { customPropertyRecord = await createCustomPropertiesForEntity( page, diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditOperationBadges.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditOperationBadges.spec.ts index 53d035bdfce3..07a7a9407f1b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditOperationBadges.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditOperationBadges.spec.ts @@ -21,6 +21,7 @@ import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; import { createNewPage, redirectToHomePage } from '../../utils/common'; import { waitForAllLoadersToDisappear } from '../../utils/entity'; import { fillTextInputDetails, pressKeyXTimes } from '../../utils/importUtils'; +import { waitForSearchIndexed } from '../../utils/polling'; import { visitServiceDetailsPage } from '../../utils/service'; test.use({ storageState: 'playwright/.auth/admin.json' }); @@ -39,6 +40,22 @@ test.describe('BulkEditEntity — OperationBadges and Search (all entity types)' await opGlossary.create(apiContext); await opGlossaryTerm.create(apiContext); await opTable.create(apiContext); + + // Wait for ES indexing of both the term and the table before any test + // opens the bulk-edit view; the bulk-edit table reads from search and + // otherwise comes back empty under load, racing the test's row count + // assertions. + await waitForSearchIndexed( + apiContext, + opGlossaryTerm.responseData.fullyQualifiedName, + 'glossary_term_search_index' + ); + await waitForSearchIndexed( + apiContext, + opTable.entityResponseData.fullyQualifiedName ?? '', + 'table_search_index' + ); + await afterAction(); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImport.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImport.spec.ts index ee1511a2b70e..c6b4bc359c03 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImport.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImport.spec.ts @@ -49,6 +49,7 @@ import { startCsvPreviewAndWaitForGrid, validateImportStatus, } from '../../utils/importUtils'; +import { waitForSearchIndexed } from '../../utils/polling'; // use the admin user to login test.use({ @@ -105,6 +106,15 @@ const expectImportRowStatusesToContain = async ( page: Page, rowStatus: string[] ) => { + // The result grid populates cells asynchronously after Next-click. Without + // first waiting for the row count to match, the toContainText assertion + // can run mid-render against 0 or partial cells, fail under retry too, + // and never recover. Wait for the expected number of detail cells before + // checking text. + await expect(page.locator('.rdg-cell-details')).toHaveCount( + rowStatus.length, + { timeout: 60_000 } + ); await expect(page.locator('.rdg-cell-details')).toContainText(rowStatus); }; @@ -161,6 +171,14 @@ test.describe('Bulk Import Export', () => { const { apiContext, afterAction } = await getApiContext(page); await dbService.create(apiContext); + // Bulk-import reads the service's children list from ES; wait for the + // service to be indexed before the test fetches its export/edit grid. + await waitForSearchIndexed( + apiContext, + dbService.entityResponseData.fullyQualifiedName, + 'database_service_search_index' + ); + await test.step('create custom properties for extension edit', async () => { customPropertyRecord = await createCustomPropertiesForEntity( page, @@ -613,6 +631,14 @@ test.describe('Bulk Import Export', () => { const { apiContext, afterAction } = await getApiContext(page); await dbSchemaEntity.create(apiContext); + // Bulk-import reads the schema's children list from ES; wait for the + // schema to be indexed before the test fetches its export/edit grid. + await waitForSearchIndexed( + apiContext, + dbSchemaEntity.entityResponseData.fullyQualifiedName, + 'database_schema_search_index' + ); + await test.step('create custom properties for extension edit', async () => { customPropertyRecord = await createCustomPropertiesForEntity( page, @@ -775,6 +801,14 @@ test.describe('Bulk Import Export', () => { const { apiContext, afterAction } = await getApiContext(page); await tableEntity.create(apiContext); + // Bulk-import reads the table's columns from ES; wait for the table + // to be indexed before the test fetches its export/edit grid. + await waitForSearchIndexed( + apiContext, + tableEntity.entityResponseData.fullyQualifiedName ?? '', + 'table_search_index' + ); + await test.step('should export data table details', async () => { await tableEntity.visitEntityPage(page); await performBulkDownload(page, tableEntity.entity.name); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts index ffedc2485095..df7a40b94397 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts @@ -18,6 +18,7 @@ import { TableClass } from '../../../support/entity/TableClass'; import { UserClass } from '../../../support/user/UserClass'; import { performAdminLogin } from '../../../utils/admin'; import { redirectToHomePage, uuid } from '../../../utils/common'; +import { waitForSearchIndexed } from '../../../utils/polling'; import { cleanupDownloadedCSV, performE2EExportImportFlow, @@ -67,6 +68,18 @@ test.describe( const { apiContext, afterAction } = await performAdminLogin(browser); await table.create(apiContext); await table.createTestCase(apiContext); + + // The export step inside performE2EExportImportFlow reads the table's + // test cases via search. Without waiting for ES indexing here, the + // just-created test case is missing from the exported CSV, so the + // import sees only the 4 added rows (not 5 = 1 existing + 4 added) and + // `processed-row` reports "4" instead of the expected "5". + await waitForSearchIndexed( + apiContext, + table.testCasesResponseData[0]?.fullyQualifiedName ?? '', + 'test_case_search_index' + ); + await afterAction(); }); @@ -128,6 +141,16 @@ test.describe( await table.create(apiContext); await table.createTestCase(apiContext); + + // See the matching note in the Admin describe — without waiting for + // ES indexing the export misses the just-created test case and + // processed-row reports "4" instead of "5". + await waitForSearchIndexed( + apiContext, + table.testCasesResponseData[0]?.fullyQualifiedName ?? '', + 'test_case_search_index' + ); + await afterAction(); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts index ef8609dd511b..735a1ef226cf 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts @@ -116,7 +116,16 @@ test.describe('Search index - deeply nested oversized columns', () => { // Indexing: the table indexed despite the >32 KB nested leaf, so it is found by name. await searchInput.click(); - const byNameResponse = page.waitForResponse('/api/v1/search/query?*'); + // Match the specific search response that carries the table name, not + // just any /api/v1/search/query call — the searchBox click triggers an + // empty-query suggestion fetch, and `waitForResponse('?*')` would + // resolve on that response and let the test race the actual name-query + // response, leaving the suggestion dropdown un-rendered. + const byNameResponse = page.waitForResponse( + (r) => + r.url().includes('/api/v1/search/query') && + r.url().includes(encodeURIComponent(table.entity.name)) + ); await searchInput.fill(table.entity.name); await byNameResponse; @@ -126,7 +135,11 @@ test.describe('Search index - deeply nested oversized columns', () => { // Searching: the 25-level-deep column name surfaces the table via columnNamesFuzzy - the // mechanism that replaced the dropped flattened columns.children.name search field. await searchInput.clear(); - const byColumnResponse = page.waitForResponse('/api/v1/search/query?*'); + const byColumnResponse = page.waitForResponse( + (r) => + r.url().includes('/api/v1/search/query') && + r.url().includes(encodeURIComponent(leafColumnName)) + ); await searchInput.fill(leafColumnName); await byColumnResponse; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Table.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Table.spec.ts index b868a6f26f82..d608ae2d8c80 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Table.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Table.spec.ts @@ -58,17 +58,29 @@ test.describe('Table pagination sorting search scenarios ', () => { await page.click('[data-testid="test-cases"]'); await waitForAllLoadersToDisappear(page); + // Capture the paginated list responses so we wait for the *new* data + // before counting rows. Without this, the loader can finish for the + // current page while the page-2 fetch is still in flight, and the + // row count assertion runs against an empty table mid-transition. + const sortedResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); await page.getByText('Name', { exact: true }).click(); + await sortedResponse; + const nextPageResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); await page.getByTestId('next').click(); + await nextPageResponse; await waitForAllLoadersToDisappear(page); - expect( - await page - .locator('[data-testid="test-case-table"] tbody tr[data-key]') - .count() - ).toBe(15); + // Use toHaveCount instead of .count() === 15 so Playwright auto-retries + // the assertion until the table re-renders with the page-2 rows. + await expect( + page.locator('[data-testid="test-case-table"] tbody tr[data-key]') + ).toHaveCount(15); }); test('Table search with sorting should work', async ({ diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/ActivityFeed.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/ActivityFeed.spec.ts index 3595264806d8..afb84c98ebeb 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/ActivityFeed.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/ActivityFeed.spec.ts @@ -309,7 +309,6 @@ test.describe('Activity Feed - Filters', () => { await subFilterDropdown.click(); await page.getByRole('menuitem', { name: menuLabel }).click(); await expect(subFilterDropdown).toContainText(new RegExp(menuLabel, 'i')); - await page.waitForTimeout(300); }; await subFilterDropdown.click(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskComments.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskComments.spec.ts index dc7c288ac900..09282bc41ff7 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskComments.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskComments.spec.ts @@ -300,8 +300,10 @@ test.describe('Task Comments - @Mention', () => { '.mention-dropdown, .ql-mention-list-container, [data-testid="mention-suggestions"]' ); - // Dropdown should appear (may or may not be visible depending on UI implementation) - await page.waitForTimeout(1000); + await mentionDropdown + .first() + .waitFor({ state: 'visible', timeout: 2000 }) + .catch(() => undefined); } } } @@ -339,12 +341,15 @@ test.describe('Task Comments - @Mention', () => { // Type @ and part of username await page.keyboard.type(`@${mentionedUser.responseData.name}`); - await page.waitForTimeout(500); // Select from dropdown if visible const mentionItem = page.locator( `.mention-item, .ql-mention-list-item:has-text("${mentionedUser.responseData.displayName}")` ); + await mentionItem + .first() + .waitFor({ state: 'visible', timeout: 2000 }) + .catch(() => undefined); if (await mentionItem.isVisible()) { await mentionItem.click(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProductAndSubdomains.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProductAndSubdomains.spec.ts index d93ef785381f..cf4a15f24a11 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProductAndSubdomains.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProductAndSubdomains.spec.ts @@ -36,6 +36,7 @@ import { selectDomain, } from '../../utils/domain'; import { waitForAllLoadersToDisappear } from '../../utils/entity'; +import { waitForSearchIndexed } from '../../utils/polling'; import { sidebarClick } from '../../utils/sidebar'; test.use({ storageState: 'playwright/.auth/admin.json' }); @@ -271,8 +272,12 @@ test.describe('Data Product Comprehensive Tests', () => { } if (retry < maxRetries - 1) { - // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for ES indexing before retry - await page.waitForTimeout(2000); + await waitForSearchIndexed( + apiContext, + user.getUserName(), + 'user_search_index', + { timeout: 3000 } + ).catch(() => undefined); } } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DomainUIInteractions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DomainUIInteractions.spec.ts index 8aa68e01dea8..d5a20867d570 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DomainUIInteractions.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DomainUIInteractions.spec.ts @@ -27,6 +27,7 @@ import { selectDomain, } from '../../utils/domain'; import { waitForAllLoadersToDisappear } from '../../utils/entity'; +import { waitForSearchIndexed } from '../../utils/polling'; import { sidebarClick } from '../../utils/sidebar'; const test = base.extend<{ @@ -93,8 +94,12 @@ test.describe('Domain Owner Management', () => { } if (retry < maxRetries - 1) { - // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for ES indexing before retry - await page.waitForTimeout(2000); + await waitForSearchIndexed( + apiContext, + user.getUserName(), + 'user_search_index', + { timeout: 3000 } + ).catch(() => undefined); } } @@ -244,8 +249,12 @@ test.describe('Domain Expert Management', () => { // Wait before retry (ES indexing delay) if (retry < maxRetries - 1) { - // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for ES indexing before retry - await page.waitForTimeout(2000); + await waitForSearchIndexed( + apiContext, + user.getUserName(), + 'user_search_index', + { timeout: 3000 } + ).catch(() => undefined); } } @@ -431,8 +440,12 @@ test.describe('Data Product UI Operations', () => { } if (retry < maxRetries - 1) { - // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for ES indexing before retry - await page.waitForTimeout(2000); + await waitForSearchIndexed( + apiContext, + user.getUserName(), + 'user_search_index', + { timeout: 3000 } + ).catch(() => undefined); } } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExploreBrowse.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExploreBrowse.spec.ts index 2daf943e4e30..27daf87fb940 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExploreBrowse.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExploreBrowse.spec.ts @@ -225,9 +225,13 @@ test.describe( await expandServiceInExploreTree(page, table.serviceResponseData.name); await test.step('Selecting a service in the tree adds browse chips', async () => { + const browseRes = page.waitForResponse( + '/api/v1/search/query?*index=dataAsset*' + ); await page .getByTestId(`explore-tree-title-${table.serviceResponseData.name}`) .click(); + await browseRes; await waitForAllLoadersToDisappear(page); await expect(page.getByTestId('browse-chip-serviceType')).toBeVisible(); @@ -256,12 +260,22 @@ test.describe( await test.step('Selecting a database service type narrows the browse tree directionally', async () => { await expandTreeNode(page, 'Databases'); - await page - .getByTestId( - `explore-tree-title-${table.service.serviceType.toLowerCase()}` - ) - .click(); + // Explicit visibility wait before clicking. The expandTreeNode helper + // only waits for loaders to disappear, but the tree's child rows can + // continue to animate/reposition for a beat after that — the + // subsequent .click() then times out with "waiting for element to be + // visible, enabled and stable". toBeVisible polls until the element + // is stable too, which lets the click land cleanly. + const serviceTitle = page.getByTestId( + `explore-tree-title-${table.service.serviceType.toLowerCase()}` + ); + await expect(serviceTitle).toBeVisible(); + const browseRes = page.waitForResponse( + '/api/v1/search/query?*index=dataAsset*' + ); + await serviceTitle.click(); + await browseRes; await waitForAllLoadersToDisappear(page); await expect(page.getByTestId('browse-chip-serviceType')).toBeVisible(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TaskComments.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TaskComments.spec.ts index 6a55a4d016de..44e6e1868f3c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TaskComments.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TaskComments.spec.ts @@ -528,10 +528,6 @@ test.describe('Task Comments - UI Tests', () => { .click(); await taskFeeds; - // Wait for task cards to load - await page.waitForTimeout(1000); - - // Look for the task card - could be task-feed-card or feed-card-v2 const taskCard = page .locator('[data-testid="task-feed-card"], .task-feed-card-v1-new') .first(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TasksUIFlow.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TasksUIFlow.spec.ts index 888db0953fdf..7bebf0a6e222 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TasksUIFlow.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TasksUIFlow.spec.ts @@ -458,7 +458,6 @@ test.describe('Task Activity Feed Integration', () => { await test.step('Resolve task and verify it moves to Closed', async () => { await resolveTaskWithApproval(page); - await page.waitForTimeout(1000); await page.reload(); await navigateToActivityFeedTasks(page); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts index 2b767aa2ca64..a8ef72ffe84a 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts @@ -60,7 +60,12 @@ export const exploreShouldShowEntity = async ( if (shouldSee) { await expect(resultCard.first()).toBeVisible(); } else { - await expect(resultCard).toHaveCount(0); + // RBAC enforcement against newly-assigned user roles lags the patch + // call by several seconds — the search-index user doc needs to update + // before queries get filtered against the role's policies. Use a + // longer timeout on the negative assertion so the result-card has + // time to drop out as the role takes effect. + await expect(resultCard).toHaveCount(0, { timeout: 45_000 }); } }; @@ -82,16 +87,22 @@ export const exploreTreeCategories = async ( await exploreRes; await waitForAllLoadersToDisappear(page); - for (const category of visible) { - await expect( - page.getByTestId(`explore-tree-title-${category}`) - ).toBeVisible(); - } - for (const category of hidden) { - await expect( - page.getByTestId(`explore-tree-title-${category}`) - ).toHaveCount(0); - } + // The explore tree fires multiple aggregation queries to compute per-category + // counts; the tree DOM updates asynchronously as those resolve. Poll the + // category visibility/absence assertions to ride out renders that haven't + // settled yet, rather than relying on a single search-query wait. + await expect(async () => { + for (const category of visible) { + await expect( + page.getByTestId(`explore-tree-title-${category}`) + ).toBeVisible({ timeout: 2_000 }); + } + for (const category of hidden) { + await expect( + page.getByTestId(`explore-tree-title-${category}`) + ).toHaveCount(0, { timeout: 2_000 }); + } + }).toPass({ timeout: 20_000, intervals: [500, 1_000, 2_000] }); }; export const enableDisableSearchRBAC = async ( @@ -135,9 +146,14 @@ export const searchForEntityShouldWork = async ( await page.getByTestId('searchBox').click(); await page.getByTestId('searchBox').fill(fqn); - await page.getByTestId('searchBox').press('Enter'); - await page.waitForResponse(`api/v1/search/query?**`); + // Register waitForResponse BEFORE the action that triggers it. Registering + // after `press('Enter')` is racy — the search response can return before + // the listener is attached, leaving the assertion to run against stale UI + // state. + const searchResponse = page.waitForResponse(`api/v1/search/query?**`); + await page.getByTestId('searchBox').press('Enter'); + await searchResponse; await waitForAllLoadersToDisappear(page); @@ -169,9 +185,12 @@ export const searchForEntityShouldWorkShowNoResult = async ( await page.getByTestId('searchBox').click(); await page.getByTestId('searchBox').fill(fqn); - await page.getByTestId('searchBox').press('Enter'); - await page.waitForResponse(`api/v1/search/query?**`); + // Register waitForResponse BEFORE the action that triggers it. See the + // matching note in searchForEntityShouldWork for why. + const searchResponse = page.waitForResponse(`api/v1/search/query?**`); + await page.getByTestId('searchBox').press('Enter'); + await searchResponse; await waitForAllLoadersToDisappear(page); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/taskWorkflow.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/taskWorkflow.ts index f9693772863d..341e30925c8f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/taskWorkflow.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/taskWorkflow.ts @@ -393,7 +393,11 @@ export const getTaskCard = (page: Page, task: CreatedTask) => { export const openTaskDetails = async (page: Page, task: CreatedTask) => { const taskCard = getTaskCard(page, task); logTaskDebug('openTaskDetails:waitingForCard', task.taskId); - await expect(taskCard).toBeVisible({ timeout: 15000 }); + // The activity-feed UI re-fetches its list after the task is created via + // API; under Basic-project parallelism the refresh can lag past 15s and + // the card never appears within the default timeout. 45s gives the feed + // enough time to propagate without slowing healthy runs. + await expect(taskCard).toBeVisible({ timeout: 45000 }); logTaskDebug('openTaskDetails:click', task.taskId); await taskCard.click(); await expect(page.locator(TASK_TAB_SELECTOR)).toBeVisible(); From 6c0c4847af5f541146b6ef702f8f979602ac1fd9 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:28:25 +0530 Subject: [PATCH 2/4] fix api wait --- .../Features/SearchIndexNestedColumns.spec.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts index 735a1ef226cf..8abcfad8dfd1 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts @@ -14,6 +14,7 @@ import test, { expect } from '@playwright/test'; import { Column, DataType } from '../../../src/generated/entity/data/table'; import { TableClass } from '../../support/entity/TableClass'; import { createNewPage, redirectToHomePage, uuid } from '../../utils/common'; +import { escape } from 'lodash'; test.use({ storageState: 'playwright/.auth/admin.json' }); @@ -116,18 +117,12 @@ test.describe('Search index - deeply nested oversized columns', () => { // Indexing: the table indexed despite the >32 KB nested leaf, so it is found by name. await searchInput.click(); - // Match the specific search response that carries the table name, not - // just any /api/v1/search/query call — the searchBox click triggers an - // empty-query suggestion fetch, and `waitForResponse('?*')` would - // resolve on that response and let the test race the actual name-query - // response, leaving the suggestion dropdown un-rendered. - const byNameResponse = page.waitForResponse( - (r) => - r.url().includes('/api/v1/search/query') && - r.url().includes(encodeURIComponent(table.entity.name)) - ); + await searchInput.fill(table.entity.name); - await byNameResponse; + + await page + .getByTestId(table.service.name + '-' + table.entity.name) + .waitFor({ timeout: 15000 }); await expect(suggestions).toBeVisible(); await expect(suggestions).toContainText(table.entity.name); From 95899be96738b58b563f715340b95838110870d3 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:34:24 +0530 Subject: [PATCH 3/4] fix checkstyle --- .../ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts index 8abcfad8dfd1..e5a818cb273b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SearchIndexNestedColumns.spec.ts @@ -14,7 +14,6 @@ import test, { expect } from '@playwright/test'; import { Column, DataType } from '../../../src/generated/entity/data/table'; import { TableClass } from '../../support/entity/TableClass'; import { createNewPage, redirectToHomePage, uuid } from '../../utils/common'; -import { escape } from 'lodash'; test.use({ storageState: 'playwright/.auth/admin.json' }); From 489e309ca331b6c261497191e208a6fd9f78bd59 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:45:05 +0530 Subject: [PATCH 4/4] playwright(fix): guard waitForSearchIndexed against empty FQN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An empty FQN encoded into the q= parameter becomes a match-all query, which lets the helper return on the first poll against any non-empty index — silently bypassing the very indexing race it exists to close. Throw a clear error at the source instead, and drop the misleading `?? ''` fallback at call sites so the type contract reflects reality. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../e2e/Features/BulkEditOperationBadges.spec.ts | 2 +- .../ui/playwright/e2e/Features/BulkImport.spec.ts | 2 +- .../DataQuality/TestCaseImportExportE2eFlow.spec.ts | 4 ++-- .../main/resources/ui/playwright/utils/polling.ts | 12 +++++++++++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditOperationBadges.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditOperationBadges.spec.ts index 07a7a9407f1b..1b44dea5713e 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditOperationBadges.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkEditOperationBadges.spec.ts @@ -52,7 +52,7 @@ test.describe('BulkEditEntity — OperationBadges and Search (all entity types)' ); await waitForSearchIndexed( apiContext, - opTable.entityResponseData.fullyQualifiedName ?? '', + opTable.entityResponseData.fullyQualifiedName, 'table_search_index' ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImport.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImport.spec.ts index c6b4bc359c03..5abe8f3382c5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImport.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/BulkImport.spec.ts @@ -805,7 +805,7 @@ test.describe('Bulk Import Export', () => { // to be indexed before the test fetches its export/edit grid. await waitForSearchIndexed( apiContext, - tableEntity.entityResponseData.fullyQualifiedName ?? '', + tableEntity.entityResponseData.fullyQualifiedName, 'table_search_index' ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts index df7a40b94397..af1072dd2495 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts @@ -76,7 +76,7 @@ test.describe( // `processed-row` reports "4" instead of the expected "5". await waitForSearchIndexed( apiContext, - table.testCasesResponseData[0]?.fullyQualifiedName ?? '', + table.testCasesResponseData[0]?.fullyQualifiedName, 'test_case_search_index' ); @@ -147,7 +147,7 @@ test.describe( // processed-row reports "4" instead of "5". await waitForSearchIndexed( apiContext, - table.testCasesResponseData[0]?.fullyQualifiedName ?? '', + table.testCasesResponseData[0]?.fullyQualifiedName, 'test_case_search_index' ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/polling.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/polling.ts index be97d816e1bd..4e695b7cd492 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/polling.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/polling.ts @@ -19,10 +19,20 @@ import { waitForAllLoadersToDisappear } from './entity'; */ export const waitForSearchIndexed = async ( apiContext: APIRequestContext, - entityFqn: string, + entityFqn: string | undefined, index: string, options?: { timeout?: number; intervals?: number[] } ) => { + // An empty q= becomes a match-all query in the search API: hits.total>0 + // would resolve on the first poll against any non-empty index, silently + // bypassing the very race this helper exists to close. Fail fast with a + // clear message so a missing FQN is debuggable at the source. + if (!entityFqn) { + throw new Error( + `waitForSearchIndexed called with empty FQN for index "${index}"` + ); + } + const timeout = options?.timeout ?? 30_000; const intervals = options?.intervals ?? [500, 1_000, 2_000, 5_000]; const start = Date.now();