From 13d5cd1d95aec1e6658d59a056ce866b468c7957 Mon Sep 17 00:00:00 2001 From: karanh37 Date: Fri, 21 Aug 2026 11:29:57 +0530 Subject: [PATCH 1/6] test(ui): shorten welcome-banner wait in removeLandingBanner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Playwright removeLandingBanner helper waited up to 5000ms for the welcome-screen close button. For storageState sessions the banner never renders (auth.setup records the user in the `loggedInUsers` localStorage key that gates it), so that call always ran out the full timeout — 5s of dead wait per invocation across many specs. Only specs that log in a freshly created user on a new context actually show the banner, and it paints shortly after currentUser resolves, so a bounded wait is still needed. Reduce the timeout from 5000ms to 2000ms, which comfortably covers the fresh-login case while cutting the wasted wait everywhere else. No test asserts on the banner. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/resources/ui/playwright/utils/common.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 dc152265ed00..6429d7eadc46 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -131,17 +131,23 @@ export const redirectToExplorePage = async (page: Page) => { export const removeLandingBanner = async (page: Page) => { try { const welcomePageCloseButton = page.getByTestId('welcome-screen-close-btn'); + + // storageState sessions never show the banner (auth.setup records the user + // in the `loggedInUsers` localStorage key that gates it), but specs that log + // in a freshly created user on a new context do render it on first /my-data, + // and it paints a tick after currentUser resolves — so wait briefly for it + // rather than racing an instant check. Absence is the common case, so keep + // the timeout short. await welcomePageCloseButton .waitFor({ state: 'visible', - timeout: 5000, + timeout: 2000, }) .catch(() => { // Do nothing if the welcome banner does not exist return; }); - // Close the welcome banner if it exists if (await welcomePageCloseButton.isVisible()) { await welcomePageCloseButton.click(); } From e9c4615774275eca13c422e587cf29285e0316ce Mon Sep 17 00:00:00 2001 From: karanh37 Date: Fri, 21 Aug 2026 11:44:06 +0530 Subject: [PATCH 2/6] test(ui): suppress welcome banner at source, drop removeLandingBanner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the timeout reduction: eliminate the welcome-banner dismiss dance entirely instead of waiting on it. The landing page renders the welcome banner only when the logged-in user's `name` is absent from the `loggedInUsers` localStorage key (MyDataPage.component.tsx). storageState sessions already have it seeded (auth.setup logs each user in, which records them), which is why the old removeLandingBanner waitFor always ran out its full timeout — it waited on an element that never appears. Only specs that log in a freshly created UserClass on a new context actually rendered the banner. Seed `loggedInUsers` in UserClass.login() via addInitScript, before the first navigation (mirroring disableEtagConditionalReads). The seeded name equals the app's currentUser.name — responseData.name for a created user, the email local-part for a pure login such as admin — so the banner never renders for any session. With the banner suppressed at the source, the reactive dismissals are dead code and are removed: - delete removeLandingBanner (common.ts) and all ~27 call sites - delete closeWelcomeScreenIfVisible and its inline blocks (searchRBAC.ts) - delete the inline dismiss block in entity.ts Tour.spec.ts keeps its own conditional guards: that suite drives the welcome/tour flow directly and the guards are harmless no-ops now. No test asserts on the welcome banner. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../e2e/Features/ActivityFeed.spec.ts | 4 -- .../e2e/Features/CuratedAssets.spec.ts | 8 +-- .../DomainDataProductsWidgets.spec.ts | 5 +- .../e2e/Flow/CustomizeLandingPage.spec.ts | 7 +-- .../e2e/Flow/CustomizeWidgets.spec.ts | 7 +-- .../ui/playwright/support/user/UserClass.ts | 7 +++ .../ui/playwright/utils/activityFeed.ts | 3 +- .../resources/ui/playwright/utils/common.ts | 61 ++++++++++--------- .../playwright/utils/customizeLandingPage.ts | 6 -- .../resources/ui/playwright/utils/entity.ts | 12 ---- .../ui/playwright/utils/searchRBAC.ts | 30 --------- 11 files changed, 44 insertions(+), 106 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 ec4f15752488..e73437509887 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 @@ -30,7 +30,6 @@ import { REACTION_EMOJIS, reactOnFeedCard } from '../../utils/activityFeed'; import { performAdminLogin } from '../../utils/admin'; import { redirectToHomePage, - removeLandingBanner, uuid, visitOwnProfilePage, } from '../../utils/common'; @@ -168,7 +167,6 @@ test.describe('FeedWidget on landing page', () => { try { // Set persona as default await redirectToHomePage(adminPage); - await removeLandingBanner(adminPage); await waitForAllLoadersToDisappear(adminPage); await setUserDefaultPersona(adminPage, testPersona.data.displayName); @@ -199,7 +197,6 @@ test.describe('FeedWidget on landing page', () => { } await redirectToHomePage(adminPage); - await removeLandingBanner(adminPage); await waitForAllLoadersToDisappear(adminPage); } finally { await adminPage.close(); @@ -224,7 +221,6 @@ test.describe('FeedWidget on landing page', () => { test.beforeEach(async ({ page }) => { await adminUser.login(page); await redirectToHomePage(page); - await removeLandingBanner(page); await waitForAllLoadersToDisappear(page); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CuratedAssets.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CuratedAssets.spec.ts index eb460b91acbe..b03d3f2d2822 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CuratedAssets.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CuratedAssets.spec.ts @@ -16,7 +16,7 @@ import { PersonaClass } from '../../support/persona/PersonaClass'; import { UserClass } from '../../support/user/UserClass'; import { performAdminLogin } from '../../utils/admin'; import { selectOption } from '../../utils/advancedSearch'; -import { redirectToHomePage, removeLandingBanner } from '../../utils/common'; +import { redirectToHomePage } from '../../utils/common'; import { addCuratedAssetPlaceholder, CURATED_ASSETS_WIDGET_KEY, @@ -101,7 +101,6 @@ test.describe('Curated Assets Widget', () => { await setUserDefaultPersona(page, persona.responseData.displayName); await redirectToHomePage(page); - await removeLandingBanner(page); await page.getByTestId('sidebar-toggle').click(); }); @@ -188,7 +187,6 @@ test.describe('Curated Assets Widget', () => { ).toBeVisible(); await redirectToHomePage(page); - await removeLandingBanner(page); await waitForAllLoadersToDisappear(page, 'entity-list-skeleton'); @@ -387,7 +385,6 @@ test.describe('Curated Assets Widget', () => { // Wait for auto-save to complete before navigating await redirectToHomePage(page); - await removeLandingBanner(page); await waitForAllLoadersToDisappear(page, 'entity-list-skeleton'); @@ -503,7 +500,6 @@ test.describe('Curated Assets Widget', () => { // Navigate to landing page to verify widget await redirectToHomePage(page); - await removeLandingBanner(page); await waitForAllLoadersToDisappear(page, 'entity-list-skeleton'); @@ -646,7 +642,6 @@ test.describe('Curated Assets Widget', () => { // Navigate to landing page to verify widget await redirectToHomePage(page); - await removeLandingBanner(page); await waitForAllLoadersToDisappear(page, 'entity-list-skeleton'); @@ -685,7 +680,6 @@ test.describe('Curated Assets Widget', () => { await page.locator('[data-testid="save-button"]').click(); await redirectToHomePage(page); - await removeLandingBanner(page); // Verify placeholder is not visible when no widget is configured await expect( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/LandingPageWidgets/DomainDataProductsWidgets.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/LandingPageWidgets/DomainDataProductsWidgets.spec.ts index 1e0f0064fbc2..b194f55c9402 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/LandingPageWidgets/DomainDataProductsWidgets.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/LandingPageWidgets/DomainDataProductsWidgets.spec.ts @@ -20,7 +20,7 @@ import { TopicClass } from '../../../support/entity/TopicClass'; import { PersonaClass } from '../../../support/persona/PersonaClass'; import { UserClass } from '../../../support/user/UserClass'; import { performAdminLogin } from '../../../utils/admin'; -import { redirectToHomePage, removeLandingBanner } from '../../../utils/common'; +import { redirectToHomePage } from '../../../utils/common'; import { addAndVerifyWidget, setUserDefaultPersona, @@ -95,7 +95,6 @@ test.describe.serial('Domain and Data Product Asset Counts', () => { test.slow(); // Slow Test test.beforeEach(async ({ page }, testInfo) => { await redirectToHomePage(page, false); - await removeLandingBanner(page); await waitForAllLoadersToDisappear(page).catch(() => undefined); if (testInfo.title !== 'Assign Widgets') { @@ -112,7 +111,6 @@ test.describe.serial('Domain and Data Product Asset Counts', () => { dataProduct.responseData.id ?? '' ); await redirectToHomePage(page, false); - await removeLandingBanner(page); await waitForAllLoadersToDisappear(page).catch(() => undefined); } }); @@ -134,7 +132,6 @@ test.describe.serial('Domain and Data Product Asset Counts', () => { test('Verify Widgets are having 0 count initially', async ({ page }) => { await redirectToHomePage(page, false); - await removeLandingBanner(page); await waitForAllLoadersToDisappear(page).catch(() => undefined); await verifyWidgetCountOnCurrentPage( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/CustomizeLandingPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/CustomizeLandingPage.spec.ts index d633b89ec41b..660665af5581 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/CustomizeLandingPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/CustomizeLandingPage.spec.ts @@ -15,11 +15,7 @@ import { PLAYWRIGHT_BASIC_TEST_TAG_OBJ } from '../../constant/config'; import { PersonaClass } from '../../support/persona/PersonaClass'; import { UserClass } from '../../support/user/UserClass'; import { performAdminLogin } from '../../utils/admin'; -import { - redirectToHomePage, - removeLandingBanner, - toastNotification, -} from '../../utils/common'; +import { redirectToHomePage, toastNotification } from '../../utils/common'; import { checkAllDefaultWidgets, navigateToCustomizeLandingPage, @@ -339,7 +335,6 @@ test.describe( await saveCustomizeLayoutPage(adminPage); await redirectToHomePage(adminPage, false); - await removeLandingBanner(adminPage); await waitForAllLoadersToDisappear(adminPage).catch(() => undefined); await waitForLandingPageWidget(adminPage, 'KnowledgePanel.MyData'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/CustomizeWidgets.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/CustomizeWidgets.spec.ts index a89ae28378c9..55b1e028c530 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/CustomizeWidgets.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/CustomizeWidgets.spec.ts @@ -22,11 +22,7 @@ import { PersonaClass } from '../../support/persona/PersonaClass'; import { UserClass } from '../../support/user/UserClass'; import { insertActivityEventForTest } from '../../utils/activityAPI'; import { performAdminLogin } from '../../utils/admin'; -import { - getApiContext, - redirectToHomePage, - removeLandingBanner, -} from '../../utils/common'; +import { getApiContext, redirectToHomePage } from '../../utils/common'; import { addAndVerifyWidget, removeAndVerifyWidget, @@ -238,7 +234,6 @@ test.afterAll( test.beforeEach(async ({ page }) => { await redirectToHomePage(page); - await removeLandingBanner(page); await waitForAllLoadersToDisappear(page); await waitForAllLoadersToDisappear(page, 'entity-list-skeleton'); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts index f999c23dad78..0f187bc026a5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts @@ -21,6 +21,7 @@ import { okJson, withNotFoundRetry } from '../../utils/apiResponse'; import { disableEtagConditionalReads, generateRandomUsername, + suppressWelcomeScreen, uuid, } from '../../utils/common'; import { PolicyClass, PolicyRulesType } from '../access-control/PoliciesClass'; @@ -259,6 +260,12 @@ export class UserClass { userName = this.data.email, password = this.data.password ) { + // Seed `loggedInUsers` before the first navigation so the landing-page + // welcome banner never renders for this session. Prefer the authoritative + // entity name from create(); fall back to the login email's local-part + // (the server-assigned username) for a pure login such as admin. + await suppressWelcomeScreen(page, this.responseData?.name ?? userName); + await page.goto('/signin'); try { await page.waitForURL('**/signin', { timeout: 5000 }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/activityFeed.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/activityFeed.ts index 0501f0210e4f..1f5c9498792a 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/activityFeed.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/activityFeed.ts @@ -11,7 +11,7 @@ * limitations under the License. */ import { expect, Locator, Page } from '@playwright/test'; -import { descriptionBox, removeLandingBanner } from './common'; +import { descriptionBox } from './common'; import { waitForAllLoadersToDisappear } from './entity'; import { waitForPageLoaded } from './polling'; import { TaskDetails } from './task'; @@ -149,7 +149,6 @@ export const addMentionCommentInFeed = async ( const fetchFeedResponse = page.waitForResponse( '/api/v1/feed?type=Conversation*' ); - await removeLandingBanner(page); await fetchFeedResponse; } 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 6429d7eadc46..9bccfc5df978 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -105,6 +105,38 @@ export const disableEtagConditionalReads = async (page: Page) => { } }; +const LOGGED_IN_USERS_KEY = 'loggedInUsers'; + +/** + * Suppress the landing-page welcome banner at the source. + * + * MyDataPage renders the welcome banner only when the logged-in user's `name` + * is absent from the `loggedInUsers` localStorage list (see + * MyDataPage.component.tsx). Seeding that list with the user's name before the + * first navigation means the banner never renders for the session, so no test + * has to dismiss it. `userName` must equal the app's `currentUser.name` — for a + * created UserClass that is `responseData.name`; the email local-part is the + * server-assigned fallback for a pure login (e.g. admin). + */ +export const suppressWelcomeScreen = async (page: Page, userName: string) => { + const name = userName.includes('@') ? userName.split('@')[0] : userName; + const seed = ({ key, value }: { key: string; value: string }) => { + const existing = (localStorage.getItem(key) ?? '') + .split(',') + .filter(Boolean); + if (!existing.includes(value)) { + localStorage.setItem(key, [...existing, value].join(',')); + } + }; + const arg = { key: LOGGED_IN_USERS_KEY, value: name }; + + await page.addInitScript(seed, arg); + + if (/^https?:/.test(page.url())) { + await page.evaluate(seed, arg); + } +}; + export const redirectToHomePage = async ( page: Page, _waitForLoaders = true @@ -128,35 +160,6 @@ export const redirectToExplorePage = async (page: Page) => { await waitForAllLoadersToDisappear(page); }; -export const removeLandingBanner = async (page: Page) => { - try { - const welcomePageCloseButton = page.getByTestId('welcome-screen-close-btn'); - - // storageState sessions never show the banner (auth.setup records the user - // in the `loggedInUsers` localStorage key that gates it), but specs that log - // in a freshly created user on a new context do render it on first /my-data, - // and it paints a tick after currentUser resolves — so wait briefly for it - // rather than racing an instant check. Absence is the common case, so keep - // the timeout short. - await welcomePageCloseButton - .waitFor({ - state: 'visible', - timeout: 2000, - }) - .catch(() => { - // Do nothing if the welcome banner does not exist - return; - }); - - if (await welcomePageCloseButton.isVisible()) { - await welcomePageCloseButton.click(); - } - } catch { - // Do nothing if the welcome banner does not exist - return; - } -}; - type CreateNewPageResult = { afterAction: () => Promise; apiContext: APIRequestContext; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts index 0db748a1c445..0e006e408b49 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts @@ -13,7 +13,6 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { redirectToHomePage, - removeLandingBanner, toastNotification, visitOwnProfilePage, } from './common'; @@ -287,7 +286,6 @@ export const toNameableEntity = ( }; export const checkAllDefaultWidgets = async (page: Page) => { - await removeLandingBanner(page); await waitForAllLoadersToDisappear(page); await waitForAllLoadersToDisappear(page, 'entity-list-skeleton'); @@ -417,7 +415,6 @@ export const addAndVerifyWidget = async ( await redirectToHomePage(page, false); await waitForAllLoadersToDisappear(page).catch(() => undefined); - await removeLandingBanner(page); // The save response is awaited and its toast asserted above, and `redirectToHomePage` // disables ETag conditional reads, so the first read-back is authoritative — the widget @@ -737,7 +734,6 @@ export const verifyWidgetHeaderNavigation = async ( // Home keeps background requests alive on some persona routes; use the lighter // redirect path and wait on rendered state instead of networkidle. await redirectToHomePage(page, false); - await removeLandingBanner(page); await waitForAllLoadersToDisappear(page).catch(() => undefined); await waitForAllLoadersToDisappear(page, 'entity-list-skeleton').catch( () => undefined @@ -755,7 +751,6 @@ export const verifyDomainCountInDomainWidget = async ( ].join(', '); await redirectToHomePage(page, false); - await removeLandingBanner(page); await expect .poll( @@ -795,7 +790,6 @@ export const verifyDataProductCountInDataProductWidget = async ( const widgetCardSelector = `[data-testid="data-product-card-${dataProductId}"] [data-testid="data-product-asset-count"]`; await redirectToHomePage(page, false); - await removeLandingBanner(page); await expect .poll( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts index 8dc4b4b8396c..893019eb0096 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -36,7 +36,6 @@ import { getEntityTypeSearchIndexMapping, readElementInListWithScroll, redirectToHomePage, - removeLandingBanner, toastNotification, uuid, } from './common'; @@ -94,15 +93,6 @@ export const visitEntityPage = async (data: { await waitForAllLoadersToDisappear(page); - // Dismiss welcome screen if visible - const isWelcomeScreenVisible = await page - .getByTestId('welcome-screen') - .isVisible(); - - if (isWelcomeScreenVisible) { - await page.getByTestId('welcome-screen-close-btn').click(); - } - const searchResponse = page.waitForResponse( (response) => response.url().includes('/api/v1/search/query') && @@ -141,7 +131,6 @@ export const visitEntityPageByFqn = async (data: { }) => { const { page, endpoint, fqn } = data; await waitForAllLoadersToDisappear(page); - await removeLandingBanner(page); const routeSegment = ENTITY_PATH[endpoint as keyof typeof ENTITY_PATH]; if (!routeSegment) { @@ -1528,7 +1517,6 @@ const revealFollowingWidget = async (page: Page): Promise => { const loadFollowingWidget = async (page: Page): Promise => { await redirectToHomePage(page, false); - await removeLandingBanner(page); await waitForAllLoadersToDisappear(page).catch(() => undefined); const followingWidgetPanel = await revealFollowingWidget(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 593702f8eab8..5b79a5494b4e 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts @@ -16,16 +16,6 @@ import { redirectToHomePage } from './common'; import { waitForAllLoadersToDisappear } from './entity'; import { sidebarClick } from './sidebar'; -const closeWelcomeScreenIfVisible = async (page: Page) => { - const isWelcomeScreenVisible = await page - .getByTestId('welcome-screen') - .isVisible(); - - if (isWelcomeScreenVisible) { - await page.getByTestId('welcome-screen-close-btn').click(); - } -}; - /** * Navigate the given (already-logged-in) user to the Explore page, search for an * entity by FQN, and assert whether its result card is shown — used to verify @@ -37,7 +27,6 @@ export const exploreShouldShowEntity = async ( displayName: string, shouldSee: boolean ) => { - await closeWelcomeScreenIfVisible(page); await redirectToHomePage(page); const exploreRes = page.waitForResponse('/api/v1/search/query?*'); @@ -85,7 +74,6 @@ export const exploreTreeCategories = async ( page: Page, { visible, hidden }: { visible: string[]; hidden: string[] } ) => { - await closeWelcomeScreenIfVisible(page); await redirectToHomePage(page); const exploreRes = page.waitForResponse('/api/v1/search/query?*'); @@ -172,15 +160,6 @@ export const searchForEntityShouldWork = async ( page: Page, entityName: string ) => { - // Wait for welcome screen and close it if visible - const isWelcomeScreenVisible = await page - .getByTestId('welcome-screen') - .isVisible(); - - if (isWelcomeScreenVisible) { - await page.getByTestId('welcome-screen-close-btn').click(); - } - await page.getByTestId('searchBox').click(); await page.getByTestId('searchBox').fill(fqn); @@ -213,15 +192,6 @@ export const searchForEntityShouldWorkShowNoResult = async ( displayName: string, page: Page ) => { - // Wait for welcome screen and close it if visible - const isWelcomeScreenVisible = await page - .getByTestId('welcome-screen') - .isVisible(); - - if (isWelcomeScreenVisible) { - await page.getByTestId('welcome-screen-close-btn').click(); - } - await page.getByTestId('searchBox').click(); await page.getByTestId('searchBox').fill(fqn); From d1acb3e36fd0e06982d42a94b4a42951dc6b3881 Mon Sep 17 00:00:00 2001 From: karanh37 Date: Fri, 21 Aug 2026 16:53:33 +0530 Subject: [PATCH 3/6] test(ui): let login opt out of welcome-banner suppression for Tour The welcome-banner suppression seeds `loggedInUsers` in UserClass.login, which hid the banner for every session. But the "Tour should work from welcome screen" test enters the tour by clicking the banner's own CTA ("Take a product tour to get started!", rendered only in WelcomeScreen.component.tsx), so suppressing the banner broke it. Add a `suppressWelcomeScreen` login option (default true, so every other spec keeps the suppression) and forward it through AdminClass. Tour.spec logs in with `suppressWelcomeScreen: false` so the banner renders; its other tests keep their existing "dismiss if visible" guards. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../resources/ui/playwright/e2e/Flow/Tour.spec.ts | 6 +++++- .../ui/playwright/support/user/AdminClass.ts | 5 +++-- .../ui/playwright/support/user/UserClass.ts | 14 +++++++++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts index 5a794c1efd95..9fab6e2c956a 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts @@ -173,7 +173,11 @@ test.describe( }); test.beforeEach('Visit entity details page', async ({ page }) => { - await user.login(page); + // Tour is entered from the welcome banner, so this suite must NOT suppress + // it. The other tour tests already guard against the banner if present. + await user.login(page, undefined, undefined, { + suppressWelcomeScreen: false, + }); }); test('Tour should work from help section', async ({ page }) => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/user/AdminClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/user/AdminClass.ts index 2295aeae00b3..9df2acaf5338 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/user/AdminClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/user/AdminClass.ts @@ -22,8 +22,9 @@ export class AdminClass extends UserClass { async login( page: Page, userName = DEFAULT_ADMIN_USER.userName, - password = DEFAULT_ADMIN_USER.password + password = DEFAULT_ADMIN_USER.password, + options: { suppressWelcomeScreen?: boolean } = {} ) { - await super.login(page, userName, password); + await super.login(page, userName, password, options); } } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts index 0f187bc026a5..c8b780476725 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts @@ -258,13 +258,21 @@ export class UserClass { async login( page: Page, userName = this.data.email, - password = this.data.password + password = this.data.password, + options: { suppressWelcomeScreen?: boolean } = {} ) { + const { suppressWelcomeScreen: shouldSuppressWelcomeScreen = true } = + options; + // Seed `loggedInUsers` before the first navigation so the landing-page // welcome banner never renders for this session. Prefer the authoritative // entity name from create(); fall back to the login email's local-part - // (the server-assigned username) for a pure login such as admin. - await suppressWelcomeScreen(page, this.responseData?.name ?? userName); + // (the server-assigned username) for a pure login such as admin. Tests that + // exercise the welcome banner itself (e.g. Tour) opt out with + // `suppressWelcomeScreen: false`. + if (shouldSuppressWelcomeScreen) { + await suppressWelcomeScreen(page, this.responseData?.name ?? userName); + } await page.goto('/signin'); try { From 65bcda65532424dc9aaf59f87ec6beaecd5737ef Mon Sep 17 00:00:00 2001 From: Harsh Vador <58542468+harsh-vador@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:50:38 +0530 Subject: [PATCH 4/6] test(playwright): fix five AUT nightly flakes at their cause (#31894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(tests): profile in setUpClass so test_list_entity_profiles has data (#31882) pytest orders unittest TestCase methods alphabetically, so test_list_entity_profiles runs first -- before test_profiler_workflow has created any profile. The module sets TestLoader.sortTestMethodsUsing = None to force definition order, but that only affects unittest's own loader: pytest collects TestCase methods itself and ignores it. Verified against an unmodified checkout -- collection still yields list_entity_profiles first with that line in place, so it has never had any effect here. The test still passed, because it asserts on a global 24h window across every table rather than on data it owns, and hard-deleted tables used to leak their profiler rows into that window. #31556 stopped that leak (issue #27041), and the latent ordering bug surfaced: shard-2 now fails with "0 not greater than 0" on every PR that actually runs the ingestion integration matrix, blocking the merge queue. PRs that do not touch ingestion skip the matrix and report green, which is why this looked branch-specific rather than repo-wide. Run the profiler in setUpClass, where fixture data belongs, so no test depends on another's ordering. Drop the ineffective loader hack, and turn the `if profiles_all.entities:` guard into an assertion -- that guard swallowed an empty unfiltered listing and hid which of the two calls was actually empty, which is the signal needed to diagnose this. * fix(ui): make whole Domain and Data Product rows clickable (#31876) * fix(ui): make whole Domain and Data Product rows clickable * addressed comments * Added unit test for the fix * test(playwright): fix five AUT nightly flakes at their cause Collate's AUT nightly run 32476747983 retried 25 tests on each database lane. Fourteen of the distinct specs live here. These five have a cause the artifacts explain; the rest are listed below rather than guessed at. Lineage node clicks (LineageInteraction, both lanes) clickLineageNode clicked the node the moment the caller's getLineage wait returned. React Flow mounts nodes in its own layout pass after that, so the click auto-waited with no timeout of its own and the spec died as a bare "Test timeout of 60000ms exceeded" naming nothing. Assert the node is visible first. Ingestion wizard Next (ServiceIngestion) Creating the service triggers AutoPilot, whose toast renders bottom-center — over the wizard footer. The trace shows the click on next-button intercepted by the toast's own alert-message span, then auto-waiting until the test timed out. Wait for that toast to dismiss before advancing. Knowledge Center owner chip (ExplorePageRightPanel_KnowledgeCenter, both lanes) The Explore summary panel renders owners from the search document, refreshed asynchronously after the owner PATCH. A panel that rendered before the refresh will never show the chip, so the 60s wait was waiting on the wrong thing. Re-open the entity until it is there. Glossary hierarchy modal (GlossaryHierarchy) getByLabel('Select Parent') is page-scoped and also matches the control of a hierarchy modal an earlier step left in the DOM; the click then spent the whole test on a hidden element. Scope it to the modal and check it is visible and enabled first. Language switch (Glossary — Dutch) The menuitem click ran against an ant-dropdown mid-enter-animation, which is exactly what waitForAntdPopupToSettle exists for. Use it on both switches. Not addressed here, for lack of evidence rather than lack of interest: AdvancedSearchSuggestions, SearchSettings, ContextCenterArticles, ContextCenterMemories, ServiceEntity, GlossaryP2Tests and SSOConfiguration all failed as bare 60s timeouts with no anchor and passed on retry in seconds. Marking them slow would hide a regression just as easily as fix a flake, so they need a trace first. PlatformLineage and DataProductRename are already fixed on main by #31736. Co-Authored-By: Claude Opus 5 (1M context) * test(playwright): bound the wizard Next click instead of waiting the toast out Review point: waiting for the AutoPilot toast to detach is a no-op when the toast has not rendered yet, so a toast that appears a moment later still intercepts the click. Correct, and waiting for it to appear first is not the answer either — the toast is fired by the create call several steps earlier and auto-closes after 5s (showSuccessToast(..., 5000) in AddServicePage), so by the time this line runs it may equally have already closed. Any gate on its presence is wrong for one of the two orderings. Bound the click instead. Playwright retries an intercepted click for the whole action timeout, and 30s outlasts the toast in every ordering: not yet rendered, on screen now, or already gone. Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Himanshu Khairajani <46777429+Khairajani@users.noreply.github.com> Co-authored-by: Anujkumar Yadav Co-authored-by: Claude Opus 5 (1M context) --- .../integration/profiler/test_sqa_profiler.py | 112 ++++++++---------- ...lorePageRightPanel_KnowledgeCenter.spec.ts | 39 +++++- .../ui/playwright/e2e/Pages/Glossary.spec.ts | 5 + .../entity/ingestion/ServiceBaseClass.ts | 14 ++- .../resources/ui/playwright/utils/glossary.ts | 16 ++- .../resources/ui/playwright/utils/lineage.ts | 12 +- .../DataProduct/DataProductListPage.tsx | 19 ++- .../DomainListing/DomainListPage.tsx | 4 +- .../domain/ui/domainFieldRenderers.test.tsx | 79 ++++++++++++ .../atoms/domain/ui/domainFieldRenderers.tsx | 13 +- .../domain/ui/useDomainTableColumns.test.tsx | 78 ++++++++++++ .../atoms/domain/ui/useDomainTableColumns.tsx | 9 +- 12 files changed, 319 insertions(+), 81 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/ui/domainFieldRenderers.test.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/ui/useDomainTableColumns.test.tsx diff --git a/ingestion/tests/integration/profiler/test_sqa_profiler.py b/ingestion/tests/integration/profiler/test_sqa_profiler.py index b154981024f8..92c5f0906a2a 100644 --- a/ingestion/tests/integration/profiler/test_sqa_profiler.py +++ b/ingestion/tests/integration/profiler/test_sqa_profiler.py @@ -19,7 +19,7 @@ import json import time from typing import List # noqa: UP035 -from unittest import TestCase, TestLoader +from unittest import TestCase from _openmetadata_testutils.ometa import int_admin_ometa from metadata.generated.schema.configuration.profilerConfiguration import ( @@ -40,8 +40,6 @@ PROFILER_INGESTION_CONFIG_TEMPLATE, ) -TestLoader.sortTestMethodsUsing = None # type: ignore - class TestSQAProfiler(TestCase): @classmethod @@ -67,10 +65,43 @@ def setUpClass(cls): ingestion_workflow.execute() ingestion_workflow.raise_from_status() ingestion_workflow.stop() + + # Profile here rather than inside a test method. pytest orders TestCase + # methods alphabetically, so test_list_entity_profiles runs *before* + # test_profiler_workflow and would otherwise assert on profiles that do + # not exist yet. It only ever passed because hard-deleted tables used to + # leak their profiler rows into the window it queries (issue #27041, fixed + # in #31556). Profiles are class fixture data, so they belong here. + cls.run_profiler_workflows() except Exception as e: cls.container_builder.stop_all_containers() raise e # noqa: TRY201 + @classmethod + def run_profiler_workflows(cls): + """Run the profiler over every container, using the active profiler settings.""" + for container in cls.container_builder.containers: + config = PROFILER_INGESTION_CONFIG_TEMPLATE.format( + type=container.connector_type, + service_config=container.get_config(), + service_name=type(container).__name__, + ) + profiler_workflow = ProfilerWorkflow.create(json.loads(config)) + profiler_workflow.execute() + profiler_workflow.print_status() + profiler_workflow.raise_from_status() + profiler_workflow.stop() + + def list_profiled_tables(self): + """The tables the fixture ingested, across every container.""" + tables: List[Table] = [] # noqa: UP006 + for container in self.container_builder.containers: + service_name = type(container).__name__ + cfg = json.loads(container.get_config()) + db_name = cfg.get("database") or cfg.get("databaseSchema", "default") + tables.extend(self.metadata.list_all_entities(Table, params={"database": f"{service_name}.{db_name}"})) + return tables + @classmethod def tearDownClass(cls): cls.container_builder.stop_all_containers() @@ -93,31 +124,8 @@ def _clean_up_settings(cls): cls.metadata.create_or_update_settings(settings) def test_profiler_workflow(self): - """test a simple profiler workflow on a table in each service and validate the profile is created""" - for container in self.container_builder.containers: - try: - config = PROFILER_INGESTION_CONFIG_TEMPLATE.format( - type=container.connector_type, - service_config=container.get_config(), - service_name=type(container).__name__, - ) - profiler_workflow = ProfilerWorkflow.create( - json.loads(config), - ) - profiler_workflow.execute() - profiler_workflow.print_status() - profiler_workflow.raise_from_status() - profiler_workflow.stop() - except Exception as e: - self.fail(f"Profiler workflow failed for {type(container).__name__} with error {e}") - - tables: List[Table] = [] # noqa: UP006 - for container in self.container_builder.containers: - service_name = type(container).__name__ - cfg = json.loads(container.get_config()) - db_name = cfg.get("database") or cfg.get("databaseSchema", "default") - tables.extend(self.metadata.list_all_entities(Table, params={"database": f"{service_name}.{db_name}"})) - for table in tables: + """validate the profile the fixture's profiler run created for a table in each service""" + for table in self.list_profiled_tables(): if table.name.root != "users": continue table = self.metadata.get_latest_table_profile(table.fullyQualifiedName) # noqa: PLW2901 @@ -146,34 +154,10 @@ def test_profiler_workflow_w_globale_config(self): ) self.metadata.create_or_update_settings(settings) - service_names = [] + # Re-profile so the metric-level settings above take effect. + self.run_profiler_workflows() - for container in self.container_builder.containers: - try: - service_name = type(container).__name__ - service_names.append(service_name) - config = PROFILER_INGESTION_CONFIG_TEMPLATE.format( - type=container.connector_type, - service_config=container.get_config(), - service_name=service_name, - ) - profiler_workflow = ProfilerWorkflow.create( - json.loads(config), - ) - profiler_workflow.execute() - profiler_workflow.print_status() - profiler_workflow.raise_from_status() - profiler_workflow.stop() - except Exception as e: - self.fail(f"Profiler workflow failed for {service_name} with error {e}") - - tables: List[Table] = [] # noqa: UP006 - for container in self.container_builder.containers: - sn = type(container).__name__ - cfg = json.loads(container.get_config()) - db_name = cfg.get("database") or cfg.get("databaseSchema", "default") - tables.extend(self.metadata.list_all_entities(Table, params={"database": f"{sn}.{db_name}"})) - for table in tables: + for table in self.list_profiled_tables(): if table.name.root != "users": continue table = self.metadata.get_latest_table_profile(table.fullyQualifiedName) # noqa: PLW2901 @@ -201,13 +185,17 @@ def test_list_entity_profiles(self): self.assertTrue(hasattr(profiles_all, "total")) self.assertTrue(hasattr(profiles_all, "entities")) - if profiles_all.entities: - first = profiles_all.entities[0] - self.assertIsInstance(first, EntityProfile) - self.assertIsNotNone(first.id) - self.assertIsNotNone(first.entityReference) - self.assertIsNotNone(first.timestamp) - self.assertIsNotNone(first.profileData) + # Assert rather than guard: setUpClass profiles every container, so an empty + # window is a real failure. Skipping it here only pushed the failure two lines + # down, hiding whether the unfiltered listing was empty too. + self.assertGreater(len(profiles_all.entities), 0) + + first = profiles_all.entities[0] + self.assertIsInstance(first, EntityProfile) + self.assertIsNotNone(first.id) + self.assertIsNotNone(first.entityReference) + self.assertIsNotNone(first.timestamp) + self.assertIsNotNone(first.profileData) profiles_table = get_profiles(Table, start_ts, end_ts, ProfileTypeEnum.table) self.assertGreater(len(profiles_table.entities), 0) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts index bf4d2a7c2b24..a87d8587188c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { Page } from '@playwright/test'; import { KnowledgeCenterClass } from '../../support/entity/KnowledgeCenterClass'; import { expect, test as baseTest } from '../../support/fixtures/userPages'; import { Glossary } from '../../support/glossary/Glossary'; @@ -68,6 +69,31 @@ export const test = baseTest.extend<{ }, }); +/** + * Assert the owner chip is in the summary panel, re-opening the entity if it is + * not. The panel renders owners from the Explore search document, which is + * refreshed asynchronously after the owner PATCH — a panel that rendered before + * that refresh will never show the chip, so waiting on it is waiting on the + * wrong thing. Re-navigating re-reads it. + */ +async function expectOwnerInPanel( + page: Page, + entityName: string, + ownerName: string +) { + const ownerChip = page + .locator('[data-testid="entity-summary-panel-container"]') + .getByTestId(ownerName); + + await expect(async () => { + if (!(await ownerChip.isVisible())) { + await navigateToKCEntity(page, entityName); + } + + await expect(ownerChip).toBeVisible({ timeout: 10_000 }); + }).toPass({ timeout: 60_000, intervals: [2_000, 5_000] }); +} + test.describe('Knowledge Center Right Panel Test Suite', () => { test.beforeAll(async ({ browser }) => { test.slow(true); @@ -163,7 +189,6 @@ test.describe('Knowledge Center Right Panel Test Suite', () => { test('Should update owners for knowledgeCenter', async ({ adminPage, rightPanel, - overview, }) => { await navigateToKCEntity( adminPage, @@ -174,7 +199,11 @@ test.describe('Knowledge Center Right Panel Test Suite', () => { rightPanel.setEntityConfigByType('knowledgeCenter'); await addOwnerInKCPanel(adminPage, user1.getUserDisplayName()); - await overview.shouldShowOwner(user1.getUserDisplayName()); + await expectOwnerInPanel( + adminPage, + getEntityDisplayName(knowledgeCenter.responseData), + user1.getUserDisplayName() + ); }); }); @@ -277,7 +306,11 @@ test.describe('Knowledge Center Right Panel Test Suite', () => { rightPanel.setEntityConfigByType('knowledgeCenter'); await addOwnerInKCPanel(adminPage, user1.getUserDisplayName()); - await overview.shouldShowOwner(user1.getUserDisplayName()); + await expectOwnerInPanel( + adminPage, + getEntityDisplayName(knowledgeCenter.responseData), + user1.getUserDisplayName() + ); await overview.removeOwner([user1.getUserDisplayName()], 'Users'); await waitForAllLoadersToDisappear(adminPage); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 81a5fde38ba9..42dff396e9c9 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -44,6 +44,7 @@ import { redirectToHomePage, uuid, visitGlossaryPage, + waitForAntdPopupToSettle, } from '../../utils/common'; import { addMultiOwner, @@ -2194,10 +2195,12 @@ test.describe('Glossary tests', () => { .filter({ hasText: 'EN' }) .first(); await languageDropdown.click(); + await waitForAntdPopupToSettle(page); const germanOption = page.getByRole('menuitem', { name: 'Deutsch - DE', }); + await expect(germanOption).toBeVisible(); await germanOption.click(); await waitForAllLoadersToDisappear(page); @@ -2230,10 +2233,12 @@ test.describe('Glossary tests', () => { .filter({ hasText: 'DE' }) .first(); await languageDropdown.click(); + await waitForAntdPopupToSettle(page); const englishOption = page.getByRole('menuitem', { name: 'English - EN', }); + await expect(englishOption).toBeVisible(); await englishOption.click(); }); } finally { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts index 4a8603cae938..bb823e56e09d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts @@ -218,7 +218,19 @@ class ServiceBaseClass { await waitForIngestionWorkflowForm(page); await this.fillIngestionDetails(page); - await page.click('[data-testid="next-button"]'); + // Creating the service triggers AutoPilot, whose success toast renders + // bottom-center — directly over the wizard footer — and auto-closes after 5s + // (showSuccessToast(..., 5000) in AddServicePage). A click landing inside + // that window is intercepted by the toast, and with no per-action timeout + // the retry loop runs to the end of the test instead. + // + // Bounding the click is what fixes it, not waiting the toast out: the toast + // is fired by the create call several steps earlier, so whether it is on + // screen when we get here depends on how fast those steps ran. Gating on it + // being gone is a no-op when it has not rendered yet and when it has already + // closed. A bounded click covers every ordering — Playwright retries the + // intercepted click for the whole timeout, which outlasts the toast. + await page.click('[data-testid="next-button"]', { timeout: 30_000 }); // Go back and data should persist await page.click('[data-testid="previous-button"]'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts index 7f5b25a80259..f0bab8cb0efc 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts @@ -1189,16 +1189,26 @@ export const changeTermHierarchyFromModal = async ( await page.getByTestId('manage-button').click(); await page.getByTestId('change-parent-button').click(); - await expect(page.locator('[role="dialog"]')).toBeVisible(); + const hierarchyModal = page.locator( + '[data-testid="change-parent-hierarchy-modal"]' + ); + await expect(hierarchyModal).toBeVisible(); + + // Scope to this modal: the page-level label also matches the control of a + // hierarchy modal left in the DOM by an earlier step, and the click then waits + // out the whole test on a hidden element. + const parentSelect = hierarchyModal.getByLabel('Select Parent'); + await expect(parentSelect).toBeVisible(); + await expect(parentSelect).toBeEnabled(); + await parentSelect.click(); - await page.getByLabel('Select Parent').click(); await page.locator('.async-tree-select-list-dropdown').waitFor({ state: 'visible', }); if (isGlossaryTerm) { const searchRes = page.waitForResponse(`/api/v1/search/query?q=*`); - await page.getByLabel('Select Parent').fill(entityDisplayName); + await parentSelect.fill(entityDisplayName); await searchRes; } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts index f08225490254..1ce1f52e6c1c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts @@ -1015,10 +1015,16 @@ export const toggleLineageFilters = async (page: Page, tableFqn: string) => { }; export const clickLineageNode = async (page: Page, nodeFqn: string) => { - await page + // React Flow mounts nodes after its own layout pass, which runs well after the + // getLineage response the caller waited on. Clicking straight away leaves the + // action auto-waiting with no timeout of its own, so a graph that is slow to + // lay out surfaces as a bare test timeout with nothing naming the node. + const nodeTitle = page .locator(`[data-testid="lineage-node-${nodeFqn}"]`) - .locator(`[data-testid="entity-header-display-name"]`) - .click(); + .locator(`[data-testid="entity-header-display-name"]`); + + await expect(nodeTitle).toBeVisible(); + await nodeTitle.click(); }; export const updateLineageConfigFromModal = async ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx index 87f53bac40dd..198121bdbf6c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx @@ -23,7 +23,14 @@ import { import { Globe01, Package, Plus } from '@untitledui/icons'; import classNames from 'classnames'; import { isEmpty } from 'lodash'; -import { FC, ReactNode, useCallback, useMemo, useState } from 'react'; +import { + FC, + MouseEvent, + ReactNode, + useCallback, + useMemo, + useState, +} from 'react'; import { useTranslation } from 'react-i18next'; import { NO_DATA, ROUTES } from '../../constants/constants'; import { LEARNING_PAGE_IDS } from '../../constants/Learning.constants'; @@ -168,12 +175,18 @@ const DataProductListPage = ({ entity.name && entity.displayName !== entity.name; + const handleNameClick = (event: MouseEvent) => { + event.stopPropagation(); + dataProductListing.actionHandlers.onEntityClick?.(entity); + }; + return ( + gap={3} + onClick={handleNameClick}> { const { renderDomainCard } = useDomainCardTemplates(); const { columns: domainColumns, renderCell: renderDomainCell } = - useDomainTableColumns(); + useDomainTableColumns({ + onEntityClick: domainListing.actionHandlers.onEntityClick, + }); const selectedDomainEntities = useMemo( () => diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/ui/domainFieldRenderers.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/ui/domainFieldRenderers.test.tsx new file mode 100644 index 000000000000..cd0ee0a389d7 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/ui/domainFieldRenderers.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { fireEvent, render, screen } from '@testing-library/react'; +import { ReactNode } from 'react'; +import { Domain } from '../../../../../generated/entity/domains/domain'; +import { renderDomainNameCell } from './domainFieldRenderers'; + +jest.mock('@openmetadata/ui-core-components', () => ({ + Avatar: () => , + Box: ({ + children, + onClick, + }: { + children: ReactNode; + onClick?: () => void; + }) => ( +
+ {children} +
+ ), + Typography: ({ children }: { children: ReactNode }) => ( + {children} + ), +})); + +jest.mock('../../../../../utils/TooltipUtils', () => ({ + renderBreakableTooltip: (value: string) => value, +})); + +const DOMAIN = { + id: 'domain-id', + name: 'engineering', + displayName: 'Engineering', + fullyQualifiedName: 'engineering', +} as Domain; + +describe('renderDomainNameCell', () => { + it('navigates once when the name cell is clicked', () => { + const onClick = jest.fn(); + + render(<>{renderDomainNameCell(DOMAIN, onClick)}); + fireEvent.click(screen.getByText('Engineering')); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('stops the click from bubbling to the row so navigation is not duplicated', () => { + const onClick = jest.fn(); + const rowClick = jest.fn(); + + render( +
{renderDomainNameCell(DOMAIN, onClick)}
+ ); + fireEvent.click(screen.getByText('Engineering')); + + expect(onClick).toHaveBeenCalledTimes(1); + expect(rowClick).not.toHaveBeenCalled(); + }); + + it('does not attach a click handler when no onClick is provided', () => { + const rowClick = jest.fn(); + + render(
{renderDomainNameCell(DOMAIN)}
); + fireEvent.click(screen.getByText('Engineering')); + + // With no cell handler the click falls through to the row unchanged. + expect(rowClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/ui/domainFieldRenderers.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/ui/domainFieldRenderers.tsx index f8f779d318f7..86ddbcd3dd15 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/ui/domainFieldRenderers.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/ui/domainFieldRenderers.tsx @@ -12,7 +12,7 @@ */ import { Avatar, Box, Typography } from '@openmetadata/ui-core-components'; -import { ReactNode } from 'react'; +import { MouseEvent, ReactNode } from 'react'; import { NO_DATA } from '../../../../../constants/constants'; import { DataProduct } from '../../../../../generated/entity/domains/dataProduct'; import { Domain } from '../../../../../generated/entity/domains/domain'; @@ -69,16 +69,23 @@ export const LIST_EMPTY_STATE_CLASS = 'tw:flex tw:flex-1 tw:min-h-60 tw:items-center tw:justify-center'; export const renderDomainNameCell = ( - entity: Domain | DataProduct + entity: Domain | DataProduct, + onClick?: () => void ): ReactNode => { const entityName = getEntityName(entity); + const handleNameClick = (event: MouseEvent) => { + event.stopPropagation(); + onClick?.(); + }; + return ( + gap={3} + onClick={onClick ? handleNameClick : undefined}> ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +jest.mock('@openmetadata/ui-core-components', () => ({ + Avatar: () => , + Box: ({ + children, + onClick, + }: { + children: ReactNode; + onClick?: () => void; + }) => ( +
+ {children} +
+ ), + Typography: ({ children }: { children: ReactNode }) => ( + {children} + ), +})); + +jest.mock('../../../../../utils/TooltipUtils', () => ({ + renderBreakableTooltip: (value: string) => value, +})); + +const DOMAIN = { + id: 'domain-id', + name: 'engineering', + displayName: 'Engineering', + fullyQualifiedName: 'engineering', +} as Domain; + +describe('useDomainTableColumns', () => { + it('routes a name-cell click to onEntityClick with the row entity', () => { + const onEntityClick = jest.fn(); + + const { result } = renderHook(() => + useDomainTableColumns({ onEntityClick }) + ); + + render(<>{result.current.renderCell(DOMAIN, 'name')}); + fireEvent.click(screen.getByText('Engineering')); + + expect(onEntityClick).toHaveBeenCalledTimes(1); + expect(onEntityClick).toHaveBeenCalledWith(DOMAIN); + }); + + it('renders the name cell without a click handler when onEntityClick is omitted', () => { + const rowClick = jest.fn(); + + const { result } = renderHook(() => useDomainTableColumns()); + + render( +
{result.current.renderCell(DOMAIN, 'name')}
+ ); + fireEvent.click(screen.getByText('Engineering')); + + expect(rowClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/ui/useDomainTableColumns.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/ui/useDomainTableColumns.tsx index 8eb4971fe339..d96e23a42991 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/ui/useDomainTableColumns.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/ui/useDomainTableColumns.tsx @@ -26,11 +26,13 @@ import { interface UseDomainTableColumnsOptions { nameLabelKey?: string; tagSize?: 'sm' | 'lg'; + onEntityClick?: (entity: Domain) => void; } export const useDomainTableColumns = ({ nameLabelKey = 'label.domain', tagSize = 'sm', + onEntityClick, }: UseDomainTableColumnsOptions = {}) => { const { t } = useTranslation(); @@ -49,7 +51,10 @@ export const useDomainTableColumns = ({ (entity: Domain, columnId: string): ReactNode => { switch (columnId) { case 'name': - return renderDomainNameCell(entity); + return renderDomainNameCell( + entity, + onEntityClick ? () => onEntityClick(entity) : undefined + ); case 'domainType': return renderDomainTypeCell(entity); case 'owners': @@ -62,7 +67,7 @@ export const useDomainTableColumns = ({ return null; } }, - [tagSize] + [tagSize, onEntityClick] ); return { columns, renderCell }; From ce9450eb42aac1205a78808b18f8a4bde7ede38f Mon Sep 17 00:00:00 2001 From: Harsh Vador <58542468+harsh-vador@users.noreply.github.com> Date: Sat, 22 Aug 2026 08:39:02 +0530 Subject: [PATCH 5/6] test(playwright): assert the hierarchy dialog, not Ant's modal root (#31904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit changeTermHierarchyFromModal waited on [data-testid="change-parent-hierarchy-modal"] itself. ChangeParentHierarchy passes that testid to Ant's , which spreads it onto .ant-modal-root — a position: fixed, zero-size container whose children carry the layout. Playwright requires a non-empty bounding box, so that element is never visible, open or closed, and the assertion could only ever fail: 19 x locator resolved to
...
- unexpected value "hidden" while the trace screenshot shows the dialog plainly open. The code this replaced asserted [role="dialog"], which does have a box. Keep the scoping — the bare 'Select Parent' label also matches the control of a hierarchy modal an earlier step left in the DOM — but hang it off the dialog inside the root. Deterministic failure across 5 call sites: GlossaryHierarchy x3, Glossary x2. Co-authored-by: Claude Opus 5 (1M context) --- .../resources/ui/playwright/utils/glossary.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts index f0bab8cb0efc..afd557948d8b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts @@ -1189,14 +1189,17 @@ export const changeTermHierarchyFromModal = async ( await page.getByTestId('manage-button').click(); await page.getByTestId('change-parent-button').click(); - const hierarchyModal = page.locator( - '[data-testid="change-parent-hierarchy-modal"]' - ); + // Ant's Modal spreads data-testid onto `.ant-modal-root`, a zero-size wrapper + // that never satisfies toBeVisible even while the dialog is on screen — the + // dialog itself is the element with a box. Scoping still matters: the bare + // `Select Parent` label also matches the control of a hierarchy modal left in + // the DOM by an earlier step, and clicking that waits out the whole test on a + // hidden element. + const hierarchyModal = page + .locator('[data-testid="change-parent-hierarchy-modal"]') + .getByRole('dialog'); await expect(hierarchyModal).toBeVisible(); - // Scope to this modal: the page-level label also matches the control of a - // hierarchy modal left in the DOM by an earlier step, and the click then waits - // out the whole test on a hidden element. const parentSelect = hierarchyModal.getByLabel('Select Parent'); await expect(parentSelect).toBeVisible(); await expect(parentSelect).toBeEnabled(); From 3532fd2d69b37384fb056295ce24877c6929b0e5 Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Sat, 22 Aug 2026 10:24:05 +0530 Subject: [PATCH 6/6] test(playwright): poll the landing-page widget reveal instead of revealing once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit waitForLandingPageWidget revealed the widget once and then handed off to expect(widget).toBeVisible(). A deferred slot only mounts its widget when it is scrolled into view, and toBeVisible cannot scroll — so whenever the layout attached after that single reveal, revealLandingPageWidget found nothing to scroll, the widget never mounted, and the assertion spent its full 15s on an element that was never going to appear. Seen in AUT run 32513179631: DomainDataProductsWidgets 'Assign Widgets' saved the layout, got the success toast, navigated to /my-data, and the trace then shows only two zero-count queries and no scroll step before the timeout. The persona docStore document fetched by that same page load contained both KnowledgePanel.Domains and KnowledgePanel.DataProducts with a 200, so the layout was saved and served correctly — only the reveal raced. Poll isLandingPageWidgetVisible, which re-reveals on every iteration and is already used this way elsewhere in this file. A widget that is genuinely absent still fails, at the poll timeout rather than instantly. Co-Authored-By: Claude Opus 5 --- .../ui/playwright/utils/customizeLandingPage.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts index 0e006e408b49..f941d4703e53 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts @@ -264,9 +264,20 @@ export const waitForLandingPageWidget = async ( ): Promise => { const widget = page.getByTestId(widgetKey); - await revealLandingPageWidget(page, widgetKey); - - await expect(widget).toBeVisible(); + // The reveal has to be retried, not done once. A deferred slot mounts its widget only + // when scrolled into view, and `expect(...).toBeVisible()` cannot scroll. So when the + // layout attaches *after* a single reveal — a fresh `/my-data` load right after saving a + // layout is the common case — `revealLandingPageWidget` finds nothing to scroll, the + // widget never mounts, and the visibility assertion then burns its entire timeout on an + // element that was never going to appear no matter how long it waited. Polling the reveal + // rides out that render delay; a widget that is genuinely missing still fails, just at the + // poll timeout rather than instantly. + await expect + .poll(() => isLandingPageWidgetVisible(page, widgetKey), { + timeout: 60_000, + intervals: [500, 1_000, 2_000, 5_000], + }) + .toBe(true); await expect(widget.getByTestId('entity-list-skeleton')).toBeHidden();