import { act, render, screen, userEvent, waitForElementToBeRemoved, } from 'sentry-test/reactTestingLibrary'; import {textWithMarkupMatcher} from 'sentry-test/utils'; import SearchBar, {SearchBarProps} from 'sentry/components/performance/searchBar'; import EventView from 'sentry/utils/discover/eventView'; // Jest's fake timers don't advance the debounce timer, so we need to mock it // with a different implementation. This could probably go in __mocks__ since // it's used in a few tests. jest.mock('lodash/debounce', () => { const debounceMap = new Map(); const mockDebounce = (fn, timeout) => (...args) => { if (debounceMap.has(fn)) { clearTimeout(debounceMap.get(fn)); } debounceMap.set( fn, setTimeout(() => { fn.apply(fn, args); debounceMap.delete(fn); }, timeout) ); }; return mockDebounce; }); describe('SearchBar', () => { let eventsMock; const organization = TestStubs.Organization(); const testProps: SearchBarProps = { onSearch: jest.fn(), organization, eventView: EventView.fromSavedQuery({ id: '', name: '', fields: [], projects: [], version: 2, }), query: '', }; beforeAll(() => { jest.useFakeTimers(); }); beforeEach(() => { MockApiClient.clearMockResponses(); jest.clearAllMocks(); eventsMock = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/eventsv2/`, body: {data: []}, }); }); afterAll(() => { jest.useRealTimers(); }); it('Sends user input as a transaction search and shows the results', async () => { eventsMock = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/eventsv2/`, body: { data: [{transaction: 'clients.call'}, {transaction: 'clients.fetch'}], }, }); render(); userEvent.type(screen.getByRole('textbox'), 'proje'); expect(screen.getByRole('textbox')).toHaveValue('proje'); act(() => { jest.runAllTimers(); }); await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); expect(eventsMock).toHaveBeenCalledTimes(1); expect(eventsMock).toHaveBeenCalledWith( '/organizations/org-slug/eventsv2/', expect.objectContaining({ query: expect.objectContaining({ query: 'transaction:*proje* event.type:transaction', }), }) ); expect(screen.getByText(textWithMarkupMatcher('clients.call'))).toBeInTheDocument(); expect(screen.getByText(textWithMarkupMatcher('clients.fetch'))).toBeInTheDocument(); }); it('Responds to keyboard navigation', async () => { const onSearch = jest.fn(); eventsMock = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/eventsv2/`, body: { data: [ {project_id: 1, transaction: 'clients.call'}, {project_id: 1, transaction: 'clients.fetch'}, ], }, }); render(); userEvent.type(screen.getByRole('textbox'), 'proje'); expect(screen.getByTestId('smart-search-dropdown')).toBeInTheDocument(); act(() => { jest.runAllTimers(); }); await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); userEvent.keyboard('{Escape}'); expect(screen.queryByTestId('smart-search-dropdown')).not.toBeInTheDocument(); userEvent.type(screen.getByRole('textbox'), 'client'); expect(screen.getByTestId('smart-search-dropdown')).toBeInTheDocument(); act(() => { jest.runAllTimers(); }); await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); userEvent.keyboard('{ArrowDown}'); userEvent.keyboard('{ArrowDown}'); userEvent.keyboard('{Enter}'); expect(screen.queryByTestId('smart-search-dropdown')).not.toBeInTheDocument(); expect(onSearch).toHaveBeenCalledTimes(1); expect(onSearch).toHaveBeenCalledWith('transaction:clients.fetch'); }); });