searchBar.spec.jsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import TagStore from 'sentry/stores/tagStore';
  4. import IssueListSearchBar from 'sentry/views/issueList/searchBar';
  5. describe('IssueListSearchBar', function () {
  6. let tagValuePromise;
  7. let supportedTags;
  8. let recentSearchMock;
  9. const {routerContext, organization} = initializeOrg({
  10. organization: {access: [], features: []},
  11. });
  12. const mockCursorPosition = (wrapper, pos) => {
  13. const component = wrapper.find('SmartSearchBar').instance();
  14. delete component.cursorPosition;
  15. Object.defineProperty(component, 'cursorPosition', {
  16. get: jest.fn().mockReturnValue(pos),
  17. configurable: true,
  18. });
  19. };
  20. beforeEach(function () {
  21. TagStore.reset();
  22. TagStore.loadTagsSuccess(TestStubs.Tags());
  23. supportedTags = TagStore.getStateTags();
  24. // Add a tag that is preseeded with values.
  25. supportedTags.is = {
  26. key: 'is',
  27. name: 'is',
  28. values: ['assigned', 'unresolved', 'ignored'],
  29. predefined: true,
  30. };
  31. tagValuePromise = Promise.resolve([]);
  32. recentSearchMock = MockApiClient.addMockResponse({
  33. url: '/organizations/org-slug/recent-searches/',
  34. method: 'GET',
  35. body: [],
  36. });
  37. });
  38. afterEach(function () {
  39. MockApiClient.clearMockResponses();
  40. });
  41. describe('updateAutoCompleteItems()', function () {
  42. it('sets state with complete tag', async () => {
  43. const loader = (key, value) => {
  44. expect(key).toEqual('url');
  45. expect(value).toEqual('fu');
  46. return tagValuePromise;
  47. };
  48. const props = {
  49. organization,
  50. query: 'url:"fu"',
  51. tagValueLoader: loader,
  52. supportedTags,
  53. onSearch: jest.fn(),
  54. };
  55. const searchBar = mountWithTheme(<IssueListSearchBar {...props} />, routerContext);
  56. mockCursorPosition(searchBar, 5);
  57. searchBar.find('textarea').simulate('click');
  58. searchBar.find('textarea').simulate('focus');
  59. await tick();
  60. expect(searchBar.find('SearchDropdown').prop('searchSubstring')).toEqual('"fu"');
  61. expect(searchBar.find('SearchDropdown').prop('items')).toEqual([]);
  62. });
  63. it('sets state when value has colon', async () => {
  64. const loader = (key, value) => {
  65. expect(key).toEqual('url');
  66. expect(value).toEqual('http://example.com');
  67. return tagValuePromise;
  68. };
  69. const props = {
  70. organization,
  71. projectId: '456',
  72. query: 'url:"http://example.com"',
  73. tagValueLoader: loader,
  74. supportedTags,
  75. onSearch: jest.fn(),
  76. };
  77. const searchBar = mountWithTheme(<IssueListSearchBar {...props} />, routerContext);
  78. mockCursorPosition(searchBar, 5);
  79. searchBar.find('textarea').simulate('click');
  80. searchBar.find('textarea').simulate('focus');
  81. await tick();
  82. expect(searchBar.state.searchTerm).toEqual();
  83. expect(searchBar.find('SearchDropdown').prop('searchSubstring')).toEqual(
  84. '"http://example.com"'
  85. );
  86. expect(searchBar.find('SearchDropdown').prop('items')).toEqual([]);
  87. });
  88. it('does not request values when tag is `timesSeen`', async () => {
  89. // This should never get called
  90. const loader = jest.fn(x => x);
  91. const props = {
  92. organization,
  93. projectId: '456',
  94. query: 'timesSeen:',
  95. tagValueLoader: loader,
  96. supportedTags,
  97. onSearch: jest.fn(),
  98. };
  99. const searchBar = mountWithTheme(<IssueListSearchBar {...props} />, routerContext);
  100. searchBar.find('textarea').simulate('click');
  101. searchBar.find('textarea').simulate('focus');
  102. await tick();
  103. expect(loader).not.toHaveBeenCalled();
  104. });
  105. });
  106. describe('Recent Searches', function () {
  107. it('saves search query as a recent search', async function () {
  108. const saveRecentSearch = MockApiClient.addMockResponse({
  109. url: '/organizations/org-slug/recent-searches/',
  110. method: 'POST',
  111. body: {},
  112. });
  113. const loader = (key, value) => {
  114. expect(key).toEqual('url');
  115. expect(value).toEqual('fu');
  116. return tagValuePromise;
  117. };
  118. const onSearch = jest.fn();
  119. const props = {
  120. organization,
  121. query: 'url:"fu"',
  122. onSearch,
  123. tagValueLoader: loader,
  124. supportedTags,
  125. };
  126. const searchBar = mountWithTheme(<IssueListSearchBar {...props} />, routerContext);
  127. mockCursorPosition(searchBar, 5);
  128. searchBar.find('textarea').simulate('focus');
  129. searchBar.find('textarea').simulate('click');
  130. await tick();
  131. expect(searchBar.find('SearchDropdown').prop('searchSubstring')).toEqual('"fu"');
  132. expect(searchBar.find('SearchDropdown').prop('items')).toEqual([]);
  133. jest.useRealTimers();
  134. searchBar.find('form').simulate('submit');
  135. expect(onSearch).toHaveBeenCalledWith('url:"fu"');
  136. await tick();
  137. searchBar.update();
  138. expect(saveRecentSearch).toHaveBeenCalledWith(
  139. expect.anything(),
  140. expect.objectContaining({
  141. data: {
  142. query: 'url:"fu"',
  143. type: 0,
  144. },
  145. })
  146. );
  147. });
  148. it('queries for recent searches', async function () {
  149. const props = {
  150. organization,
  151. query: 'timesSeen:',
  152. tagValueLoader: () => {},
  153. savedSearchType: 0,
  154. displayRecentSearches: true,
  155. supportedTags,
  156. };
  157. jest.useRealTimers();
  158. const wrapper = mountWithTheme(<IssueListSearchBar {...props} />, routerContext);
  159. wrapper.find('textarea').simulate('focus');
  160. wrapper.find('textarea').simulate('change', {target: {value: 'is:'}});
  161. await tick();
  162. wrapper.update();
  163. expect(recentSearchMock).toHaveBeenCalledWith(
  164. expect.anything(),
  165. expect.objectContaining({
  166. query: {
  167. query: 'is:',
  168. limit: 3,
  169. type: 0,
  170. },
  171. })
  172. );
  173. });
  174. it('cycles through keyboard navigation for selection', async function () {
  175. const props = {
  176. organization,
  177. query: 'timesSeen:',
  178. tagValueLoader: () => {},
  179. savedSearchType: 0,
  180. displayRecentSearches: true,
  181. supportedTags,
  182. };
  183. jest.useRealTimers();
  184. const wrapper = mountWithTheme(<IssueListSearchBar {...props} />, routerContext);
  185. wrapper
  186. .find('textarea')
  187. .simulate('focus')
  188. .simulate('change', {target: {value: 'is:'}});
  189. await tick();
  190. wrapper.update();
  191. expect(
  192. wrapper.find('SearchListItem').at(0).find('li').prop('className')
  193. ).not.toContain('active');
  194. wrapper.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  195. expect(wrapper.find('SearchListItem').at(0).find('li').prop('className')).toContain(
  196. 'active'
  197. );
  198. wrapper.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  199. expect(wrapper.find('SearchListItem').at(1).find('li').prop('className')).toContain(
  200. 'active'
  201. );
  202. wrapper.find('textarea').simulate('keyDown', {key: 'ArrowUp'});
  203. wrapper.find('textarea').simulate('keyDown', {key: 'ArrowUp'});
  204. expect(
  205. wrapper.find('SearchListItem').last().find('li').prop('className')
  206. ).toContain('active');
  207. });
  208. });
  209. describe('Pinned Searches', function () {
  210. let pinSearch;
  211. let unpinSearch;
  212. beforeEach(function () {
  213. MockApiClient.clearMockResponses();
  214. pinSearch = MockApiClient.addMockResponse({
  215. url: '/organizations/org-slug/pinned-searches/',
  216. method: 'PUT',
  217. body: {},
  218. });
  219. unpinSearch = MockApiClient.addMockResponse({
  220. url: '/organizations/org-slug/pinned-searches/',
  221. method: 'DELETE',
  222. body: {},
  223. });
  224. MockApiClient.addMockResponse({
  225. url: '/organizations/org-slug/recent-searches/',
  226. method: 'GET',
  227. body: [],
  228. });
  229. });
  230. it('has pin icon', function () {
  231. const props = {
  232. query: 'url:"fu"',
  233. onSearch: jest.fn(),
  234. tagValueLoader: () => Promise.resolve([]),
  235. supportedTags,
  236. organization,
  237. };
  238. const searchBar = mountWithTheme(<IssueListSearchBar {...props} />, routerContext);
  239. expect(searchBar.find('ActionButton[data-test-id="pin-icon"]')).toHaveLength(1);
  240. });
  241. it('pins a search from the searchbar', function () {
  242. const props = {
  243. query: 'url:"fu"',
  244. onSearch: jest.fn(),
  245. tagValueLoader: () => Promise.resolve([]),
  246. supportedTags,
  247. organization,
  248. };
  249. const searchBar = mountWithTheme(<IssueListSearchBar {...props} />, routerContext);
  250. searchBar.find('ActionButton[data-test-id="pin-icon"] button').simulate('click');
  251. expect(pinSearch).toHaveBeenLastCalledWith(
  252. expect.anything(),
  253. expect.objectContaining({
  254. method: 'PUT',
  255. data: {
  256. query: 'url:"fu"',
  257. type: 0,
  258. },
  259. })
  260. );
  261. });
  262. it('unpins a search from the searchbar', function () {
  263. const props = {
  264. query: 'url:"fu"',
  265. onSearch: jest.fn(),
  266. tagValueLoader: () => Promise.resolve([]),
  267. supportedTags,
  268. organization,
  269. savedSearch: {id: '1', isPinned: true, query: 'url:"fu"'},
  270. };
  271. const searchBar = mountWithTheme(<IssueListSearchBar {...props} />, routerContext);
  272. searchBar
  273. .find('ActionButton[aria-label="Unpin this search"] button')
  274. .simulate('click');
  275. expect(unpinSearch).toHaveBeenLastCalledWith(
  276. expect.anything(),
  277. expect.objectContaining({
  278. method: 'DELETE',
  279. data: {
  280. type: 0,
  281. },
  282. })
  283. );
  284. });
  285. });
  286. });