index.spec.jsx 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {Client} from 'sentry/api';
  3. import {SmartSearchBar} from 'sentry/components/smartSearchBar';
  4. import {ShortcutType} from 'sentry/components/smartSearchBar/types';
  5. import {shortcuts} from 'sentry/components/smartSearchBar/utils';
  6. import TagStore from 'sentry/stores/tagStore';
  7. describe('SmartSearchBar', function () {
  8. let location, options, organization, supportedTags;
  9. let environmentTagValuesMock;
  10. const tagValuesMock = jest.fn(() => Promise.resolve([]));
  11. const mockCursorPosition = (component, pos) => {
  12. delete component.cursorPosition;
  13. Object.defineProperty(component, 'cursorPosition', {
  14. get: jest.fn().mockReturnValue(pos),
  15. configurable: true,
  16. });
  17. };
  18. beforeEach(function () {
  19. TagStore.reset();
  20. TagStore.loadTagsSuccess(TestStubs.Tags());
  21. tagValuesMock.mockClear();
  22. supportedTags = TagStore.getAllTags();
  23. supportedTags.firstRelease = {
  24. key: 'firstRelease',
  25. name: 'firstRelease',
  26. };
  27. supportedTags.is = {
  28. key: 'is',
  29. name: 'is',
  30. };
  31. organization = TestStubs.Organization({id: '123'});
  32. location = {
  33. pathname: '/organizations/org-slug/recent-searches/',
  34. query: {
  35. projectId: '0',
  36. },
  37. };
  38. options = TestStubs.routerContext([
  39. {
  40. organization,
  41. location,
  42. router: {location},
  43. },
  44. ]);
  45. MockApiClient.clearMockResponses();
  46. MockApiClient.addMockResponse({
  47. url: '/organizations/org-slug/recent-searches/',
  48. body: [],
  49. });
  50. environmentTagValuesMock = MockApiClient.addMockResponse({
  51. url: '/projects/123/456/tags/environment/values/',
  52. body: [],
  53. });
  54. });
  55. afterEach(function () {
  56. MockApiClient.clearMockResponses();
  57. });
  58. it('quotes in values with spaces when autocompleting', async function () {
  59. jest.useRealTimers();
  60. const getTagValuesMock = jest.fn().mockImplementation(() => {
  61. return Promise.resolve(['this is filled with spaces']);
  62. });
  63. const onSearch = jest.fn();
  64. const props = {
  65. orgId: 'org-slug',
  66. projectId: '0',
  67. query: '',
  68. location,
  69. organization,
  70. supportedTags,
  71. onGetTagValues: getTagValuesMock,
  72. onSearch,
  73. };
  74. const searchBar = mountWithTheme(
  75. <SmartSearchBar {...props} api={new Client()} />,
  76. options
  77. );
  78. searchBar.find('textarea').simulate('focus');
  79. searchBar.find('textarea').simulate('change', {target: {value: 'device:this'}});
  80. await tick();
  81. const preventDefault = jest.fn();
  82. searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  83. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  84. await tick();
  85. expect(searchBar.find('textarea').props().value).toEqual(
  86. 'device:"this is filled with spaces" '
  87. );
  88. });
  89. it('escapes quotes in values properly when autocompleting', async function () {
  90. jest.useRealTimers();
  91. const getTagValuesMock = jest.fn().mockImplementation(() => {
  92. return Promise.resolve(['this " is " filled " with " quotes']);
  93. });
  94. const onSearch = jest.fn();
  95. const props = {
  96. orgId: 'org-slug',
  97. projectId: '0',
  98. query: '',
  99. location,
  100. organization,
  101. supportedTags,
  102. onGetTagValues: getTagValuesMock,
  103. onSearch,
  104. };
  105. const searchBar = mountWithTheme(
  106. <SmartSearchBar {...props} api={new Client()} />,
  107. options
  108. );
  109. searchBar.find('textarea').simulate('focus');
  110. searchBar.find('textarea').simulate('change', {target: {value: 'device:this'}});
  111. await tick();
  112. const preventDefault = jest.fn();
  113. searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  114. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  115. await tick();
  116. expect(searchBar.find('textarea').props().value).toEqual(
  117. 'device:"this \\" is \\" filled \\" with \\" quotes" '
  118. );
  119. });
  120. it('does not preventDefault when there are no search items and is loading and enter is pressed', async function () {
  121. jest.useRealTimers();
  122. const getTagValuesMock = jest.fn().mockImplementation(() => {
  123. return new Promise(() => {});
  124. });
  125. const onSearch = jest.fn();
  126. const props = {
  127. orgId: 'org-slug',
  128. projectId: '0',
  129. query: '',
  130. location,
  131. organization,
  132. supportedTags,
  133. onGetTagValues: getTagValuesMock,
  134. onSearch,
  135. };
  136. const searchBar = mountWithTheme(
  137. <SmartSearchBar {...props} api={new Client()} />,
  138. options
  139. );
  140. searchBar.find('textarea').simulate('focus');
  141. searchBar.find('textarea').simulate('change', {target: {value: 'browser:'}});
  142. await tick();
  143. // press enter
  144. const preventDefault = jest.fn();
  145. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  146. expect(onSearch).not.toHaveBeenCalled();
  147. expect(preventDefault).not.toHaveBeenCalled();
  148. });
  149. it('calls preventDefault when there are existing search items and is loading and enter is pressed', async function () {
  150. jest.useRealTimers();
  151. const getTagValuesMock = jest.fn().mockImplementation(() => {
  152. return new Promise(() => {});
  153. });
  154. const onSearch = jest.fn();
  155. const props = {
  156. orgId: 'org-slug',
  157. projectId: '0',
  158. query: '',
  159. location,
  160. organization,
  161. supportedTags,
  162. onGetTagValues: getTagValuesMock,
  163. onSearch,
  164. };
  165. const searchBar = mountWithTheme(
  166. <SmartSearchBar {...props} api={new Client()} />,
  167. options
  168. );
  169. searchBar.find('textarea').simulate('focus');
  170. searchBar.find('textarea').simulate('change', {target: {value: 'bro'}});
  171. await tick();
  172. // Can't select with tab
  173. searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  174. searchBar.find('textarea').simulate('keyDown', {key: 'Tab'});
  175. expect(onSearch).not.toHaveBeenCalled();
  176. searchBar.find('textarea').simulate('change', {target: {value: 'browser:'}});
  177. await tick();
  178. // press enter
  179. const preventDefault = jest.fn();
  180. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  181. expect(onSearch).not.toHaveBeenCalled();
  182. // Prevent default since we need to select an item
  183. expect(preventDefault).toHaveBeenCalled();
  184. });
  185. describe('componentWillReceiveProps()', function () {
  186. it('should add a space when setting state.query', function () {
  187. const searchBar = mountWithTheme(
  188. <SmartSearchBar
  189. organization={organization}
  190. location={location}
  191. supportedTags={supportedTags}
  192. query="one"
  193. />,
  194. options
  195. );
  196. expect(searchBar.state().query).toEqual('one ');
  197. });
  198. it('should update state.query if props.query is updated from outside', function () {
  199. const searchBar = mountWithTheme(
  200. <SmartSearchBar
  201. organization={organization}
  202. location={location}
  203. supportedTags={supportedTags}
  204. query="one"
  205. />,
  206. options
  207. );
  208. searchBar.setProps({query: 'two'});
  209. expect(searchBar.state().query).toEqual('two ');
  210. });
  211. it('should update state.query if props.query is updated to null/undefined from outside', function () {
  212. const searchBar = mountWithTheme(
  213. <SmartSearchBar
  214. organization={organization}
  215. location={location}
  216. supportedTags={supportedTags}
  217. query="one"
  218. />,
  219. options
  220. );
  221. searchBar.setProps({query: null});
  222. expect(searchBar.state().query).toEqual('');
  223. });
  224. it('should not reset user textarea if a noop props change happens', function () {
  225. const searchBar = mountWithTheme(
  226. <SmartSearchBar
  227. organization={organization}
  228. location={location}
  229. supportedTags={supportedTags}
  230. query="one"
  231. />,
  232. options
  233. );
  234. searchBar.setState({query: 'two'});
  235. searchBar.setProps({query: 'one'});
  236. expect(searchBar.state().query).toEqual('two');
  237. });
  238. it('should reset user textarea if a meaningful props change happens', function () {
  239. const searchBar = mountWithTheme(
  240. <SmartSearchBar
  241. organization={organization}
  242. location={location}
  243. supportedTags={supportedTags}
  244. query="one"
  245. />,
  246. options
  247. );
  248. searchBar.setState({query: 'two'});
  249. searchBar.setProps({query: 'three'});
  250. expect(searchBar.state().query).toEqual('three ');
  251. });
  252. });
  253. describe('clearSearch()', function () {
  254. it('clears the query', function () {
  255. const props = {
  256. organization,
  257. location,
  258. query: 'is:unresolved ruby',
  259. defaultQuery: 'is:unresolved',
  260. supportedTags,
  261. };
  262. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  263. searchBar.clearSearch();
  264. expect(searchBar.state.query).toEqual('');
  265. });
  266. it('calls onSearch()', async function () {
  267. const props = {
  268. organization,
  269. location,
  270. query: 'is:unresolved ruby',
  271. defaultQuery: 'is:unresolved',
  272. supportedTags,
  273. onSearch: jest.fn(),
  274. };
  275. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  276. await searchBar.clearSearch();
  277. expect(props.onSearch).toHaveBeenCalledWith('');
  278. });
  279. });
  280. describe('onQueryFocus()', function () {
  281. it('displays the drop down', function () {
  282. const searchBar = mountWithTheme(
  283. <SmartSearchBar
  284. organization={organization}
  285. location={location}
  286. supportedTags={supportedTags}
  287. onGetTagValues={tagValuesMock}
  288. />,
  289. options
  290. ).instance();
  291. expect(searchBar.state.inputHasFocus).toBe(false);
  292. searchBar.onQueryFocus();
  293. expect(searchBar.state.inputHasFocus).toBe(true);
  294. });
  295. it('displays dropdown in hasPinnedSearch mode', function () {
  296. const searchBar = mountWithTheme(
  297. <SmartSearchBar
  298. organization={organization}
  299. location={location}
  300. supportedTags={supportedTags}
  301. onGetTagValues={tagValuesMock}
  302. hasPinnedSearch
  303. />,
  304. options
  305. ).instance();
  306. expect(searchBar.state.inputHasFocus).toBe(false);
  307. searchBar.onQueryFocus();
  308. expect(searchBar.state.inputHasFocus).toBe(true);
  309. });
  310. });
  311. describe('onQueryBlur()', function () {
  312. it('hides the drop down', function () {
  313. const searchBar = mountWithTheme(
  314. <SmartSearchBar
  315. organization={organization}
  316. location={location}
  317. supportedTags={supportedTags}
  318. />,
  319. options
  320. ).instance();
  321. searchBar.state.inputHasFocus = true;
  322. jest.useFakeTimers();
  323. searchBar.onQueryBlur({target: {value: 'test'}});
  324. jest.advanceTimersByTime(201); // doesn't close until 200ms
  325. expect(searchBar.state.inputHasFocus).toBe(false);
  326. });
  327. });
  328. describe('onPaste()', function () {
  329. it('trims pasted content', function () {
  330. const onChange = jest.fn();
  331. const wrapper = mountWithTheme(
  332. <SmartSearchBar
  333. organization={organization}
  334. location={location}
  335. supportedTags={supportedTags}
  336. onChange={onChange}
  337. />,
  338. options
  339. );
  340. wrapper.setState({inputHasFocus: true});
  341. const input = ' something ';
  342. wrapper
  343. .find('textarea')
  344. .simulate('paste', {clipboardData: {getData: () => input, value: input}});
  345. wrapper.update();
  346. expect(onChange).toHaveBeenCalledWith('something', expect.anything());
  347. });
  348. });
  349. describe('onKeyUp()', function () {
  350. describe('escape', function () {
  351. it('blurs the textarea', function () {
  352. const wrapper = mountWithTheme(
  353. <SmartSearchBar
  354. organization={organization}
  355. location={location}
  356. supportedTags={supportedTags}
  357. />,
  358. options
  359. );
  360. wrapper.setState({inputHasFocus: true});
  361. const instance = wrapper.instance();
  362. jest.spyOn(instance, 'blur');
  363. wrapper.find('textarea').simulate('keyup', {key: 'Escape'});
  364. expect(instance.blur).toHaveBeenCalledTimes(1);
  365. });
  366. });
  367. });
  368. describe('render()', function () {
  369. it('invokes onSearch() when submitting the form', function () {
  370. const stubbedOnSearch = jest.fn();
  371. const wrapper = mountWithTheme(
  372. <SmartSearchBar
  373. onSearch={stubbedOnSearch}
  374. organization={organization}
  375. location={location}
  376. query="is:unresolved"
  377. supportedTags={supportedTags}
  378. />,
  379. options
  380. );
  381. wrapper.find('form').simulate('submit', {
  382. preventDefault() {},
  383. });
  384. expect(stubbedOnSearch).toHaveBeenCalledWith('is:unresolved');
  385. });
  386. it('invokes onSearch() when search is cleared', async function () {
  387. jest.useRealTimers();
  388. const props = {
  389. organization,
  390. location,
  391. query: 'is:unresolved',
  392. supportedTags,
  393. onSearch: jest.fn(),
  394. };
  395. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  396. wrapper.find('button[aria-label="Clear search"]').simulate('click');
  397. await tick();
  398. expect(props.onSearch).toHaveBeenCalledWith('');
  399. });
  400. it('invokes onSearch() on submit in hasPinnedSearch mode', function () {
  401. const stubbedOnSearch = jest.fn();
  402. const wrapper = mountWithTheme(
  403. <SmartSearchBar
  404. onSearch={stubbedOnSearch}
  405. organization={organization}
  406. query="is:unresolved"
  407. location={location}
  408. supportedTags={supportedTags}
  409. hasPinnedSearch
  410. />,
  411. options
  412. );
  413. wrapper.find('form').simulate('submit');
  414. expect(stubbedOnSearch).toHaveBeenCalledWith('is:unresolved');
  415. });
  416. });
  417. it('handles an empty query', function () {
  418. const props = {
  419. query: '',
  420. defaultQuery: 'is:unresolved',
  421. organization,
  422. location,
  423. supportedTags,
  424. };
  425. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  426. expect(wrapper.state('query')).toEqual('');
  427. });
  428. describe('updateAutoCompleteItems()', function () {
  429. beforeEach(function () {
  430. jest.useFakeTimers();
  431. });
  432. it('sets state when empty', function () {
  433. const props = {
  434. query: '',
  435. organization,
  436. location,
  437. supportedTags,
  438. };
  439. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  440. searchBar.updateAutoCompleteItems();
  441. expect(searchBar.state.searchTerm).toEqual('');
  442. expect(searchBar.state.searchGroups).toEqual([]);
  443. expect(searchBar.state.activeSearchItem).toEqual(-1);
  444. });
  445. it('sets state when incomplete tag', async function () {
  446. const props = {
  447. query: 'fu',
  448. organization,
  449. location,
  450. supportedTags,
  451. };
  452. jest.useRealTimers();
  453. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  454. const searchBar = wrapper.instance();
  455. wrapper.find('textarea').simulate('focus');
  456. searchBar.updateAutoCompleteItems();
  457. await tick();
  458. wrapper.update();
  459. expect(searchBar.state.searchTerm).toEqual('fu');
  460. expect(searchBar.state.searchGroups).toEqual([
  461. expect.objectContaining({children: []}),
  462. ]);
  463. expect(searchBar.state.activeSearchItem).toEqual(-1);
  464. });
  465. it('sets state when incomplete tag has negation operator', async function () {
  466. const props = {
  467. query: '!fu',
  468. organization,
  469. location,
  470. supportedTags,
  471. };
  472. jest.useRealTimers();
  473. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  474. const searchBar = wrapper.instance();
  475. wrapper.find('textarea').simulate('focus');
  476. searchBar.updateAutoCompleteItems();
  477. await tick();
  478. wrapper.update();
  479. expect(searchBar.state.searchTerm).toEqual('fu');
  480. expect(searchBar.state.searchGroups).toEqual([
  481. expect.objectContaining({children: []}),
  482. ]);
  483. expect(searchBar.state.activeSearchItem).toEqual(-1);
  484. });
  485. it('sets state when incomplete tag as second textarea', async function () {
  486. const props = {
  487. query: 'is:unresolved fu',
  488. organization,
  489. location,
  490. supportedTags,
  491. };
  492. jest.useRealTimers();
  493. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  494. const searchBar = wrapper.instance();
  495. // Cursor is at end of line
  496. mockCursorPosition(searchBar, 15);
  497. searchBar.updateAutoCompleteItems();
  498. await tick();
  499. wrapper.update();
  500. expect(searchBar.state.searchTerm).toEqual('fu');
  501. // 2 items because of headers ("Tags")
  502. expect(searchBar.state.searchGroups).toHaveLength(1);
  503. expect(searchBar.state.activeSearchItem).toEqual(-1);
  504. });
  505. it('does not request values when tag is environments', function () {
  506. const props = {
  507. query: 'environment:production',
  508. excludeEnvironment: true,
  509. location,
  510. organization,
  511. supportedTags,
  512. };
  513. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  514. searchBar.updateAutoCompleteItems();
  515. jest.advanceTimersByTime(301);
  516. expect(environmentTagValuesMock).not.toHaveBeenCalled();
  517. });
  518. it('does not request values when tag is `timesSeen`', function () {
  519. // This should never get called
  520. const mock = MockApiClient.addMockResponse({
  521. url: '/projects/123/456/tags/timesSeen/values/',
  522. body: [],
  523. });
  524. const props = {
  525. query: 'timesSeen:',
  526. organization,
  527. supportedTags,
  528. };
  529. const searchBar = mountWithTheme(
  530. <SmartSearchBar {...props} api={new Client()} />,
  531. options
  532. ).instance();
  533. searchBar.updateAutoCompleteItems();
  534. jest.advanceTimersByTime(301);
  535. expect(mock).not.toHaveBeenCalled();
  536. });
  537. it('requests values when tag is `firstRelease`', function () {
  538. const mock = MockApiClient.addMockResponse({
  539. url: '/organizations/org-slug/releases/',
  540. body: [],
  541. });
  542. const props = {
  543. orgId: 'org-slug',
  544. projectId: '0',
  545. query: 'firstRelease:',
  546. location,
  547. organization,
  548. supportedTags,
  549. };
  550. const searchBar = mountWithTheme(
  551. <SmartSearchBar {...props} api={new Client()} />,
  552. options
  553. ).instance();
  554. mockCursorPosition(searchBar, 13);
  555. searchBar.updateAutoCompleteItems();
  556. jest.advanceTimersByTime(301);
  557. expect(mock).toHaveBeenCalledWith(
  558. '/organizations/org-slug/releases/',
  559. expect.objectContaining({
  560. method: 'GET',
  561. query: {
  562. project: '0',
  563. per_page: 5, // Limit results to 5 for autocomplete
  564. },
  565. })
  566. );
  567. });
  568. it('shows operator autocompletion', async function () {
  569. const props = {
  570. query: 'is:unresolved',
  571. organization,
  572. location,
  573. supportedTags,
  574. };
  575. jest.useRealTimers();
  576. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  577. const searchBar = wrapper.instance();
  578. // Cursor is on ':'
  579. mockCursorPosition(searchBar, 3);
  580. searchBar.updateAutoCompleteItems();
  581. await tick();
  582. wrapper.update();
  583. // two search groups because of operator suggestions
  584. expect(searchBar.state.searchGroups).toHaveLength(2);
  585. expect(searchBar.state.activeSearchItem).toEqual(-1);
  586. });
  587. it('responds to cursor changes', async function () {
  588. const props = {
  589. query: 'is:unresolved',
  590. organization,
  591. location,
  592. supportedTags,
  593. };
  594. jest.useRealTimers();
  595. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  596. const searchBar = wrapper.instance();
  597. // Cursor is on ':'
  598. mockCursorPosition(searchBar, 3);
  599. searchBar.updateAutoCompleteItems();
  600. await tick();
  601. wrapper.update();
  602. // two search groups tags and values
  603. expect(searchBar.state.searchGroups).toHaveLength(2);
  604. expect(searchBar.state.activeSearchItem).toEqual(-1);
  605. mockCursorPosition(searchBar, 1);
  606. searchBar.updateAutoCompleteItems();
  607. await tick();
  608. wrapper.update();
  609. // one search group because showing tags
  610. expect(searchBar.state.searchGroups).toHaveLength(1);
  611. expect(searchBar.state.activeSearchItem).toEqual(-1);
  612. });
  613. it('shows errors on incorrect tokens', function () {
  614. const props = {
  615. query: 'tag: is: has: ',
  616. organization,
  617. location,
  618. supportedTags,
  619. };
  620. jest.useRealTimers();
  621. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  622. wrapper.find('Filter').forEach(filter => {
  623. expect(filter.prop('invalid')).toBe(true);
  624. });
  625. });
  626. it('handles autocomplete race conditions when cursor position changed', async function () {
  627. const props = {
  628. query: 'is:',
  629. organization,
  630. location,
  631. supportedTags,
  632. };
  633. jest.useFakeTimers();
  634. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  635. const searchBar = wrapper.instance();
  636. // Cursor is on ':'
  637. searchBar.generateValueAutocompleteGroup = jest.fn(
  638. () =>
  639. new Promise(resolve => {
  640. setTimeout(() => {
  641. resolve({
  642. searchItems: [],
  643. recentSearchItems: [],
  644. tagName: 'test',
  645. type: 'value',
  646. });
  647. }, [300]);
  648. })
  649. );
  650. mockCursorPosition(searchBar, 3);
  651. searchBar.updateAutoCompleteItems();
  652. jest.advanceTimersByTime(200);
  653. // Move cursor off of the place the update was called before it's done at 300ms
  654. mockCursorPosition(searchBar, 0);
  655. jest.advanceTimersByTime(101);
  656. // Get the pending promises to resolve
  657. await Promise.resolve();
  658. wrapper.update();
  659. expect(searchBar.state.searchGroups).toHaveLength(0);
  660. });
  661. it('handles race conditions when query changes from default state', async function () {
  662. const props = {
  663. query: '',
  664. organization,
  665. location,
  666. supportedTags,
  667. };
  668. jest.useFakeTimers();
  669. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  670. const searchBar = wrapper.instance();
  671. // Cursor is on ':'
  672. searchBar.getRecentSearches = jest.fn(
  673. () =>
  674. new Promise(resolve => {
  675. setTimeout(() => {
  676. resolve([]);
  677. }, [300]);
  678. })
  679. );
  680. mockCursorPosition(searchBar, 0);
  681. searchBar.updateAutoCompleteItems();
  682. jest.advanceTimersByTime(200);
  683. // Change query before it's done at 300ms
  684. searchBar.updateQuery('is:');
  685. jest.advanceTimersByTime(101);
  686. // Get the pending promises to resolve
  687. await Promise.resolve();
  688. wrapper.update();
  689. expect(searchBar.state.searchGroups).toHaveLength(0);
  690. });
  691. it('correctly groups nested keys', async function () {
  692. const props = {
  693. query: 'nest',
  694. organization,
  695. location,
  696. supportedTags: {
  697. nested: {
  698. key: 'nested',
  699. name: 'nested',
  700. },
  701. 'nested.child': {
  702. key: 'nested.child',
  703. name: 'nested.child',
  704. },
  705. },
  706. };
  707. jest.useRealTimers();
  708. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  709. const searchBar = wrapper.instance();
  710. // Cursor is at end of line
  711. mockCursorPosition(searchBar, 4);
  712. searchBar.updateAutoCompleteItems();
  713. await tick();
  714. wrapper.update();
  715. expect(searchBar.state.searchGroups).toHaveLength(1);
  716. expect(searchBar.state.searchGroups[0].children).toHaveLength(1);
  717. expect(searchBar.state.searchGroups[0].children[0].title).toBe('nested');
  718. expect(searchBar.state.searchGroups[0].children[0].children).toHaveLength(1);
  719. expect(searchBar.state.searchGroups[0].children[0].children[0].title).toBe(
  720. 'nested.child'
  721. );
  722. });
  723. it('correctly groups nested keys without a parent', async function () {
  724. const props = {
  725. query: 'nest',
  726. organization,
  727. location,
  728. supportedTags: {
  729. 'nested.child1': {
  730. key: 'nested.child1',
  731. name: 'nested.child1',
  732. },
  733. 'nested.child2': {
  734. key: 'nested.child2',
  735. name: 'nested.child2',
  736. },
  737. },
  738. };
  739. jest.useRealTimers();
  740. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  741. const searchBar = wrapper.instance();
  742. // Cursor is at end of line
  743. mockCursorPosition(searchBar, 4);
  744. searchBar.updateAutoCompleteItems();
  745. await tick();
  746. wrapper.update();
  747. expect(searchBar.state.searchGroups).toHaveLength(1);
  748. expect(searchBar.state.searchGroups[0].children).toHaveLength(1);
  749. expect(searchBar.state.searchGroups[0].children[0].title).toBe('nested');
  750. expect(searchBar.state.searchGroups[0].children[0].children).toHaveLength(2);
  751. expect(searchBar.state.searchGroups[0].children[0].children[0].title).toBe(
  752. 'nested.child1'
  753. );
  754. expect(searchBar.state.searchGroups[0].children[0].children[1].title).toBe(
  755. 'nested.child2'
  756. );
  757. });
  758. });
  759. describe('onAutoComplete()', function () {
  760. it('completes terms from the list', function () {
  761. const props = {
  762. query: 'event.type:error ',
  763. organization,
  764. location,
  765. supportedTags,
  766. };
  767. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  768. searchBar.onAutoComplete('myTag:', {type: 'tag'});
  769. expect(searchBar.state.query).toEqual('event.type:error myTag:');
  770. });
  771. it('completes values if cursor is not at the end', function () {
  772. const props = {
  773. query: 'id: event.type:error ',
  774. organization,
  775. location,
  776. supportedTags,
  777. };
  778. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  779. mockCursorPosition(searchBar, 3);
  780. searchBar.onAutoComplete('12345', {type: 'tag-value'});
  781. expect(searchBar.state.query).toEqual('id:12345 event.type:error ');
  782. });
  783. it('completes values if cursor is at the end', function () {
  784. const props = {
  785. query: 'event.type:error id:',
  786. organization,
  787. location,
  788. supportedTags,
  789. };
  790. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  791. mockCursorPosition(searchBar, 20);
  792. searchBar.onAutoComplete('12345', {type: 'tag-value'});
  793. expect(searchBar.state.query).toEqual('event.type:error id:12345 ');
  794. });
  795. it('triggers onChange', function () {
  796. const onChange = jest.fn();
  797. const props = {
  798. query: 'event.type:error id:',
  799. organization,
  800. location,
  801. supportedTags,
  802. };
  803. const searchBar = mountWithTheme(
  804. <SmartSearchBar {...props} onChange={onChange} />,
  805. options
  806. ).instance();
  807. mockCursorPosition(searchBar, 20);
  808. searchBar.onAutoComplete('12345', {type: 'tag-value'});
  809. expect(onChange).toHaveBeenCalledWith(
  810. 'event.type:error id:12345 ',
  811. expect.anything()
  812. );
  813. });
  814. it('keeps the negation operator is present', function () {
  815. const props = {
  816. query: '',
  817. organization,
  818. location,
  819. supportedTags,
  820. };
  821. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  822. const searchBar = smartSearchBar.instance();
  823. const textarea = smartSearchBar.find('textarea');
  824. // start typing part of the tag prefixed by the negation operator!
  825. textarea.simulate('change', {target: {value: 'event.type:error !ti'}});
  826. mockCursorPosition(searchBar, 20);
  827. // use autocompletion to do the rest
  828. searchBar.onAutoComplete('title:', {});
  829. expect(searchBar.state.query).toEqual('event.type:error !title:');
  830. });
  831. it('handles special case for user tag', function () {
  832. const props = {
  833. query: '',
  834. organization,
  835. location,
  836. supportedTags,
  837. };
  838. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  839. const searchBar = smartSearchBar.instance();
  840. const textarea = smartSearchBar.find('textarea');
  841. textarea.simulate('change', {target: {value: 'user:'}});
  842. mockCursorPosition(searchBar, 5);
  843. searchBar.onAutoComplete('id:1', {});
  844. expect(searchBar.state.query).toEqual('user:"id:1" ');
  845. });
  846. });
  847. it('quotes in predefined values with spaces when autocompleting', async function () {
  848. jest.useRealTimers();
  849. const onSearch = jest.fn();
  850. supportedTags.predefined = {
  851. key: 'predefined',
  852. name: 'predefined',
  853. predefined: true,
  854. values: ['predefined tag with spaces'],
  855. };
  856. const props = {
  857. orgId: 'org-slug',
  858. projectId: '0',
  859. query: '',
  860. location,
  861. organization,
  862. supportedTags,
  863. onSearch,
  864. };
  865. const searchBar = mountWithTheme(
  866. <SmartSearchBar {...props} api={new Client()} />,
  867. options
  868. );
  869. searchBar.find('textarea').simulate('focus');
  870. searchBar
  871. .find('textarea')
  872. .simulate('change', {target: {value: 'predefined:predefined'}});
  873. await tick();
  874. const preventDefault = jest.fn();
  875. searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  876. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  877. await tick();
  878. expect(searchBar.find('textarea').props().value).toEqual(
  879. 'predefined:"predefined tag with spaces" '
  880. );
  881. });
  882. it('escapes quotes in predefined values properly when autocompleting', async function () {
  883. jest.useRealTimers();
  884. const onSearch = jest.fn();
  885. supportedTags.predefined = {
  886. key: 'predefined',
  887. name: 'predefined',
  888. predefined: true,
  889. values: ['"predefined" "tag" "with" "quotes"'],
  890. };
  891. const props = {
  892. orgId: 'org-slug',
  893. projectId: '0',
  894. query: '',
  895. location,
  896. organization,
  897. supportedTags,
  898. onSearch,
  899. };
  900. const searchBar = mountWithTheme(
  901. <SmartSearchBar {...props} api={new Client()} />,
  902. options
  903. );
  904. searchBar.find('textarea').simulate('focus');
  905. searchBar
  906. .find('textarea')
  907. .simulate('change', {target: {value: 'predefined:predefined'}});
  908. await tick();
  909. const preventDefault = jest.fn();
  910. searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  911. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  912. await tick();
  913. expect(searchBar.find('textarea').props().value).toEqual(
  914. 'predefined:"\\"predefined\\" \\"tag\\" \\"with\\" \\"quotes\\"" '
  915. );
  916. });
  917. describe('quick actions', () => {
  918. it('delete first token', async () => {
  919. const props = {
  920. query: 'is:unresolved sdk.name:sentry-cocoa has:key',
  921. organization,
  922. location,
  923. supportedTags,
  924. };
  925. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  926. searchBar.updateAutoCompleteItems();
  927. mockCursorPosition(searchBar, 1);
  928. await tick();
  929. const deleteAction = shortcuts.find(a => a.shortcutType === ShortcutType.Delete);
  930. expect(deleteAction).toBeDefined();
  931. if (deleteAction) {
  932. searchBar.runShortcut(deleteAction);
  933. await tick();
  934. expect(searchBar.state.query).toEqual('sdk.name:sentry-cocoa has:key');
  935. }
  936. });
  937. it('delete middle token', async () => {
  938. const props = {
  939. query: 'is:unresolved sdk.name:sentry-cocoa has:key',
  940. organization,
  941. location,
  942. supportedTags,
  943. };
  944. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  945. searchBar.updateAutoCompleteItems();
  946. mockCursorPosition(searchBar, 18);
  947. await tick();
  948. const deleteAction = shortcuts.find(a => a.shortcutType === ShortcutType.Delete);
  949. expect(deleteAction).toBeDefined();
  950. if (deleteAction) {
  951. searchBar.runShortcut(deleteAction);
  952. await tick();
  953. expect(searchBar.state.query).toEqual('is:unresolved has:key');
  954. }
  955. });
  956. it('exclude token', async () => {
  957. const props = {
  958. query: 'is:unresolved sdk.name:sentry-cocoa has:key',
  959. organization,
  960. location,
  961. supportedTags,
  962. };
  963. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  964. searchBar.updateAutoCompleteItems();
  965. mockCursorPosition(searchBar, 18);
  966. await tick();
  967. const excludeAction = shortcuts.find(shortcut => shortcut.text === 'Exclude');
  968. expect(excludeAction).toBeDefined();
  969. if (excludeAction) {
  970. searchBar.runShortcut(excludeAction);
  971. await tick();
  972. expect(searchBar.state.query).toEqual(
  973. 'is:unresolved !sdk.name:sentry-cocoa has:key '
  974. );
  975. }
  976. });
  977. it('include token', async () => {
  978. const props = {
  979. query: 'is:unresolved !sdk.name:sentry-cocoa has:key',
  980. organization,
  981. location,
  982. supportedTags,
  983. };
  984. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  985. searchBar.updateAutoCompleteItems();
  986. mockCursorPosition(searchBar, 18);
  987. await tick();
  988. const includeAction = shortcuts.find(shortcut => shortcut.text === 'Include');
  989. expect(includeAction).toBeDefined();
  990. if (includeAction) {
  991. searchBar.runShortcut(includeAction);
  992. await tick();
  993. expect(searchBar.state.query).toEqual(
  994. 'is:unresolved sdk.name:sentry-cocoa has:key '
  995. );
  996. }
  997. });
  998. });
  999. describe('Invalid field state', () => {
  1000. it('Shows invalid field state when invalid field is used', async () => {
  1001. const props = {
  1002. query: 'invalid:',
  1003. organization,
  1004. location,
  1005. supportedTags,
  1006. };
  1007. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  1008. const searchBarInst = searchBar.instance();
  1009. mockCursorPosition(searchBarInst, 8);
  1010. searchBar.find('textarea').simulate('focus');
  1011. searchBarInst.updateAutoCompleteItems();
  1012. await tick();
  1013. expect(searchBarInst.state.searchGroups).toHaveLength(1);
  1014. expect(searchBarInst.state.searchGroups[0].title).toEqual('Keys');
  1015. expect(searchBarInst.state.searchGroups[0].type).toEqual('invalid-tag');
  1016. expect(searchBar.text()).toContain("The field invalid isn't supported here");
  1017. });
  1018. it('Does not show invalid field state when valid field is used', async () => {
  1019. const props = {
  1020. query: 'is:',
  1021. organization,
  1022. location,
  1023. supportedTags,
  1024. };
  1025. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  1026. const searchBarInst = searchBar.instance();
  1027. mockCursorPosition(searchBarInst, 3);
  1028. searchBarInst.updateAutoCompleteItems();
  1029. await tick();
  1030. expect(searchBar.text()).not.toContain("isn't supported here");
  1031. });
  1032. });
  1033. });