diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ExploreDiscovery.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ExploreDiscovery.spec.ts index 271435faa77..f4829e13a08 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ExploreDiscovery.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ExploreDiscovery.spec.ts @@ -19,13 +19,16 @@ import { getJsonTreeObject } from '../../utils/exploreDiscovery'; test.use({ storageState: 'playwright/.auth/admin.json' }); const table = new TableClass(); +const table1 = new TableClass(); test.describe('Explore Assets Discovery', () => { test.beforeAll(async ({ browser }) => { const { apiContext, afterAction } = await createNewPage(browser); await table.create(apiContext); + await table1.create(apiContext); await table.delete(apiContext, false); + // await table1.delete(apiContext, false); await afterAction(); }); @@ -34,6 +37,7 @@ test.describe('Explore Assets Discovery', () => { const { apiContext, afterAction } = await createNewPage(browser); await table.delete(apiContext); + await table1.delete(apiContext); await afterAction(); }); @@ -183,4 +187,46 @@ test.describe('Explore Assets Discovery', () => { ) ).not.toBeAttached(); }); + + test('Should not display soft deleted assets in search suggestions', async ({ + page, + }) => { + await table1.visitEntityPage(page); + await page.waitForLoadState('networkidle'); + + await page.getByTestId('manage-button').click(); + await page.getByTestId('delete-button').click(); + + await expect( + page + .locator('.ant-modal-title') + .getByText( + `Delete table "${ + table1.entityResponseData.displayName ?? + table1.entityResponseData.name + }"` + ) + ).toBeVisible(); + + await page.getByTestId('confirmation-text-input').click(); + await page.getByTestId('confirmation-text-input').fill('DELETE'); + + await expect(page.getByTestId('confirm-button')).toBeEnabled(); + + await page.getByTestId('confirm-button').click(); + + await page.reload(); + + await expect(page.getByTestId('deleted-badge')).toBeVisible(); + + await redirectToHomePage(page); + await page.waitForLoadState('networkidle'); + + await page.getByTestId('searchBox').click(); + await page.getByTestId('searchBox').fill(table1.entityResponseData.name); + + expect( + page.locator('.ant-popover-inner-content').textContent() + ).not.toContain(table1.entityResponseData.name); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.test.tsx new file mode 100644 index 00000000000..a7293295a7a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.test.tsx @@ -0,0 +1,171 @@ +/* + * Copyright 2025 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. + */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* + * Copyright 2022 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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 { useTranslation } from 'react-i18next'; +import { useTourProvider } from '../../context/TourProvider/TourProvider'; +import { SearchIndex } from '../../enums/search.enum'; +import { searchQuery } from '../../rest/searchAPI'; +import Suggestions from './Suggestions'; + +// Mock dependencies +jest.mock('../../rest/searchAPI'); +jest.mock('react-i18next', () => ({ + useTranslation: jest.fn(), +})); +jest.mock('../../context/TourProvider/TourProvider'); +jest.mock('../../utils/SearchUtils', () => ({ + filterOptionsByIndex: jest.fn((options, index) => { + return options.filter((option: any) => option._index === index); + }), + getGroupLabel: jest.fn((index) => `Group ${index}`), + getSuggestionElement: jest.fn((suggestion) => ( +
+ {suggestion._source.name} +
+ )), +})); +jest.mock('../../utils/SearchClassBase', () => ({ + getEntitiesSuggestions: jest.fn(() => []), +})); +jest.mock('../../utils/CommonUtils', () => ({ + Transi18next: ({ i18nKey, values }: { i18nKey: string; values: any }) => ( + + {i18nKey} {values?.keyword || ''} + + ), +})); + +// Mock location.search for the component +Object.defineProperty(window, 'location', { + value: { + search: '', + }, + writable: true, +}); + +const mockSearchQuery = searchQuery as jest.MockedFunction; +const mockUseTranslation = useTranslation as jest.MockedFunction< + typeof useTranslation +>; +const mockUseTourProvider = useTourProvider as jest.MockedFunction< + typeof useTourProvider +>; + +const defaultProps = { + searchText: 'test', + setIsOpen: jest.fn(), + isOpen: true, + searchCriteria: SearchIndex.TABLE, + isNLPActive: false, + onSearchTextUpdate: jest.fn(), +}; + +describe('Suggestions Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUseTranslation.mockReturnValue({ + t: jest.fn((key: string) => key), + i18n: { language: 'en' }, + } as any); + mockUseTourProvider.mockReturnValue({ + isTourOpen: false, + updateTourPage: jest.fn(), + updateTourSearch: jest.fn(), + } as any); + }); + + describe('AI Query Suggestions', () => { + it('should render all AI query suggestions', () => { + render(); + + const aiQueries = [ + 'Tables owned by marketing', + 'Tables with Tier1 classification', + 'Find dashboards tagged with PII.Sensitive', + 'Topics with schema fields containing address', + 'Tables tagged with tier1 or tier2', + ]; + + aiQueries.forEach((query) => { + expect(screen.getByText(query)).toBeInTheDocument(); + }); + }); + + it('should call onSearchTextUpdate when AI query button is clicked', () => { + const mockOnSearchTextUpdate = jest.fn(); + + render( + + ); + + const firstQueryButton = screen.getByText('Tables owned by marketing'); + fireEvent.click(firstQueryButton); + + expect(mockOnSearchTextUpdate).toHaveBeenCalledWith( + 'Tables owned by marketing' + ); + }); + }); + + describe('Props Handling', () => { + it('should handle empty searchText', () => { + render(); + + expect(mockSearchQuery).not.toHaveBeenCalled(); + }); + + it('should handle isNLPActive prop correctly', () => { + render(); + + expect(mockSearchQuery).not.toHaveBeenCalled(); + }); + }); + + describe('Component Behavior', () => { + it('should show no results message when searchText is provided but no results', () => { + render(); + + // The component should show the no results message + expect(screen.getByTestId('transi18next')).toBeInTheDocument(); + }); + + it('should not call searchQuery when tour is open', () => { + mockUseTourProvider.mockReturnValue({ + isTourOpen: true, + updateTourPage: jest.fn(), + updateTourSearch: jest.fn(), + } as any); + + render(); + + expect(mockSearchQuery).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.tsx index 66fd8b747f0..121384406ab 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.tsx @@ -292,6 +292,7 @@ const Suggestions = ({ searchIndex: searchCriteria ?? SearchIndex.DATA_ASSET, queryFilter: quickFilter, pageSize: PAGE_SIZE_BASE, + includeDeleted: false, }); setOptions(res.hits.hits as unknown as Option[]);