import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import SearchBar from 'sentry/components/events/searchBar';
import TagStore from 'sentry/stores/tagStore';
const selectNthAutocompleteItem = async index => {
userEvent.click(screen.getByTestId('smart-search-input'));
const items = await screen.findAllByTestId('search-autocomplete-item');
userEvent.click(items.at(index));
};
const setQuery = query => {
userEvent.click(screen.getByTestId('smart-search-input'));
userEvent.type(screen.getByTestId('smart-search-input'), query);
};
describe('Events > SearchBar', function () {
let options;
let tagValuesMock;
let organization;
let props;
beforeEach(function () {
organization = TestStubs.Organization();
props = {
organization,
projectIds: [1, 2],
};
TagStore.reset();
TagStore.loadTagsSuccess([
{count: 3, key: 'gpu', name: 'Gpu'},
{count: 3, key: 'mytag', name: 'Mytag'},
{count: 0, key: 'browser', name: 'Browser'},
]);
options = TestStubs.routerContext();
MockApiClient.addMockResponse({
url: '/organizations/org-slug/recent-searches/',
method: 'POST',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/recent-searches/',
body: [],
});
tagValuesMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/tags/gpu/values/',
body: [{count: 2, name: 'Nvidia 1080ti'}],
});
});
afterEach(function () {
MockApiClient.clearMockResponses();
});
it('autocompletes measurement names', async function () {
const initializationObj = initializeOrg({
organization: {
features: ['performance-view'],
},
});
props.organization = initializationObj.organization;
render(, {context: options});
setQuery('fcp');
const autocomplete = await screen.findByTestId('search-autocomplete-item');
expect(autocomplete).toBeInTheDocument();
expect(autocomplete).toHaveTextContent('measurements.fcp');
});
it('autocompletes release semver queries', async function () {
const initializationObj = initializeOrg();
props.organization = initializationObj.organization;
render(, {context: options});
setQuery('release.');
const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
expect(autocomplete).toHaveLength(5);
expect(autocomplete.at(0)).toHaveTextContent('release');
expect(autocomplete.at(1)).toHaveTextContent('.build');
});
it('autocomplete has suggestions correctly', async function () {
render(, {context: options});
setQuery('has:');
const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
expect(autocomplete.at(0)).toHaveTextContent('has:');
const itemIndex = autocomplete.findIndex(item => item.textContent === 'gpu');
expect(itemIndex).toBeGreaterThan(-1);
await selectNthAutocompleteItem(itemIndex);
// the trailing space is important here as without it, autocomplete suggestions will
// try to complete `has:gpu` thinking the token has not ended yet
expect(screen.getByTestId('smart-search-input')).toHaveValue('has:gpu ');
});
it('searches and selects an event field value', async function () {
render(, {context: options});
setQuery('gpu:');
expect(tagValuesMock).toHaveBeenCalledWith(
'/organizations/org-slug/tags/gpu/values/',
expect.objectContaining({
query: {project: ['1', '2'], statsPeriod: '14d', includeTransactions: '1'},
})
);
const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
expect(autocomplete.at(2)).toHaveTextContent('Nvidia 1080ti');
await selectNthAutocompleteItem(2);
expect(screen.getByTestId('smart-search-input')).toHaveValue('gpu:"Nvidia 1080ti" ');
});
it('if `useFormWrapper` is false, pressing enter when there are no dropdown items selected should blur and call `onSearch` callback', async function () {
const onBlur = jest.fn();
const onSearch = jest.fn();
render(
,
{context: options}
);
setQuery('gpu:');
expect(tagValuesMock).toHaveBeenCalledWith(
'/organizations/org-slug/tags/gpu/values/',
expect.objectContaining({
query: {project: ['1', '2'], statsPeriod: '14d', includeTransactions: '1'},
})
);
const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
expect(autocomplete.at(2)).toHaveTextContent('Nvidia 1080ti');
await selectNthAutocompleteItem(2);
userEvent.type(screen.getByTestId('smart-search-input'), '{enter}');
expect(onSearch).toHaveBeenCalledTimes(1);
});
it('filters dropdown to accommodate for num characters left in query', async function () {
render(, {context: options});
setQuery('g');
const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
expect(autocomplete.at(0)).toHaveTextContent('g');
expect(autocomplete).toHaveLength(2);
});
it('returns zero dropdown suggestions if out of characters', async function () {
render(, {context: options});
setQuery('g');
expect(await screen.findByText('No items found')).toBeInTheDocument();
});
it('sets maxLength property', function () {
render(, {context: options});
expect(screen.getByTestId('smart-search-input')).toHaveAttribute('maxLength', '10');
});
it('does not requery for event field values if query does not change', function () {
render(, {context: options});
setQuery('gpu:');
// Click will fire "updateAutocompleteItems"
userEvent.click(screen.getByTestId('smart-search-input'));
expect(tagValuesMock).toHaveBeenCalledTimes(1);
});
it('removes highlight when query is empty', async function () {
render(, {context: options});
setQuery('gpu');
const autocomplete = await screen.findByTestId('search-autocomplete-item');
expect(autocomplete).toBeInTheDocument();
expect(autocomplete).toHaveTextContent('gpu');
// Should have nothing highlighted
userEvent.clear(screen.getByTestId('smart-search-input'));
expect(await screen.findByText('Keys')).toBeInTheDocument();
});
it('ignores negation ("!") at the beginning of search term', async function () {
render(, {context: options});
setQuery('!gp');
const autocomplete = await screen.findByTestId('search-autocomplete-item');
expect(autocomplete).toBeInTheDocument();
expect(autocomplete).toHaveTextContent('gpu');
});
it('ignores wildcard ("*") at the beginning of tag value query', async function () {
render(, {context: options});
setQuery('!gpu:*');
expect(tagValuesMock).toHaveBeenCalledWith(
'/organizations/org-slug/tags/gpu/values/',
expect.objectContaining({
query: {project: ['1', '2'], statsPeriod: '14d', includeTransactions: '1'},
})
);
await selectNthAutocompleteItem(0);
expect(screen.getByTestId('smart-search-input')).toHaveValue('!gpu:"Nvidia 1080ti" ');
});
it('stops searching after no values are returned', async function () {
const emptyTagValuesMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/tags/browser/values/',
body: [],
});
render(, {context: options});
// Do 3 searches, the first will find nothing, so no more requests should be made
setQuery('browser:Nothing');
expect(await screen.findByText('No items found')).toBeInTheDocument();
expect(emptyTagValuesMock).toHaveBeenCalled();
emptyTagValuesMock.mockClear();
// Add E character
setQuery('E');
setQuery('Els');
// No Additional calls
expect(emptyTagValuesMock).not.toHaveBeenCalled();
});
it('continues searching after no values if query changes', function () {
const emptyTagValuesMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/tags/browser/values/',
body: [],
});
render(, {context: options});
setQuery('browser:Nothing');
expect(emptyTagValuesMock).toHaveBeenCalled();
emptyTagValuesMock.mockClear();
userEvent.clear(screen.getByTestId('smart-search-input'));
setQuery('browser:Something');
expect(emptyTagValuesMock).toHaveBeenCalled();
});
it('searches for custom measurements', async function () {
const initializationObj = initializeOrg({
organization: {
features: ['performance-view'],
},
});
props.organization = initializationObj.organization;
render(
);
userEvent.type(screen.getByRole('textbox'), 'custom');
expect(await screen.findByText('measurements')).toBeInTheDocument();
expect(screen.getByText(/\.ratio/)).toBeInTheDocument();
});
});