searchBar.spec.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  3. import TagStore from 'sentry/stores/tagStore';
  4. import IssueListSearchBar from 'sentry/views/issueList/searchBar';
  5. import {OrganizationContext} from '../organizationContext';
  6. describe('IssueListSearchBar', function () {
  7. let tagValuePromise;
  8. let supportedTags;
  9. let recentSearchMock;
  10. let defaultProps;
  11. const {routerContext, organization} = initializeOrg();
  12. beforeEach(function () {
  13. TagStore.reset();
  14. TagStore.loadTagsSuccess(TestStubs.Tags());
  15. supportedTags = TagStore.getState();
  16. // Add a tag that is preseeded with values.
  17. supportedTags.is = {
  18. key: 'is',
  19. name: 'is',
  20. values: ['assigned', 'unresolved', 'ignored'],
  21. predefined: true,
  22. };
  23. tagValuePromise = Promise.resolve([]);
  24. defaultProps = {
  25. organization,
  26. query: '',
  27. tagValueLoader: () => tagValuePromise,
  28. supportedTags,
  29. onSearch: jest.fn(),
  30. onSidebarToggle: () => {},
  31. sort: 'date',
  32. };
  33. recentSearchMock = MockApiClient.addMockResponse({
  34. url: '/organizations/org-slug/recent-searches/',
  35. method: 'GET',
  36. body: [],
  37. });
  38. });
  39. afterEach(function () {
  40. MockApiClient.clearMockResponses();
  41. });
  42. describe('updateAutoCompleteItems()', function () {
  43. it('sets state with complete tag', function () {
  44. const loader = jest.fn();
  45. render(
  46. <OrganizationContext.Provider value={organization}>
  47. <IssueListSearchBar {...defaultProps} tagValueLoader={loader} />
  48. </OrganizationContext.Provider>,
  49. {context: routerContext}
  50. );
  51. userEvent.type(screen.getByRole('textbox'), 'url:"fu"');
  52. expect(loader).toHaveBeenCalledWith('url', 'fu');
  53. expect(screen.getByTestId('smart-search-dropdown')).toBeInTheDocument();
  54. expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
  55. });
  56. it('sets state when value has colon', function () {
  57. const loader = jest.fn();
  58. render(
  59. <OrganizationContext.Provider value={organization}>
  60. <IssueListSearchBar {...defaultProps} tagValueLoader={loader} />
  61. </OrganizationContext.Provider>,
  62. {context: routerContext}
  63. );
  64. userEvent.type(screen.getByRole('textbox'), 'url:');
  65. expect(loader).toHaveBeenCalledWith('url', '');
  66. });
  67. it('does not request values when tag is `timesSeen`', function () {
  68. // This should never get called
  69. const loader = jest.fn(x => x);
  70. render(
  71. <OrganizationContext.Provider value={organization}>
  72. <IssueListSearchBar {...defaultProps} tagValueLoader={loader} />
  73. </OrganizationContext.Provider>,
  74. {context: routerContext}
  75. );
  76. userEvent.type(screen.getByRole('textbox'), 'timesSeen:');
  77. expect(loader).not.toHaveBeenCalled();
  78. });
  79. });
  80. describe('Recent Searches', function () {
  81. it('saves search query as a recent search', function () {
  82. const saveRecentSearch = MockApiClient.addMockResponse({
  83. url: '/organizations/org-slug/recent-searches/',
  84. method: 'POST',
  85. body: {},
  86. });
  87. const loader = jest.fn();
  88. const onSearch = jest.fn();
  89. render(
  90. <OrganizationContext.Provider value={organization}>
  91. <IssueListSearchBar
  92. {...defaultProps}
  93. tagValueLoader={loader}
  94. onSearch={onSearch}
  95. />
  96. </OrganizationContext.Provider>,
  97. {context: routerContext}
  98. );
  99. userEvent.type(screen.getByRole('textbox'), 'url:"fu"');
  100. expect(loader).toHaveBeenCalledWith('url', 'fu');
  101. expect(screen.getByTestId('smart-search-dropdown')).toBeInTheDocument();
  102. expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
  103. userEvent.keyboard('{Enter}');
  104. expect(onSearch).toHaveBeenCalledWith('url:"fu"');
  105. expect(saveRecentSearch).toHaveBeenCalledWith(
  106. expect.anything(),
  107. expect.objectContaining({
  108. data: {
  109. query: 'url:"fu"',
  110. type: 0,
  111. },
  112. })
  113. );
  114. });
  115. it('queries for recent searches', function () {
  116. render(
  117. <OrganizationContext.Provider value={organization}>
  118. <IssueListSearchBar {...defaultProps} />
  119. </OrganizationContext.Provider>,
  120. {context: routerContext}
  121. );
  122. userEvent.type(screen.getByRole('textbox'), 'is:');
  123. expect(recentSearchMock).toHaveBeenCalledWith(
  124. expect.anything(),
  125. expect.objectContaining({
  126. query: {
  127. query: 'is:',
  128. limit: 3,
  129. type: 0,
  130. },
  131. })
  132. );
  133. });
  134. it('cycles through keyboard navigation for selection', async function () {
  135. render(
  136. <OrganizationContext.Provider value={organization}>
  137. <IssueListSearchBar {...defaultProps} />
  138. </OrganizationContext.Provider>,
  139. {context: routerContext}
  140. );
  141. const textarea = screen.getByRole('textbox');
  142. // Keyboard navigate to first item and select
  143. userEvent.type(textarea, 't');
  144. await waitFor(() =>
  145. expect(screen.getAllByTestId('search-autocomplete-item')[0]).toBeInTheDocument()
  146. );
  147. userEvent.keyboard('{ArrowDown}{Tab}');
  148. expect(textarea).not.toHaveValue('t');
  149. const firstItemValue = textarea.textContent;
  150. // Keyboard navigate to second item and select
  151. userEvent.keyboard('{selectall}{backspace}t');
  152. await waitFor(() =>
  153. expect(screen.getAllByTestId('search-autocomplete-item')[0]).toBeInTheDocument()
  154. );
  155. userEvent.keyboard('{ArrowDown}{ArrowDown}{Tab}');
  156. expect(textarea).not.toHaveValue(firstItemValue);
  157. // Keyboard navigate to second item, then back to first item and select
  158. userEvent.keyboard('{selectall}{backspace}t');
  159. await waitFor(() =>
  160. expect(screen.getAllByTestId('search-autocomplete-item')[0]).toBeInTheDocument()
  161. );
  162. userEvent.keyboard('{ArrowDown}{ArrowDown}{ArrowUp}{Tab}');
  163. expect(textarea).toHaveValue(firstItemValue);
  164. });
  165. });
  166. describe('Pinned Searches', function () {
  167. let pinSearch;
  168. let unpinSearch;
  169. beforeEach(function () {
  170. MockApiClient.clearMockResponses();
  171. pinSearch = MockApiClient.addMockResponse({
  172. url: '/organizations/org-slug/pinned-searches/',
  173. method: 'PUT',
  174. body: {},
  175. });
  176. unpinSearch = MockApiClient.addMockResponse({
  177. url: '/organizations/org-slug/pinned-searches/',
  178. method: 'DELETE',
  179. body: {},
  180. });
  181. MockApiClient.addMockResponse({
  182. url: '/organizations/org-slug/recent-searches/',
  183. method: 'GET',
  184. body: [],
  185. });
  186. });
  187. it('has pin icon', function () {
  188. render(
  189. <OrganizationContext.Provider value={organization}>
  190. <IssueListSearchBar {...defaultProps} />
  191. </OrganizationContext.Provider>,
  192. {context: routerContext}
  193. );
  194. expect(screen.getByTestId('pin-icon')).toBeInTheDocument();
  195. });
  196. it('pins a search from the searchbar', function () {
  197. render(
  198. <OrganizationContext.Provider value={organization}>
  199. <IssueListSearchBar {...defaultProps} query='url:"fu"' />
  200. </OrganizationContext.Provider>,
  201. {context: routerContext}
  202. );
  203. userEvent.click(screen.getByRole('button', {name: 'Pin this search'}));
  204. expect(pinSearch).toHaveBeenLastCalledWith(
  205. expect.anything(),
  206. expect.objectContaining({
  207. method: 'PUT',
  208. data: {
  209. query: 'url:"fu"',
  210. sort: 'date',
  211. type: 0,
  212. },
  213. })
  214. );
  215. });
  216. it('unpins a search from the searchbar', function () {
  217. render(
  218. <OrganizationContext.Provider value={organization}>
  219. <IssueListSearchBar
  220. {...defaultProps}
  221. query='url:"fu"'
  222. savedSearch={{
  223. id: '1',
  224. name: 'Saved Search',
  225. isPinned: true,
  226. query: 'url:"fu"',
  227. sort: 'date',
  228. dateCreated: '',
  229. isOrgCustom: false,
  230. isGlobal: false,
  231. type: 0,
  232. }}
  233. />
  234. </OrganizationContext.Provider>,
  235. {context: routerContext}
  236. );
  237. userEvent.click(screen.getByRole('button', {name: 'Unpin this search'}));
  238. expect(unpinSearch).toHaveBeenLastCalledWith(
  239. expect.anything(),
  240. expect.objectContaining({
  241. method: 'DELETE',
  242. data: {
  243. type: 0,
  244. },
  245. })
  246. );
  247. });
  248. });
  249. });