smartSearchBar.spec.jsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. import React from 'react';
  2. import {shallow, mount} from 'enzyme';
  3. import {Client} from 'app/api';
  4. import {SmartSearchBar, addSpace, removeSpace} from 'app/components/smartSearchBar';
  5. import TagStore from 'app/stores/tagStore';
  6. describe('addSpace()', function() {
  7. it('should add a space when there is no trailing space', function() {
  8. expect(addSpace('one')).toEqual('one ');
  9. });
  10. it('should not add another space when there is already one', function() {
  11. expect(addSpace('one ')).toEqual('one ');
  12. });
  13. it('should leave the empty string alone', function() {
  14. expect(addSpace('')).toEqual('');
  15. });
  16. });
  17. describe('removeSpace()', function() {
  18. it('should remove a trailing space', function() {
  19. expect(removeSpace('one ')).toEqual('one');
  20. });
  21. it('should not remove the last character if it is not a space', function() {
  22. expect(removeSpace('one')).toEqual('one');
  23. });
  24. it('should leave the empty string alone', function() {
  25. expect(removeSpace('')).toEqual('');
  26. });
  27. });
  28. describe('SmartSearchBar', function() {
  29. let options, organization, supportedTags;
  30. let environmentTagValuesMock;
  31. const tagValuesMock = jest.fn(() => Promise.resolve([]));
  32. beforeEach(function() {
  33. TagStore.reset();
  34. TagStore.onLoadTagsSuccess(TestStubs.Tags());
  35. tagValuesMock.mockClear();
  36. supportedTags = {};
  37. organization = TestStubs.Organization({id: '123'});
  38. options = TestStubs.routerContext([{organization}]);
  39. environmentTagValuesMock = MockApiClient.addMockResponse({
  40. url: '/projects/123/456/tags/environment/values/',
  41. body: [],
  42. });
  43. });
  44. afterEach(function() {
  45. MockApiClient.clearMockResponses();
  46. });
  47. describe('componentWillReceiveProps()', function() {
  48. it('should add a space when setting state.query', function() {
  49. const searchBar = shallow(
  50. <SmartSearchBar supportedTags={supportedTags} query="one" />,
  51. options
  52. );
  53. expect(searchBar.state().query).toEqual('one ');
  54. });
  55. it('should update state.query if props.query is updated from outside', function() {
  56. const searchBar = shallow(
  57. <SmartSearchBar supportedTags={supportedTags} query="one" />,
  58. options
  59. );
  60. searchBar.setProps({query: 'two'});
  61. expect(searchBar.state().query).toEqual('two ');
  62. });
  63. it('should not reset user input if a noop props change happens', function() {
  64. const searchBar = shallow(
  65. <SmartSearchBar supportedTags={supportedTags} query="one" />,
  66. options
  67. );
  68. searchBar.setState({query: 'two'});
  69. searchBar.setProps({query: 'one'});
  70. expect(searchBar.state().query).toEqual('two');
  71. });
  72. it('should reset user input if a meaningful props change happens', function() {
  73. const searchBar = shallow(
  74. <SmartSearchBar supportedTags={supportedTags} query="one" />,
  75. options
  76. );
  77. searchBar.setState({query: 'two'});
  78. searchBar.setProps({query: 'three'});
  79. expect(searchBar.state().query).toEqual('three ');
  80. });
  81. });
  82. describe('getQueryTerms()', function() {
  83. it('should extract query terms from a query string', function() {
  84. let query = 'tagname: ';
  85. expect(SmartSearchBar.getQueryTerms(query, query.length)).toEqual(['tagname:']);
  86. query = 'tagname:derp browser:';
  87. expect(SmartSearchBar.getQueryTerms(query, query.length)).toEqual([
  88. 'tagname:derp',
  89. 'browser:',
  90. ]);
  91. query = ' browser:"Chrome 33.0" ';
  92. expect(SmartSearchBar.getQueryTerms(query, query.length)).toEqual([
  93. 'browser:"Chrome 33.0"',
  94. ]);
  95. });
  96. });
  97. describe('getLastTermIndex()', function() {
  98. it('should provide the index of the last query term, given cursor index', function() {
  99. let query = 'tagname:';
  100. expect(SmartSearchBar.getLastTermIndex(query, 0)).toEqual(8);
  101. query = 'tagname:foo'; // 'f' (index 9)
  102. expect(SmartSearchBar.getLastTermIndex(query, 9)).toEqual(11);
  103. query = 'tagname:foo anothertag:bar'; // 'f' (index 9)
  104. expect(SmartSearchBar.getLastTermIndex(query, 9)).toEqual(11);
  105. });
  106. });
  107. describe('clearSearch()', function() {
  108. it('clears the query', function() {
  109. const props = {
  110. orgId: '123',
  111. projectId: '456',
  112. query: 'is:unresolved ruby',
  113. defaultQuery: 'is:unresolved',
  114. supportedTags,
  115. };
  116. const searchBar = shallow(<SmartSearchBar {...props} />, options).instance();
  117. searchBar.clearSearch();
  118. expect(searchBar.state.query).toEqual('');
  119. });
  120. it('calls onSearch()', async function() {
  121. const props = {
  122. orgId: '123',
  123. projectId: '456',
  124. query: 'is:unresolved ruby',
  125. defaultQuery: 'is:unresolved',
  126. supportedTags,
  127. onSearch: jest.fn(),
  128. };
  129. const searchBar = shallow(<SmartSearchBar {...props} />, options).instance();
  130. await searchBar.clearSearch();
  131. expect(props.onSearch).toHaveBeenCalledWith('');
  132. });
  133. });
  134. describe('onQueryFocus()', function() {
  135. it('displays the drop down', function() {
  136. const searchBar = shallow(
  137. <SmartSearchBar
  138. orgId="123"
  139. projectId="456"
  140. supportedTags={supportedTags}
  141. onGetTagValues={tagValuesMock}
  142. />,
  143. options
  144. ).instance();
  145. expect(searchBar.state.dropdownVisible).toBe(false);
  146. searchBar.onQueryFocus();
  147. expect(searchBar.state.dropdownVisible).toBe(true);
  148. });
  149. it('displays dropdown in hasPinnedSearch mode', function() {
  150. const searchBar = shallow(
  151. <SmartSearchBar
  152. orgId="123"
  153. projectId="456"
  154. supportedTags={supportedTags}
  155. onGetTagValues={tagValuesMock}
  156. hasPinnedSearch
  157. />,
  158. options
  159. ).instance();
  160. expect(searchBar.state.dropdownVisible).toBe(false);
  161. searchBar.onQueryFocus();
  162. expect(searchBar.state.dropdownVisible).toBe(true);
  163. });
  164. });
  165. describe('onQueryBlur()', function() {
  166. it('hides the drop down', function() {
  167. const searchBar = shallow(
  168. <SmartSearchBar orgId="123" projectId="456" supportedTags={supportedTags} />,
  169. options
  170. ).instance();
  171. searchBar.state.dropdownVisible = true;
  172. jest.useFakeTimers();
  173. searchBar.onQueryBlur();
  174. jest.advanceTimersByTime(201); // doesn't close until 200ms
  175. expect(searchBar.state.dropdownVisible).toBe(false);
  176. });
  177. });
  178. describe('onKeyUp()', function() {
  179. describe('escape', function() {
  180. it('blurs the input', function() {
  181. const wrapper = shallow(
  182. <SmartSearchBar orgId="123" projectId="456" supportedTags={supportedTags} />,
  183. options
  184. );
  185. wrapper.setState({dropdownVisible: true});
  186. const instance = wrapper.instance();
  187. jest.spyOn(instance, 'blur');
  188. wrapper.find('input').simulate('keyup', {key: 'Escape', keyCode: '27'});
  189. expect(instance.blur).toHaveBeenCalledTimes(1);
  190. });
  191. });
  192. });
  193. describe('render()', function() {
  194. it('invokes onSearch() when submitting the form', function() {
  195. const stubbedOnSearch = jest.fn();
  196. const wrapper = mount(
  197. <SmartSearchBar
  198. onSearch={stubbedOnSearch}
  199. organization={organization}
  200. orgId="123"
  201. projectId="456"
  202. query="is:unresolved"
  203. supportedTags={supportedTags}
  204. />,
  205. options
  206. );
  207. wrapper.find('form').simulate('submit', {
  208. preventDefault() {},
  209. });
  210. expect(stubbedOnSearch).toHaveBeenCalledWith('is:unresolved');
  211. });
  212. it('invokes onSearch() when search is cleared', async function() {
  213. jest.useRealTimers();
  214. const props = {
  215. organization,
  216. orgId: '123',
  217. projectId: '456',
  218. query: 'is:unresolved',
  219. supportedTags,
  220. onSearch: jest.fn(),
  221. };
  222. const wrapper = mount(<SmartSearchBar {...props} />, options);
  223. wrapper.find('.search-clear-form').simulate('click');
  224. await tick();
  225. expect(props.onSearch).toHaveBeenCalledWith('');
  226. });
  227. it('invokes onSearch() on submit in hasPinnedSearch mode', function() {
  228. const stubbedOnSearch = jest.fn();
  229. const wrapper = mount(
  230. <SmartSearchBar
  231. onSearch={stubbedOnSearch}
  232. organization={organization}
  233. orgId="123"
  234. projectId="456"
  235. query="is:unresolved"
  236. supportedTags={supportedTags}
  237. hasPinnedSearch
  238. />,
  239. options
  240. );
  241. wrapper.find('form').simulate('submit');
  242. expect(stubbedOnSearch).toHaveBeenCalledWith('is:unresolved');
  243. });
  244. });
  245. it('handles an empty query', function() {
  246. const props = {
  247. orgId: '123',
  248. projectId: '456',
  249. query: '',
  250. defaultQuery: 'is:unresolved',
  251. organization,
  252. supportedTags,
  253. };
  254. const wrapper = mount(<SmartSearchBar {...props} />, options);
  255. expect(wrapper.state('query')).toEqual('');
  256. });
  257. describe('updateAutoCompleteItems()', function() {
  258. beforeEach(function() {
  259. jest.useFakeTimers();
  260. });
  261. it('sets state when empty', function() {
  262. const props = {
  263. orgId: '123',
  264. projectId: '456',
  265. query: '',
  266. organization,
  267. supportedTags,
  268. };
  269. const searchBar = mount(<SmartSearchBar {...props} />, options).instance();
  270. searchBar.updateAutoCompleteItems();
  271. expect(searchBar.state.searchTerm).toEqual('');
  272. expect(searchBar.state.searchItems).toEqual([]);
  273. expect(searchBar.state.activeSearchItem).toEqual(0);
  274. });
  275. it('sets state when incomplete tag', async function() {
  276. const props = {
  277. orgId: '123',
  278. projectId: '456',
  279. query: 'fu',
  280. organization,
  281. supportedTags,
  282. };
  283. jest.useRealTimers();
  284. const wrapper = mount(<SmartSearchBar {...props} />, options);
  285. const searchBar = wrapper.instance();
  286. searchBar.updateAutoCompleteItems();
  287. await tick();
  288. wrapper.update();
  289. expect(searchBar.state.searchTerm).toEqual('fu');
  290. expect(searchBar.state.searchItems).toEqual([
  291. expect.objectContaining({children: []}),
  292. ]);
  293. expect(searchBar.state.activeSearchItem).toEqual(0);
  294. });
  295. it('sets state when incomplete tag has negation operator', async function() {
  296. const props = {
  297. orgId: '123',
  298. projectId: '456',
  299. query: '!fu',
  300. organization,
  301. supportedTags,
  302. };
  303. jest.useRealTimers();
  304. const wrapper = mount(<SmartSearchBar {...props} />, options);
  305. const searchBar = wrapper.instance();
  306. searchBar.updateAutoCompleteItems();
  307. await tick();
  308. wrapper.update();
  309. expect(searchBar.state.searchTerm).toEqual('fu');
  310. expect(searchBar.state.searchItems).toEqual([
  311. expect.objectContaining({children: []}),
  312. ]);
  313. expect(searchBar.state.activeSearchItem).toEqual(0);
  314. });
  315. it('sets state when incomplete tag as second input', async function() {
  316. const props = {
  317. orgId: '123',
  318. projectId: '456',
  319. query: 'is:unresolved fu',
  320. organization,
  321. supportedTags,
  322. };
  323. jest.useRealTimers();
  324. const wrapper = mount(<SmartSearchBar {...props} />, options);
  325. const searchBar = wrapper.instance();
  326. searchBar.getCursorPosition = jest.fn();
  327. searchBar.getCursorPosition.mockReturnValue(15); // end of line
  328. searchBar.updateAutoCompleteItems();
  329. await tick();
  330. wrapper.update();
  331. expect(searchBar.state.searchTerm).toEqual('fu');
  332. // 1 items because of headers ("Tags")
  333. expect(searchBar.state.searchItems).toHaveLength(1);
  334. expect(searchBar.state.activeSearchItem).toEqual(0);
  335. });
  336. it('does not request values when tag is environments', function() {
  337. const props = {
  338. orgId: '123',
  339. projectId: '456',
  340. query: 'environment:production',
  341. excludeEnvironment: true,
  342. organization,
  343. supportedTags,
  344. };
  345. const searchBar = mount(<SmartSearchBar {...props} />, options).instance();
  346. searchBar.updateAutoCompleteItems();
  347. jest.advanceTimersByTime(301);
  348. expect(environmentTagValuesMock).not.toHaveBeenCalled();
  349. });
  350. it('does not request values when tag is `timesSeen`', function() {
  351. // This should never get called
  352. const mock = MockApiClient.addMockResponse({
  353. url: '/projects/123/456/tags/timesSeen/values/',
  354. body: [],
  355. });
  356. const props = {
  357. orgId: '123',
  358. projectId: '456',
  359. query: 'timesSeen:',
  360. organization,
  361. supportedTags,
  362. };
  363. const searchBar = mount(<SmartSearchBar {...props} />, options).instance();
  364. searchBar.updateAutoCompleteItems();
  365. jest.advanceTimersByTime(301);
  366. expect(mock).not.toHaveBeenCalled();
  367. });
  368. });
  369. describe('onTogglePinnedSearch', function() {
  370. let pinRequest, unpinRequest;
  371. beforeEach(function() {
  372. pinRequest = MockApiClient.addMockResponse({
  373. url: '/organizations/org-slug/pinned-searches/',
  374. method: 'PUT',
  375. body: [],
  376. });
  377. unpinRequest = MockApiClient.addMockResponse({
  378. url: '/organizations/org-slug/pinned-searches/',
  379. method: 'DELETE',
  380. body: [],
  381. });
  382. MockApiClient.addMockResponse({
  383. url: '/organizations/org-slug/recent-searches/',
  384. method: 'POST',
  385. body: {},
  386. });
  387. });
  388. it('adds pins', async function() {
  389. const wrapper = mount(
  390. <SmartSearchBar
  391. api={new Client()}
  392. organization={organization}
  393. orgId={organization.slug}
  394. projectId="456"
  395. query="is:unresolved"
  396. supportedTags={supportedTags}
  397. savedSearchType={0}
  398. hasPinnedSearch
  399. />,
  400. options
  401. );
  402. wrapper.find('button[aria-label="Pin this search"]').simulate('click');
  403. await wrapper.update();
  404. expect(pinRequest).toHaveBeenCalled();
  405. expect(unpinRequest).not.toHaveBeenCalled();
  406. });
  407. it('removes pins', async function() {
  408. const pinnedSearch = TestStubs.Search({isPinned: true});
  409. const wrapper = mount(
  410. <SmartSearchBar
  411. api={new Client()}
  412. organization={organization}
  413. orgId={organization.slug}
  414. projectId="456"
  415. query="is:unresolved"
  416. supportedTags={supportedTags}
  417. savedSearchType={0}
  418. pinnedSearch={pinnedSearch}
  419. hasPinnedSearch
  420. />,
  421. options
  422. );
  423. wrapper.find('button[aria-label="Unpin this search"]').simulate('click');
  424. await wrapper.update();
  425. expect(pinRequest).not.toHaveBeenCalled();
  426. expect(unpinRequest).toHaveBeenCalled();
  427. });
  428. });
  429. });