index.spec.jsx 29 KB

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