diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts index 258480c1000a..73133c267395 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts @@ -16,24 +16,22 @@ import { EntityTypeEndpoint } from '../../support/entity/Entity.interface'; import { TableClass } from '../../support/entity/TableClass'; import { TagClass } from '../../support/tag/TagClass'; import { - ACTIVITY_TEST_TIMEOUT, addTagToTable, createConversationThread, - createDescriptionActivityEventFromPage, FEED_ITEM_TIMEOUT, getActivityFeedItems, getFeedItemByText, getTableFqn, getTableLeafName, + insertActivityEventForTest, openActivityFeedAndWaitForApi, - patchTableDescription, THUMBS_UP_EMOJI, toggleThumbsUpReaction, visitTableActivityFeed, waitForActivityEvent, } from '../../utils/activityAPI'; import { postActivityComment } from '../../utils/activityFeed'; -import { performAdminLogin } from '../../utils/admin'; +import { createAdminApiContext } from '../../utils/admin'; import { getApiContext, redirectToHomePage, uuid } from '../../utils/common'; import { addOwner, @@ -45,7 +43,7 @@ import { test } from '../fixtures/pages'; // Investigation needed: // 1. Profile actual event propagation time — measure how long it typically takes from a PATCH/PUT call to the event appearing in /api/v1/activity, then tighten the ceiling. // 2. If synchronous flushing is not feasible, restructure tests to assert entity state directly via the entity API, seed a pre-built activity event, and verify only that the UI renders it decoupling UI assertions from event latency. -test.describe.fixme( +test.describe( 'Activity API - Entity Changes', { tag: [DOMAIN_TAGS.DISCOVERY] }, () => { @@ -53,8 +51,8 @@ test.describe.fixme( let entityChangesTag: TagClass; let adminDisplayName: string; - test.beforeAll('Setup: create table and tag', async ({ browser }) => { - const { apiContext, afterAction } = await performAdminLogin(browser); + test.beforeAll('Setup: create table and tag', async () => { + const { apiContext, afterAction } = await createAdminApiContext(); entityChangesTable = new TableClass(); entityChangesTag = new TagClass({}); @@ -79,8 +77,7 @@ test.describe.fixme( test('creates an activity event when the description is updated', async ({ page, }) => { - test.setTimeout(ACTIVITY_TEST_TIMEOUT); - + test.slow(); const newDescription = `Test description updated at ${Date.now()}`; const entityFqn = getTableFqn(entityChangesTable); @@ -96,7 +93,7 @@ test.describe.fixme( ); }); - await test.step('Verify the description event through API and UI', async () => { + await test.step('Verify event, actor, and entity link through API and UI', async () => { const descriptionEvent = await waitForActivityEvent({ entityFqn, eventType: 'DescriptionUpdated', @@ -106,22 +103,27 @@ test.describe.fixme( page, entityFqn ); - const renderedDescriptionEvent = activityResponse.data?.find( + const renderedEvent = activityResponse.data?.find( (event) => event.eventType === 'DescriptionUpdated' && JSON.stringify(event).includes(newDescription) ); const feedItem = await getFeedItemByText(page, newDescription); + const entityLink = feedItem.locator('a[href*="/table/"]').first(); + const href = await entityLink.getAttribute('href'); expect(descriptionEvent).toBeDefined(); - expect(renderedDescriptionEvent).toBeDefined(); + expect(renderedEvent).toBeDefined(); await expect(feedItem).toContainText(/description/i); + await expect(feedItem).toContainText(adminDisplayName); + await expect(entityLink).toBeVisible(); + expect(href).toContain('table'); + expect(href).toContain(getTableLeafName(entityChangesTable)); }); }); test('creates an activity event when tags are added', async ({ page }) => { - test.setTimeout(ACTIVITY_TEST_TIMEOUT); - + test.slow(); const entityFqn = getTableFqn(entityChangesTable); const tagDisplayName = entityChangesTag.getTagDisplayName(); @@ -158,8 +160,7 @@ test.describe.fixme( }); test('creates an activity event when owner is added', async ({ page }) => { - test.setTimeout(ACTIVITY_TEST_TIMEOUT); - + test.slow(); const entityFqn = getTableFqn(entityChangesTable); await test.step('Add the owner from the entity page', async () => { @@ -193,83 +194,36 @@ test.describe.fixme( await expect(feedItem).toBeVisible({ timeout: FEED_ITEM_TIMEOUT }); }); }); - - test('shows the actor who made the activity change', async ({ page }) => { - test.setTimeout(ACTIVITY_TEST_TIMEOUT); - - const entityFqn = getTableFqn(entityChangesTable); - const uniqueDescription = `Actor test description ${Date.now()}`; - - await test.step('Make a table change as the logged-in admin user', async () => { - const { apiContext, afterAction } = await getApiContext(page); - - try { - await patchTableDescription( - apiContext, - entityChangesTable, - uniqueDescription - ); - } finally { - await afterAction(); - } - - await waitForActivityEvent({ - entityFqn, - eventType: 'DescriptionUpdated', - text: uniqueDescription, - }); - }); - - await test.step('Verify the actor is visible in the matching feed item', async () => { - await visitTableActivityFeed(page, entityChangesTable); - - const feedItem = await getFeedItemByText(page, uniqueDescription); - - await expect(feedItem).toContainText(adminDisplayName); - }); - }); - - test('links activity items to the correct entity', async ({ page }) => { - test.setTimeout(ACTIVITY_TEST_TIMEOUT); - - const description = `Entity link description ${uuid()}`; - - await test.step('Create an activity event for the table', async () => { - await createDescriptionActivityEventFromPage( - page, - entityChangesTable, - description - ); - }); - - await test.step('Verify the feed card has the table entity link', async () => { - await visitTableActivityFeed(page, entityChangesTable); - - const feedItem = await getFeedItemByText(page, description); - const entityLink = feedItem.locator('a[href*="/table/"]').first(); - - await expect(entityLink).toBeVisible(); - - const href = await entityLink.getAttribute('href'); - - expect(href).toContain('table'); - expect(href).toContain(getTableLeafName(entityChangesTable)); - }); - }); } ); -test.describe.fixme( +test.describe( 'Activity API - Reactions', { tag: [DOMAIN_TAGS.DISCOVERY] }, () => { - const reactionsTable = new TableClass(); + let reactionsTable: TableClass; + let addReactionFeedText: string; + let removeReactionFeedText: string; + + test.beforeAll('Setup: create table and feed items', async () => { + const { apiContext, afterAction } = await createAdminApiContext(); - test.beforeAll('Setup: create table', async ({ browser }) => { - const { apiContext, afterAction } = await performAdminLogin(browser); + reactionsTable = new TableClass(); + addReactionFeedText = `Test activity for adding reaction ${uuid()}`; + removeReactionFeedText = `Test activity for removing reaction ${uuid()}`; try { await reactionsTable.create(apiContext); + await insertActivityEventForTest( + apiContext, + reactionsTable, + addReactionFeedText + ); + await insertActivityEventForTest( + apiContext, + reactionsTable, + removeReactionFeedText + ); } finally { await afterAction(); } @@ -281,21 +235,12 @@ test.describe.fixme( }); test('adds a reaction to a feed item', async ({ page }) => { - test.setTimeout(ACTIVITY_TEST_TIMEOUT); - - const description = `Test activity for adding reaction ${uuid()}`; - - await test.step('Create and open an activity feed item', async () => { - await createDescriptionActivityEventFromPage( - page, - reactionsTable, - description - ); + await test.step('Open the activity feed', async () => { await visitTableActivityFeed(page, reactionsTable); }); await test.step('Add thumbs-up reaction and verify it is visible', async () => { - const feedItem = await getFeedItemByText(page, description); + const feedItem = await getFeedItemByText(page, addReactionFeedText); await toggleThumbsUpReaction(feedItem, page); await expect( @@ -305,21 +250,12 @@ test.describe.fixme( }); test('removes an existing reaction from a feed item', async ({ page }) => { - test.setTimeout(ACTIVITY_TEST_TIMEOUT); - - const description = `Test activity for removing reaction ${uuid()}`; - - await test.step('Create and open an activity feed item', async () => { - await createDescriptionActivityEventFromPage( - page, - reactionsTable, - description - ); + await test.step('Open the activity feed', async () => { await visitTableActivityFeed(page, reactionsTable); }); await test.step('Add and then remove thumbs-up reaction', async () => { - const feedItem = await getFeedItemByText(page, description); + const feedItem = await getFeedItemByText(page, removeReactionFeedText); await toggleThumbsUpReaction(feedItem, page); await expect( @@ -335,17 +271,33 @@ test.describe.fixme( } ); -test.describe.fixme( +test.describe( 'Activity API - Comments', { tag: [DOMAIN_TAGS.DISCOVERY] }, () => { - const commentsTable = new TableClass(); + let commentsTable: TableClass; + let commentFeedText: string; + let layoutFeedText: string; - test.beforeAll('Setup: create table', async ({ browser }) => { - const { apiContext, afterAction } = await performAdminLogin(browser); + test.beforeAll('Setup: create table and feed items', async () => { + const { apiContext, afterAction } = await createAdminApiContext(); + + commentsTable = new TableClass(); + commentFeedText = `Test activity for comments ${uuid()}`; + layoutFeedText = `Test activity detail layout ${uuid()}`; try { await commentsTable.create(apiContext); + await insertActivityEventForTest( + apiContext, + commentsTable, + commentFeedText + ); + await insertActivityEventForTest( + apiContext, + commentsTable, + layoutFeedText + ); } finally { await afterAction(); } @@ -357,22 +309,14 @@ test.describe.fixme( }); test('adds a comment to a feed item', async ({ page }) => { - test.setTimeout(ACTIVITY_TEST_TIMEOUT); - - const description = `Test activity for comments ${uuid()}`; const commentText = `Test comment ${uuid()}`; - await test.step('Create and open an activity feed item', async () => { - await createDescriptionActivityEventFromPage( - page, - commentsTable, - description - ); + await test.step('Open the activity feed', async () => { await visitTableActivityFeed(page, commentsTable); }); await test.step('Open the feed detail and post a comment', async () => { - const feedItem = await getFeedItemByText(page, description); + const feedItem = await getFeedItemByText(page, commentFeedText); await feedItem.click(); await waitForAllLoadersToDisappear(page); @@ -381,21 +325,12 @@ test.describe.fixme( }); test('shows the activity detail layout', async ({ page }) => { - test.setTimeout(ACTIVITY_TEST_TIMEOUT); - - const description = `Test activity detail layout ${uuid()}`; - - await test.step('Create and open an activity feed item', async () => { - await createDescriptionActivityEventFromPage( - page, - commentsTable, - description - ); + await test.step('Open the activity feed', async () => { await visitTableActivityFeed(page, commentsTable); }); await test.step('Open the detail view and verify layout regions', async () => { - const feedItem = await getFeedItemByText(page, description); + const feedItem = await getFeedItemByText(page, layoutFeedText); await feedItem.click(); await waitForAllLoadersToDisappear(page); @@ -411,14 +346,16 @@ test.describe.fixme( } ); -test.describe.fixme( +test.describe( 'Activity API - Homepage Widget', { tag: [DOMAIN_TAGS.DISCOVERY] }, () => { - const homepageTable = new TableClass(); + let homepageTable: TableClass; + + test.beforeAll('Setup: create table and activity', async () => { + const { apiContext, afterAction } = await createAdminApiContext(); - test.beforeAll('Setup: create table and activity', async ({ browser }) => { - const { apiContext, afterAction } = await performAdminLogin(browser); + homepageTable = new TableClass(); try { await homepageTable.create(apiContext); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/activityAPI.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/activityAPI.ts index 62087ad1adf3..95b35d040c00 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/activityAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/activityAPI.ts @@ -14,10 +14,10 @@ import { APIRequestContext, expect, Locator, Page } from '@playwright/test'; import { TableClass } from '../support/entity/TableClass'; import { TagClass } from '../support/tag/TagClass'; import { createAdminApiContext } from './admin'; -import { getApiContext } from './common'; +import { fullUuid, getApiContext } from './common'; import { waitForAllLoadersToDisappear } from './entity'; -export const ACTIVITY_EVENT_TIMEOUT = 300_000; +export const ACTIVITY_EVENT_TIMEOUT = 200_000; export const ACTIVITY_TEST_TIMEOUT = ACTIVITY_EVENT_TIMEOUT + 60_000; export const ACTIVITY_FEED_RESPONSE_TIMEOUT = 15_000; export const FEED_ITEM_TIMEOUT = 30_000; @@ -116,32 +116,51 @@ export const waitForActivityEvent = async ({ )}?days=30&limit=50`; let events: ActivityApiEvent[] = []; - try { - await expect - .poll( - async () => { - const response = await apiContext.get(activityUrl); + let lastStatus = 0; + let lastEventTypes: string[] = []; - if (!response.ok()) { - return false; + try { + try { + await expect + .poll( + async () => { + const response = await apiContext.get(activityUrl); + lastStatus = response.status(); + + if (!response.ok()) { + const body = await response.text().catch(() => ''); + throw new Error( + `Activity API returned ${lastStatus} for ${entityFqn}: ${body}` + ); + } + + const body = (await response.json()) as ActivityApiResponse; + events = body.data ?? []; + lastEventTypes = events.map((e) => e.eventType ?? 'unknown'); + + return events.some( + (event) => + event.eventType === eventType && + (text === undefined || JSON.stringify(event).includes(text)) + ); + }, + { + timeout: ACTIVITY_EVENT_TIMEOUT, + intervals: [5_000, 10_000], + message: `Waiting for ${eventType} event for ${entityFqn}`, } - - const body = (await response.json()) as ActivityApiResponse; - events = body.data ?? []; - - return events.some( - (event) => - event.eventType === eventType && - (text === undefined || JSON.stringify(event).includes(text)) - ); - }, - { - timeout: ACTIVITY_EVENT_TIMEOUT, - intervals: [1_000, 2_000, 5_000, 10_000], - message: `Timed out waiting for ${eventType} event for ${entityFqn}`, - } - ) - .toBe(true); + ) + .toBe(true); + } catch (err) { + if (lastStatus !== 0) { + throw new Error( + `Timed out waiting for ${eventType} event for ${entityFqn}. Last status: ${lastStatus}, events found: [${lastEventTypes.join( + ', ' + )}]` + ); + } + throw err; + } return events.find( (event) => @@ -280,6 +299,43 @@ export const createDescriptionActivityEventFromPage = async ( }); }; +/** + * Inserts an activity event directly into the activity stream via the test-only endpoint, + * bypassing the async change-event pipeline. Use this in test setup when you need a feed + * item to exist but are not testing the pipeline itself. + */ +export const insertActivityEventForTest = async ( + apiContext: APIRequestContext, + table: TableClass, + text: string +) => { + const userResponse = await apiContext.get('/api/v1/users/loggedInUser'); + const adminUser = await userResponse.json(); + const tableData = table.entityResponseData; + + const fqn = tableData.fullyQualifiedName ?? ''; + + const response = await apiContext.post('/api/v1/activity/test-insert', { + data: { + id: fullUuid(), + eventType: 'DescriptionUpdated', + about: `<#E::table::${fqn}>`, + entity: { + id: tableData.id, + type: 'table', + name: tableData.name, + fullyQualifiedName: fqn, + }, + actor: { id: adminUser.id, type: 'user' }, + timestamp: Date.now(), + summary: text, + newValue: text, + }, + }); + + expect(response.ok()).toBeTruthy(); +}; + export const addTagToTable = async ( apiContext: APIRequestContext, table: TableClass,