searchBar.spec.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  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. wrapper.setState;
  161. setQuery(wrapper, 'g');
  162. await tick();
  163. wrapper.update();
  164. expect(wrapper.find('SearchDropdown').prop('searchSubstring')).toEqual('g');
  165. expect(wrapper.find('SearchDropdown SearchItemTitleWrapper')).toEqual({});
  166. expect(
  167. wrapper.find('SearchListItem[data-test-id="search-autocomplete-item"]')
  168. ).toHaveLength(1);
  169. });
  170. it('returns zero dropdown suggestions if out of characters', async function () {
  171. const wrapper = mountWithTheme(<SearchBar {...props} maxQueryLength={2} />, options);
  172. await tick();
  173. wrapper.update();
  174. wrapper.setState;
  175. setQuery(wrapper, 'g');
  176. await tick();
  177. wrapper.update();
  178. expect(wrapper.find('SearchDropdown').prop('searchSubstring')).toEqual('g');
  179. expect(wrapper.find('SearchDropdown SearchItemTitleWrapper')).toEqual({});
  180. expect(
  181. wrapper.find('SearchListItem[data-test-id="search-autocomplete-item"]')
  182. ).toHaveLength(0);
  183. });
  184. it('sets maxLength property', async function () {
  185. const wrapper = mountWithTheme(<SearchBar {...props} maxQueryLength={10} />, options);
  186. await tick();
  187. expect(wrapper.find('textarea').prop('maxLength')).toBe(10);
  188. });
  189. it('does not requery for event field values if query does not change', async function () {
  190. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  191. await tick();
  192. wrapper.update();
  193. setQuery(wrapper, 'gpu:');
  194. await tick();
  195. wrapper.update();
  196. // Click will fire "updateAutocompleteItems"
  197. wrapper.find('textarea').simulate('click');
  198. await tick();
  199. wrapper.update();
  200. expect(tagValuesMock).toHaveBeenCalledTimes(1);
  201. });
  202. it('removes highlight when query is empty', async function () {
  203. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  204. await tick();
  205. wrapper.update();
  206. setQuery(wrapper, 'gpu');
  207. await tick();
  208. wrapper.update();
  209. expect(wrapper.find('SearchItemTitleWrapper strong').text()).toBe('gpu');
  210. // Should have nothing highlighted
  211. setQuery(wrapper, '');
  212. await tick();
  213. wrapper.update();
  214. expect(wrapper.find('SearchItemTitleWrapper strong')).toHaveLength(0);
  215. });
  216. it('ignores negation ("!") at the beginning of search term', async function () {
  217. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  218. await tick();
  219. wrapper.update();
  220. setQuery(wrapper, '!gp');
  221. await tick();
  222. wrapper.update();
  223. expect(
  224. wrapper.find('SearchListItem[data-test-id="search-autocomplete-item"]')
  225. ).toHaveLength(1);
  226. expect(
  227. wrapper.find('SearchListItem[data-test-id="search-autocomplete-item"]').text()
  228. ).toMatch(/^gpu/);
  229. });
  230. it('ignores wildcard ("*") at the beginning of tag value query', async function () {
  231. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  232. await tick();
  233. wrapper.update();
  234. setQuery(wrapper, '!gpu:*');
  235. await tick();
  236. wrapper.update();
  237. expect(tagValuesMock).toHaveBeenCalledWith(
  238. '/organizations/org-slug/tags/gpu/values/',
  239. expect.objectContaining({
  240. query: {project: ['1', '2'], statsPeriod: '14d', includeTransactions: '1'},
  241. })
  242. );
  243. selectNthAutocompleteItem(wrapper, 0);
  244. expect(wrapper.find('textarea').prop('value')).toBe('!gpu:"Nvidia 1080ti" ');
  245. });
  246. it('stops searching after no values are returned', async function () {
  247. const emptyTagValuesMock = MockApiClient.addMockResponse({
  248. url: '/organizations/org-slug/tags/browser/values/',
  249. body: [],
  250. });
  251. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  252. await tick();
  253. wrapper.update();
  254. // Do 3 searches, the first will find nothing, so no more requests should be made
  255. setQuery(wrapper, 'browser:Nothing');
  256. await tick();
  257. setQuery(wrapper, 'browser:NothingE');
  258. await tick();
  259. setQuery(wrapper, 'browser:NothingEls');
  260. await tick();
  261. expect(emptyTagValuesMock).toHaveBeenCalledTimes(1);
  262. });
  263. it('continues searching after no values if query changes', async function () {
  264. const emptyTagValuesMock = MockApiClient.addMockResponse({
  265. url: '/organizations/org-slug/tags/browser/values/',
  266. body: [],
  267. });
  268. const wrapper = mountWithTheme(<SearchBar {...props} />, options);
  269. await tick();
  270. wrapper.update();
  271. setQuery(wrapper, 'browser:Nothing');
  272. setQuery(wrapper, 'browser:Something');
  273. expect(emptyTagValuesMock).toHaveBeenCalledTimes(2);
  274. });
  275. });