searchBar.spec.tsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. import {Organization} from 'sentry-fixture/organization';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  4. import SearchBar from 'sentry/components/events/searchBar';
  5. import TagStore from 'sentry/stores/tagStore';
  6. import {Organization as TOrganization} from 'sentry/types';
  7. const selectNthAutocompleteItem = async index => {
  8. await userEvent.click(screen.getByTestId('smart-search-input'), {delay: null});
  9. const items = await screen.findAllByTestId('search-autocomplete-item');
  10. const item = items.at(index);
  11. if (item === undefined) {
  12. throw new Error('Invalid item index');
  13. }
  14. await userEvent.click(item, {delay: null});
  15. };
  16. async function setQuery(query) {
  17. const input = screen.getByTestId('smart-search-input');
  18. await userEvent.click(input, {delay: null});
  19. await userEvent.paste(query, {delay: null});
  20. }
  21. describe('Events > SearchBar', function () {
  22. let options;
  23. let tagValuesMock;
  24. let organization: TOrganization;
  25. let props: React.ComponentProps<typeof SearchBar>;
  26. beforeEach(function () {
  27. organization = Organization();
  28. props = {
  29. organization,
  30. projectIds: [1, 2],
  31. };
  32. TagStore.reset();
  33. TagStore.loadTagsSuccess([
  34. {totalValues: 3, key: 'gpu', name: 'Gpu'},
  35. {totalValues: 3, key: 'mytag', name: 'Mytag'},
  36. {totalValues: 0, key: 'browser', name: 'Browser'},
  37. ]);
  38. options = TestStubs.routerContext();
  39. MockApiClient.addMockResponse({
  40. url: '/organizations/org-slug/recent-searches/',
  41. method: 'POST',
  42. body: [],
  43. });
  44. MockApiClient.addMockResponse({
  45. url: '/organizations/org-slug/recent-searches/',
  46. body: [],
  47. });
  48. tagValuesMock = MockApiClient.addMockResponse({
  49. url: '/organizations/org-slug/tags/gpu/values/',
  50. body: [{totalValues: 2, name: 'Nvidia 1080ti'}],
  51. });
  52. });
  53. afterEach(function () {
  54. MockApiClient.clearMockResponses();
  55. });
  56. it('autocompletes measurement names', async function () {
  57. const initializationObj = initializeOrg({
  58. organization: {
  59. features: ['performance-view'],
  60. },
  61. });
  62. props.organization = initializationObj.organization;
  63. render(<SearchBar {...props} />, {context: options});
  64. await setQuery('fcp');
  65. const autocomplete = await screen.findByTestId('search-autocomplete-item');
  66. expect(autocomplete).toBeInTheDocument();
  67. expect(autocomplete).toHaveTextContent('measurements.fcp');
  68. });
  69. it('autocompletes release semver queries', async function () {
  70. const initializationObj = initializeOrg();
  71. props.organization = initializationObj.organization;
  72. render(<SearchBar {...props} />, {context: options});
  73. await setQuery('release.');
  74. const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
  75. expect(autocomplete).toHaveLength(5);
  76. expect(autocomplete.at(0)).toHaveTextContent('release');
  77. expect(autocomplete.at(1)).toHaveTextContent('.build');
  78. });
  79. it('autocomplete has suggestions correctly', async function () {
  80. render(<SearchBar {...props} />, {context: options});
  81. await setQuery('has:');
  82. const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
  83. expect(autocomplete.at(0)).toHaveTextContent('has:');
  84. const itemIndex = autocomplete.findIndex(item => item.textContent === 'gpu');
  85. expect(itemIndex).toBeGreaterThan(-1);
  86. await selectNthAutocompleteItem(itemIndex);
  87. // the trailing space is important here as without it, autocomplete suggestions will
  88. // try to complete `has:gpu` thinking the token has not ended yet
  89. expect(screen.getByTestId('smart-search-input')).toHaveValue('has:gpu ');
  90. });
  91. it('searches and selects an event field value', async function () {
  92. render(<SearchBar {...props} />, {context: options});
  93. await setQuery('gpu:');
  94. expect(tagValuesMock).toHaveBeenCalledWith(
  95. '/organizations/org-slug/tags/gpu/values/',
  96. expect.objectContaining({
  97. query: {project: ['1', '2'], statsPeriod: '14d', includeTransactions: '1'},
  98. })
  99. );
  100. const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
  101. expect(autocomplete.at(2)).toHaveTextContent('Nvidia 1080ti');
  102. await selectNthAutocompleteItem(2);
  103. expect(screen.getByTestId('smart-search-input')).toHaveValue('gpu:"Nvidia 1080ti" ');
  104. });
  105. it('if `useFormWrapper` is false, async pressing enter when there are no dropdown items selected should blur and call `onSearch` callback', async function () {
  106. const onBlur = jest.fn();
  107. const onSearch = jest.fn();
  108. render(
  109. <SearchBar {...props} useFormWrapper={false} onSearch={onSearch} onBlur={onBlur} />,
  110. {context: options}
  111. );
  112. await setQuery('gpu:');
  113. expect(tagValuesMock).toHaveBeenCalledWith(
  114. '/organizations/org-slug/tags/gpu/values/',
  115. expect.objectContaining({
  116. query: {project: ['1', '2'], statsPeriod: '14d', includeTransactions: '1'},
  117. })
  118. );
  119. const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
  120. expect(autocomplete.at(2)).toHaveTextContent('Nvidia 1080ti');
  121. await selectNthAutocompleteItem(2);
  122. await userEvent.type(screen.getByTestId('smart-search-input'), '{enter}');
  123. expect(onSearch).toHaveBeenCalledTimes(1);
  124. });
  125. it('filters dropdown to accommodate for num characters left in query', async function () {
  126. render(<SearchBar {...props} maxQueryLength={5} />, {context: options});
  127. await setQuery('g');
  128. const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
  129. expect(autocomplete.at(0)).toHaveTextContent('g');
  130. expect(autocomplete).toHaveLength(2);
  131. });
  132. it('returns zero dropdown suggestions if out of characters', async function () {
  133. render(<SearchBar {...props} maxQueryLength={2} />, {context: options});
  134. await setQuery('g');
  135. expect(await screen.findByText('No items found')).toBeInTheDocument();
  136. });
  137. it('sets maxLength property', function () {
  138. render(<SearchBar {...props} maxQueryLength={10} />, {context: options});
  139. expect(screen.getByTestId('smart-search-input')).toHaveAttribute('maxLength', '10');
  140. });
  141. it('does not requery for event field values if query does not change', async function () {
  142. render(<SearchBar {...props} />, {context: options});
  143. await setQuery('gpu:');
  144. // Click will fire "updateAutocompleteItems"
  145. await userEvent.click(screen.getByTestId('smart-search-input'), {delay: null});
  146. expect(tagValuesMock).toHaveBeenCalledTimes(1);
  147. });
  148. it('removes highlight when query is empty', async function () {
  149. render(<SearchBar {...props} />, {context: options});
  150. await setQuery('gpu');
  151. const autocomplete = await screen.findByTestId('search-autocomplete-item');
  152. expect(autocomplete).toBeInTheDocument();
  153. expect(autocomplete).toHaveTextContent('gpu');
  154. // Should have nothing highlighted
  155. await userEvent.clear(screen.getByTestId('smart-search-input'));
  156. expect(await screen.findByText('Keys')).toBeInTheDocument();
  157. });
  158. it('ignores negation ("!") at the beginning of search term', async function () {
  159. render(<SearchBar {...props} />, {context: options});
  160. await setQuery('!gp');
  161. const autocomplete = await screen.findByTestId('search-autocomplete-item');
  162. expect(autocomplete).toBeInTheDocument();
  163. expect(autocomplete).toHaveTextContent('gpu');
  164. });
  165. it('ignores wildcard ("*") at the beginning of tag value query', async function () {
  166. render(<SearchBar {...props} />, {context: options});
  167. await setQuery('!gpu:*');
  168. expect(tagValuesMock).toHaveBeenCalledWith(
  169. '/organizations/org-slug/tags/gpu/values/',
  170. expect.objectContaining({
  171. query: {project: ['1', '2'], statsPeriod: '14d', includeTransactions: '1'},
  172. })
  173. );
  174. await selectNthAutocompleteItem(0);
  175. expect(screen.getByTestId('smart-search-input')).toHaveValue('!gpu:"Nvidia 1080ti" ');
  176. });
  177. it('stops searching after no values are returned', async function () {
  178. const emptyTagValuesMock = MockApiClient.addMockResponse({
  179. url: '/organizations/org-slug/tags/browser/values/',
  180. body: [],
  181. });
  182. render(<SearchBar {...props} />, {context: options});
  183. // Do 3 searches, the first will find nothing, so no more requests should be made
  184. await setQuery('browser:Nothing');
  185. expect(await screen.findByText('No items found')).toBeInTheDocument();
  186. expect(emptyTagValuesMock).toHaveBeenCalled();
  187. emptyTagValuesMock.mockClear();
  188. // Add E character
  189. await setQuery('E');
  190. await setQuery('Els');
  191. // No Additional calls
  192. expect(emptyTagValuesMock).not.toHaveBeenCalled();
  193. });
  194. it('continues searching after no values if query changes', async function () {
  195. const emptyTagValuesMock = MockApiClient.addMockResponse({
  196. url: '/organizations/org-slug/tags/browser/values/',
  197. body: [],
  198. });
  199. render(<SearchBar {...props} />, {context: options});
  200. await setQuery('browser:Nothing');
  201. expect(emptyTagValuesMock).toHaveBeenCalled();
  202. emptyTagValuesMock.mockClear();
  203. await userEvent.clear(screen.getByTestId('smart-search-input'));
  204. await setQuery('browser:Something');
  205. expect(emptyTagValuesMock).toHaveBeenCalled();
  206. });
  207. it('searches for custom measurements', async function () {
  208. const initializationObj = initializeOrg({
  209. organization: {
  210. features: ['performance-view'],
  211. },
  212. });
  213. props.organization = initializationObj.organization;
  214. render(
  215. <SearchBar
  216. {...props}
  217. customMeasurements={{
  218. 'measurements.custom.ratio': {
  219. key: 'measurements.custom.ratio',
  220. name: 'measurements.custom.ratio',
  221. functions: [],
  222. fieldType: 'test',
  223. unit: '',
  224. },
  225. }}
  226. />
  227. );
  228. await userEvent.type(screen.getByRole('textbox'), 'custom');
  229. expect(await screen.findByText('measurements')).toBeInTheDocument();
  230. expect(screen.getByText(/\.ratio/)).toBeInTheDocument();
  231. });
  232. });