searchBar.spec.jsx 11 KB

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