Skip to content

Commit 6f83626

Browse files
Fixes #26970: fix bot name search on the Bots page (#27365)
* Fix bot name matching on the Bots page search * Fixes #26970: migrate Bots search to API-based name/email lookup with robust partial email matching * Fix flaky Playwright setup by making Table/User creation idempotent and hardening glossary/tag and cleanup flows * Fixes #26970: avoid full bot scan by switching search result resolution to direct getBotByName lookups * Fixes #26970: align bot user search with deleted toggle and tighten tour retry timeout handling * Fixes #26970: keep bot search API-driven, align wildcard matching with name/email expectations, and revert unrelated Playwright changes * fix: stabilize bot search behavior and flaky Playwright flows across bots, glossary, lineage, and announcements * fix: limit PR scope to bot search by reverting unrelated Tour Playwright changes * fix: remove unrelated Playwright changes and keep bot search scope focused * fix: optimize bot search scalability with paginated user-index retrieval and bounded-concurrency bot resolution * fix: harden Bots API search with bounded pagination/concurrency and consistent active-search refresh behavior * fix: prevent stale bot search state and strengthen Bots Playwright coverage with deterministic positive/negative assertions * test: scope Playwright fixes to bot flow and remove unrelated test changes * Add local bot search and stabilize tests * Fixes: keep Bot search API-driven for complete results and stabilize bot cleanup assertions in Playwright * chore: revert out-of-scope bot Playwright test changes * test: add bot search e2e coverage and tighten bot API response assertions * test: stabilize bot search no-match assertions using filter placeholder testid * fix: add search API wait in bot Playwright flow and refactor bot-user mapping helper * fix: extract reusable searchbar helper with search API wait and deduplicate bot user mapping logic * fix(playwright): make bot search test stable by removing brittle API wait * Refactor bot search integration and Playwright synchronization for reliable name/email query behavior * Fix bot search regressions by preserving getBotByName compatibility export, stabilizing BotListV1 memoized enrichment, and skipping Playwright search API wait for empty terms * fix: stabilize bot search Playwright API wait to prevent encoded-query timeout flakes * } from '../generated/api/teams/createUser'; * fix: eliminate Playwright search response race by pre-registering waiter and matching query-specific GET /search/query responses * fix: normalize bot search queryFilter format and harden bot-user resolution flow * refactor code * fix bot search * fix checkstyle * fix display name search * address gitar * remove unwanted code * address gitar and improve performance * address gitar * fix bot spec --------- Co-authored-by: Harsh Vador <58542468+harsh-vador@users.noreply.github.com> Co-authored-by: Harsh Vador <harsh.vador@somaiya.edu>
1 parent 46e0bce commit 6f83626

12 files changed

Lines changed: 448 additions & 64 deletions

File tree

openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import { expect, test } from '@playwright/test';
1414
import { PLAYWRIGHT_BASIC_TEST_TAG_OBJ } from '../../constant/config';
1515
import {
16+
BOT_DETAILS,
1617
createBot,
1718
deleteBot,
1819
redirectToBotPage,
@@ -21,6 +22,8 @@ import {
2122
updateBotDetails,
2223
verifyGenerateTokenAPIContract,
2324
} from '../../utils/bot';
25+
import { searchFromSearchInput } from '../../utils/common';
26+
import { waitForAllLoadersToDisappear } from '../../utils/entity';
2427

2528
// use the admin user to login
2629
test.use({ storageState: 'playwright/.auth/admin.json' });
@@ -48,6 +51,56 @@ test.describe(
4851
await updateBotDetails(page);
4952
});
5053

54+
await redirectToBotPage(page);
55+
56+
const searchInput = page.getByTestId('searchbar');
57+
const createdBotLink = page.getByTestId(
58+
`bot-link-${BOT_DETAILS.updatedBotName}`
59+
);
60+
61+
await test.step('Search bot by display name', async () => {
62+
await searchFromSearchInput(
63+
page,
64+
searchInput,
65+
BOT_DETAILS.updatedBotName
66+
);
67+
68+
await expect(createdBotLink).toBeVisible();
69+
});
70+
71+
await test.step('Search bot by bot name', async () => {
72+
await searchFromSearchInput(page, searchInput, BOT_DETAILS.botName);
73+
74+
await expect(createdBotLink).toBeVisible();
75+
});
76+
77+
await test.step('Search bot by email', async () => {
78+
await searchFromSearchInput(page, searchInput, BOT_DETAILS.botEmail);
79+
80+
await expect(createdBotLink).toBeVisible();
81+
});
82+
83+
await test.step('Search with no match shows empty state', async () => {
84+
await searchFromSearchInput(
85+
page,
86+
searchInput,
87+
`${BOT_DETAILS.updatedBotName}-no-match`
88+
);
89+
90+
await expect(
91+
page.getByTestId('search-error-placeholder')
92+
).toBeVisible();
93+
});
94+
95+
await test.step('Clear search restores full list', async () => {
96+
await searchInput.clear();
97+
await searchInput.fill('');
98+
await expect(searchInput).toHaveValue('');
99+
await waitForAllLoadersToDisappear(page);
100+
101+
await expect(createdBotLink).toBeVisible();
102+
});
103+
51104
await test.step('Verify generateToken API contract', async () => {
52105
await verifyGenerateTokenAPIContract(page);
53106
});

openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { revokeToken } from './user';
2525

2626
const botName = `a-bot-pw%test-${uuid()}`;
2727

28-
const BOT_DETAILS = {
28+
export const BOT_DETAILS = {
2929
botName: botName,
3030
botEmail: `${botName}@mail.com`,
3131
description: `This is bot description for ${botName}`,
@@ -70,9 +70,15 @@ export const createBot = async (page: Page) => {
7070

7171
await page.locator(descriptionBox).fill(BOT_DETAILS.description);
7272

73-
const saveResponse = page.waitForResponse('/api/v1/bots');
73+
const saveResponse = page.waitForResponse(
74+
(response) =>
75+
response.url().includes('/api/v1/bots') &&
76+
response.request().method() === 'POST'
77+
);
7478
await page.click('[data-testid="save-user"]');
75-
await saveResponse;
79+
const createBotResponse = await saveResponse;
80+
81+
expect(createBotResponse.status()).toBe(201);
7682

7783
// Verify bot is getting added in the bots listing page
7884
await expect(
@@ -115,7 +121,10 @@ export const deleteBot = async (page: Page) => {
115121

116122
await toastNotification(page, /deleted successfully!/);
117123

118-
await expect(page.locator('.ant-table-tbody')).not.toContainText(botName);
124+
await page.getByTestId('searchbar').clear();
125+
await page.getByTestId('searchbar').fill(BOT_DETAILS.updatedBotName);
126+
await waitForAllLoadersToDisappear(page);
127+
await expect(page.getByTestId('search-error-placeholder')).toBeVisible();
119128
};
120129

121130
export const updateBotDetails = async (page: Page) => {

openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,25 @@ export const clickOutside = async (page: Page) => {
198198
});
199199
};
200200

201+
export const searchFromSearchInput = async (
202+
page: Page,
203+
searchInput: Locator,
204+
searchTerm: string
205+
) => {
206+
const searchResponsePromise = page.waitForResponse(
207+
(response) =>
208+
response.url().includes('/api/v1/search/query') &&
209+
response.request().method() === 'GET'
210+
);
211+
212+
await searchInput.clear();
213+
await searchInput.fill(searchTerm);
214+
await expect(searchInput).toHaveValue(searchTerm);
215+
216+
const searchResponse = await searchResponsePromise;
217+
expect(searchResponse.status()).toBe(200);
218+
};
219+
201220
export const visitOwnProfilePage = async (page: Page) => {
202221
await page.locator('[data-testid="dropdown-profile"] svg').click();
203222
await page.locator('[role="menu"].profile-dropdown').waitFor({

openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.test.tsx

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,17 @@
1010
* See the License for the specific language governing permissions and
1111
* limitations under the License.
1212
*/
13-
import { fireEvent, render, screen } from '@testing-library/react';
13+
import {
14+
act,
15+
fireEvent,
16+
render,
17+
screen,
18+
waitFor,
19+
} from '@testing-library/react';
1420
import { MemoryRouter } from 'react-router-dom';
1521
import LimitWrapper from '../../../../hoc/LimitWrapper';
22+
import { getBots } from '../../../../rest/botsAPI';
23+
import { searchQuery } from '../../../../rest/searchAPI';
1624
import BotListV1 from './BotListV1.component';
1725

1826
const mockHandleAddBotClick = jest.fn();
@@ -24,6 +32,37 @@ const mockProps = {
2432
handleShowDeleted: mockHandleShowDeleted,
2533
};
2634

35+
const MOCK_BOTS = [
36+
{
37+
id: 'bot-1-id',
38+
name: 'AutoClassification-bot',
39+
fullyQualifiedName: 'AutoClassification-bot',
40+
displayName: 'AutoClassificationBot',
41+
description: 'Auto classify',
42+
deleted: false,
43+
},
44+
{
45+
id: 'bot-2-id',
46+
name: 'testbot',
47+
fullyQualifiedName: 'testbot',
48+
displayName: 'testbots',
49+
description: 'Test bot',
50+
deleted: false,
51+
},
52+
];
53+
54+
const MOCK_SEARCH_USER_HIT = {
55+
_source: {
56+
id: 'user-2-id',
57+
name: 'testbot',
58+
fullyQualifiedName: 'testbot',
59+
displayName: 'testbots',
60+
email: 'testbot@test.com',
61+
isBot: true,
62+
entityType: 'user',
63+
},
64+
};
65+
2766
jest.mock('../../../../hoc/LimitWrapper', () => {
2867
return jest.fn().mockImplementation(() => <>LimitWrapper</>);
2968
});
@@ -39,6 +78,33 @@ jest.mock('../../../../utils/EntityUtils', () => ({
3978
getTitleCase: jest.fn((text) => text.charAt(0).toUpperCase() + text.slice(1)),
4079
}));
4180

81+
jest.mock('../../../../rest/botsAPI', () => ({
82+
getBots: jest.fn(),
83+
}));
84+
85+
jest.mock('../../../../rest/searchAPI', () => ({
86+
searchQuery: jest.fn(),
87+
}));
88+
89+
const mockGetBots = getBots as jest.MockedFunction<typeof getBots>;
90+
const mockSearchQuery = searchQuery as jest.MockedFunction<typeof searchQuery>;
91+
92+
beforeEach(() => {
93+
jest.clearAllMocks();
94+
95+
mockGetBots.mockResolvedValue({
96+
data: MOCK_BOTS,
97+
paging: { total: MOCK_BOTS.length },
98+
} as unknown as Awaited<ReturnType<typeof getBots>>);
99+
100+
mockSearchQuery.mockResolvedValue({
101+
hits: {
102+
total: { value: 0 },
103+
hits: [],
104+
},
105+
} as unknown as Awaited<ReturnType<typeof searchQuery>>);
106+
});
107+
42108
describe('BotListV1', () => {
43109
it('renders the component', () => {
44110
render(<BotListV1 {...mockProps} />, { wrapper: MemoryRouter });
@@ -58,11 +124,70 @@ describe('BotListV1', () => {
58124
render(<BotListV1 {...mockProps} />, { wrapper: MemoryRouter });
59125
const addBotButton = screen.getByText('LimitWrapper');
60126
fireEvent.click(addBotButton);
61-
// Add your assertions here
62127

63128
expect(LimitWrapper).toHaveBeenCalledWith(
64129
expect.objectContaining({ resource: 'bot' }),
65130
{}
66131
);
67132
});
133+
134+
it('searches bot user index with wildcard across name, displayName, fqn, email', async () => {
135+
mockSearchQuery.mockResolvedValueOnce({
136+
hits: {
137+
total: { value: 1 },
138+
hits: [MOCK_SEARCH_USER_HIT],
139+
},
140+
} as unknown as Awaited<ReturnType<typeof searchQuery>>);
141+
142+
render(<BotListV1 {...mockProps} />, { wrapper: MemoryRouter });
143+
144+
await waitFor(() => {
145+
expect(mockGetBots).toHaveBeenCalled();
146+
});
147+
148+
const searchInput = await screen.findByTestId('searchbar');
149+
await act(async () => {
150+
fireEvent.change(searchInput, { target: { value: 'testbot' } });
151+
});
152+
153+
await waitFor(() => {
154+
const searchCall = mockSearchQuery.mock.calls.find((call) => {
155+
const arg = call[0] as { query?: string; queryFilter?: unknown };
156+
const filterStr = JSON.stringify(arg.queryFilter);
157+
158+
return (
159+
arg.query === '' &&
160+
filterStr.includes('*testbot*') &&
161+
filterStr.includes('email.keyword') &&
162+
filterStr.includes('name.keyword') &&
163+
filterStr.includes('displayName.keyword') &&
164+
filterStr.includes('fullyQualifiedName.keyword')
165+
);
166+
});
167+
168+
expect(searchCall).toBeDefined();
169+
});
170+
});
171+
172+
it('resolves matched bot user to bot entity via lowercased name match', async () => {
173+
mockSearchQuery.mockResolvedValueOnce({
174+
hits: {
175+
total: { value: 1 },
176+
hits: [MOCK_SEARCH_USER_HIT],
177+
},
178+
} as unknown as Awaited<ReturnType<typeof searchQuery>>);
179+
180+
render(<BotListV1 {...mockProps} />, { wrapper: MemoryRouter });
181+
182+
await waitFor(() => {
183+
expect(mockGetBots).toHaveBeenCalled();
184+
});
185+
186+
const searchInput = await screen.findByTestId('searchbar');
187+
await act(async () => {
188+
fireEvent.change(searchInput, { target: { value: 'testbot' } });
189+
});
190+
191+
expect(await screen.findByTestId('bot-link-testbots')).toBeInTheDocument();
192+
});
68193
});

0 commit comments

Comments
 (0)