fix(ui): soft deleted assets visible in search suggestions (#22887)

* fix soft deleted assets in search suggestions

* add e2e test
This commit is contained in:
Pranita Fulsundar 2025-08-12 15:29:34 +05:30 committed by GitHub
parent 6c948e68bc
commit bf87f1267c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 218 additions and 0 deletions

View File

@ -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);
});
});

View File

@ -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) => (
<div data-testid={`suggestion-${suggestion._id}`} key={suggestion._id}>
{suggestion._source.name}
</div>
)),
}));
jest.mock('../../utils/SearchClassBase', () => ({
getEntitiesSuggestions: jest.fn(() => []),
}));
jest.mock('../../utils/CommonUtils', () => ({
Transi18next: ({ i18nKey, values }: { i18nKey: string; values: any }) => (
<span data-testid="transi18next">
{i18nKey} {values?.keyword || ''}
</span>
),
}));
// Mock location.search for the component
Object.defineProperty(window, 'location', {
value: {
search: '',
},
writable: true,
});
const mockSearchQuery = searchQuery as jest.MockedFunction<typeof searchQuery>;
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(<Suggestions {...defaultProps} isNLPActive searchText="" />);
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(
<Suggestions
{...defaultProps}
isNLPActive
searchText=""
onSearchTextUpdate={mockOnSearchTextUpdate}
/>
);
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(<Suggestions {...defaultProps} searchText="" />);
expect(mockSearchQuery).not.toHaveBeenCalled();
});
it('should handle isNLPActive prop correctly', () => {
render(<Suggestions {...defaultProps} isNLPActive />);
expect(mockSearchQuery).not.toHaveBeenCalled();
});
});
describe('Component Behavior', () => {
it('should show no results message when searchText is provided but no results', () => {
render(<Suggestions {...defaultProps} />);
// 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(<Suggestions {...defaultProps} />);
expect(mockSearchQuery).not.toHaveBeenCalled();
});
});
});

View File

@ -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[]);