searchBar.spec.jsx 9.3 KB

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