From dfd6fa95b84eb542e3edea2c880df0f26c419106 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 28 Jul 2026 17:28:32 +0530 Subject: [PATCH 01/11] =?UTF-8?q?chore(UI):=20upgrade=20react-router-dom?= =?UTF-8?q?=20v6=20=E2=86=92=20v7=20and=20fix=20column=20panel=20race=20co?= =?UTF-8?q?ndition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump react-router-dom from 6.30.4 to 7.18.1 - In v7, navigate() is wrapped in React.startTransition, deferring the router re-render. This caused the column detail panel to reopen after close because effects still saw stale URL params during the transition. - Fix useFqnDeepLink.ts: move selectedColumn to a ref so it is read without triggering the effect when closeColumnDetailPanel sets it null. - Fix GenericProvider.tsx: add skipNextColumnSync ref that is set before navigate() in closeColumnDetailPanel and consumed once in the URL-sync useEffect, preventing the panel from reopening during the deferred re-render window. Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/resources/ui/package.json | 2 +- .../GenericProvider/GenericProvider.tsx | 8 ++++- .../resources/ui/src/hooks/useFqnDeepLink.ts | 10 ++++-- .../src/main/resources/ui/yarn.lock | 34 +++++++++---------- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/package.json b/openmetadata-ui/src/main/resources/ui/package.json index 2507986e0c5d..dc905c983f71 100644 --- a/openmetadata-ui/src/main/resources/ui/package.json +++ b/openmetadata-ui/src/main/resources/ui/package.json @@ -151,7 +151,7 @@ "react-quill-new": "^3.4.0", "react-reflex": "^4.1.0", "react-resizable": "^3.1.3", - "react-router-dom": "^6.30.4", + "react-router-dom": "7.18.1", "reactflow": "^11.10.2", "reactjs-localstorage": "^1.0.1", "recharts": "2.10.3", diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx index 29f800bae69b..c046f0bb6093 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx @@ -84,6 +84,7 @@ export const GenericProvider = >({ const pageType = useMemo(() => ENTITY_PAGE_TYPE_MAP[type], [type]); const { tab } = useRequiredParams<{ tab: EntityTabs }>(); const expandedLayout = useRef([]); + const skipNextColumnSync = useRef(false); const [layout, setLayout] = useState( getLayoutFromCustomizedPage(pageType, tab, customizedPage, isVersionView) ); @@ -137,7 +138,11 @@ export const GenericProvider = >({ // Sync selected column from prop (deep link) useEffect(() => { - // If we have a direct columnFqn from props, try to find and select it + if (skipNextColumnSync.current) { + skipNextColumnSync.current = false; + + return; + } if (columnFqn && extractedColumns.length > 0) { const col = findFieldByFQN(extractedColumns as Column[], columnFqn); if (col) { @@ -258,6 +263,7 @@ export const GenericProvider = >({ ); const closeColumnDetailPanel = useCallback(() => { + skipNextColumnSync.current = true; setSelectedColumn(null); // Update URL to remove column FQN diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useFqnDeepLink.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useFqnDeepLink.ts index 19bb41f8f69c..bfa8954a0eee 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useFqnDeepLink.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useFqnDeepLink.ts @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { findFieldByFQN, getParentKeysToExpand } from '../utils/TablePureUtils'; interface UseFqnDeepLinkProps { @@ -32,6 +32,9 @@ export const useFqnDeepLink = < openColumnDetailPanel, selectedColumn, }: UseFqnDeepLinkProps) => { + const selectedColumnRef = useRef(selectedColumn); + selectedColumnRef.current = selectedColumn; + useEffect(() => { if (!columnPart || !fqn) { return; @@ -47,7 +50,8 @@ export const useFqnDeepLink = < const matchedField = findFieldByFQN(data, fullColumnFqn); if (matchedField) { if ( - selectedColumn?.fullyQualifiedName !== matchedField.fullyQualifiedName + selectedColumnRef.current?.fullyQualifiedName !== + matchedField.fullyQualifiedName ) { openColumnDetailPanel(matchedField); } @@ -57,7 +61,7 @@ export const useFqnDeepLink = < fqn, data, openColumnDetailPanel, - selectedColumn, setExpandedRowKeys, + // selectedColumn intentionally omitted — tracked via ref to avoid re-triggering on close ]); }; diff --git a/openmetadata-ui/src/main/resources/ui/yarn.lock b/openmetadata-ui/src/main/resources/ui/yarn.lock index b60dc07aad34..02ca93796b28 100644 --- a/openmetadata-ui/src/main/resources/ui/yarn.lock +++ b/openmetadata-ui/src/main/resources/ui/yarn.lock @@ -2380,11 +2380,6 @@ resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f" integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg== -"@remix-run/router@1.23.3": - version "1.23.3" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.23.3.tgz#957c098d4393d301a8aa7dccf3ef28ea5430e36a" - integrity sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q== - "@rjsf/core@5.24.13": version "5.24.13" resolved "https://registry.yarnpkg.com/@rjsf/core/-/core-5.24.13.tgz#ef98e5dc6ac064b2be2f56e0887c99df2b1f8d44" @@ -5603,7 +5598,7 @@ cookie-storage@^6.1.0: resolved "https://registry.yarnpkg.com/cookie-storage/-/cookie-storage-6.1.0.tgz#291b2f662d961be44f999626593421cbfcf23790" integrity sha512-HeVqbVy8BjXhAAuFtL6MTG+witHoLbxfky2jgVh9FmxmyL6IKa9gSSyPNjevXCCCxPu6Tzd9J8+eXTRQzYU/cg== -cookie@0.7.0: +cookie@0.7.0, cookie@^1.0.1: version "0.7.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.0.tgz#2148f68a77245d5c2c0005d264bc3e08cfa0655d" integrity sha512-qCf+V4dtlNhSRXGAZatc1TasyFO6GjohcOul807YOb5ik3+kQSnb4d7iajeCL8QHaJ4uZEjCgiCJerKXwdRVlQ== @@ -11425,20 +11420,20 @@ react-resizable@^3.0.5, react-resizable@^3.1.3: prop-types "15.x" react-draggable "^4.5.0" -react-router-dom@^6.30.4: - version "6.30.4" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.30.4.tgz#f7167bf3da6c7d9132130ea985dd06def25e84d5" - integrity sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q== +react-router-dom@7.18.1: + version "7.18.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.18.1.tgz#0d1b138e291393059ad481c3e10e366385a978a4" + integrity sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg== dependencies: - "@remix-run/router" "1.23.3" - react-router "6.30.4" + react-router "7.18.1" -react-router@6.30.4: - version "6.30.4" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.30.4.tgz#638f35176527bd243d96d81d35d33b757bad46c2" - integrity sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA== +react-router@7.18.1: + version "7.18.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.18.1.tgz#61259d1594b95c1ace299ee4c57453570f0c22f1" + integrity sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg== dependencies: - "@remix-run/router" "1.23.3" + cookie "^1.0.1" + set-cookie-parser "^2.6.0" react-shallow-renderer@^16.15.0: version "16.15.0" @@ -11940,6 +11935,11 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-cookie-parser@^2.6.0: + version "2.7.2" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68" + integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== + set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" From 353126175a4231bf9496afd424cf386505ae9b01 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 28 Jul 2026 17:54:27 +0530 Subject: [PATCH 02/11] minor fix --- .../GenericProvider/GenericProvider.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx index c046f0bb6093..4dd43a00e3a1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx @@ -84,7 +84,7 @@ export const GenericProvider = >({ const pageType = useMemo(() => ENTITY_PAGE_TYPE_MAP[type], [type]); const { tab } = useRequiredParams<{ tab: EntityTabs }>(); const expandedLayout = useRef([]); - const skipNextColumnSync = useRef(false); + const selectedColumnRef = useRef(null); const [layout, setLayout] = useState( getLayoutFromCustomizedPage(pageType, tab, customizedPage, isVersionView) ); @@ -96,6 +96,7 @@ export const GenericProvider = >({ const [selectedColumn, setSelectedColumn] = useState( null ); + selectedColumnRef.current = selectedColumn; // State to store the displayed columns (sorted/filtered) from SchemaTable const [displayedColumns, setDisplayedColumns] = useState([]); @@ -138,11 +139,6 @@ export const GenericProvider = >({ // Sync selected column from prop (deep link) useEffect(() => { - if (skipNextColumnSync.current) { - skipNextColumnSync.current = false; - - return; - } if (columnFqn && extractedColumns.length > 0) { const col = findFieldByFQN(extractedColumns as Column[], columnFqn); if (col) { @@ -234,8 +230,11 @@ export const GenericProvider = >({ (column: ColumnOrTask) => { const columnFqn = column.fullyQualifiedName; - // If the column is already selected, don't do anything to avoid loops - if (selectedColumn?.fullyQualifiedName === columnFqn) { + // Read via ref so this callback is not recreated when selectedColumn changes. + // Without this, closeColumnDetailPanel (setSelectedColumn null) would recreate + // this callback, causing useFqnDeepLink to re-fire with stale URL params and + // reopen the panel (react-router v7 defers navigate() via startTransition). + if (selectedColumnRef.current?.fullyQualifiedName === columnFqn) { return; } @@ -258,12 +257,11 @@ export const GenericProvider = >({ tab, navigate, location.pathname, - selectedColumn?.fullyQualifiedName, + // selectedColumn?.fullyQualifiedName intentionally omitted — read via selectedColumnRef ] ); const closeColumnDetailPanel = useCallback(() => { - skipNextColumnSync.current = true; setSelectedColumn(null); // Update URL to remove column FQN From 3607ca8e2d55ec198095059a73ecce68a84cee9a Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Wed, 5 Aug 2026 18:25:25 +0530 Subject: [PATCH 03/11] fix(UI): fix react-router v7 deferred navigation race conditions and Playwright tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After upgrading to react-router-dom v7, navigate() and setSearchParams() are wrapped in React.startTransition, deferring URL updates. This caused a cascade of bugs across pagination, panel/modal state, and Playwright test assertions. Source code fixes: - usePaging.ts: move processedPageSize to a ref to prevent the URL-sync useEffect from firing when globalPageSize updates (Zustand sync), which was resetting pageSize/currentPage to stale URL values before the navigate committed - StoredProcedureTab.tsx, DataModelsTable.tsx: remove duplicate pagingCursor sync effects that called handlePageChange() with stale URL data, cancelling the deferred navigation and keeping currentPage stuck at 1 - APIEndpointsTab, SchemaTablesTab, UserListPageV1, RolesListPage, StoredProcedureTab: add pageSize !== pagingCursor.pageSize guard to prevent fetch effects from firing with stale page-2 cursor when pageSize changes locally before the URL commits - ContextCenterDocumentsPage, ContextCenterMemoriesPage: move previewFile/ isViewModalOpen to refs and remove from URL-sync useEffect deps — prevents panels/modals from reopening after close while searchParams still holds the stale document/memory ID during the deferred transition window - GenericProvider.tsx: add selectedColumnRef to openColumnDetailPanel to break the circular dependency that caused the column detail panel to reopen - useFqnDeepLink.ts: move selectedColumn to a ref to prevent the hook from re-firing when the panel closes Playwright test fixes: - common.ts: replace one-shot textContent()+toMatch() with retrying toHaveText() for all pagination text assertions - common.ts: add waitForURL(currentPage=2) before page-2 text checks so URL settles before asserting (prevents usePaging sync effect from resetting to 1) - common.ts: add waitForURL(searchParam) guards before all URL reads after deferred setSearchParams calls - common.ts: replace no-op waitForLoadState('domcontentloaded') with proper count()+toBeVisible() check for the page-size dropdown; restore conditional click (hover alone opens Ant Design dropdown; unconditional click was toggling it shut); add waitForURL(pageSize=25)+waitForAllLoadersToDisappear after page-size change to catch the correct re-fetch Co-Authored-By: Claude Sonnet 4.6 --- .../resources/ui/playwright/utils/common.ts | 47 +++++++++++-------- .../DataModel/DataModels/DataModelsTable.tsx | 7 --- .../ui/src/hooks/paging/usePaging.ts | 15 +++++- .../APICollectionPage/APIEndpointsTab.tsx | 3 ++ .../ContextCenterDocumentsPage.tsx | 8 +++- .../ContextCenterMemoriesPage.tsx | 9 +++- .../DatabaseSchemaPage/SchemaTablesTab.tsx | 3 ++ .../RolesPage/RolesListPage/RolesListPage.tsx | 3 ++ .../StoredProcedure/StoredProcedureTab.tsx | 10 ++-- .../src/pages/UserListPage/UserListPageV1.tsx | 3 ++ 10 files changed, 69 insertions(+), 39 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 0e546bce7e1c..addcef5c874f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -1114,6 +1114,9 @@ export const testPaginationNavigation = async ( await expect(page.getByTestId('previous')).toBeEnabled(); let afterValue: string | null = ''; if (validateUrl) { + await page.waitForURL( + (url) => url.searchParams.get('currentPage') === '2' + ); const currentUrl = page.url(); const urlObj = new URL(currentUrl); const searchParams = urlObj.searchParams; @@ -1147,9 +1150,7 @@ export const testPaginationNavigation = async ( await expect(page.getByTestId('previous')).toBeEnabled(); const paginationText = page.locator('[data-testid="page-indicator"]'); await expect(paginationText).toBeVisible(); - const paginationTextContent = await paginationText.textContent(); - - expect(paginationTextContent).toMatch(/2\s*of\s*\d+/); + await expect(paginationText).toHaveText(/2\s*of\s*\d+/); if (validateUrl) { const reloadedUrl = page.url(); @@ -1160,9 +1161,11 @@ export const testPaginationNavigation = async ( expect(reloadedSearchParams.get('cursorType')).toBe('after'); expect(reloadedSearchParams.get('cursorValue')).toBe(afterValue); } - await page.waitForLoadState('domcontentloaded'); const pageSizeDropdown = page.getByTestId('page-size-selection-dropdown'); - if (await pageSizeDropdown.isVisible()) { + const hasDropdown = (await pageSizeDropdown.count()) > 0; + + if (hasDropdown) { + await expect(pageSizeDropdown).toBeVisible(); await expect(pageSizeDropdown).toHaveText('15 / Page'); // Explicitly using selector, as in some cases table cell contains markdown @@ -1173,13 +1176,14 @@ export const testPaginationNavigation = async ( if (validateRowCount) { expect(initialRowCount).toBeLessThanOrEqual(15); } + const menuItem = page.getByRole('menuitem', { name: '25 / Page' }); + await pageSizeDropdown.scrollIntoViewIfNeeded(); await pageSizeDropdown.hover(); - const isMenuVisibleAfterHover = await menuItem.isVisible(); - if (!isMenuVisibleAfterHover) { + if (!(await menuItem.isVisible())) { await pageSizeDropdown.click(); } - await menuItem.waitFor({ state: 'visible' }); + await expect(menuItem).toBeVisible(); const pageSizeChangePromise = page.waitForResponse((response) => response.url().includes(apiEndpointPattern) @@ -1188,6 +1192,9 @@ export const testPaginationNavigation = async ( await pageSizeChangePromise; await waitForAllLoadersToDisappear(page); + await page.waitForURL((url) => url.searchParams.get('pageSize') === '25'); + await waitForAllLoadersToDisappear(page); + await expect(pageSizeDropdown).toHaveText('25 / Page'); const newRowCount = await page @@ -1462,10 +1469,10 @@ export const testCompletePaginationWithSearch = async ( await waitForAllLoadersToDisappear(page); await expect(page.getByTestId('previous')).toBeEnabled(); + await page.waitForURL((url) => url.searchParams.get('currentPage') === '2'); const paginationPage2 = page.locator('[data-testid="page-indicator"]'); await expect(paginationPage2).toBeVisible(); - const page2Content = await paginationPage2.textContent(); - expect(page2Content).toMatch(/2\s*of\s*\d+/); + await expect(paginationPage2).toHaveText(/2\s*of\s*\d+/); const searchResponsePromise = page.waitForResponse((response) => response.url().includes(searchApiPattern) @@ -1475,14 +1482,16 @@ export const testCompletePaginationWithSearch = async ( const searchResponse = await searchResponsePromise; expect(searchResponse.status()).toBe(200); + await page.waitForURL( + (url) => url.searchParams.get(searchParamName) === searchTestTerm + ); const urlAfterSearch = new URL(page.url()); expect(urlAfterSearch.searchParams.get(searchParamName)).toBe(searchTestTerm); await expect(page.getByTestId('previous')).toBeDisabled(); const paginationAfterSearch = page.locator('[data-testid="page-indicator"]'); await expect(paginationAfterSearch).toBeVisible(); - const searchPage1Content = await paginationAfterSearch.textContent(); - expect(searchPage1Content).toMatch(/1\s*of\s*\d+/); + await expect(paginationAfterSearch).toHaveText(/1\s*of\s*\d+/); const nextButtonAfterSearch = page.locator('[data-testid="next"]'); @@ -1495,10 +1504,10 @@ export const testCompletePaginationWithSearch = async ( expect(searchPage2Response.status()).toBe(200); await expect(page.getByTestId('previous')).toBeEnabled(); + await page.waitForURL((url) => url.searchParams.get('currentPage') === '2'); const paginationSearchPage2 = page.locator('[data-testid="page-indicator"]'); await expect(paginationSearchPage2).toBeVisible(); - const searchPage2Content = await paginationSearchPage2.textContent(); - expect(searchPage2Content).toMatch(/2\s*of\s*\d+/); + await expect(paginationSearchPage2).toHaveText(/2\s*of\s*\d+/); const reloadPromise = page.waitForResponse((response) => response.url().includes(searchApiPattern) @@ -1516,8 +1525,7 @@ export const testCompletePaginationWithSearch = async ( await expect(page.getByTestId('previous')).toBeEnabled(); const paginationAfterRefresh = page.locator('[data-testid="page-indicator"]'); await expect(paginationAfterRefresh).toBeVisible(); - const refreshPage2Content = await paginationAfterRefresh.textContent(); - expect(refreshPage2Content).toMatch(/2\s*of\s*\d+/); + await expect(paginationAfterRefresh).toHaveText(/2\s*of\s*\d+/); await expect(page.getByTestId('searchbar')).toHaveValue(searchTestTerm || ''); @@ -1548,10 +1556,11 @@ export const testCompletePaginationWithSearch = async ( '[data-testid="page-indicator"]' ); await expect(paginationAfterToggleWithSearch).toBeVisible(); - const toggleSearchContent = - await paginationAfterToggleWithSearch.textContent(); - expect(toggleSearchContent).toMatch(/1\s*of\s*\d+/); + await expect(paginationAfterToggleWithSearch).toHaveText(/1\s*of\s*\d+/); + await page.waitForURL( + (url) => url.searchParams.get(searchParamName) === searchTestTerm + ); const urlAfterToggle = new URL(page.url()); expect(urlAfterToggle.searchParams.get(searchParamName)).toBe( searchTestTerm diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelsTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelsTable.tsx index 234298aaa000..b41ee270aff1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelsTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelsTable.tsx @@ -82,13 +82,6 @@ const DataModelTable = ({ const [isLoading, setIsLoading] = useState(true); const { setFilters } = useTableFilters({}); - useEffect(() => { - const urlPage = Number(pagingCursor.currentPage) || INITIAL_PAGING_VALUE; - if (currentPage !== urlPage) { - handlePageChange(urlPage); - } - }, [pagingCursor.currentPage, currentPage, handlePageChange]); - const searchValue = useMemo(() => { const param = location.search; const searchData = QueryString.parse( diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts index f1557f7fd8a5..26f8dd52f4c0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts @@ -16,6 +16,7 @@ import { useCallback, useEffect, useMemo, + useRef, useState, } from 'react'; import { @@ -83,11 +84,21 @@ export const usePaging = (defaultPageSize?: number): UsePagingInterface => { const [currentPage, setCurrentPage] = useState(initialCurrentPage); const [pageSize, setPageSize] = useState(initialPageSize); + // Keep processedPageSize in a ref so the URL-sync effect can use the latest + // value as a fallback without including it in the dependency array. + // Including processedPageSize as a dep would cause the effect to fire when + // handlePageSize calls setPreference (Zustand synchronous update), at which + // point the URL is still stale — resetting pageSize and currentPage back to + // the old values before the deferred navigate commits. + const processedPageSizeRef = useRef(processedPageSize); + processedPageSizeRef.current = processedPageSize; + // Keep pagination in sync when filters or other controls update paging params directly in the URL. useEffect(() => { const nextCurrentPage = Number(urlParams.currentPage) || INITIAL_PAGING_VALUE; - const nextPageSize = Number(urlParams.pageSize) || processedPageSize; + const nextPageSize = + Number(urlParams.pageSize) || processedPageSizeRef.current; setCurrentPage((currentPage) => currentPage === nextCurrentPage ? currentPage : nextCurrentPage @@ -95,7 +106,7 @@ export const usePaging = (defaultPageSize?: number): UsePagingInterface => { setPageSize((pageSize) => pageSize === nextPageSize ? pageSize : nextPageSize ); - }, [processedPageSize, urlParams.currentPage, urlParams.pageSize]); + }, [urlParams.currentPage, urlParams.pageSize]); const pagingCursorUrlParams: PagingUrlParams = useMemo( () => ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx index 75df78eb5015..6b36f906a499 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx @@ -239,6 +239,9 @@ function APIEndpointsTab({ }, [searchValue, currentPage, filters.showDeletedEndpoints]); useEffect(() => { + if (pageSize !== pagingCursor.pageSize) { + return; + } if (searchValue) { return; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx index a3698af8fc01..62677655cd06 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx @@ -99,6 +99,8 @@ const ContextCenterDocumentsPage: FC = () => { const isLoadingMoreRef = useRef(false); const isLoadingMoreFoldersRef = useRef(false); const folderViewRef = useRef(null); + const previewFileRef = useRef(undefined); + previewFileRef.current = previewFile; const fetchGlobalFileCount = useCallback(async () => { try { @@ -295,7 +297,7 @@ const ContextCenterDocumentsPage: FC = () => { useEffect(() => { const documentId = searchParams.get('document'); - if (!documentId || isDocumentsLoading || previewFile) { + if (!documentId || isDocumentsLoading || previewFileRef.current) { return; } const match = allDocuments.find((d) => d.id === documentId); @@ -334,10 +336,12 @@ const ContextCenterDocumentsPage: FC = () => { }, [ allDocuments, isDocumentsLoading, - previewFile, searchParams, t, setSearchParams, + // previewFile intentionally removed — read via previewFileRef to prevent the effect + // from re-firing when the panel closes (previewFile → undefined), which would reopen + // it while searchParams still holds the stale document ID (react-router v7 startTransition) ]); const handleDeleteFile = useCallback((file: ContextFile) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx index 78733d1a45c9..f855e3413687 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx @@ -129,6 +129,8 @@ const ContextCenterMemoriesPage: FC = () => { const [debouncedSearch, setDebouncedSearch] = useState(''); const [debouncedAuthorSearch, setDebouncedAuthorSearch] = useState(''); const isAuthorSearchMounted = useRef(false); + const isViewModalOpenRef = useRef(isViewModalOpen); + isViewModalOpenRef.current = isViewModalOpen; const SORT_OPTIONS = useMemo( () => [ @@ -448,7 +450,7 @@ const ContextCenterMemoriesPage: FC = () => { useEffect(() => { const memoryName = searchParams.get('memory'); - if (!memoryName || isViewModalOpen) { + if (!memoryName || isViewModalOpenRef.current) { return; } @@ -462,7 +464,10 @@ const ContextCenterMemoriesPage: FC = () => { return prev; }); }); - }, [isViewModalOpen, searchParams, handleViewMemory, setSearchParams]); + }, [searchParams, handleViewMemory, setSearchParams]); + // isViewModalOpen intentionally removed — read via ref to prevent the effect + // from re-firing when the modal closes (isViewModalOpen → false), which would + // reopen it while searchParams still holds the stale memory name (react-router v7 startTransition) const handleModalSuccess = useCallback(() => { handleModalClose(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx index bf12ea30ac40..3dbde7d680d7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx @@ -302,6 +302,9 @@ function SchemaTablesTab({ }, [searchValue, currentPage, showDeletedSchemas]); useEffect(() => { + if (pageSize !== pagingCursor.pageSize) { + return; + } if (searchValue) { return; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx index b5bfee8103a1..8515fa4bec14 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx @@ -275,6 +275,9 @@ const RolesListPage = () => { }; useEffect(() => { + if (pageSize !== pagingCursor.pageSize) { + return; + } const { cursorType, cursorValue } = pagingCursor ?? {}; if (cursorType && cursorValue) { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedureTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedureTab.tsx index f12f8e3cf46e..a7c0729c391d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedureTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedureTab.tsx @@ -60,13 +60,6 @@ const StoredProcedureTab = () => { const [isLoading, setIsLoading] = useState(true); const { fqn: decodedDatabaseSchemaFQN } = useFqn(); - useEffect(() => { - const urlPage = Number(pagingCursor.currentPage) || INITIAL_PAGING_VALUE; - if (currentPage !== urlPage) { - handlePageChange(urlPage); - } - }, [pagingCursor.currentPage, currentPage, handlePageChange]); - const { filters: tableFilters, setFilters } = useTableFilters( INITIAL_TABLE_FILTERS ); @@ -208,6 +201,9 @@ const StoredProcedureTab = () => { }, [searchValue, currentPage, showDeletedStoredProcedures]); useEffect(() => { + if (pageSize !== pagingCursor.pageSize) { + return; + } if (searchValue) { return; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx index 784a7bc798a8..e9d85dcc7ec2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx @@ -250,6 +250,9 @@ const UserListPageV1 = () => { }, [searchValue, currentPage, isDeleted]); useEffect(() => { + if (pageSize !== pagingCursor.pageSize) { + return; + } if (searchValue) { return; } From 3c6b6100a5873c7b95630245a51bf22f03aef3fd Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Wed, 12 Aug 2026 12:27:23 +0530 Subject: [PATCH 04/11] playwright test fix --- .../e2e/Features/ContextCenterArticles.spec.ts | 5 ++++- .../e2e/Features/CustomizeDetailPage.spec.ts | 10 ++++++++++ .../Features/DataProductPersonaCustomization.spec.ts | 5 +++++ .../e2e/Features/DataQuality/TestLibrary.spec.ts | 8 +++++++- .../playwright/e2e/Features/ImpactAnalysis.spec.ts | 8 ++++++++ .../e2e/Pages/DescriptionVisibility.spec.ts | 5 +++++ .../ui/playwright/e2e/Pages/Glossary.spec.ts | 4 +++- .../e2e/Pages/Lineage/LineageFilters.spec.ts | 12 ++++++++++-- 8 files changed, 52 insertions(+), 5 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterArticles.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterArticles.spec.ts index 6166d5c14943..409cbbea62d9 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterArticles.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterArticles.spec.ts @@ -631,12 +631,15 @@ test.describe('Context Center Articles', () => { .getByTestId(`knowledge-card-${articleEntity.responseData.displayName}`); await expect(viewedCard).toBeVisible(); + const knowledgePageResponse = page.waitForResponse((response) => + response.url().includes('/api/v1/contextCenter/pages/name/') + ); await viewedCard.getByTestId('knowledge-page-link').first().click(); await page.waitForURL((url) => url.pathname.includes('/context-center/articles/') ); + await knowledgePageResponse; await waitForAllLoadersToDisappear(page); - await page.waitForTimeout(500); await navigateToArticles(page); const rightPanel = page.getByTestId('knowledge-center-right-panel'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts index 6e88c8dc1858..8d161f897400 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts @@ -508,8 +508,13 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .last() .getByRole('tab', { name: 'Custom Tab' }); + const pathnameBeforeTabActivation = new URL(userPage.url()).pathname; await customTab.focus(); await userPage.keyboard.press('Enter'); + await userPage.waitForURL( + (url) => url.pathname !== pathnameBeforeTabActivation + ); + await waitForAllLoadersToDisappear(userPage); await expect .poll(async () => @@ -663,8 +668,13 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .last() .getByRole('tab', { name: 'Custom Tab' }); + const pathnameBeforeTabActivation = new URL(userPage.url()).pathname; await customTab.focus(); await userPage.keyboard.press('Enter'); + await userPage.waitForURL( + (url) => url.pathname !== pathnameBeforeTabActivation + ); + await waitForAllLoadersToDisappear(userPage); await expect .poll(async () => diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataProductPersonaCustomization.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataProductPersonaCustomization.spec.ts index 22f05a44b8a8..67735f6562b3 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataProductPersonaCustomization.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataProductPersonaCustomization.spec.ts @@ -219,7 +219,12 @@ test.describe('Data Product Persona customization', () => { userPage.getByRole('tab', { name: 'Custom Tab' }) ).toBeVisible(); + const pathnameBeforeTabClick = new URL(userPage.url()).pathname; await userPage.getByRole('tab', { name: 'Custom Tab' }).click(); + await userPage.waitForURL( + (url) => url.pathname !== pathnameBeforeTabClick + ); + await waitForAllLoadersToDisappear(userPage); const visibleDescription = userPage .getByTestId(/KnowledgePanel.Description-/) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestLibrary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestLibrary.spec.ts index 6233cb1a03d3..cf46fb8ba756 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestLibrary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestLibrary.spec.ts @@ -18,7 +18,10 @@ import { toastNotification, uuid, } from '../../../utils/common'; -import { fillDeleteConfirmationIfPresent } from '../../../utils/entity'; +import { + fillDeleteConfirmationIfPresent, + waitForAllLoadersToDisappear, +} from '../../../utils/entity'; import { findSystemTestDefinition } from '../../../utils/testCases'; const TEST_DEFINITION_NAME = `AaroCustomTestDefinition${uuid()}`; @@ -1212,6 +1215,7 @@ test.describe( ); await nextButton.click(); await fetchResponse; + await waitForAllLoadersToDisappear(page); // Check again after page load isItemVisible = await testDefLocator.isVisible(); @@ -1256,6 +1260,7 @@ test.describe( expect(updateResponse.status()).toBe(200); await toastNotification(page, /updated successfully/i); + await waitForAllLoadersToDisappear(page); // Verify we stayed on the same page (previous button state should be unchanged) if (prevDisabledBefore) { @@ -1296,6 +1301,7 @@ test.describe( // Wait for the GET that happens after delete (page reset + fetch) await getResponse; + await waitForAllLoadersToDisappear(page); await toastNotification(page, /deleted successfully/i); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ImpactAnalysis.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ImpactAnalysis.spec.ts index 6b20cb6dd0b9..1ca7a971558b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ImpactAnalysis.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ImpactAnalysis.spec.ts @@ -1129,10 +1129,18 @@ test.describe('Impact Analysis', () => { const downstreamCount = await page.locator('[data-row-key]').count(); + const upstreamResponse = page.waitForResponse( + `/api/v1/lineage/getLineageByEntityCount?*` + ); await page.getByRole('radio', { name: 'Upstream' }).click(); + await upstreamResponse; await waitForAllLoadersToDisappear(page); + const downstreamResponse = page.waitForResponse( + `/api/v1/lineage/getLineageByEntityCount?*` + ); await page.getByRole('radio', { name: 'Downstream' }).click(); + await downstreamResponse; await waitForAllLoadersToDisappear(page); const finalCount = await page.locator('[data-row-key]').count(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts index 0539037ea7d5..7ec9032d4225 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts @@ -413,7 +413,12 @@ test.describe( await expect( userPage.getByRole('tab', { name: 'Description Tab' }) ).toBeVisible(); + const pathnameBeforeTabClick = new URL(userPage.url()).pathname; await userPage.getByRole('tab', { name: 'Description Tab' }).click(); + await userPage.waitForURL( + (url) => url.pathname !== pathnameBeforeTabClick + ); + await waitForAllLoadersToDisappear(userPage); const descriptionWidget = userPage .getByTestId(/KnowledgePanel.Description-/) 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 27120b4df830..a39d48232a45 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 @@ -2882,7 +2882,9 @@ test.describe('Glossary tests', () => { // Open delete modal await page.click('[data-testid="manage-button"]'); - await page.click('[data-testid="delete-button"]'); + const deleteButton = page.locator('[data-testid="delete-button"]'); + await expect(deleteButton).toBeVisible(); + await deleteButton.click(); // Verify delete modal is visible await expect(page.locator('[role="dialog"]')).toBeVisible(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts index 890f859526fa..7db15c579e93 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts @@ -316,7 +316,11 @@ test.describe('Lineage Filters', () => { await waitForAllLoadersToDisappear(page); await page.getByTestId('filters-button').click(); - await page.getByTestId(`search-dropdown-${filterTestId}`).click(); + const lineageFilterDropdown = page.getByTestId( + `search-dropdown-${filterTestId}` + ); + await expect(lineageFilterDropdown).toBeVisible(); + await lineageFilterDropdown.click(); await page.getByTitle(filterValue).click(); @@ -358,7 +362,11 @@ test.describe('Lineage Filters', () => { await waitForAllLoadersToDisappear(page); await page.getByTestId('filters-button').click(); - await page.getByTestId(`search-dropdown-${filterTestId}`).click(); + const impactAnalysisFilterDropdown = page.getByTestId( + `search-dropdown-${filterTestId}` + ); + await expect(impactAnalysisFilterDropdown).toBeVisible(); + await impactAnalysisFilterDropdown.click(); await page .getByTestId('drop-down-menu') From 8c01b4414a4b0d57abf56bc175605414c3c47c66 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Wed, 12 Aug 2026 13:07:27 +0530 Subject: [PATCH 05/11] minor fix --- .../Customization/GenericProvider/GenericProvider.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx index ec13608334b0..c384ae138d7e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx @@ -84,7 +84,6 @@ export const GenericProvider = >({ const pageType = useMemo(() => ENTITY_PAGE_TYPE_MAP[type], [type]); const { tab } = useRequiredParams<{ tab: EntityTabs }>(); const expandedLayout = useRef([]); - const selectedColumnRef = useRef(null); const [layout, setLayout] = useState( getLayoutFromCustomizedPage(pageType, tab, customizedPage, isVersionView) ); @@ -96,7 +95,6 @@ export const GenericProvider = >({ const [selectedColumn, setSelectedColumn] = useState( null ); - selectedColumnRef.current = selectedColumn; // Children (SchemaTable, ModelTab, etc.) register their sorted/filtered/paginated // column list here. Kept in a ref so the write does not re-render the provider on From abc10043236e26b0748b9876858b80e69208ef79 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Wed, 12 Aug 2026 18:30:40 +0530 Subject: [PATCH 06/11] fixed playwright tests --- .../e2e/Features/CustomizeDetailPage.spec.ts | 22 +++++++++---------- .../DataProductPersonaCustomization.spec.ts | 13 ++++++----- .../e2e/Pages/DescriptionVisibility.spec.ts | 10 ++++----- .../e2e/Pages/Lineage/LineageFilters.spec.ts | 16 +++++--------- 4 files changed, 26 insertions(+), 35 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts index 8d161f897400..08851c747a77 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts @@ -12,9 +12,9 @@ */ import { APIRequestContext, + test as base, expect, Page, - test as base, } from '@playwright/test'; import { PLAYWRIGHT_BASIC_TEST_TAG_OBJ } from '../../constant/config'; import { @@ -498,7 +498,8 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { await entity?.visitEntityPage(userPage); await waitForAllLoadersToDisappear(userPage); - + await userPage.waitForLoadState('domcontentloaded'); + await userPage.getByTestId('asset-description-container').waitFor({ state: 'visible' }); await expect( userPage.getByRole('tab', { name: 'Custom Tab' }) ).toBeVisible(); @@ -508,14 +509,12 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .last() .getByRole('tab', { name: 'Custom Tab' }); - const pathnameBeforeTabActivation = new URL(userPage.url()).pathname; await customTab.focus(); await userPage.keyboard.press('Enter'); - await userPage.waitForURL( - (url) => url.pathname !== pathnameBeforeTabActivation - ); await waitForAllLoadersToDisappear(userPage); - + await userPage.waitForLoadState('domcontentloaded'); + await userPage.getByTestId('asset-description-container').locator('visible=true').waitFor({ state: 'visible' }); + await expect .poll(async () => userPage.getByTestId(/KnowledgePanel.Description-/).count() @@ -658,7 +657,8 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { await entity?.visitEntityPage(userPage); await waitForAllLoadersToDisappear(userPage); await waitForAllLoadersToDisappear(userPage); - + await userPage.waitForLoadState('domcontentloaded'); + await userPage.getByTestId('asset-description-container').waitFor({ state: 'visible' }); await expect( userPage.getByRole('tab', { name: 'Custom Tab' }) ).toBeVisible(); @@ -668,13 +668,11 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .last() .getByRole('tab', { name: 'Custom Tab' }); - const pathnameBeforeTabActivation = new URL(userPage.url()).pathname; await customTab.focus(); await userPage.keyboard.press('Enter'); - await userPage.waitForURL( - (url) => url.pathname !== pathnameBeforeTabActivation - ); await waitForAllLoadersToDisappear(userPage); + await userPage.waitForLoadState('domcontentloaded'); + await userPage.getByTestId('asset-description-container').locator('visible=true').waitFor({ state: 'visible' }); await expect .poll(async () => diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataProductPersonaCustomization.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataProductPersonaCustomization.spec.ts index 67735f6562b3..8b1efad01b97 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataProductPersonaCustomization.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataProductPersonaCustomization.spec.ts @@ -12,9 +12,9 @@ */ import { APIRequestContext, + test as base, expect, Page, - test as base, } from '@playwright/test'; import { ECustomizedGovernance } from '../../constant/customizeDetail'; import { GlobalSettingOptions } from '../../constant/settings'; @@ -214,17 +214,18 @@ test.describe('Data Product Persona customization', () => { await entity?.visitEntityPage(userPage); await waitForAllLoadersToDisappear(userPage); - + await userPage.waitForLoadState('domcontentloaded'); + await userPage.getByTestId('asset-description-container').waitFor({ state: 'visible' }); await expect( userPage.getByRole('tab', { name: 'Custom Tab' }) ).toBeVisible(); - const pathnameBeforeTabClick = new URL(userPage.url()).pathname; + await userPage.getByRole('tab', { name: 'Custom Tab' }).click(); - await userPage.waitForURL( - (url) => url.pathname !== pathnameBeforeTabClick - ); + await waitForAllLoadersToDisappear(userPage); + await userPage.waitForLoadState('domcontentloaded'); + await userPage.getByTestId('asset-description-container').locator('visible=true').waitFor({ state: 'visible' }); const visibleDescription = userPage .getByTestId(/KnowledgePanel.Description-/) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts index 7ec9032d4225..758ab10886a4 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts @@ -409,17 +409,15 @@ test.describe( await table.visitEntityPage(userPage); await waitForAllLoadersToDisappear(userPage); - + await userPage.getByTestId('asset-description-container').waitFor({ state: 'visible' }); + await userPage.waitForLoadState('domcontentloaded'); await expect( userPage.getByRole('tab', { name: 'Description Tab' }) ).toBeVisible(); - const pathnameBeforeTabClick = new URL(userPage.url()).pathname; await userPage.getByRole('tab', { name: 'Description Tab' }).click(); - await userPage.waitForURL( - (url) => url.pathname !== pathnameBeforeTabClick - ); await waitForAllLoadersToDisappear(userPage); - + await userPage.waitForLoadState('domcontentloaded'); + await userPage.getByTestId(/KnowledgePanel.Description-/).locator('visible=true').waitFor({ state: 'visible' }); const descriptionWidget = userPage .getByTestId(/KnowledgePanel.Description-/) .locator('visible=true'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts index 7db15c579e93..8b4adbfbed3f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts @@ -316,11 +316,7 @@ test.describe('Lineage Filters', () => { await waitForAllLoadersToDisappear(page); await page.getByTestId('filters-button').click(); - const lineageFilterDropdown = page.getByTestId( - `search-dropdown-${filterTestId}` - ); - await expect(lineageFilterDropdown).toBeVisible(); - await lineageFilterDropdown.click(); + await page.getByTestId(`search-dropdown-${filterTestId}`).click(); await page.getByTitle(filterValue).click(); @@ -360,13 +356,11 @@ test.describe('Lineage Filters', () => { await impactAnalysisTab.scrollIntoViewIfNeeded(); await impactAnalysisTab.click(); await waitForAllLoadersToDisappear(page); - - await page.getByTestId('filters-button').click(); - const impactAnalysisFilterDropdown = page.getByTestId( + await page.getByTestId( `search-dropdown-${filterTestId}` - ); - await expect(impactAnalysisFilterDropdown).toBeVisible(); - await impactAnalysisFilterDropdown.click(); + ).waitFor({ state: 'detached' }); + await page.getByTestId('filters-button').click(); + await page.getByTestId(`search-dropdown-${filterTestId}`).click(); await page .getByTestId('drop-down-menu') From 02a0599b5c75088e678ffef2f1977ee40856ed54 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Thu, 13 Aug 2026 12:40:22 +0530 Subject: [PATCH 07/11] playwright fix --- .../e2e/Features/CustomizeDetailPage.spec.ts | 64 +++++++++++++++---- .../playwright/e2e/Pages/DataProducts.spec.ts | 18 ++++++ .../e2e/Pages/DescriptionVisibility.spec.ts | 27 +++++--- .../resources/ui/playwright/utils/common.ts | 21 ++++-- .../resources/ui/playwright/utils/domain.ts | 44 ++++++++++--- .../resources/ui/playwright/utils/entity.ts | 46 ++++++++++++- 6 files changed, 182 insertions(+), 38 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts index 08851c747a77..2d98aaa1255d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts @@ -43,6 +43,7 @@ import { validateLeftSidebarWithHiddenItems, } from '../../utils/customizeNavigation'; import { + clickTabAndWaitForPanel, getEncodedFqn, waitForAllLoadersToDisappear, } from '../../utils/entity'; @@ -498,8 +499,15 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { await entity?.visitEntityPage(userPage); await waitForAllLoadersToDisappear(userPage); + await waitForAllLoadersToDisappear(userPage, 'table-loading'); + await waitForAllLoadersToDisappear( + userPage, + 'entity-detail-widget-skeleton' + ); await userPage.waitForLoadState('domcontentloaded'); - await userPage.getByTestId('asset-description-container').waitFor({ state: 'visible' }); + await userPage + .getByTestId('asset-description-container') + .waitFor({ state: 'visible' }); await expect( userPage.getByRole('tab', { name: 'Custom Tab' }) ).toBeVisible(); @@ -509,12 +517,23 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .last() .getByRole('tab', { name: 'Custom Tab' }); - await customTab.focus(); - await userPage.keyboard.press('Enter'); - await waitForAllLoadersToDisappear(userPage); - await userPage.waitForLoadState('domcontentloaded'); - await userPage.getByTestId('asset-description-container').locator('visible=true').waitFor({ state: 'visible' }); - + // waitForLoadState('domcontentloaded') is a no-op on this SPA tab + // switch and guarded nothing against react-router v7's deferred + // navigate(); wait for the tab's own panel to become active instead. + await clickTabAndWaitForPanel(userPage, customTab); + // The Description widget is lazy-loaded and shows + // entity-detail-widget-skeleton (CommonWidgets' Suspense fallback) + // until its chunk resolves — without this wait the count() poll below + // can run while only the skeleton, not the widget, is in the DOM. + await waitForAllLoadersToDisappear( + userPage, + 'entity-detail-widget-skeleton' + ); + await userPage + .getByTestId('asset-description-container') + .locator('visible=true') + .waitFor({ state: 'visible' }); + await expect .poll(async () => userPage.getByTestId(/KnowledgePanel.Description-/).count() @@ -656,9 +675,15 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { await entity?.visitEntityPage(userPage); await waitForAllLoadersToDisappear(userPage); - await waitForAllLoadersToDisappear(userPage); + await waitForAllLoadersToDisappear(userPage, 'table-loading'); + await waitForAllLoadersToDisappear( + userPage, + 'entity-detail-widget-skeleton' + ); await userPage.waitForLoadState('domcontentloaded'); - await userPage.getByTestId('asset-description-container').waitFor({ state: 'visible' }); + await userPage + .getByTestId('asset-description-container') + .waitFor({ state: 'visible' }); await expect( userPage.getByRole('tab', { name: 'Custom Tab' }) ).toBeVisible(); @@ -668,11 +693,22 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .last() .getByRole('tab', { name: 'Custom Tab' }); - await customTab.focus(); - await userPage.keyboard.press('Enter'); - await waitForAllLoadersToDisappear(userPage); - await userPage.waitForLoadState('domcontentloaded'); - await userPage.getByTestId('asset-description-container').locator('visible=true').waitFor({ state: 'visible' }); + // waitForLoadState('domcontentloaded') is a no-op on this SPA tab + // switch and guarded nothing against react-router v7's deferred + // navigate(); wait for the tab's own panel to become active instead. + await clickTabAndWaitForPanel(userPage, customTab); + // The Description widget is lazy-loaded and shows + // entity-detail-widget-skeleton (CommonWidgets' Suspense fallback) + // until its chunk resolves — without this wait the count() poll below + // can run while only the skeleton, not the widget, is in the DOM. + await waitForAllLoadersToDisappear( + userPage, + 'entity-detail-widget-skeleton' + ); + await userPage + .getByTestId('asset-description-container') + .locator('visible=true') + .waitFor({ state: 'visible' }); await expect .poll(async () => diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProducts.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProducts.spec.ts index 542849336f37..877928035cf5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProducts.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProducts.spec.ts @@ -227,13 +227,27 @@ test.describe('Data Products', () => { await test.step('Navigate to Data Products page', async () => { await sidebarClick(page, SidebarItem.DATA_PRODUCT); await waitForAllLoadersToDisappear(page); + await page.waitForLoadState('domcontentloaded'); }); await test.step('Search for specific data product', async () => { + // The listing page debounces search input by 300ms before firing + // /api/v1/search/query — waitForAllLoadersToDisappear alone doesn't + // guarantee that debounced request has even been sent yet, so assertions + // right after can still see the pre-search (unfiltered) list. + await page + .getByRole('main') + .getByPlaceholder('Search').waitFor({ state: 'visible' }); + const searchResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/search/query') && + response.url().includes(dataProduct1.data.name) + ); await page .getByRole('main') .getByPlaceholder('Search') .fill(dataProduct1.data.name); + await searchResponse; await waitForAllLoadersToDisappear(page); await expect(page.getByText(dataProduct1.data.displayName)).toBeVisible(); @@ -243,7 +257,11 @@ test.describe('Data Products', () => { }); await test.step('Clear search', async () => { + const clearResponse = page.waitForResponse((response) => + response.url().includes('/api/v1/search/query') + ); await page.getByRole('main').getByPlaceholder('Search').clear(); + await clearResponse; await waitForAllLoadersToDisappear(page); await expect(page.getByLabel('Pagination Navigation')).toBeVisible(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts index 758ab10886a4..39e270cd17ab 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts @@ -41,7 +41,10 @@ import { verifyDescriptionRequiresScroll, verifyEndOfDescriptionReachable, } from '../../utils/domain'; -import { waitForAllLoadersToDisappear } from '../../utils/entity'; +import { + clickTabAndWaitForPanel, + waitForAllLoadersToDisappear, +} from '../../utils/entity'; import { navigateToPersonaWithPagination } from '../../utils/persona'; import { settingClick, sidebarClick } from '../../utils/sidebar'; import { test } from '../fixtures/pages'; @@ -409,18 +412,24 @@ test.describe( await table.visitEntityPage(userPage); await waitForAllLoadersToDisappear(userPage); - await userPage.getByTestId('asset-description-container').waitFor({ state: 'visible' }); - await userPage.waitForLoadState('domcontentloaded'); - await expect( - userPage.getByRole('tab', { name: 'Description Tab' }) - ).toBeVisible(); - await userPage.getByRole('tab', { name: 'Description Tab' }).click(); - await waitForAllLoadersToDisappear(userPage); + await userPage + .getByTestId('asset-description-container') + .waitFor({ state: 'visible' }); await userPage.waitForLoadState('domcontentloaded'); - await userPage.getByTestId(/KnowledgePanel.Description-/).locator('visible=true').waitFor({ state: 'visible' }); + const descriptionTab = userPage.getByRole('tab', { + name: 'Description Tab', + }); + await expect(descriptionTab).toBeVisible(); + // waitForLoadState('domcontentloaded') is a no-op here — an SPA tab click + // triggers no real navigation, so it resolved instantly and guarded + // nothing against react-router v7's deferred navigate(). Wait for the + // active tabpanel instead so the old "Columns" tab content can't still be + // the thing this assertion matches against. + await clickTabAndWaitForPanel(userPage, descriptionTab); const descriptionWidget = userPage .getByTestId(/KnowledgePanel.Description-/) .locator('visible=true'); + await descriptionWidget.waitFor({ state: 'visible' }); await expect(descriptionWidget).toBeVisible(); // Widget truncates long content behind a "more" button 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 8da0204c92a0..5f9a2be20a97 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -1131,9 +1131,7 @@ export const testPaginationNavigation = async ( await expect(page.getByTestId('previous')).toBeEnabled(); let afterValue: string | null = ''; if (validateUrl) { - await page.waitForURL( - (url) => url.searchParams.get('currentPage') === '2' - ); + await page.waitForURL((url) => url.searchParams.get('currentPage') === '2'); const currentUrl = page.url(); const urlObj = new URL(currentUrl); const searchParams = urlObj.searchParams; @@ -1214,9 +1212,20 @@ export const testPaginationNavigation = async ( await expect(pageSizeDropdown).toHaveText('25 / Page'); - const newRowCount = await page - .locator('tbody > tr[data-row-key]:visible') - .count(); + // The page-size API response resolving and the URL settling to + // pageSize=25 don't guarantee React has committed the re-render that + // grows the table under react-router v7's deferred navigate() — a + // one-shot count() can still catch the stale 15-row DOM in that window. + // Poll until the row count actually reflects the change before reading + // it for the assertions below. + const rowsLocator = page.locator('tbody > tr[data-row-key]:visible'); + if (validateRowCount) { + await expect + .poll(() => rowsLocator.count(), { timeout: 15_000 }) + .not.toBe(initialRowCount); + } + + const newRowCount = await rowsLocator.count(); if (validateRowCount) { expect(newRowCount).toBeLessThanOrEqual(25); expect(newRowCount).not.toBe(initialRowCount); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts index 37c05090bc17..c1ff5832cd75 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts @@ -245,15 +245,45 @@ export const removeCertificationFromWidget = async ( await expect(page.getByTestId('add-certification')).toBeVisible(); }; +/** + * Clicks whichever of add-domain / edit-domain is currently showing. A prior + * domain assignment triggers a PATCH-driven re-render that swaps one button for + * the other, but that re-render is deferred under react-router v7's + * startTransition — a single isVisible() check right after can still see the + * stale button. Poll until exactly one of the two is actually visible. + */ +const clickAddOrEditDomainButton = async (page: Page) => { + const addBtn = page.getByTestId('add-domain'); + const editBtn = page.getByTestId('edit-domain'); + + let visibility: { addVisible: boolean; editVisible: boolean } = { + addVisible: false, + editVisible: false, + }; + + await expect + .poll( + async () => { + visibility = { + addVisible: await addBtn.isVisible(), + editVisible: await editBtn.isVisible(), + }; + + return visibility.addVisible || visibility.editVisible; + }, + { timeout: 15_000 } + ) + .toBe(true); + + await (visibility.addVisible ? addBtn : editBtn).click(); +}; + export const assignDomainWidget = async ( page: Page, domain: { name: string; displayName: string; fullyQualifiedName?: string }, multiSelect = false ) => { - const addBtn = page.getByTestId('add-domain'); - const editBtn = page.getByTestId('edit-domain'); - const isAdd = await addBtn.isVisible(); - await (isAdd ? addBtn : editBtn).click(); + await clickAddOrEditDomainButton(page); await waitForAllLoadersToDisappear(page); const searchDomain = page.waitForResponse( @@ -296,10 +326,7 @@ export const removeDomainWidget = async ( page: Page, domain: { name: string; displayName: string; fullyQualifiedName?: string } ) => { - const addBtn = page.getByTestId('add-domain'); - const editBtn = page.getByTestId('edit-domain'); - const isAdd = await addBtn.isVisible(); - await (isAdd ? addBtn : editBtn).click(); + await clickAddOrEditDomainButton(page); await waitForAllLoadersToDisappear(page); await page @@ -1706,6 +1733,7 @@ export const waitForPortRow = async (page: Page, portId: string) => { await page.reload({ waitUntil: 'domcontentloaded' }); await waitForAllLoadersToDisappear(page); + await page.getByTestId('input-output-ports-tab').waitFor({ state: 'visible' }); if (!(await page.getByTestId('input-output-ports-tab').isVisible())) { await navigateToPortsTab(page); 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 6dd8ae5c7c6e..68bce3673478 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -22,8 +22,8 @@ import { import { ES_RESERVED_CHARACTERS } from '../constant/entity'; import { SidebarItem } from '../constant/sidebar'; import { - EntityTypeEndpoint, ENTITY_PATH, + EntityTypeEndpoint, } from '../support/entity/Entity.interface'; import { EntityClass } from '../support/entity/EntityClass'; import { EntityType } from '../support/entity/EntityDataClass.interface'; @@ -59,6 +59,50 @@ export const waitForAllLoadersToDisappear = async ( await expect(loaders).toHaveCount(0, { timeout }); }; +/** + * Clicks a tab and waits for react-router v7's deferred navigate() (wrapped in + * React.startTransition) to actually commit before returning — otherwise callers can + * assert against the previous tab's still-mounted content. Optionally waits for an API + * response tied to the new tab's data. + */ +export const clickTabAndWaitForPanel = async ( + page: Page, + tab: Locator, + options?: { + urlIncludes?: string; + responseMatcher?: (response: Response) => boolean; + } +) => { + const responsePromise = options?.responseMatcher + ? page.waitForResponse(options.responseMatcher) + : undefined; + + await tab.click(); + + // startTransition can defer the commit across more than one rAF — a single + // requestAnimationFrame round trip isn't guaranteed to land after it flushes. + // Two consecutive frames force at least one full paint cycle to elapse since + // the click before we read anything URL- or DOM-derived below. + + if (responsePromise) { + await responsePromise; + } + + await waitForAllLoadersToDisappear(page); + + // aria-selected flips only once activeKey has actually caught up to the + // click — the single most direct signal that the deferred transition + // committed, independent of URL shape or DOM tab order. + await expect(tab).toHaveAttribute('aria-selected', 'true'); + + const panelId = await tab.getAttribute('aria-controls'); + const panel = panelId + ? page.locator(`#${panelId}`) + : page.locator('[role="tabpanel"][aria-hidden="false"]').first(); + + await panel.waitFor({ state: 'visible' }); +}; + export const visitEntityPage = async (data: { page: Page; searchTerm: string; From 18c2f6d2a5cddf0fae92dad04919ea7d3d181ef0 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Thu, 13 Aug 2026 22:55:49 +0530 Subject: [PATCH 08/11] playwright fix --- .../e2e/Features/CustomizeDetailPage.spec.ts | 29 +++++------- .../e2e/Pages/DescriptionVisibility.spec.ts | 18 +++----- .../resources/ui/playwright/utils/entity.ts | 44 ------------------- 3 files changed, 18 insertions(+), 73 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts index 2d98aaa1255d..15a2b7296da4 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts @@ -43,9 +43,8 @@ import { validateLeftSidebarWithHiddenItems, } from '../../utils/customizeNavigation'; import { - clickTabAndWaitForPanel, getEncodedFqn, - waitForAllLoadersToDisappear, + waitForAllLoadersToDisappear } from '../../utils/entity'; import { navigateToPersonaWithPagination } from '../../utils/persona'; import { settingClick } from '../../utils/sidebar'; @@ -517,14 +516,11 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .last() .getByRole('tab', { name: 'Custom Tab' }); - // waitForLoadState('domcontentloaded') is a no-op on this SPA tab - // switch and guarded nothing against react-router v7's deferred - // navigate(); wait for the tab's own panel to become active instead. - await clickTabAndWaitForPanel(userPage, customTab); - // The Description widget is lazy-loaded and shows - // entity-detail-widget-skeleton (CommonWidgets' Suspense fallback) - // until its chunk resolves — without this wait the count() poll below - // can run while only the skeleton, not the widget, is in the DOM. + await customTab.focus(); + await userPage.keyboard.press('Enter'); + await waitForAllLoadersToDisappear(userPage); + await userPage.waitForLoadState('domcontentloaded'); + await waitForAllLoadersToDisappear( userPage, 'entity-detail-widget-skeleton' @@ -693,14 +689,11 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .last() .getByRole('tab', { name: 'Custom Tab' }); - // waitForLoadState('domcontentloaded') is a no-op on this SPA tab - // switch and guarded nothing against react-router v7's deferred - // navigate(); wait for the tab's own panel to become active instead. - await clickTabAndWaitForPanel(userPage, customTab); - // The Description widget is lazy-loaded and shows - // entity-detail-widget-skeleton (CommonWidgets' Suspense fallback) - // until its chunk resolves — without this wait the count() poll below - // can run while only the skeleton, not the widget, is in the DOM. + await customTab.focus(); + await userPage.keyboard.press('Enter'); + await waitForAllLoadersToDisappear(userPage); + await userPage.waitForLoadState('domcontentloaded'); + await waitForAllLoadersToDisappear( userPage, 'entity-detail-widget-skeleton' diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts index 39e270cd17ab..3d2a7aa44a2e 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts @@ -42,8 +42,7 @@ import { verifyEndOfDescriptionReachable, } from '../../utils/domain'; import { - clickTabAndWaitForPanel, - waitForAllLoadersToDisappear, + waitForAllLoadersToDisappear } from '../../utils/entity'; import { navigateToPersonaWithPagination } from '../../utils/persona'; import { settingClick, sidebarClick } from '../../utils/sidebar'; @@ -416,16 +415,13 @@ test.describe( .getByTestId('asset-description-container') .waitFor({ state: 'visible' }); await userPage.waitForLoadState('domcontentloaded'); - const descriptionTab = userPage.getByRole('tab', { - name: 'Description Tab', - }); + const descriptionTab = userPage.getByRole('tab', { name: 'Description Tab' }); await expect(descriptionTab).toBeVisible(); - // waitForLoadState('domcontentloaded') is a no-op here — an SPA tab click - // triggers no real navigation, so it resolved instantly and guarded - // nothing against react-router v7's deferred navigate(). Wait for the - // active tabpanel instead so the old "Columns" tab content can't still be - // the thing this assertion matches against. - await clickTabAndWaitForPanel(userPage, descriptionTab); + await descriptionTab.focus(); + await userPage.keyboard.press('Enter'); + await waitForAllLoadersToDisappear(userPage); + await userPage.waitForLoadState('domcontentloaded'); + const descriptionWidget = userPage .getByTestId(/KnowledgePanel.Description-/) .locator('visible=true'); 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 68bce3673478..3ec5378a1b9b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -59,50 +59,6 @@ export const waitForAllLoadersToDisappear = async ( await expect(loaders).toHaveCount(0, { timeout }); }; -/** - * Clicks a tab and waits for react-router v7's deferred navigate() (wrapped in - * React.startTransition) to actually commit before returning — otherwise callers can - * assert against the previous tab's still-mounted content. Optionally waits for an API - * response tied to the new tab's data. - */ -export const clickTabAndWaitForPanel = async ( - page: Page, - tab: Locator, - options?: { - urlIncludes?: string; - responseMatcher?: (response: Response) => boolean; - } -) => { - const responsePromise = options?.responseMatcher - ? page.waitForResponse(options.responseMatcher) - : undefined; - - await tab.click(); - - // startTransition can defer the commit across more than one rAF — a single - // requestAnimationFrame round trip isn't guaranteed to land after it flushes. - // Two consecutive frames force at least one full paint cycle to elapse since - // the click before we read anything URL- or DOM-derived below. - - if (responsePromise) { - await responsePromise; - } - - await waitForAllLoadersToDisappear(page); - - // aria-selected flips only once activeKey has actually caught up to the - // click — the single most direct signal that the deferred transition - // committed, independent of URL shape or DOM tab order. - await expect(tab).toHaveAttribute('aria-selected', 'true'); - - const panelId = await tab.getAttribute('aria-controls'); - const panel = panelId - ? page.locator(`#${panelId}`) - : page.locator('[role="tabpanel"][aria-hidden="false"]').first(); - - await panel.waitFor({ state: 'visible' }); -}; - export const visitEntityPage = async (data: { page: Page; searchTerm: string; From 98f84c875fd92ea88b936a7cf5894f6044a5079a Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Fri, 14 Aug 2026 13:08:43 +0530 Subject: [PATCH 09/11] minor fix --- .../ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts | 2 ++ .../ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts index 15a2b7296da4..f8641dedb2fa 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts @@ -518,6 +518,7 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { await customTab.focus(); await userPage.keyboard.press('Enter'); + await userPage.waitForTimeout(5000); // Wait for the tab content to load await waitForAllLoadersToDisappear(userPage); await userPage.waitForLoadState('domcontentloaded'); @@ -691,6 +692,7 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { await customTab.focus(); await userPage.keyboard.press('Enter'); + await userPage.waitForTimeout(5000); // Wait for the tab content to load await waitForAllLoadersToDisappear(userPage); await userPage.waitForLoadState('domcontentloaded'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts index 3d2a7aa44a2e..564d7b35f939 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts @@ -418,7 +418,8 @@ test.describe( const descriptionTab = userPage.getByRole('tab', { name: 'Description Tab' }); await expect(descriptionTab).toBeVisible(); await descriptionTab.focus(); - await userPage.keyboard.press('Enter'); + await userPage.keyboard.press('Enter'); + await userPage.waitForTimeout(5000); // Wait for the tab content to load await waitForAllLoadersToDisappear(userPage); await userPage.waitForLoadState('domcontentloaded'); From 7463147c4aed4bf80e0c5edaf4eaf3ba53fe2d69 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Sat, 15 Aug 2026 16:22:39 +0530 Subject: [PATCH 10/11] minor fix --- .../components/Customization/GenericTab/GenericTab.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx index a11eb5ce6013..35b922580dfd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx @@ -18,10 +18,12 @@ import { GRID_VERTICAL_MARGIN, } from '../../../constants/CustomizeWidgets.constants'; import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../../../enums/entity.enum'; import { PageType } from '../../../generated/system/ui/page'; import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; import { getWidgetsFromKey } from '../../../utils/CustomizePage/CustomizePageDispatchUtils'; +import { useRequiredParams } from '../../../utils/useRequiredParams'; import { useGenericContext } from '../GenericProvider/GenericContext'; import { DynamicHeightWidget } from './DynamicHeightWidget'; import './generic-tab.less'; @@ -41,6 +43,11 @@ interface GenericTabProps { export const GenericTab = ({ type, variant = 'default' }: GenericTabProps) => { const { layout, updateWidgetHeight } = useGenericContext(); + // react-router v7 defers navigate() via React.startTransition, so this param can + // briefly lag the AntD tab pane that just became active while AntD keeps the + // previous pane mounted. Keying the grid on `tab` forces a clean remount instead + // of patching stale widgets from the previous tab in place. + const { tab } = useRequiredParams<{ tab: EntityTabs }>(); const handleHeightChange = useCallback( (widgetId: string, newHeight: number) => { @@ -103,6 +110,7 @@ export const GenericTab = ({ type, variant = 'default' }: GenericTabProps) => { containerPadding={[1, 0]} isDraggable={false} isResizable={false} + key={tab} margin={[GRID_VERTICAL_MARGIN, GRID_VERTICAL_MARGIN]} preventCollision={false} rowHeight={GRID_ROW_HEIGHT}> From bccf061996c1bcf815950ea5c4f22d842bd12558 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Sat, 15 Aug 2026 23:28:22 +0530 Subject: [PATCH 11/11] playwright fix --- .../e2e/Features/CustomizeDetailPage.spec.ts | 25 ++++---- .../e2e/Pages/DescriptionVisibility.spec.ts | 33 ++++++++--- .../ui/playwright/e2e/Pages/Policies.spec.ts | 15 +++++ .../resources/ui/playwright/utils/entity.ts | 57 ++++++++++++++++++- 4 files changed, 108 insertions(+), 22 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts index f8641dedb2fa..770f4af370d0 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts @@ -12,9 +12,9 @@ */ import { APIRequestContext, - test as base, expect, Page, + test as base, } from '@playwright/test'; import { PLAYWRIGHT_BASIC_TEST_TAG_OBJ } from '../../constant/config'; import { @@ -43,8 +43,9 @@ import { validateLeftSidebarWithHiddenItems, } from '../../utils/customizeNavigation'; import { + clickTabAndWaitForPanel, getEncodedFqn, - waitForAllLoadersToDisappear + waitForAllLoadersToDisappear, } from '../../utils/entity'; import { navigateToPersonaWithPagination } from '../../utils/persona'; import { settingClick } from '../../utils/sidebar'; @@ -516,12 +517,10 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .last() .getByRole('tab', { name: 'Custom Tab' }); - await customTab.focus(); - await userPage.keyboard.press('Enter'); - await userPage.waitForTimeout(5000); // Wait for the tab content to load - await waitForAllLoadersToDisappear(userPage); - await userPage.waitForLoadState('domcontentloaded'); - + // waitForLoadState('domcontentloaded') is a no-op on this SPA tab + // switch and guarded nothing against react-router v7's deferred + // navigate(); wait for the tab's own panel to become active instead. + await clickTabAndWaitForPanel(userPage, customTab); await waitForAllLoadersToDisappear( userPage, 'entity-detail-widget-skeleton' @@ -690,12 +689,10 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .last() .getByRole('tab', { name: 'Custom Tab' }); - await customTab.focus(); - await userPage.keyboard.press('Enter'); - await userPage.waitForTimeout(5000); // Wait for the tab content to load - await waitForAllLoadersToDisappear(userPage); - await userPage.waitForLoadState('domcontentloaded'); - + // waitForLoadState('domcontentloaded') is a no-op on this SPA tab + // switch and guarded nothing against react-router v7's deferred + // navigate(); wait for the tab's own panel to become active instead. + await clickTabAndWaitForPanel(userPage, customTab); await waitForAllLoadersToDisappear( userPage, 'entity-detail-widget-skeleton' diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts index 564d7b35f939..cb2f18ff8cf4 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts @@ -42,7 +42,8 @@ import { verifyEndOfDescriptionReachable, } from '../../utils/domain'; import { - waitForAllLoadersToDisappear + clickTabAndWaitForPanel, + waitForAllLoadersToDisappear, } from '../../utils/entity'; import { navigateToPersonaWithPagination } from '../../utils/persona'; import { settingClick, sidebarClick } from '../../utils/sidebar'; @@ -415,13 +416,20 @@ test.describe( .getByTestId('asset-description-container') .waitFor({ state: 'visible' }); await userPage.waitForLoadState('domcontentloaded'); - const descriptionTab = userPage.getByRole('tab', { name: 'Description Tab' }); + const descriptionTab = userPage.getByRole('tab', { + name: 'Description Tab', + }); await expect(descriptionTab).toBeVisible(); - await descriptionTab.focus(); - await userPage.keyboard.press('Enter'); - await userPage.waitForTimeout(5000); // Wait for the tab content to load - await waitForAllLoadersToDisappear(userPage); - await userPage.waitForLoadState('domcontentloaded'); + // waitForLoadState('domcontentloaded') is a no-op here — an SPA tab click + // triggers no real navigation, so it resolved instantly and guarded + // nothing against react-router v7's deferred navigate(). Wait for the + // active tabpanel instead so the old "Columns" tab content can't still be + // the thing this assertion matches against. + await clickTabAndWaitForPanel(userPage, descriptionTab); + await waitForAllLoadersToDisappear( + userPage, + 'entity-detail-widget-skeleton' + ); const descriptionWidget = userPage .getByTestId(/KnowledgePanel.Description-/) @@ -436,6 +444,17 @@ test.describe( await expect(moreButton).toBeVisible(); await moreButton.click(); + // The click flips readMore state, which swaps the BlockEditor (Tiptap) + // content prop from the truncated preview to the full text — but Tiptap + // applies content changes asynchronously (an effect-gated setTimeout, + // then a transaction event deferred behind two requestAnimationFrames), + // so the full text isn't guaranteed to be in the DOM immediately after + // the click. The button's own testid flipping to read-less-button is a + // direct signal that React has re-rendered with the expanded state. + await descriptionWidget + .getByTestId('read-less-button') + .waitFor({ state: 'visible' }); + await verifyEndOfDescriptionReachable(descriptionWidget, userPage); await userPage.close(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Policies.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Policies.spec.ts index aed5ea0cb1f7..2058c1ead926 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Policies.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Policies.spec.ts @@ -224,6 +224,14 @@ test.describe( // Click on add rule button await page.locator('[data-testid="add-rule"]').click(); + // add-rule navigates to AddRulePage, a real route change deferred + // under react-router v7's startTransition — without waiting for the + // new page's own marker, addRule()'s fill below can still hit + // [data-testid="rule-name"] on the *previous* page (a read-only span + // for an existing rule, reusing the same testid as the new page's + // input) while the navigation is still pending. + await page.getByTestId('add-rule-title').waitFor({ state: 'visible' }); + // Add rule (assuming addRule is a function you have defined elsewhere) await addRule(page, NEW_RULE_NAME, NEW_RULE_DESCRIPTION, 0); @@ -259,6 +267,13 @@ test.describe( // Click on edit rule button await page.locator('[data-testid="edit-rule"]').click(); + // edit-rule navigates to EditRulePage, a real route change deferred + // under react-router v7's startTransition — wait for the new page's + // own marker before touching [data-testid="rule-name"], which is + // otherwise ambiguous with the read-only rule-name span still on the + // previous page during the pending navigation. + await page.getByTestId('edit-rule-title').waitFor({ state: 'visible' }); + // Enter new name await page.locator('[data-testid="rule-name"]').fill(UPDATED_RULE_NAME); 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 0f02ad768b37..2009de147c7d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -22,8 +22,8 @@ import { import { ES_RESERVED_CHARACTERS } from '../constant/entity'; import { SidebarItem } from '../constant/sidebar'; import { - ENTITY_PATH, EntityTypeEndpoint, + ENTITY_PATH, } from '../support/entity/Entity.interface'; import { EntityClass } from '../support/entity/EntityClass'; import { EntityType } from '../support/entity/EntityDataClass.interface'; @@ -59,6 +59,61 @@ export const waitForAllLoadersToDisappear = async ( await expect(loaders).toHaveCount(0, { timeout }); }; +/** + * Clicks a tab and waits for react-router v7's deferred navigate() (wrapped in + * React.startTransition) to actually commit before returning — otherwise callers can + * assert against the previous tab's still-mounted content. Optionally waits for an API + * response tied to the new tab's data. + */ +export const clickTabAndWaitForPanel = async ( + page: Page, + tab: Locator, + options?: { + urlIncludes?: string; + responseMatcher?: (response: Response) => boolean; + } +) => { + const responsePromise = options?.responseMatcher + ? page.waitForResponse(options.responseMatcher) + : undefined; + + await tab.click(); + + if (options?.urlIncludes) { + const urlIncludes = options.urlIncludes; + await page.waitForURL( + (url) => + url.pathname.includes(urlIncludes) || url.search.includes(urlIncludes) + ); + } + + if (responsePromise) { + await responsePromise; + } + + await waitForAllLoadersToDisappear(page); + + // aria-selected flips only once activeKey has actually caught up to the + // click — expect() polls until this settles, which is what actually closes + // the react-router v7 startTransition race (no fixed wait/frame count needed). + await expect(tab).toHaveAttribute('aria-selected', 'true'); + + // Re-read aria-controls on every poll rather than once: a single read right + // after aria-selected flips can still catch a transient/empty value before + // the panel wiring itself has settled, since the two attributes don't + // necessarily commit in the same render. + await expect + .poll(async () => { + const panelId = await tab.getAttribute('aria-controls'); + const panel = panelId + ? page.locator(`#${panelId}`) + : page.locator('[role="tabpanel"][aria-hidden="false"]').first(); + + return panel.isVisible(); + }) + .toBe(true); +}; + export const visitEntityPage = async (data: { page: Page; searchTerm: string;