searchBar.spec.jsx 11 KB

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