searchBar.spec.jsx 9.5 KB

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