index.spec.jsx 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116
  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. });
  692. describe('onAutoComplete()', function () {
  693. it('completes terms from the list', function () {
  694. const props = {
  695. query: 'event.type:error ',
  696. organization,
  697. location,
  698. supportedTags,
  699. };
  700. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  701. searchBar.onAutoComplete('myTag:', {type: 'tag'});
  702. expect(searchBar.state.query).toEqual('event.type:error myTag:');
  703. });
  704. it('completes values if cursor is not at the end', function () {
  705. const props = {
  706. query: 'id: event.type:error ',
  707. organization,
  708. location,
  709. supportedTags,
  710. };
  711. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  712. mockCursorPosition(searchBar, 3);
  713. searchBar.onAutoComplete('12345', {type: 'tag-value'});
  714. expect(searchBar.state.query).toEqual('id:12345 event.type:error ');
  715. });
  716. it('completes values if cursor is at the end', function () {
  717. const props = {
  718. query: 'event.type:error id:',
  719. organization,
  720. location,
  721. supportedTags,
  722. };
  723. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  724. mockCursorPosition(searchBar, 20);
  725. searchBar.onAutoComplete('12345', {type: 'tag-value'});
  726. expect(searchBar.state.query).toEqual('event.type:error id:12345 ');
  727. });
  728. it('triggers onChange', function () {
  729. const onChange = jest.fn();
  730. const props = {
  731. query: 'event.type:error id:',
  732. organization,
  733. location,
  734. supportedTags,
  735. };
  736. const searchBar = mountWithTheme(
  737. <SmartSearchBar {...props} onChange={onChange} />,
  738. options
  739. ).instance();
  740. mockCursorPosition(searchBar, 20);
  741. searchBar.onAutoComplete('12345', {type: 'tag-value'});
  742. expect(onChange).toHaveBeenCalledWith(
  743. 'event.type:error id:12345 ',
  744. expect.anything()
  745. );
  746. });
  747. it('keeps the negation operator is present', function () {
  748. const props = {
  749. query: '',
  750. organization,
  751. location,
  752. supportedTags,
  753. };
  754. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  755. const searchBar = smartSearchBar.instance();
  756. const textarea = smartSearchBar.find('textarea');
  757. // start typing part of the tag prefixed by the negation operator!
  758. textarea.simulate('change', {target: {value: 'event.type:error !ti'}});
  759. mockCursorPosition(searchBar, 20);
  760. // use autocompletion to do the rest
  761. searchBar.onAutoComplete('title:', {});
  762. expect(searchBar.state.query).toEqual('event.type:error !title:');
  763. });
  764. it('handles special case for user tag', function () {
  765. const props = {
  766. query: '',
  767. organization,
  768. location,
  769. supportedTags,
  770. };
  771. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  772. const searchBar = smartSearchBar.instance();
  773. const textarea = smartSearchBar.find('textarea');
  774. textarea.simulate('change', {target: {value: 'user:'}});
  775. mockCursorPosition(searchBar, 5);
  776. searchBar.onAutoComplete('id:1', {});
  777. expect(searchBar.state.query).toEqual('user:"id:1" ');
  778. });
  779. });
  780. it('quotes in predefined values with spaces when autocompleting', async function () {
  781. jest.useRealTimers();
  782. const onSearch = jest.fn();
  783. supportedTags.predefined = {
  784. key: 'predefined',
  785. name: 'predefined',
  786. predefined: true,
  787. values: ['predefined tag with spaces'],
  788. };
  789. const props = {
  790. orgId: 'org-slug',
  791. projectId: '0',
  792. query: '',
  793. location,
  794. organization,
  795. supportedTags,
  796. onSearch,
  797. };
  798. const searchBar = mountWithTheme(
  799. <SmartSearchBar {...props} api={new Client()} />,
  800. options
  801. );
  802. searchBar.find('textarea').simulate('focus');
  803. searchBar
  804. .find('textarea')
  805. .simulate('change', {target: {value: 'predefined:predefined'}});
  806. await tick();
  807. const preventDefault = jest.fn();
  808. searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  809. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  810. await tick();
  811. expect(searchBar.find('textarea').props().value).toEqual(
  812. 'predefined:"predefined tag with spaces" '
  813. );
  814. });
  815. it('escapes quotes in predefined values properly when autocompleting', async function () {
  816. jest.useRealTimers();
  817. const onSearch = jest.fn();
  818. supportedTags.predefined = {
  819. key: 'predefined',
  820. name: 'predefined',
  821. predefined: true,
  822. values: ['"predefined" "tag" "with" "quotes"'],
  823. };
  824. const props = {
  825. orgId: 'org-slug',
  826. projectId: '0',
  827. query: '',
  828. location,
  829. organization,
  830. supportedTags,
  831. onSearch,
  832. };
  833. const searchBar = mountWithTheme(
  834. <SmartSearchBar {...props} api={new Client()} />,
  835. options
  836. );
  837. searchBar.find('textarea').simulate('focus');
  838. searchBar
  839. .find('textarea')
  840. .simulate('change', {target: {value: 'predefined:predefined'}});
  841. await tick();
  842. const preventDefault = jest.fn();
  843. searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  844. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  845. await tick();
  846. expect(searchBar.find('textarea').props().value).toEqual(
  847. 'predefined:"\\"predefined\\" \\"tag\\" \\"with\\" \\"quotes\\"" '
  848. );
  849. });
  850. describe('quick actions', () => {
  851. it('delete first token', async () => {
  852. const props = {
  853. query: 'is:unresolved sdk.name:sentry-cocoa has:key',
  854. organization,
  855. location,
  856. supportedTags,
  857. };
  858. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  859. searchBar.updateAutoCompleteItems();
  860. mockCursorPosition(searchBar, 1);
  861. await tick();
  862. const deleteAction = shortcuts.find(a => a.shortcutType === ShortcutType.Delete);
  863. expect(deleteAction).toBeDefined();
  864. if (deleteAction) {
  865. searchBar.runShortcut(deleteAction);
  866. await tick();
  867. expect(searchBar.state.query).toEqual('sdk.name:sentry-cocoa has:key');
  868. }
  869. });
  870. it('delete middle token', async () => {
  871. const props = {
  872. query: 'is:unresolved sdk.name:sentry-cocoa has:key',
  873. organization,
  874. location,
  875. supportedTags,
  876. };
  877. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  878. searchBar.updateAutoCompleteItems();
  879. mockCursorPosition(searchBar, 18);
  880. await tick();
  881. const deleteAction = shortcuts.find(a => a.shortcutType === ShortcutType.Delete);
  882. expect(deleteAction).toBeDefined();
  883. if (deleteAction) {
  884. searchBar.runShortcut(deleteAction);
  885. await tick();
  886. expect(searchBar.state.query).toEqual('is:unresolved has:key');
  887. }
  888. });
  889. it('exclude token', async () => {
  890. const props = {
  891. query: 'is:unresolved sdk.name:sentry-cocoa has:key',
  892. organization,
  893. location,
  894. supportedTags,
  895. };
  896. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  897. searchBar.updateAutoCompleteItems();
  898. mockCursorPosition(searchBar, 18);
  899. await tick();
  900. const excludeAction = shortcuts.find(shortcut => shortcut.text === 'Exclude');
  901. expect(excludeAction).toBeDefined();
  902. if (excludeAction) {
  903. searchBar.runShortcut(excludeAction);
  904. await tick();
  905. expect(searchBar.state.query).toEqual(
  906. 'is:unresolved !sdk.name:sentry-cocoa has:key '
  907. );
  908. }
  909. });
  910. it('include token', async () => {
  911. const props = {
  912. query: 'is:unresolved !sdk.name:sentry-cocoa has:key',
  913. organization,
  914. location,
  915. supportedTags,
  916. };
  917. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  918. searchBar.updateAutoCompleteItems();
  919. mockCursorPosition(searchBar, 18);
  920. await tick();
  921. const includeAction = shortcuts.find(shortcut => shortcut.text === 'Include');
  922. expect(includeAction).toBeDefined();
  923. if (includeAction) {
  924. searchBar.runShortcut(includeAction);
  925. await tick();
  926. expect(searchBar.state.query).toEqual(
  927. 'is:unresolved sdk.name:sentry-cocoa has:key '
  928. );
  929. }
  930. });
  931. });
  932. describe('Invalid field state', () => {
  933. it('Shows invalid field state when invalid field is used', async () => {
  934. const props = {
  935. query: 'invalid:',
  936. organization,
  937. location,
  938. supportedTags,
  939. };
  940. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  941. const searchBarInst = searchBar.instance();
  942. mockCursorPosition(searchBarInst, 8);
  943. searchBar.find('textarea').simulate('focus');
  944. searchBarInst.updateAutoCompleteItems();
  945. await tick();
  946. expect(searchBarInst.state.searchGroups).toHaveLength(1);
  947. expect(searchBarInst.state.searchGroups[0].title).toEqual('Tags');
  948. expect(searchBarInst.state.searchGroups[0].type).toEqual('invalid-tag');
  949. expect(searchBar.text()).toContain("The field invalid isn't supported here");
  950. });
  951. it('Does not show invalid field state when valid field is used', async () => {
  952. const props = {
  953. query: 'is:',
  954. organization,
  955. location,
  956. supportedTags,
  957. };
  958. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  959. const searchBarInst = searchBar.instance();
  960. mockCursorPosition(searchBarInst, 3);
  961. searchBarInst.updateAutoCompleteItems();
  962. await tick();
  963. expect(searchBar.text()).not.toContain("isn't supported here");
  964. });
  965. });
  966. });