diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterArticles.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterArticles.spec.ts index 55de2563f045..29051aed6d7b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterArticles.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterArticles.spec.ts @@ -51,6 +51,7 @@ import { scrollListingToCard, verifyArticleSearch, waitForArticleInFollows, + waitForDraftPersisted, } from '../../utils/ContextCenterUtil'; import { addMultiOwner, @@ -275,7 +276,10 @@ test.describe('Context Center Articles', () => { await test.step('dashboard view all articles opens article list', async () => { await navigateToDashboard(page); - await page.getByTestId('article-detail-card').click(); + await page + .getByTestId('article-detail-card') + .getByRole('button', { name: 'View All Articles' }) + .click(); await expect(page).toHaveURL(/\/context-center\/articles/); }); @@ -1520,7 +1524,7 @@ test.describe('Context Center Articles', () => { await navigateToArticle(page, draftArticleA.fullyQualifiedName); await page.fill('.om-block-editor', reloadDescription); await page.getByText('Unsaved').waitFor({ state: 'visible' }); - await page.waitForTimeout(400); + await waitForDraftPersisted(page, draftArticleA.id, reloadDescription); }); await test.step('Reload the page (simulates browser refresh before auto-save)', async () => { @@ -1599,7 +1603,11 @@ test.describe('Context Center Articles', () => { await navigateToArticle(page, articleToDelete.fullyQualifiedName); await page.fill('.om-block-editor', 'This draft should be deleted'); await page.getByText('Unsaved').waitFor({ state: 'visible' }); - await page.waitForTimeout(400); + await waitForDraftPersisted( + page, + articleToDelete.id, + 'This draft should be deleted' + ); }); await test.step('Navigate away to ensure draft is persisted in localStorage', async () => { @@ -1647,14 +1655,14 @@ test.describe('Context Center Articles', () => { await navigateToArticle(page, draftArticleA.fullyQualifiedName); await page.fill('.om-block-editor', contentA); await page.getByText('Unsaved').waitFor({ state: 'visible' }); - await page.waitForTimeout(400); + await waitForDraftPersisted(page, draftArticleA.id, contentA); }); await test.step('Navigate to draft article B and type without saving', async () => { await navigateToArticle(page, draftArticleB.fullyQualifiedName); await page.fill('.om-block-editor', contentB); await page.getByText('Unsaved').waitFor({ state: 'visible' }); - await page.waitForTimeout(400); + await waitForDraftPersisted(page, draftArticleB.id, contentB); }); await test.step('Reload Article B — its own draft should be synced', async () => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterDocument.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterDocument.spec.ts index 7de0d01af4ad..2a99e5fd8eec 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterDocument.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterDocument.spec.ts @@ -20,6 +20,7 @@ import { expectBulkIdsRequest, expectSelectedCount, getDocumentRowByName, + getDocumentSearchInput, getFolderTreeItem, navigateToDocuments, parseResponseJson, @@ -291,7 +292,7 @@ test.describe('Context Center - Documents Page', () => { await navigateToDocuments(page); - const row = getDocumentRowByName(page, fileName); + const row = await searchAndGetDocumentRow(page, fileName); await expect(row).toBeVisible(); await row.scrollIntoViewIfNeeded(); @@ -381,7 +382,9 @@ test.describe('Context Center - Documents Page', () => { await navigateToDocuments(page); await selectFolderInSidebar(page, lastPageFolder.name); await expect( - getDocumentRowByName(page, fileName).getByTestId('document-folder-name') + ( + await searchAndGetDocumentRow(page, fileName) + ).getByTestId('document-folder-name') ).toHaveText(lastPageFolder.name); }); }); @@ -625,7 +628,7 @@ test.describe('Context Center - Documents Page', () => { await navigateToDocuments(page); - const row = getDocumentRowByName(page, fileName); + const row = await searchAndGetDocumentRow(page, fileName); await expect(row).toBeVisible(); await row.scrollIntoViewIfNeeded(); @@ -722,7 +725,7 @@ test.describe('Context Center - Documents Page', () => { await navigateToDocuments(page); - const row = getDocumentRowByName(page, fileName); + const row = await searchAndGetDocumentRow(page, fileName); await expect(row).toBeVisible(); await row.scrollIntoViewIfNeeded(); @@ -774,7 +777,7 @@ test.describe('Context Center - Documents Page', () => { await navigateToDocuments(page); - const row = getDocumentRowByName(page, fileName); + const row = await searchAndGetDocumentRow(page, fileName); await expect(row).toBeVisible(); await row.scrollIntoViewIfNeeded(); @@ -849,7 +852,7 @@ test.describe('Context Center - Documents Page', () => { await navigateToDocuments(page); - const row = getDocumentRowByName(page, fileName); + const row = await searchAndGetDocumentRow(page, fileName); await expect(row).toBeVisible(); await row.scrollIntoViewIfNeeded(); await row.click(); @@ -894,7 +897,7 @@ test.describe('Context Center - Documents Page', () => { await navigateToDocuments(page); - const row = getDocumentRowByName(page, fileName); + const row = await searchAndGetDocumentRow(page, fileName); await expect(row).toBeVisible(); await row.scrollIntoViewIfNeeded(); @@ -983,14 +986,14 @@ test.describe('Context Center - Documents Page', () => { await navigateToDocuments(page); await expect( - getDocumentRowByName(page, firstFileName).getByTestId( - 'document-folder-name' - ) + ( + await searchAndGetDocumentRow(page, firstFileName) + ).getByTestId('document-folder-name') ).toHaveText(folderName); await expect( - getDocumentRowByName(page, secondFileName).getByTestId( - 'document-folder-name' - ) + ( + await searchAndGetDocumentRow(page, secondFileName) + ).getByTestId('document-folder-name') ).toHaveText(folderName); }); @@ -1042,8 +1045,12 @@ test.describe('Context Center - Documents Page', () => { doc2.id, ]); - await expect(getDocumentRowByName(page, firstName)).not.toBeVisible(); - await expect(getDocumentRowByName(page, secondName)).not.toBeVisible(); + await expect( + await searchAndGetDocumentRow(page, firstName) + ).not.toBeVisible(); + await expect( + await searchAndGetDocumentRow(page, secondName) + ).not.toBeVisible(); await page.goto('/context-center/archive'); await page @@ -1082,7 +1089,7 @@ test.describe('Context Center - Documents Page', () => { await navigateToDocuments(page); - const row = getDocumentRowByName(page, fileName); + const row = await searchAndGetDocumentRow(page, fileName); await expect(row).toBeVisible(); await row.scrollIntoViewIfNeeded(); await row.getByTestId('manage-button').click(); @@ -1149,8 +1156,12 @@ test.describe('Context Center - Documents Page', () => { // Before selecting a folder: both documents should be visible and counts // reflect the global total (≥2 files; DocumentsView and DocumentFolderView // show the same number). - await expect(getDocumentRowByName(page, docInFolderName)).toBeVisible(); - await expect(getDocumentRowByName(page, docOutsideName)).toBeVisible(); + await expect( + await searchAndGetDocumentRow(page, docInFolderName) + ).toBeVisible(); + await expect( + await searchAndGetDocumentRow(page, docOutsideName) + ).toBeVisible(); const documentsViewCount = page.getByTestId('documents-view-file-count'); const folderViewCount = page.getByTestId('folder-view-file-count'); @@ -1168,6 +1179,14 @@ test.describe('Context Center - Documents Page', () => { 10 ); expect(folderCount).toBeGreaterThanOrEqual(globalCount); + const browseResPromise = page.waitForResponse( + (res) => + res.url().includes('/api/v1/contextCenter/drive/files') && + !res.url().includes('search') + ); + await getDocumentSearchInput(page).clear(); + await browseResPromise; + await waitForAllLoadersToDisappear(page); // Click the folder — triggers a server-side refetch scoped to that folder. await selectFolderInSidebar(page, folderName); @@ -1186,7 +1205,7 @@ test.describe('Context Center - Documents Page', () => { ); expect(folderCountAfter).toBeGreaterThanOrEqual(globalCount); - const inFolderRow = getDocumentRowByName(page, docInFolderName); + const inFolderRow = await searchAndGetDocumentRow(page, docInFolderName); await inFolderRow.scrollIntoViewIfNeeded(); await inFolderRow.getByTestId('manage-button').click(); await expect(page.getByTestId('move-btn')).toBeVisible(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterMemories.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterMemories.spec.ts index 22b3810f1280..62955b2baadf 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterMemories.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterMemories.spec.ts @@ -75,6 +75,11 @@ test.describe( const { apiContext, afterAction } = await createNewPage(browser); await linkedTable.create(apiContext); + await waitForSearchIndexed( + apiContext, + linkedTable.entityResponseData.fullyQualifiedName, + 'table_search_index' + ); // dataConsumerPage is a per-test fixture and isn't resolved inside // beforeAll, so open a page against the same storage state directly. @@ -1280,7 +1285,9 @@ test.describe( await expect(dialog).toBeVisible(); await dialog.getByTestId('memory-type-select').click(); - await page.getByRole('option', { name: /faq/i }).click(); + const faqOption = page.getByRole('option', { name: /faq/i }); + await faqOption.click(); + await faqOption.waitFor({ state: 'detached' }); const updateResPromise = page.waitForResponse( new RegExp(`${MEMORIES_API}/${editMemoryId}`) @@ -1317,7 +1324,9 @@ test.describe( await editVisibilityBtn.click(); await dialog.getByTestId('memory-visibility-select').click(); - await page.getByRole('option', { name: /private/i }).click(); + const privateOption = page.getByRole('option', { name: /private/i }); + await privateOption.click(); + await privateOption.waitFor({ state: 'detached' }); const updateResPromise = page.waitForResponse( new RegExp(`${MEMORIES_API}/${editMemoryId}`) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceCreationPermissions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceCreationPermissions.spec.ts index 66b5dbc2ecb7..d84452effb53 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceCreationPermissions.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceCreationPermissions.spec.ts @@ -111,11 +111,35 @@ const visitAgentCard = async (page: Page) => { const openPipelineActions = async (page: Page) => { const actionButton = (await visitAgentCard(page)).getByTestId('more-actions'); - await actionButton.waitFor(); - await actionButton.click(); - await page.getByTestId('actions-dropdown').waitFor(); + const actionsDropdown = page.getByTestId('actions-dropdown'); + + // AgentOverflowMenu recomputes its item list from the `permissions` prop on + // every render, but the async per-FQN permission fetch can still be in + // flight when the menu is first opened — some items (edit-gated: redeploy, + // edit, pause/resume) are briefly absent. Close and reopen until the + // permission-gated items are present instead of polling a single stale + // open instance. + await expect + .poll( + async () => { + await actionButton.click(); + await actionsDropdown.waitFor(); + const hasReDeploy = await actionsDropdown + .getByTestId('re-deploy-button') + .isVisible() + .catch(() => false); + if (!hasReDeploy) { + await page.keyboard.press('Escape'); + await actionsDropdown.waitFor({ state: 'hidden' }); + } + + return hasReDeploy; + }, + { intervals: [1_000, 2_000, 3_000], timeout: 30_000 } + ) + .toBe(true); }; test.describe( @@ -645,8 +669,12 @@ test.describe( }) => { await openPipelineActions(page); - await expect(page.getByTestId('edit-button')).toBeVisible(); - await expect(page.getByTestId('re-deploy-button')).toBeVisible(); + const actionsDropdown = page.getByTestId('actions-dropdown'); + + await expect(actionsDropdown.getByTestId('edit-button')).toBeVisible(); + await expect( + actionsDropdown.getByTestId('re-deploy-button') + ).toBeVisible(); await expect( getAgentCard(page, ingestionPipelineName).getByTestId( 'run-agent-button' diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/ContextCenterUtil.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/ContextCenterUtil.ts index f49004991e1e..2b465777e423 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/ContextCenterUtil.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/ContextCenterUtil.ts @@ -1050,6 +1050,39 @@ export const readDraftStore = async ( } }; +/** + * The draft-content debounce (300ms in KnowledgePageDetailComponent) means the + * "Unsaved" badge appearing gives no guarantee the debounced write to + * localStorage has actually landed yet. Polling the real draft store removes + * that race instead of guessing a fixed timeout that can lose the race under + * CI load. + * + * The timeout is deliberately kept well under the real-save debounce + * (SHORT_DELAY, 3000ms): once that autosave completes it clears the draft + * from localStorage (see endTrackedSave -> removeDraft), so a long poll here + * risks racing the autosave itself and observing the draft after it has + * already been removed rather than confirming it landed in time. + */ +export const waitForDraftPersisted = async ( + page: Page, + articleId: string, + expectedDescription: string +) => { + await expect + .poll( + async () => { + const drafts = await readDraftStore(page); + const draft = drafts[articleId] as { description?: string } | undefined; + + // The draft stores the editor's HTML output (e.g. wrapped in

...

), + // not the plain text that was typed, so match on substring containment. + return draft?.description?.includes(expectedDescription) ?? false; + }, + { timeout: 2000, intervals: [50, 100, 200] } + ) + .toBe(true); +}; + /** * A minimal valid 1x1 transparent PNG, used as an in-memory upload fixture * since this repo has no binary image fixtures under playwright/test-data/.