searchBar.spec.tsx 9.8 KB

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