From cfef0d1c80036b3e10c49c1834edcc1e9b4698b4 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Tue, 9 Sep 2025 12:05:20 +0530 Subject: [PATCH] Fix flaky activity feed tests (#23298) --- .../e2e/Features/ActivityFeed.spec.ts | 303 +++++++++--------- 1 file changed, 152 insertions(+), 151 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts index c5b28fe8490..053fbda355d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts @@ -16,9 +16,11 @@ import { PersonaClass } from '../../support/persona/PersonaClass'; import { UserClass } from '../../support/user/UserClass'; import { REACTION_EMOJIS, reactOnFeed } from '../../utils/activityFeed'; import { performAdminLogin } from '../../utils/admin'; -import { redirectToHomePage, removeLandingBanner } from '../../utils/common'; -import { navigateToCustomizeLandingPage } from '../../utils/customizeLandingPage'; -import { selectPersona } from '../../utils/customizeNavigation'; +import { redirectToHomePage } from '../../utils/common'; +import { + navigateToCustomizeLandingPage, + setUserDefaultPersona, +} from '../../utils/customizeLandingPage'; const adminUser = new UserClass(); const user1 = new UserClass(); @@ -28,68 +30,64 @@ const testPersona = new PersonaClass(); test.describe('FeedWidget on landing page', () => { test.beforeAll( - 'setup: seed entities, users, create persona, customize widget, and create feed activity', + 'setup: seed entities, users, create persona, and customize widget', async ({ browser }) => { + test.slow(true); + + const { apiContext, afterAction } = await performAdminLogin(browser); + try { - const { apiContext, afterAction } = await performAdminLogin(browser); - try { - // Create admin and a standard user - await adminUser.create(apiContext); - await adminUser.setAdminRole(apiContext); - await user1.create(apiContext); + // Create users and entities + await adminUser.create(apiContext); + await adminUser.setAdminRole(apiContext); + await user1.create(apiContext); + await seedEntity.create(apiContext); + await extraEntity.create(apiContext); + await testPersona.create(apiContext, [adminUser.responseData.id]); - // Create two entities to ensure feed diversity - await seedEntity.create(apiContext); - await extraEntity.create(apiContext); - - // Create a persona for testing - await testPersona.create(apiContext, [adminUser.responseData.id]); - } finally { - await afterAction(); - } - - // Log in as admin and customize the landing page for the persona + // Set up widget in a separate page context const adminPage = await browser.newPage(); await adminUser.login(adminPage); + try { - // Navigate to customize landing page for the persona + // Set persona as default + await redirectToHomePage(adminPage); + await setUserDefaultPersona(adminPage, testPersona.data.displayName); + + // Navigate to customize landing page await navigateToCustomizeLandingPage(adminPage, { personaName: testPersona.data.name, }); - // Find the Activity Feed widget and make it full size - const activityFeedWidget = adminPage.locator( - '[data-testid="KnowledgePanel.ActivityFeed"]' + // Ensure Activity Feed widget is full size + const activityFeedWidget = adminPage.getByTestId( + 'KnowledgePanel.ActivityFeed' ); - // Click the more options button (three dots menu) - const moreOptionsButton = activityFeedWidget.locator( - '[data-testid="more-options-button"]' + await expect(activityFeedWidget).toBeVisible(); + + const moreOptionsButton = activityFeedWidget.getByTestId( + 'more-options-button' ); - - await expect(moreOptionsButton).toBeVisible(); - await moreOptionsButton.click(); - - // Click "Full Size" option from the dropdown menu await adminPage.getByRole('menuitem', { name: 'Full Size' }).click(); - // Save the layout - await adminPage.locator('[data-testid="save-button"]').click(); - await adminPage.waitForLoadState('networkidle'); + // Save the layout if save button is enabled + const saveButton = adminPage.getByTestId('save-button'); + if (await saveButton.isEnabled()) { + const saveResponse = adminPage.waitForResponse('/api/v1/docStore*'); + await saveButton.click(); + await adminPage.waitForLoadState('networkidle'); + await saveResponse; + } - // Navigate back to home page await redirectToHomePage(adminPage); - - // Select the persona for the current user - await selectPersona(adminPage, testPersona); - } catch (e) { - // ignore failures here; tests have guards + await adminPage.waitForLoadState('networkidle'); } finally { await adminPage.close(); } - } catch (e) { - // proceed even if setup fails; tests handle empty state + } finally { + await afterAction(); } } ); @@ -97,19 +95,16 @@ test.describe('FeedWidget on landing page', () => { test.afterAll( 'cleanup: delete entities, users, and persona', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + try { - const { apiContext, afterAction } = await performAdminLogin(browser); - try { - await seedEntity.delete(apiContext); - await extraEntity.delete(apiContext); - await user1.delete(apiContext); - await adminUser.delete(apiContext); - await testPersona.delete(apiContext); - } finally { - await afterAction(); - } - } catch (e) { - // ignore cleanup errors + await seedEntity.delete(apiContext); + await extraEntity.delete(apiContext); + await user1.delete(apiContext); + await testPersona.delete(apiContext); + await adminUser.delete(apiContext); + } finally { + await afterAction(); } } ); @@ -117,81 +112,72 @@ test.describe('FeedWidget on landing page', () => { test.beforeEach(async ({ page }) => { await adminUser.login(page); await redirectToHomePage(page); - await removeLandingBanner(page); + await page.waitForLoadState('networkidle'); }); test('renders widget wrapper and header with sort dropdown', async ({ page, }) => { - const widget = page.locator('[data-testid="KnowledgePanel.ActivityFeed"]'); + const widget = page.getByTestId('KnowledgePanel.ActivityFeed'); await expect(widget).toBeVisible(); - // Header title and icon - const header = widget.locator('[data-testid="widget-header"]'); + // Header verification + const header = widget.getByTestId('widget-header'); await expect(header).toBeVisible(); await expect(header).toContainText('Activity Feed'); - // Sort dropdown should be visible (non-edit view) - const sortDropdown = header.locator( - '[data-testid="widget-sort-by-dropdown"]' - ); + // Sort dropdown verification + const sortDropdown = header.getByTestId('widget-sort-by-dropdown'); await expect(sortDropdown).toBeVisible(); - // Open dropdown and verify options + // Test dropdown options await sortDropdown.click(); + await page.waitForSelector('.ant-dropdown', { state: 'visible' }); await expect( - page.getByRole('menuitem', { - name: 'All Activity', - }) + page.getByRole('menuitem', { name: 'All Activity' }) ).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'My Data' })).toBeVisible(); await expect( - page.getByRole('menuitem', { - name: 'My Data', - }) - ).toBeVisible(); - await expect( - page.getByRole('menuitem', { - name: 'Following', - }) + page.getByRole('menuitem', { name: 'Following' }) ).toBeVisible(); - // Close dropdown - await page.keyboard.press('Escape'); + // Close dropdown by clicking outside + await widget.click(); + + await expect(page.locator('.ant-dropdown')).not.toBeVisible(); }); - test('clicking title navigates to Explore', async ({ page }) => { - const widget = page.locator('[data-testid="KnowledgePanel.ActivityFeed"]'); + test('clicking title navigates to explore page', async ({ page }) => { + const widget = page.getByTestId('KnowledgePanel.ActivityFeed'); await expect(widget).toBeVisible(); - // Click the header title to navigate to Explore - await widget - .locator('[data-testid="widget-header"]') - .getByText('Activity Feed') - .click(); + // Click the title to navigate + const titleLink = widget + .getByTestId('widget-header') + .getByText('Activity Feed'); + await titleLink.click(); await page.waitForLoadState('networkidle'); - await expect(page).toHaveURL(/\/explore/); - - // Navigate back home to keep context consistent for next tests - await redirectToHomePage(page); + // Verify navigation to explore + await expect(page.url()).toContain('/explore'); }); - test('feed body renders list or empty state', async ({ page }) => { - const widget = page.locator('[data-testid="KnowledgePanel.ActivityFeed"]'); + test('feed body renders content or empty state', async ({ page }) => { + const widget = page.getByTestId('KnowledgePanel.ActivityFeed'); await expect(widget).toBeVisible(); - // Feed container + // Wait for feed content to load const container = page.locator('#feedWidgetData'); await expect(container).toBeVisible(); - // Either render feed messages or show the widget-level empty state + // Check for either content or empty state const messageContainers = container.locator( '[data-testid="message-container"]' ); @@ -206,90 +192,87 @@ test.describe('FeedWidget on landing page', () => { }); test('changing filter triggers feed reload', async ({ page }) => { - const widget = page.locator('[data-testid="KnowledgePanel.ActivityFeed"]'); + const widget = page.getByTestId('KnowledgePanel.ActivityFeed'); await expect(widget).toBeVisible(); - const sortDropdown = widget.locator( - '[data-testid="widget-sort-by-dropdown"]' - ); + const sortDropdown = widget.getByTestId('widget-sort-by-dropdown'); await expect(sortDropdown).toBeVisible(); - // Switch to My Data and wait for a feed API call + // Switch to My Data filter await sortDropdown.click(); - const myData = page.getByRole('menuitem', { - name: 'My Data', - }); - if ((await myData.count()) > 0) { - const feedReq = page.waitForResponse(/\/api\/v1\/feed.*/); - await myData.click(); - await feedReq; - } + await page.waitForSelector('.ant-dropdown', { state: 'visible' }); + + const myDataOption = page.getByRole('menuitem', { name: 'My Data' }); + + const feedResponse = page.waitForResponse('/api/v1/feed*'); + await myDataOption.click(); + await page.waitForLoadState('networkidle'); + await feedResponse; // Switch back to All Activity await sortDropdown.click(); - const allActivity = page.getByRole('button', { + await page.waitForSelector('.ant-dropdown', { state: 'visible' }); + + const allActivityOption = page.getByRole('menuitem', { name: 'All Activity', }); - if ((await allActivity.count()) > 0) { - const feedReq = page.waitForResponse(/\/api\/v1\/feed.*/); - await allActivity.click(); - await feedReq; + if (await allActivityOption.isVisible()) { + const feedResponse = page.waitForResponse('/api/v1/feed*'); + await allActivityOption.click(); + await page.waitForLoadState('networkidle'); + await feedResponse; } }); - test('footer shows View More when applicable and navigates', async ({ - page, - }) => { - const widget = page.locator('[data-testid="KnowledgePanel.ActivityFeed"]'); + test('footer shows view more link when applicable', async ({ page }) => { + const widget = page.getByTestId('KnowledgePanel.ActivityFeed'); await expect(widget).toBeVisible(); - // Footer only renders when showMoreButton is true - const viewMore = widget.getByRole('link', { name: /View More/i }); - if ((await viewMore.count()) > 0) { - await expect(viewMore).toBeVisible(); + // Check if View More link exists + const viewMoreLink = widget.getByRole('link', { name: /View More/i }); - await viewMore.click(); - await page.waitForLoadState('networkidle'); + await expect(viewMoreLink).toBeVisible(); - // We should land on user Activity Feed. We just verify navigation happened - await expect(page).not.toHaveURL(/home|welcome/i); + // Click and verify navigation + await viewMoreLink.click(); + await page.waitForLoadState('networkidle'); - // Return home for subsequent tests - await redirectToHomePage(page); - } + // Should navigate away from home page + expect(page.url()).not.toMatch(/home|welcome/i); }); - test('renders feed cards via ActivityFeedListV1New in widget mode', async ({ + test('feed cards render with proper structure when available', async ({ page, }) => { const container = page.locator('#feedWidgetData'); await expect(container).toBeVisible(); - const firstCard = container - .locator('[data-testid="message-container"]') - .first(); + const messageContainers = container.locator( + '[data-testid="message-container"]' + ); - if ((await firstCard.count()) > 0) { - await expect(firstCard).toBeVisible(); + const firstCard = messageContainers.first(); - // Typical elements within a compact feed card rendered in widget mode - const headerText = firstCard.locator('[data-testid="headerText"]'); - const timestamp = firstCard.locator('[data-testid="timestamp"]'); + await expect(firstCard).toBeVisible(); - if ((await headerText.count()) > 0) { - await expect(headerText).toBeVisible(); - } - if ((await timestamp.count()) > 0) { - await expect(timestamp).toBeVisible(); - } + // Verify typical feed card elements + const headerText = firstCard.locator('[data-testid="headerText"]'); + const timestamp = firstCard.locator('[data-testid="timestamp"]'); + + // Check elements exist if available + if ((await headerText.count()) > 0) { + await expect(headerText).toBeVisible(); + } + if ((await timestamp.count()) > 0) { + await expect(timestamp).toBeVisible(); } }); - test('emoji reactions can be added and removed in widget feed cards', async ({ + test('emoji reactions can be added when feed messages exist', async ({ page, }) => { const messages = page.locator('[data-testid="message-container"]'); @@ -327,6 +310,8 @@ test.describe('FeedWidget on landing page', () => { page, }) => { const messages = page.locator('[data-testid="message-container"]'); + + // Skip if no messages available if ((await messages.count()) === 0) { return; } @@ -335,31 +320,44 @@ test.describe('FeedWidget on landing page', () => { await expect(firstMessage).toBeVisible(); - // Open thread/drawer via reply count or clicking the card + // Open thread drawer via reply count or clicking the card const replyCountBtn = firstMessage.locator('[data-testid="reply-count"]'); - if (await replyCountBtn.count()) { + + if ((await replyCountBtn.count()) > 0) { await replyCountBtn.click(); } else { await firstMessage.click(); } + // Wait for drawer to appear const drawer = page.locator('.ant-drawer-content'); await expect(drawer).toBeVisible(); - // Type a quick reply if editor is present + // Try to post a reply if comment input is available const commentInput = drawer.locator('[data-testid="comments-input-field"]'); + if (await commentInput.count()) { await commentInput.click(); - await page.fill( - '[data-testid="editor-wrapper"] .ql-editor', - 'Widget thread automated reply' - ); + await page.waitForLoadState('networkidle'); - const sendReply = page.waitForResponse(/\/api\/v1\/feed\/.*\/posts/); - await page.getByTestId('send-button').click({ force: true }); + // Fill in the editor + const editorField = page.locator( + '[data-testid="editor-wrapper"] .ql-editor' + ); + await editorField.fill('Widget thread automated reply'); + + // Wait for send button to be enabled and send reply + const sendButton = page.getByTestId('send-button'); + + await expect(sendButton).toBeEnabled(); + + const sendReply = page.waitForResponse('/api/v1/feed/*/posts'); + await page.waitForLoadState('networkidle'); + await sendButton.click(); await sendReply; + // Verify reply appears await expect( drawer.locator('[data-testid="feed-replies"]') ).toContainText('Widget thread automated reply'); @@ -372,5 +370,8 @@ test.describe('FeedWidget on landing page', () => { } else { await page.keyboard.press('Escape'); } + + // Verify drawer is closed + await expect(drawer).not.toBeVisible(); }); });