searchBar.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. import {OrganizationFixture} from 'sentry-fixture/organization';
  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. import type {Organization as TOrganization} from 'sentry/types/organization';
  7. import {Dataset} from 'sentry/views/alerts/rules/metric/types';
  8. import {datasetSupportedTags} from 'sentry/views/alerts/wizard/options';
  9. const selectNthAutocompleteItem = async index => {
  10. await userEvent.click(screen.getByTestId('smart-search-input'), {delay: null});
  11. const items = await screen.findAllByTestId('search-autocomplete-item');
  12. const item = items.at(index);
  13. if (item === undefined) {
  14. throw new Error('Invalid item index');
  15. }
  16. await userEvent.click(item, {delay: null});
  17. };
  18. async function setQuery(query) {
  19. const input = screen.getByTestId('smart-search-input');
  20. await userEvent.click(input, {delay: null});
  21. await userEvent.paste(query, {delay: null});
  22. }
  23. describe('Events > SearchBar', function () {
  24. let tagValuesMock;
  25. let organization: TOrganization;
  26. let props: React.ComponentProps<typeof SearchBar>;
  27. beforeEach(function () {
  28. organization = OrganizationFixture();
  29. props = {
  30. organization,
  31. projectIds: [1, 2],
  32. };
  33. TagStore.reset();
  34. TagStore.loadTagsSuccess([
  35. {totalValues: 3, key: 'gpu', name: 'Gpu'},
  36. {totalValues: 3, key: 'mytag', name: 'Mytag'},
  37. {totalValues: 0, key: 'browser', name: 'Browser'},
  38. ]);
  39. MockApiClient.addMockResponse({
  40. url: '/organizations/org-slug/recent-searches/',
  41. method: 'POST',
  42. body: [],
  43. });
  44. MockApiClient.addMockResponse({
  45. url: '/organizations/org-slug/recent-searches/',
  46. body: [],
  47. });
  48. MockApiClient.addMockResponse({
  49. url: '/organizations/org-slug/tags/is/values/',
  50. method: 'GET',
  51. body: [],
  52. });
  53. MockApiClient.addMockResponse({
  54. url: '/organizations/org-slug/tags/error.handled/values/',
  55. method: 'GET',
  56. body: [],
  57. });
  58. tagValuesMock = MockApiClient.addMockResponse({
  59. url: '/organizations/org-slug/tags/gpu/values/',
  60. body: [{totalValues: 2, name: 'Nvidia 1080ti'}],
  61. });
  62. });
  63. afterEach(function () {
  64. MockApiClient.clearMockResponses();
  65. });
  66. it('autocompletes measurement names', async function () {
  67. const initializationObj = initializeOrg({
  68. organization: {
  69. features: ['performance-view'],
  70. },
  71. });
  72. props.organization = initializationObj.organization;
  73. render(<SearchBar {...props} />);
  74. await setQuery('fcp');
  75. const autocomplete = await screen.findByTestId('search-autocomplete-item');
  76. expect(autocomplete).toBeInTheDocument();
  77. expect(autocomplete).toHaveTextContent('measurements.fcp');
  78. });
  79. it('autocompletes release semver queries', async function () {
  80. const initializationObj = initializeOrg();
  81. props.organization = initializationObj.organization;
  82. render(<SearchBar {...props} />);
  83. await setQuery('release.');
  84. const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
  85. expect(autocomplete).toHaveLength(5);
  86. expect(autocomplete.at(0)).toHaveTextContent('release');
  87. expect(autocomplete.at(1)).toHaveTextContent('.build');
  88. });
  89. it('autocomplete has suggestions correctly', async function () {
  90. render(<SearchBar {...props} />);
  91. await setQuery('has:');
  92. const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
  93. expect(autocomplete.at(0)).toHaveTextContent('has:');
  94. const itemIndex = autocomplete.findIndex(item => item.textContent === 'gpu');
  95. expect(itemIndex).toBeGreaterThan(-1);
  96. await selectNthAutocompleteItem(itemIndex);
  97. // the trailing space is important here as without it, autocomplete suggestions will
  98. // try to complete `has:gpu` thinking the token has not ended yet
  99. expect(screen.getByTestId('smart-search-input')).toHaveValue('has:gpu ');
  100. });
  101. it('searches and selects an event field value', async function () {
  102. render(<SearchBar {...props} />);
  103. await setQuery('gpu:');
  104. expect(tagValuesMock).toHaveBeenCalledWith(
  105. '/organizations/org-slug/tags/gpu/values/',
  106. expect.objectContaining({
  107. query: {project: ['1', '2'], statsPeriod: '14d', includeTransactions: '1'},
  108. })
  109. );
  110. const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
  111. expect(autocomplete.at(2)).toHaveTextContent('Nvidia 1080ti');
  112. await selectNthAutocompleteItem(2);
  113. expect(screen.getByTestId('smart-search-input')).toHaveValue('gpu:"Nvidia 1080ti" ');
  114. });
  115. it('if `useFormWrapper` is false, async pressing enter when there are no dropdown items selected should blur and call `onSearch` callback', async function () {
  116. const onBlur = jest.fn();
  117. const onSearch = jest.fn();
  118. render(
  119. <SearchBar {...props} useFormWrapper={false} onSearch={onSearch} onBlur={onBlur} />
  120. );
  121. await setQuery('gpu:');
  122. expect(tagValuesMock).toHaveBeenCalledWith(
  123. '/organizations/org-slug/tags/gpu/values/',
  124. expect.objectContaining({
  125. query: {project: ['1', '2'], statsPeriod: '14d', includeTransactions: '1'},
  126. })
  127. );
  128. const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
  129. expect(autocomplete.at(2)).toHaveTextContent('Nvidia 1080ti');
  130. await selectNthAutocompleteItem(2);
  131. await userEvent.type(screen.getByTestId('smart-search-input'), '{enter}');
  132. expect(onSearch).toHaveBeenCalledTimes(1);
  133. });
  134. it('filters dropdown to accommodate for num characters left in query', async function () {
  135. render(<SearchBar {...props} maxQueryLength={5} />);
  136. await setQuery('g');
  137. const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
  138. expect(autocomplete.at(0)).toHaveTextContent('g');
  139. expect(autocomplete).toHaveLength(2);
  140. });
  141. it('returns zero dropdown suggestions if out of characters', async function () {
  142. render(<SearchBar {...props} maxQueryLength={2} />);
  143. await setQuery('g');
  144. expect(await screen.findByText('No items found')).toBeInTheDocument();
  145. });
  146. it('sets maxLength property', function () {
  147. render(<SearchBar {...props} maxQueryLength={10} />);
  148. expect(screen.getByTestId('smart-search-input')).toHaveAttribute('maxLength', '10');
  149. });
  150. it('does not requery for event field values if query does not change', async function () {
  151. render(<SearchBar {...props} />);
  152. await setQuery('gpu:');
  153. // Click will fire "updateAutocompleteItems"
  154. await userEvent.click(screen.getByTestId('smart-search-input'), {delay: null});
  155. expect(tagValuesMock).toHaveBeenCalledTimes(1);
  156. });
  157. it('removes highlight when query is empty', async function () {
  158. render(<SearchBar {...props} />);
  159. await setQuery('gpu');
  160. const autocomplete = await screen.findByTestId('search-autocomplete-item');
  161. expect(autocomplete).toBeInTheDocument();
  162. expect(autocomplete).toHaveTextContent('gpu');
  163. // Should have nothing highlighted
  164. await userEvent.clear(screen.getByTestId('smart-search-input'));
  165. expect(await screen.findByText('Keys')).toBeInTheDocument();
  166. });
  167. it('ignores negation ("!") at the beginning of search term', async function () {
  168. render(<SearchBar {...props} />);
  169. await setQuery('!gp');
  170. const autocomplete = await screen.findByTestId('search-autocomplete-item');
  171. expect(autocomplete).toBeInTheDocument();
  172. expect(autocomplete).toHaveTextContent('gpu');
  173. });
  174. it('ignores wildcard ("*") at the beginning of tag value query', async function () {
  175. render(<SearchBar {...props} />);
  176. await setQuery('!gpu:*');
  177. expect(tagValuesMock).toHaveBeenCalledWith(
  178. '/organizations/org-slug/tags/gpu/values/',
  179. expect.objectContaining({
  180. query: {project: ['1', '2'], statsPeriod: '14d', includeTransactions: '1'},
  181. })
  182. );
  183. await selectNthAutocompleteItem(0);
  184. expect(screen.getByTestId('smart-search-input')).toHaveValue('!gpu:"Nvidia 1080ti" ');
  185. });
  186. it('stops searching after no values are returned', async function () {
  187. const emptyTagValuesMock = MockApiClient.addMockResponse({
  188. url: '/organizations/org-slug/tags/browser/values/',
  189. body: [],
  190. });
  191. render(<SearchBar {...props} />);
  192. // Do 3 searches, the first will find nothing, so no more requests should be made
  193. await setQuery('browser:Nothing');
  194. expect(await screen.findByText('No items found')).toBeInTheDocument();
  195. expect(emptyTagValuesMock).toHaveBeenCalled();
  196. emptyTagValuesMock.mockClear();
  197. // Add E character
  198. await setQuery('E');
  199. await setQuery('Els');
  200. // No Additional calls
  201. expect(emptyTagValuesMock).not.toHaveBeenCalled();
  202. });
  203. it('continues searching after no values if query changes', async function () {
  204. const emptyTagValuesMock = MockApiClient.addMockResponse({
  205. url: '/organizations/org-slug/tags/browser/values/',
  206. body: [],
  207. });
  208. render(<SearchBar {...props} />);
  209. await setQuery('browser:Nothing');
  210. expect(emptyTagValuesMock).toHaveBeenCalled();
  211. emptyTagValuesMock.mockClear();
  212. await userEvent.clear(screen.getByTestId('smart-search-input'));
  213. await setQuery('browser:Something');
  214. expect(emptyTagValuesMock).toHaveBeenCalled();
  215. });
  216. it('searches for custom measurements', async function () {
  217. const initializationObj = initializeOrg({
  218. organization: {
  219. features: ['performance-view'],
  220. },
  221. });
  222. props.organization = initializationObj.organization;
  223. render(
  224. <SearchBar
  225. {...props}
  226. customMeasurements={{
  227. 'measurements.custom.ratio': {
  228. key: 'measurements.custom.ratio',
  229. name: 'measurements.custom.ratio',
  230. functions: [],
  231. fieldType: 'test',
  232. unit: '',
  233. },
  234. }}
  235. />
  236. );
  237. await userEvent.type(screen.getByRole('textbox'), 'custom');
  238. expect(await screen.findByText('measurements')).toBeInTheDocument();
  239. expect(screen.getByText(/\.ratio/)).toBeInTheDocument();
  240. });
  241. it('raises Invalid file size when parsed filter unit is not a valid size unit', async () => {
  242. render(
  243. <SearchBar
  244. {...props}
  245. customMeasurements={{
  246. 'measurements.custom.kibibyte': {
  247. key: 'measurements.custom.kibibyte',
  248. name: 'measurements.custom.kibibyte',
  249. functions: [],
  250. fieldType: 'size',
  251. unit: '',
  252. },
  253. }}
  254. />
  255. );
  256. const textbox = screen.getByRole('textbox');
  257. await userEvent.click(textbox);
  258. await userEvent.type(textbox, 'measurements.custom.kibibyte:10ms ');
  259. await userEvent.keyboard('{arrowleft}');
  260. expect(
  261. screen.getByText(
  262. 'Invalid file size. Expected number followed by file size unit suffix'
  263. )
  264. ).toBeInTheDocument();
  265. });
  266. it('raises Invalid duration when parsed filter unit is not a valid duration unit', async () => {
  267. render(
  268. <SearchBar
  269. {...props}
  270. customMeasurements={{
  271. 'measurements.custom.minute': {
  272. key: 'measurements.custom.minute',
  273. name: 'measurements.custom.minute',
  274. functions: [],
  275. fieldType: 'duration',
  276. unit: '',
  277. },
  278. }}
  279. />
  280. );
  281. const textbox = screen.getByRole('textbox');
  282. await userEvent.click(textbox);
  283. await userEvent.type(textbox, 'measurements.custom.minute:10kb ');
  284. await userEvent.keyboard('{arrowleft}');
  285. expect(
  286. screen.getByText(
  287. 'Invalid duration. Expected number followed by duration unit suffix'
  288. )
  289. ).toBeInTheDocument();
  290. });
  291. it('is query works for metric alert search bar', async function () {
  292. const OrganizationIs = OrganizationFixture();
  293. render(
  294. <SearchBar
  295. {...props}
  296. supportedTags={datasetSupportedTags(Dataset.ERRORS, OrganizationIs)}
  297. />
  298. );
  299. await setQuery('is:');
  300. const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
  301. expect(autocomplete.at(0)).toHaveTextContent('is:');
  302. });
  303. it('handled query works for metric alert search bar', async function () {
  304. const OrganizationIs = OrganizationFixture();
  305. render(
  306. <SearchBar
  307. {...props}
  308. supportedTags={datasetSupportedTags(Dataset.ERRORS, OrganizationIs)}
  309. />
  310. );
  311. await setQuery('error.handled:');
  312. const autocomplete = await screen.findAllByTestId('search-autocomplete-item');
  313. expect(autocomplete.at(0)).toHaveTextContent('handled:');
  314. });
  315. });