searchBar.spec.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import SearchBar from 'sentry/components/events/searchBar';
  4. import TagStore from 'sentry/stores/tagStore';
  5. const focusTextarea = el => el.find('textarea[name="query"]').simulate('focus');
  6. const selectNthAutocompleteItem = async (el, index) => {
  7. focusTextarea(el);
  8. el.find('SearchListItem[data-test-id="search-autocomplete-item"]')
  9. .at(index)
  10. .simulate('click');
  11. const textarea = el.find('textarea');
  12. textarea
  13. .getDOMNode()
  14. .setSelectionRange(textarea.prop('value').length, textarea.prop('value').length);
  15. await tick();
  16. await el.update();
  17. };
  18. const setQuery = async (el, query) => {
  19. el.find('textarea').simulate('focus');
  20. el.find('textarea')
  21. .simulate('change', {target: {value: query}})
  22. .getDOMNode()
  23. .setSelectionRange(query.length, query.length);
  24. await tick();
  25. await el.update();
  26. };
  27. describe('Events > SearchBar', function () {
  28. let options;
  29. let tagValuesMock;
  30. let organization;
  31. let props;
  32. beforeEach(function () {
  33. organization = TestStubs.Organization();
  34. props = {
  35. organization,
  36. projectIds: [1, 2],
  37. };
  38. TagStore.reset();
  39. TagStore.loadTagsSuccess([
  40. {count: 3, key: 'gpu', name: 'Gpu'},
  41. {count: 3, key: 'mytag', name: 'Mytag'},
  42. {count: 0, key: 'browser', name: 'Browser'},
  43. ]);
  44. options = TestStubs.routerContext();
  45. MockApiClient.addMockResponse({
  46. url: '/organizations/org-slug/recent-searches/',
  47. method: 'POST',
  48. body: [],
  49. });
  50. MockApiClient.addMockResponse({
  51. url: '/organizations/org-slug/recent-searches/',
  52. body: [],
  53. });
  54. tagValuesMock = MockApiClient.addMockResponse({
  55. url: '/organizations/org-slug/tags/gpu/values/',
  56. body: [{count: 2, name: 'Nvidia 1080ti'}],
  57. });
  58. });
  59. afterEach(function () {
  60. MockApiClient.clearMockResponses();
  61. });
  62. it('autocompletes measurement names', async function () {
  63. const initializationObj = initializeOrg({
  64. organization: {
  65. features: ['performance-view'],
  66. },
  67. });
  68. props.organization = initializationObj.organization;
  69. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  70. await tick();
  71. setQuery(wrapper, 'fcp');
  72. await tick();
  73. wrapper.update();
  74. expect(wrapper.find('SearchDropdown').prop('searchSubstring')).toEqual('fcp');
  75. expect(wrapper.find('SearchDropdown SearchItemTitleWrapper').first().text()).toEqual(
  76. 'measurements.fcp'
  77. );
  78. });
  79. it('autocompletes release semver queries', async function () {
  80. const initializationObj = initializeOrg();
  81. props.organization = initializationObj.organization;
  82. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  83. await tick();
  84. setQuery(wrapper, 'release.');
  85. await tick();
  86. wrapper.update();
  87. expect(wrapper.find('SearchDropdown').prop('searchSubstring')).toEqual('release.');
  88. expect(wrapper.find('SearchDropdown FirstWordWrapper').first().text()).toEqual(
  89. 'release'
  90. );
  91. expect(wrapper.find('SearchDropdown RestOfWordsContainer').first().text()).toEqual(
  92. '.build'
  93. );
  94. });
  95. it('autocomplete has suggestions correctly', async function () {
  96. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  97. await tick();
  98. setQuery(wrapper, 'has:');
  99. await tick();
  100. wrapper.update();
  101. expect(wrapper.find('SearchDropdown').prop('searchSubstring')).toEqual('');
  102. expect(wrapper.find('SearchDropdown Value').contains('gpu')).toBe(true);
  103. const itemIndex = wrapper
  104. .find('SearchListItem[data-test-id="search-autocomplete-item"]')
  105. .map(node => node)
  106. .findIndex(node => node.text() === 'gpu');
  107. expect(itemIndex).not.toBe(-1);
  108. selectNthAutocompleteItem(wrapper, itemIndex);
  109. wrapper.update();
  110. // the trailing space is important here as without it, autocomplete suggestions will
  111. // try to complete `has:gpu` thinking the token has not ended yet
  112. expect(wrapper.find('textarea').prop('value')).toBe('has:gpu ');
  113. });
  114. it('searches and selects an event field value', async function () {
  115. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  116. await tick();
  117. setQuery(wrapper, 'gpu:');
  118. expect(tagValuesMock).toHaveBeenCalledWith(
  119. '/organizations/org-slug/tags/gpu/values/',
  120. expect.objectContaining({
  121. query: {project: ['1', '2'], statsPeriod: '14d', includeTransactions: '1'},
  122. })
  123. );
  124. await tick();
  125. wrapper.update();
  126. expect(wrapper.find('SearchDropdown').prop('searchSubstring')).toEqual('');
  127. expect(wrapper.find('SearchDropdown Value').at(2).text()).toEqual('"Nvidia 1080ti"');
  128. selectNthAutocompleteItem(wrapper, 2);
  129. wrapper.update();
  130. expect(wrapper.find('textarea').prop('value')).toBe('gpu:"Nvidia 1080ti" ');
  131. });
  132. it('if `useFormWrapper` is false, pressing enter when there are no dropdown items selected should blur and call `onSearch` callback', async function () {
  133. const onBlur = jest.fn();
  134. const onSearch = jest.fn();
  135. const wrapper = mountWithTheme(
  136. <SearchBar {...props} useFormWrapper={false} onSearch={onSearch} onBlur={onBlur} />,
  137. options
  138. );
  139. await tick();
  140. wrapper.update();
  141. setQuery(wrapper, 'gpu:');
  142. await tick();
  143. wrapper.update();
  144. expect(tagValuesMock).toHaveBeenCalledWith(
  145. '/organizations/org-slug/tags/gpu/values/',
  146. expect.objectContaining({
  147. query: {project: ['1', '2'], statsPeriod: '14d', includeTransactions: '1'},
  148. })
  149. );
  150. expect(wrapper.find('SearchDropdown').prop('searchSubstring')).toEqual('');
  151. expect(wrapper.find('SearchDropdown Value').contains('"Nvidia 1080ti"')).toBe(true);
  152. selectNthAutocompleteItem(wrapper, 2);
  153. wrapper.find('textarea').simulate('keydown', {key: 'Enter'});
  154. expect(onSearch).toHaveBeenCalledTimes(1);
  155. });
  156. it('filters dropdown to accommodate for num characters left in query', async function () {
  157. const wrapper = mountWithTheme(<SearchBar {...props} maxQueryLength={5} />, options);
  158. await tick();
  159. wrapper.update();
  160. setQuery(wrapper, 'g');
  161. await tick();
  162. wrapper.update();
  163. expect(wrapper.find('SearchDropdown').prop('searchSubstring')).toEqual('g');
  164. expect(wrapper.find('SearchDropdown SearchItemTitleWrapper')).toEqual({});
  165. expect(
  166. wrapper.find('SearchListItem[data-test-id="search-autocomplete-item"]')
  167. ).toHaveLength(2);
  168. });
  169. it('returns zero dropdown suggestions if out of characters', async function () {
  170. const wrapper = mountWithTheme(<SearchBar {...props} maxQueryLength={2} />, options);
  171. await tick();
  172. wrapper.update();
  173. setQuery(wrapper, 'g');
  174. await tick();
  175. wrapper.update();
  176. expect(wrapper.find('SearchDropdown').prop('searchSubstring')).toEqual('g');
  177. expect(wrapper.find('SearchDropdown SearchItemTitleWrapper')).toEqual({});
  178. expect(
  179. wrapper.find('SearchListItem[data-test-id="search-autocomplete-item"]')
  180. ).toHaveLength(0);
  181. });
  182. it('sets maxLength property', async function () {
  183. const wrapper = mountWithTheme(<SearchBar {...props} maxQueryLength={10} />, options);
  184. await tick();
  185. expect(wrapper.find('textarea').prop('maxLength')).toBe(10);
  186. });
  187. it('does not requery for event field values if query does not change', async function () {
  188. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  189. await tick();
  190. wrapper.update();
  191. setQuery(wrapper, 'gpu:');
  192. await tick();
  193. wrapper.update();
  194. // Click will fire "updateAutocompleteItems"
  195. wrapper.find('textarea').simulate('click');
  196. await tick();
  197. wrapper.update();
  198. expect(tagValuesMock).toHaveBeenCalledTimes(1);
  199. });
  200. it('removes highlight when query is empty', async function () {
  201. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  202. await tick();
  203. wrapper.update();
  204. setQuery(wrapper, 'gpu');
  205. await tick();
  206. wrapper.update();
  207. expect(wrapper.find('SearchItemTitleWrapper strong').text()).toBe('gpu');
  208. // Should have nothing highlighted
  209. setQuery(wrapper, '');
  210. await tick();
  211. wrapper.update();
  212. expect(wrapper.find('SearchItemTitleWrapper strong')).toHaveLength(0);
  213. });
  214. it('ignores negation ("!") at the beginning of search term', async function () {
  215. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  216. await tick();
  217. wrapper.update();
  218. setQuery(wrapper, '!gp');
  219. await tick();
  220. wrapper.update();
  221. expect(
  222. wrapper.find('SearchListItem[data-test-id="search-autocomplete-item"]')
  223. ).toHaveLength(1);
  224. expect(
  225. wrapper.find('SearchListItem[data-test-id="search-autocomplete-item"]').text()
  226. ).toMatch(/^gpu/);
  227. });
  228. it('ignores wildcard ("*") at the beginning of tag value query', async function () {
  229. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  230. await tick();
  231. wrapper.update();
  232. setQuery(wrapper, '!gpu:*');
  233. await tick();
  234. wrapper.update();
  235. expect(tagValuesMock).toHaveBeenCalledWith(
  236. '/organizations/org-slug/tags/gpu/values/',
  237. expect.objectContaining({
  238. query: {project: ['1', '2'], statsPeriod: '14d', includeTransactions: '1'},
  239. })
  240. );
  241. selectNthAutocompleteItem(wrapper, 0);
  242. expect(wrapper.find('textarea').prop('value')).toBe('!gpu:"Nvidia 1080ti" ');
  243. });
  244. it('stops searching after no values are returned', async function () {
  245. const emptyTagValuesMock = MockApiClient.addMockResponse({
  246. url: '/organizations/org-slug/tags/browser/values/',
  247. body: [],
  248. });
  249. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  250. await tick();
  251. wrapper.update();
  252. // Do 3 searches, the first will find nothing, so no more requests should be made
  253. setQuery(wrapper, 'browser:Nothing');
  254. await tick();
  255. setQuery(wrapper, 'browser:NothingE');
  256. await tick();
  257. setQuery(wrapper, 'browser:NothingEls');
  258. await tick();
  259. expect(emptyTagValuesMock).toHaveBeenCalledTimes(1);
  260. });
  261. it('continues searching after no values if query changes', async function () {
  262. const emptyTagValuesMock = MockApiClient.addMockResponse({
  263. url: '/organizations/org-slug/tags/browser/values/',
  264. body: [],
  265. });
  266. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  267. await tick();
  268. wrapper.update();
  269. setQuery(wrapper, 'browser:Nothing');
  270. setQuery(wrapper, 'browser:Something');
  271. expect(emptyTagValuesMock).toHaveBeenCalledTimes(2);
  272. });
  273. });