index.spec.jsx 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {fireEvent, render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  3. import {Client} from 'sentry/api';
  4. import {SmartSearchBar} from 'sentry/components/smartSearchBar';
  5. import {ShortcutType} from 'sentry/components/smartSearchBar/types';
  6. import {shortcuts} from 'sentry/components/smartSearchBar/utils';
  7. import TagStore from 'sentry/stores/tagStore';
  8. import {FieldKey} from 'sentry/utils/fields';
  9. describe('SmartSearchBar', function () {
  10. let location, options, organization, supportedTags;
  11. let environmentTagValuesMock;
  12. const tagValuesMock = jest.fn(() => Promise.resolve([]));
  13. const mockCursorPosition = (component, pos) => {
  14. delete component.cursorPosition;
  15. Object.defineProperty(component, 'cursorPosition', {
  16. get: jest.fn().mockReturnValue(pos),
  17. configurable: true,
  18. });
  19. };
  20. beforeEach(function () {
  21. TagStore.reset();
  22. TagStore.loadTagsSuccess(TestStubs.Tags());
  23. tagValuesMock.mockClear();
  24. supportedTags = TagStore.getStateTags();
  25. supportedTags.firstRelease = {
  26. key: 'firstRelease',
  27. name: 'firstRelease',
  28. };
  29. supportedTags.is = {
  30. key: 'is',
  31. name: 'is',
  32. };
  33. organization = TestStubs.Organization({id: '123'});
  34. location = {
  35. pathname: '/organizations/org-slug/recent-searches/',
  36. query: {
  37. projectId: '0',
  38. },
  39. };
  40. options = TestStubs.routerContext([
  41. {
  42. organization,
  43. location,
  44. router: {location},
  45. },
  46. ]);
  47. MockApiClient.clearMockResponses();
  48. MockApiClient.addMockResponse({
  49. url: '/organizations/org-slug/recent-searches/',
  50. body: [],
  51. });
  52. environmentTagValuesMock = MockApiClient.addMockResponse({
  53. url: '/projects/123/456/tags/environment/values/',
  54. body: [],
  55. });
  56. });
  57. afterEach(function () {
  58. MockApiClient.clearMockResponses();
  59. });
  60. it('quotes in values with spaces when autocompleting', async function () {
  61. jest.useRealTimers();
  62. const getTagValuesMock = jest.fn().mockImplementation(() => {
  63. return Promise.resolve(['this is filled with spaces']);
  64. });
  65. const onSearch = jest.fn();
  66. const props = {
  67. orgId: 'org-slug',
  68. projectId: '0',
  69. query: '',
  70. location,
  71. organization,
  72. supportedTags,
  73. onGetTagValues: getTagValuesMock,
  74. onSearch,
  75. };
  76. const searchBar = mountWithTheme(
  77. <SmartSearchBar {...props} api={new Client()} />,
  78. options
  79. );
  80. searchBar.find('textarea').simulate('focus');
  81. searchBar.find('textarea').simulate('change', {target: {value: 'device:this'}});
  82. await tick();
  83. const preventDefault = jest.fn();
  84. searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  85. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  86. await tick();
  87. expect(searchBar.find('textarea').props().value).toEqual(
  88. 'device:"this is filled with spaces" '
  89. );
  90. });
  91. it('escapes quotes in values properly when autocompleting', async function () {
  92. jest.useRealTimers();
  93. const getTagValuesMock = jest.fn().mockImplementation(() => {
  94. return Promise.resolve(['this " is " filled " with " quotes']);
  95. });
  96. const onSearch = jest.fn();
  97. const props = {
  98. orgId: 'org-slug',
  99. projectId: '0',
  100. query: '',
  101. location,
  102. organization,
  103. supportedTags,
  104. onGetTagValues: getTagValuesMock,
  105. onSearch,
  106. };
  107. const searchBar = mountWithTheme(
  108. <SmartSearchBar {...props} api={new Client()} />,
  109. options
  110. );
  111. searchBar.find('textarea').simulate('focus');
  112. searchBar.find('textarea').simulate('change', {target: {value: 'device:this'}});
  113. await tick();
  114. const preventDefault = jest.fn();
  115. searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  116. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  117. await tick();
  118. expect(searchBar.find('textarea').props().value).toEqual(
  119. 'device:"this \\" is \\" filled \\" with \\" quotes" '
  120. );
  121. });
  122. it('does not preventDefault when there are no search items and is loading and enter is pressed', async function () {
  123. jest.useRealTimers();
  124. const getTagValuesMock = jest.fn().mockImplementation(() => {
  125. return new Promise(() => {});
  126. });
  127. const onSearch = jest.fn();
  128. const props = {
  129. orgId: 'org-slug',
  130. projectId: '0',
  131. query: '',
  132. location,
  133. organization,
  134. supportedTags,
  135. onGetTagValues: getTagValuesMock,
  136. onSearch,
  137. };
  138. const searchBar = mountWithTheme(
  139. <SmartSearchBar {...props} api={new Client()} />,
  140. options
  141. );
  142. searchBar.find('textarea').simulate('focus');
  143. searchBar.find('textarea').simulate('change', {target: {value: 'browser:'}});
  144. await tick();
  145. // press enter
  146. const preventDefault = jest.fn();
  147. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  148. expect(onSearch).not.toHaveBeenCalled();
  149. expect(preventDefault).not.toHaveBeenCalled();
  150. });
  151. it('calls preventDefault when there are existing search items and is loading and enter is pressed', async function () {
  152. jest.useRealTimers();
  153. const getTagValuesMock = jest.fn().mockImplementation(() => {
  154. return new Promise(() => {});
  155. });
  156. const onSearch = jest.fn();
  157. const props = {
  158. orgId: 'org-slug',
  159. projectId: '0',
  160. query: '',
  161. location,
  162. organization,
  163. supportedTags,
  164. onGetTagValues: getTagValuesMock,
  165. onSearch,
  166. };
  167. const searchBar = mountWithTheme(
  168. <SmartSearchBar {...props} api={new Client()} />,
  169. options
  170. );
  171. searchBar.find('textarea').simulate('focus');
  172. searchBar.find('textarea').simulate('change', {target: {value: 'bro'}});
  173. await tick();
  174. // Can't select with tab
  175. searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  176. searchBar.find('textarea').simulate('keyDown', {key: 'Tab'});
  177. expect(onSearch).not.toHaveBeenCalled();
  178. searchBar.find('textarea').simulate('change', {target: {value: 'browser:'}});
  179. await tick();
  180. // press enter
  181. const preventDefault = jest.fn();
  182. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  183. expect(onSearch).not.toHaveBeenCalled();
  184. // Prevent default since we need to select an item
  185. expect(preventDefault).toHaveBeenCalled();
  186. });
  187. describe('componentWillReceiveProps()', function () {
  188. it('should add a space when setting state.query', function () {
  189. const searchBar = mountWithTheme(
  190. <SmartSearchBar
  191. organization={organization}
  192. location={location}
  193. supportedTags={supportedTags}
  194. query="one"
  195. />,
  196. options
  197. );
  198. expect(searchBar.state().query).toEqual('one ');
  199. });
  200. it('should update state.query if props.query is updated from outside', function () {
  201. const searchBar = mountWithTheme(
  202. <SmartSearchBar
  203. organization={organization}
  204. location={location}
  205. supportedTags={supportedTags}
  206. query="one"
  207. />,
  208. options
  209. );
  210. searchBar.setProps({query: 'two'});
  211. expect(searchBar.state().query).toEqual('two ');
  212. });
  213. it('should update state.query if props.query is updated to null/undefined from outside', function () {
  214. const searchBar = mountWithTheme(
  215. <SmartSearchBar
  216. organization={organization}
  217. location={location}
  218. supportedTags={supportedTags}
  219. query="one"
  220. />,
  221. options
  222. );
  223. searchBar.setProps({query: null});
  224. expect(searchBar.state().query).toEqual('');
  225. });
  226. it('should not reset user textarea if a noop props change happens', function () {
  227. const searchBar = mountWithTheme(
  228. <SmartSearchBar
  229. organization={organization}
  230. location={location}
  231. supportedTags={supportedTags}
  232. query="one"
  233. />,
  234. options
  235. );
  236. searchBar.setState({query: 'two'});
  237. searchBar.setProps({query: 'one'});
  238. expect(searchBar.state().query).toEqual('two');
  239. });
  240. it('should reset user textarea if a meaningful props change happens', function () {
  241. const searchBar = mountWithTheme(
  242. <SmartSearchBar
  243. organization={organization}
  244. location={location}
  245. supportedTags={supportedTags}
  246. query="one"
  247. />,
  248. options
  249. );
  250. searchBar.setState({query: 'two'});
  251. searchBar.setProps({query: 'three'});
  252. expect(searchBar.state().query).toEqual('three ');
  253. });
  254. });
  255. describe('clearSearch()', function () {
  256. it('clears the query', function () {
  257. const props = {
  258. organization,
  259. location,
  260. query: 'is:unresolved ruby',
  261. defaultQuery: 'is:unresolved',
  262. supportedTags,
  263. };
  264. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  265. searchBar.clearSearch();
  266. expect(searchBar.state.query).toEqual('');
  267. });
  268. it('calls onSearch()', async function () {
  269. const props = {
  270. organization,
  271. location,
  272. query: 'is:unresolved ruby',
  273. defaultQuery: 'is:unresolved',
  274. supportedTags,
  275. onSearch: jest.fn(),
  276. };
  277. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  278. await searchBar.clearSearch();
  279. expect(props.onSearch).toHaveBeenCalledWith('');
  280. });
  281. });
  282. describe('onQueryFocus()', function () {
  283. it('displays the drop down', function () {
  284. const searchBar = mountWithTheme(
  285. <SmartSearchBar
  286. organization={organization}
  287. location={location}
  288. supportedTags={supportedTags}
  289. onGetTagValues={tagValuesMock}
  290. />,
  291. options
  292. ).instance();
  293. expect(searchBar.state.inputHasFocus).toBe(false);
  294. searchBar.onQueryFocus();
  295. expect(searchBar.state.inputHasFocus).toBe(true);
  296. });
  297. it('displays dropdown in hasPinnedSearch mode', function () {
  298. const searchBar = mountWithTheme(
  299. <SmartSearchBar
  300. organization={organization}
  301. location={location}
  302. supportedTags={supportedTags}
  303. onGetTagValues={tagValuesMock}
  304. hasPinnedSearch
  305. />,
  306. options
  307. ).instance();
  308. expect(searchBar.state.inputHasFocus).toBe(false);
  309. searchBar.onQueryFocus();
  310. expect(searchBar.state.inputHasFocus).toBe(true);
  311. });
  312. });
  313. describe('onQueryBlur()', function () {
  314. it('hides the drop down', function () {
  315. const searchBar = mountWithTheme(
  316. <SmartSearchBar
  317. organization={organization}
  318. location={location}
  319. supportedTags={supportedTags}
  320. />,
  321. options
  322. ).instance();
  323. searchBar.state.inputHasFocus = true;
  324. jest.useFakeTimers();
  325. searchBar.onQueryBlur({target: {value: 'test'}});
  326. jest.advanceTimersByTime(201); // doesn't close until 200ms
  327. expect(searchBar.state.inputHasFocus).toBe(false);
  328. });
  329. });
  330. describe('onPaste()', function () {
  331. it('trims pasted content', function () {
  332. const onChange = jest.fn();
  333. const wrapper = mountWithTheme(
  334. <SmartSearchBar
  335. organization={organization}
  336. location={location}
  337. supportedTags={supportedTags}
  338. onChange={onChange}
  339. />,
  340. options
  341. );
  342. wrapper.setState({inputHasFocus: true});
  343. const input = ' something ';
  344. wrapper
  345. .find('textarea')
  346. .simulate('paste', {clipboardData: {getData: () => input, value: input}});
  347. wrapper.update();
  348. expect(onChange).toHaveBeenCalledWith('something', expect.anything());
  349. });
  350. });
  351. describe('onKeyUp()', function () {
  352. describe('escape', function () {
  353. it('blurs the textarea', function () {
  354. const wrapper = mountWithTheme(
  355. <SmartSearchBar
  356. organization={organization}
  357. location={location}
  358. supportedTags={supportedTags}
  359. />,
  360. options
  361. );
  362. wrapper.setState({inputHasFocus: true});
  363. const instance = wrapper.instance();
  364. jest.spyOn(instance, 'blur');
  365. wrapper.find('textarea').simulate('keyup', {key: 'Escape'});
  366. expect(instance.blur).toHaveBeenCalledTimes(1);
  367. });
  368. });
  369. });
  370. describe('render()', function () {
  371. it('invokes onSearch() when submitting the form', function () {
  372. const stubbedOnSearch = jest.fn();
  373. const wrapper = mountWithTheme(
  374. <SmartSearchBar
  375. onSearch={stubbedOnSearch}
  376. organization={organization}
  377. location={location}
  378. query="is:unresolved"
  379. supportedTags={supportedTags}
  380. />,
  381. options
  382. );
  383. wrapper.find('form').simulate('submit', {
  384. preventDefault() {},
  385. });
  386. expect(stubbedOnSearch).toHaveBeenCalledWith('is:unresolved');
  387. });
  388. it('invokes onSearch() when search is cleared', async function () {
  389. jest.useRealTimers();
  390. const props = {
  391. organization,
  392. location,
  393. query: 'is:unresolved',
  394. supportedTags,
  395. onSearch: jest.fn(),
  396. };
  397. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  398. wrapper.find('button[aria-label="Clear search"]').simulate('click');
  399. await tick();
  400. expect(props.onSearch).toHaveBeenCalledWith('');
  401. });
  402. it('invokes onSearch() on submit in hasPinnedSearch mode', function () {
  403. const stubbedOnSearch = jest.fn();
  404. const wrapper = mountWithTheme(
  405. <SmartSearchBar
  406. onSearch={stubbedOnSearch}
  407. organization={organization}
  408. query="is:unresolved"
  409. location={location}
  410. supportedTags={supportedTags}
  411. hasPinnedSearch
  412. />,
  413. options
  414. );
  415. wrapper.find('form').simulate('submit');
  416. expect(stubbedOnSearch).toHaveBeenCalledWith('is:unresolved');
  417. });
  418. });
  419. it('handles an empty query', function () {
  420. const props = {
  421. query: '',
  422. defaultQuery: 'is:unresolved',
  423. organization,
  424. location,
  425. supportedTags,
  426. };
  427. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  428. expect(wrapper.state('query')).toEqual('');
  429. });
  430. describe('updateAutoCompleteItems()', function () {
  431. beforeEach(function () {
  432. jest.useFakeTimers();
  433. });
  434. it('sets state when empty', function () {
  435. const props = {
  436. query: '',
  437. organization,
  438. location,
  439. supportedTags,
  440. };
  441. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  442. searchBar.updateAutoCompleteItems();
  443. expect(searchBar.state.searchTerm).toEqual('');
  444. expect(searchBar.state.searchGroups).toEqual([]);
  445. expect(searchBar.state.activeSearchItem).toEqual(-1);
  446. });
  447. it('sets state when incomplete tag', async function () {
  448. const props = {
  449. query: 'fu',
  450. organization,
  451. location,
  452. supportedTags,
  453. };
  454. jest.useRealTimers();
  455. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  456. const searchBar = wrapper.instance();
  457. wrapper.find('textarea').simulate('focus');
  458. searchBar.updateAutoCompleteItems();
  459. await tick();
  460. wrapper.update();
  461. expect(searchBar.state.searchTerm).toEqual('fu');
  462. expect(searchBar.state.searchGroups).toEqual([
  463. expect.objectContaining({children: []}),
  464. ]);
  465. expect(searchBar.state.activeSearchItem).toEqual(-1);
  466. });
  467. it('sets state when incomplete tag has negation operator', async function () {
  468. const props = {
  469. query: '!fu',
  470. organization,
  471. location,
  472. supportedTags,
  473. };
  474. jest.useRealTimers();
  475. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  476. const searchBar = wrapper.instance();
  477. wrapper.find('textarea').simulate('focus');
  478. searchBar.updateAutoCompleteItems();
  479. await tick();
  480. wrapper.update();
  481. expect(searchBar.state.searchTerm).toEqual('fu');
  482. expect(searchBar.state.searchGroups).toEqual([
  483. expect.objectContaining({children: []}),
  484. ]);
  485. expect(searchBar.state.activeSearchItem).toEqual(-1);
  486. });
  487. it('sets state when incomplete tag as second textarea', async function () {
  488. const props = {
  489. query: 'is:unresolved fu',
  490. organization,
  491. location,
  492. supportedTags,
  493. };
  494. jest.useRealTimers();
  495. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  496. const searchBar = wrapper.instance();
  497. // Cursor is at end of line
  498. mockCursorPosition(searchBar, 15);
  499. searchBar.updateAutoCompleteItems();
  500. await tick();
  501. wrapper.update();
  502. expect(searchBar.state.searchTerm).toEqual('fu');
  503. // 2 items because of headers ("Tags")
  504. expect(searchBar.state.searchGroups).toHaveLength(1);
  505. expect(searchBar.state.activeSearchItem).toEqual(-1);
  506. });
  507. it('does not request values when tag is environments', function () {
  508. const props = {
  509. query: 'environment:production',
  510. excludeEnvironment: true,
  511. location,
  512. organization,
  513. supportedTags,
  514. };
  515. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  516. searchBar.updateAutoCompleteItems();
  517. jest.advanceTimersByTime(301);
  518. expect(environmentTagValuesMock).not.toHaveBeenCalled();
  519. });
  520. it('does not request values when tag is `timesSeen`', function () {
  521. // This should never get called
  522. const mock = MockApiClient.addMockResponse({
  523. url: '/projects/123/456/tags/timesSeen/values/',
  524. body: [],
  525. });
  526. const props = {
  527. query: 'timesSeen:',
  528. organization,
  529. supportedTags,
  530. };
  531. const searchBar = mountWithTheme(
  532. <SmartSearchBar {...props} api={new Client()} />,
  533. options
  534. ).instance();
  535. searchBar.updateAutoCompleteItems();
  536. jest.advanceTimersByTime(301);
  537. expect(mock).not.toHaveBeenCalled();
  538. });
  539. it('requests values when tag is `firstRelease`', function () {
  540. const mock = MockApiClient.addMockResponse({
  541. url: '/organizations/org-slug/releases/',
  542. body: [],
  543. });
  544. const props = {
  545. orgId: 'org-slug',
  546. projectId: '0',
  547. query: 'firstRelease:',
  548. location,
  549. organization,
  550. supportedTags,
  551. };
  552. const searchBar = mountWithTheme(
  553. <SmartSearchBar {...props} api={new Client()} />,
  554. options
  555. ).instance();
  556. mockCursorPosition(searchBar, 13);
  557. searchBar.updateAutoCompleteItems();
  558. jest.advanceTimersByTime(301);
  559. expect(mock).toHaveBeenCalledWith(
  560. '/organizations/org-slug/releases/',
  561. expect.objectContaining({
  562. method: 'GET',
  563. query: {
  564. project: '0',
  565. per_page: 5, // Limit results to 5 for autocomplete
  566. },
  567. })
  568. );
  569. });
  570. it('shows operator autocompletion', async function () {
  571. const props = {
  572. query: 'is:unresolved',
  573. organization,
  574. location,
  575. supportedTags,
  576. };
  577. jest.useRealTimers();
  578. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  579. const searchBar = wrapper.instance();
  580. // Cursor is on ':'
  581. mockCursorPosition(searchBar, 3);
  582. searchBar.updateAutoCompleteItems();
  583. await tick();
  584. wrapper.update();
  585. // two search groups because of operator suggestions
  586. expect(searchBar.state.searchGroups).toHaveLength(2);
  587. expect(searchBar.state.activeSearchItem).toEqual(-1);
  588. });
  589. it('responds to cursor changes', async function () {
  590. const props = {
  591. query: 'is:unresolved',
  592. organization,
  593. location,
  594. supportedTags,
  595. };
  596. jest.useRealTimers();
  597. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  598. const searchBar = wrapper.instance();
  599. // Cursor is on ':'
  600. mockCursorPosition(searchBar, 3);
  601. searchBar.updateAutoCompleteItems();
  602. await tick();
  603. wrapper.update();
  604. // two search groups tags and values
  605. expect(searchBar.state.searchGroups).toHaveLength(2);
  606. expect(searchBar.state.activeSearchItem).toEqual(-1);
  607. mockCursorPosition(searchBar, 1);
  608. searchBar.updateAutoCompleteItems();
  609. await tick();
  610. wrapper.update();
  611. // one search group because showing tags
  612. expect(searchBar.state.searchGroups).toHaveLength(1);
  613. expect(searchBar.state.activeSearchItem).toEqual(-1);
  614. });
  615. it('shows errors on incorrect tokens', function () {
  616. const props = {
  617. query: 'tag: is: has: ',
  618. organization,
  619. location,
  620. supportedTags,
  621. };
  622. jest.useRealTimers();
  623. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  624. wrapper.find('Filter').forEach(filter => {
  625. expect(filter.prop('invalid')).toBe(true);
  626. });
  627. });
  628. it('handles autocomplete race conditions when cursor position changed', async function () {
  629. const props = {
  630. query: 'is:',
  631. organization,
  632. location,
  633. supportedTags,
  634. };
  635. jest.useFakeTimers();
  636. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  637. const searchBar = wrapper.instance();
  638. // Cursor is on ':'
  639. searchBar.generateValueAutocompleteGroup = jest.fn(
  640. () =>
  641. new Promise(resolve => {
  642. setTimeout(() => {
  643. resolve({
  644. searchItems: [],
  645. recentSearchItems: [],
  646. tagName: 'test',
  647. type: 'value',
  648. });
  649. }, [300]);
  650. })
  651. );
  652. mockCursorPosition(searchBar, 3);
  653. searchBar.updateAutoCompleteItems();
  654. jest.advanceTimersByTime(200);
  655. // Move cursor off of the place the update was called before it's done at 300ms
  656. mockCursorPosition(searchBar, 0);
  657. jest.advanceTimersByTime(101);
  658. // Get the pending promises to resolve
  659. await Promise.resolve();
  660. wrapper.update();
  661. expect(searchBar.state.searchGroups).toHaveLength(0);
  662. });
  663. it('handles race conditions when query changes from default state', async function () {
  664. const props = {
  665. query: '',
  666. organization,
  667. location,
  668. supportedTags,
  669. };
  670. jest.useFakeTimers();
  671. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  672. const searchBar = wrapper.instance();
  673. // Cursor is on ':'
  674. searchBar.getRecentSearches = jest.fn(
  675. () =>
  676. new Promise(resolve => {
  677. setTimeout(() => {
  678. resolve([]);
  679. }, [300]);
  680. })
  681. );
  682. mockCursorPosition(searchBar, 0);
  683. searchBar.updateAutoCompleteItems();
  684. jest.advanceTimersByTime(200);
  685. // Change query before it's done at 300ms
  686. searchBar.updateQuery('is:');
  687. jest.advanceTimersByTime(101);
  688. // Get the pending promises to resolve
  689. await Promise.resolve();
  690. wrapper.update();
  691. expect(searchBar.state.searchGroups).toHaveLength(0);
  692. });
  693. it('correctly groups nested keys', async function () {
  694. const props = {
  695. query: 'nest',
  696. organization,
  697. location,
  698. supportedTags: {
  699. nested: {
  700. key: 'nested',
  701. name: 'nested',
  702. },
  703. 'nested.child': {
  704. key: 'nested.child',
  705. name: 'nested.child',
  706. },
  707. },
  708. };
  709. jest.useRealTimers();
  710. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  711. const searchBar = wrapper.instance();
  712. // Cursor is at end of line
  713. mockCursorPosition(searchBar, 4);
  714. searchBar.updateAutoCompleteItems();
  715. await tick();
  716. wrapper.update();
  717. expect(searchBar.state.searchGroups).toHaveLength(1);
  718. expect(searchBar.state.searchGroups[0].children).toHaveLength(1);
  719. expect(searchBar.state.searchGroups[0].children[0].title).toBe('nested');
  720. expect(searchBar.state.searchGroups[0].children[0].children).toHaveLength(1);
  721. expect(searchBar.state.searchGroups[0].children[0].children[0].title).toBe(
  722. 'nested.child'
  723. );
  724. });
  725. it('correctly groups nested keys without a parent', async function () {
  726. const props = {
  727. query: 'nest',
  728. organization,
  729. location,
  730. supportedTags: {
  731. 'nested.child1': {
  732. key: 'nested.child1',
  733. name: 'nested.child1',
  734. },
  735. 'nested.child2': {
  736. key: 'nested.child2',
  737. name: 'nested.child2',
  738. },
  739. },
  740. };
  741. jest.useRealTimers();
  742. const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
  743. const searchBar = wrapper.instance();
  744. // Cursor is at end of line
  745. mockCursorPosition(searchBar, 4);
  746. searchBar.updateAutoCompleteItems();
  747. await tick();
  748. wrapper.update();
  749. expect(searchBar.state.searchGroups).toHaveLength(1);
  750. expect(searchBar.state.searchGroups[0].children).toHaveLength(1);
  751. expect(searchBar.state.searchGroups[0].children[0].title).toBe('nested');
  752. expect(searchBar.state.searchGroups[0].children[0].children).toHaveLength(2);
  753. expect(searchBar.state.searchGroups[0].children[0].children[0].title).toBe(
  754. 'nested.child1'
  755. );
  756. expect(searchBar.state.searchGroups[0].children[0].children[1].title).toBe(
  757. 'nested.child2'
  758. );
  759. });
  760. });
  761. describe('cursorSearchTerm', function () {
  762. it('selects the correct free text word', async function () {
  763. jest.useRealTimers();
  764. const props = {
  765. query: '',
  766. organization,
  767. location,
  768. supportedTags,
  769. };
  770. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  771. const searchBar = smartSearchBar.instance();
  772. const textarea = smartSearchBar.find('textarea');
  773. textarea.simulate('focus');
  774. mockCursorPosition(searchBar, 6);
  775. textarea.simulate('change', {target: {value: 'typ testest err'}});
  776. await tick();
  777. // Expect the correct search term to be selected
  778. const cursorSearchTerm = searchBar.cursorSearchTerm;
  779. expect(cursorSearchTerm.searchTerm).toEqual('testest');
  780. expect(cursorSearchTerm.start).toBe(4);
  781. expect(cursorSearchTerm.end).toBe(11);
  782. });
  783. it('selects the correct free text word (last word)', async function () {
  784. jest.useRealTimers();
  785. const props = {
  786. query: '',
  787. organization,
  788. location,
  789. supportedTags,
  790. };
  791. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  792. const searchBar = smartSearchBar.instance();
  793. const textarea = smartSearchBar.find('textarea');
  794. textarea.simulate('focus');
  795. mockCursorPosition(searchBar, 15);
  796. textarea.simulate('change', {target: {value: 'typ testest err'}});
  797. await tick();
  798. // Expect the correct search term to be selected
  799. const cursorSearchTerm = searchBar.cursorSearchTerm;
  800. expect(cursorSearchTerm.searchTerm).toEqual('err');
  801. expect(cursorSearchTerm.start).toBe(15);
  802. expect(cursorSearchTerm.end).toBe(18);
  803. });
  804. it('selects the correct free text word (first word)', async function () {
  805. jest.useRealTimers();
  806. const props = {
  807. query: '',
  808. organization,
  809. location,
  810. supportedTags,
  811. };
  812. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  813. const searchBar = smartSearchBar.instance();
  814. const textarea = smartSearchBar.find('textarea');
  815. textarea.simulate('focus');
  816. mockCursorPosition(searchBar, 1);
  817. textarea.simulate('change', {target: {value: 'typ testest err'}});
  818. await tick();
  819. // Expect the correct search term to be selected
  820. const cursorSearchTerm = searchBar.cursorSearchTerm;
  821. expect(cursorSearchTerm.searchTerm).toEqual('typ');
  822. expect(cursorSearchTerm.start).toBe(0);
  823. expect(cursorSearchTerm.end).toBe(3);
  824. });
  825. it('search term location correctly selects key of filter token', async function () {
  826. jest.useRealTimers();
  827. const props = {
  828. query: '',
  829. organization,
  830. location,
  831. supportedTags,
  832. };
  833. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  834. const searchBar = smartSearchBar.instance();
  835. const textarea = smartSearchBar.find('textarea');
  836. textarea.simulate('focus');
  837. mockCursorPosition(searchBar, 6);
  838. textarea.simulate('change', {target: {value: 'typ device:123'}});
  839. await tick();
  840. // Expect the correct search term to be selected
  841. const cursorSearchTerm = searchBar.cursorSearchTerm;
  842. expect(cursorSearchTerm.searchTerm).toEqual('device');
  843. expect(cursorSearchTerm.start).toBe(4);
  844. expect(cursorSearchTerm.end).toBe(10);
  845. });
  846. it('search term location correctly selects value of filter token', async function () {
  847. jest.useRealTimers();
  848. const props = {
  849. query: '',
  850. organization,
  851. location,
  852. supportedTags,
  853. };
  854. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  855. const searchBar = smartSearchBar.instance();
  856. const textarea = smartSearchBar.find('textarea');
  857. textarea.simulate('focus');
  858. mockCursorPosition(searchBar, 11);
  859. textarea.simulate('change', {target: {value: 'typ device:123'}});
  860. await tick();
  861. // Expect the correct search term to be selected
  862. const cursorSearchTerm = searchBar.cursorSearchTerm;
  863. expect(cursorSearchTerm.searchTerm).toEqual('123');
  864. expect(cursorSearchTerm.start).toBe(11);
  865. expect(cursorSearchTerm.end).toBe(14);
  866. });
  867. });
  868. describe('getTagKeys()', function () {
  869. it('filters both keys and descriptions', async function () {
  870. jest.useRealTimers();
  871. const props = {
  872. query: 'event',
  873. organization,
  874. location,
  875. supportedTags: {
  876. [FieldKey.DEVICE_CHARGING]: {
  877. key: FieldKey.DEVICE_CHARGING,
  878. },
  879. [FieldKey.EVENT_TYPE]: {
  880. key: FieldKey.EVENT_TYPE,
  881. },
  882. [FieldKey.DEVICE_ARCH]: {
  883. key: FieldKey.DEVICE_ARCH,
  884. },
  885. },
  886. };
  887. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  888. const searchBar = smartSearchBar.instance();
  889. mockCursorPosition(searchBar, 3);
  890. searchBar.updateAutoCompleteItems();
  891. await tick();
  892. expect(searchBar.state.flatSearchItems).toHaveLength(2);
  893. expect(searchBar.state.flatSearchItems[0].title).toBe(FieldKey.EVENT_TYPE);
  894. expect(searchBar.state.flatSearchItems[1].title).toBe(FieldKey.DEVICE_CHARGING);
  895. });
  896. it('filters only keys', async function () {
  897. jest.useRealTimers();
  898. const props = {
  899. query: 'device',
  900. organization,
  901. location,
  902. supportedTags: {
  903. [FieldKey.DEVICE_CHARGING]: {
  904. key: FieldKey.DEVICE_CHARGING,
  905. },
  906. [FieldKey.EVENT_TYPE]: {
  907. key: FieldKey.EVENT_TYPE,
  908. },
  909. [FieldKey.DEVICE_ARCH]: {
  910. key: FieldKey.DEVICE_ARCH,
  911. },
  912. },
  913. };
  914. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  915. const searchBar = smartSearchBar.instance();
  916. mockCursorPosition(searchBar, 2);
  917. searchBar.updateAutoCompleteItems();
  918. await tick();
  919. expect(searchBar.state.flatSearchItems).toHaveLength(2);
  920. expect(searchBar.state.flatSearchItems[0].title).toBe(FieldKey.DEVICE_ARCH);
  921. expect(searchBar.state.flatSearchItems[1].title).toBe(FieldKey.DEVICE_CHARGING);
  922. });
  923. it('filters only descriptions', async function () {
  924. jest.useRealTimers();
  925. const props = {
  926. query: 'time',
  927. organization,
  928. location,
  929. supportedTags: {
  930. [FieldKey.DEVICE_CHARGING]: {
  931. key: FieldKey.DEVICE_CHARGING,
  932. },
  933. [FieldKey.EVENT_TYPE]: {
  934. key: FieldKey.EVENT_TYPE,
  935. },
  936. [FieldKey.DEVICE_ARCH]: {
  937. key: FieldKey.DEVICE_ARCH,
  938. },
  939. },
  940. };
  941. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  942. const searchBar = smartSearchBar.instance();
  943. mockCursorPosition(searchBar, 4);
  944. searchBar.updateAutoCompleteItems();
  945. await tick();
  946. expect(searchBar.state.flatSearchItems).toHaveLength(1);
  947. expect(searchBar.state.flatSearchItems[0].title).toBe(FieldKey.DEVICE_CHARGING);
  948. });
  949. });
  950. describe('onAutoComplete()', function () {
  951. it('completes terms from the list', function () {
  952. const props = {
  953. query: 'event.type:error ',
  954. organization,
  955. location,
  956. supportedTags,
  957. };
  958. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  959. mockCursorPosition(searchBar, 'event.type:error '.length);
  960. searchBar.onAutoComplete('myTag:', {type: 'tag'});
  961. expect(searchBar.state.query).toEqual('event.type:error myTag:');
  962. });
  963. it('completes values if cursor is not at the end', function () {
  964. const props = {
  965. query: 'id: event.type:error ',
  966. organization,
  967. location,
  968. supportedTags,
  969. };
  970. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  971. mockCursorPosition(searchBar, 3);
  972. searchBar.onAutoComplete('12345', {type: 'tag-value'});
  973. expect(searchBar.state.query).toEqual('id:12345 event.type:error ');
  974. });
  975. it('completes values if cursor is at the end', function () {
  976. const props = {
  977. query: 'event.type:error id:',
  978. organization,
  979. location,
  980. supportedTags,
  981. };
  982. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  983. mockCursorPosition(searchBar, 20);
  984. searchBar.onAutoComplete('12345', {type: 'tag-value'});
  985. expect(searchBar.state.query).toEqual('event.type:error id:12345 ');
  986. });
  987. it('triggers onChange', function () {
  988. const onChange = jest.fn();
  989. const props = {
  990. query: 'event.type:error id:',
  991. organization,
  992. location,
  993. supportedTags,
  994. };
  995. const searchBar = mountWithTheme(
  996. <SmartSearchBar {...props} onChange={onChange} />,
  997. options
  998. ).instance();
  999. mockCursorPosition(searchBar, 20);
  1000. searchBar.onAutoComplete('12345', {type: 'tag-value'});
  1001. expect(onChange).toHaveBeenCalledWith(
  1002. 'event.type:error id:12345 ',
  1003. expect.anything()
  1004. );
  1005. });
  1006. it('keeps the negation operator present', async function () {
  1007. jest.useRealTimers();
  1008. const props = {
  1009. query: '',
  1010. organization,
  1011. location,
  1012. supportedTags,
  1013. };
  1014. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  1015. const searchBar = smartSearchBar.instance();
  1016. const textarea = smartSearchBar.find('textarea');
  1017. // start typing part of the tag prefixed by the negation operator!
  1018. textarea.simulate('focus');
  1019. textarea.simulate('change', {target: {value: 'event.type:error !ti'}});
  1020. mockCursorPosition(searchBar, 20);
  1021. await tick();
  1022. // Expect the correct search term to be selected
  1023. const cursorSearchTerm = searchBar.cursorSearchTerm;
  1024. expect(cursorSearchTerm.searchTerm).toEqual('ti');
  1025. expect(cursorSearchTerm.start).toBe(18);
  1026. expect(cursorSearchTerm.end).toBe(20);
  1027. // use autocompletion to do the rest
  1028. searchBar.onAutoComplete('title:', {});
  1029. expect(searchBar.state.query).toEqual('event.type:error !title:');
  1030. });
  1031. it('handles special case for user tag', function () {
  1032. const props = {
  1033. query: '',
  1034. organization,
  1035. location,
  1036. supportedTags,
  1037. };
  1038. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  1039. const searchBar = smartSearchBar.instance();
  1040. const textarea = smartSearchBar.find('textarea');
  1041. textarea.simulate('change', {target: {value: 'user:'}});
  1042. mockCursorPosition(searchBar, 5);
  1043. searchBar.onAutoComplete('id:1', {});
  1044. expect(searchBar.state.query).toEqual('user:"id:1" ');
  1045. });
  1046. });
  1047. it('quotes in predefined values with spaces when autocompleting', async function () {
  1048. jest.useRealTimers();
  1049. const onSearch = jest.fn();
  1050. supportedTags.predefined = {
  1051. key: 'predefined',
  1052. name: 'predefined',
  1053. predefined: true,
  1054. values: ['predefined tag with spaces'],
  1055. };
  1056. const props = {
  1057. orgId: 'org-slug',
  1058. projectId: '0',
  1059. query: '',
  1060. location,
  1061. organization,
  1062. supportedTags,
  1063. onSearch,
  1064. };
  1065. const searchBar = mountWithTheme(
  1066. <SmartSearchBar {...props} api={new Client()} />,
  1067. options
  1068. );
  1069. searchBar.find('textarea').simulate('focus');
  1070. searchBar
  1071. .find('textarea')
  1072. .simulate('change', {target: {value: 'predefined:predefined'}});
  1073. await tick();
  1074. const preventDefault = jest.fn();
  1075. searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  1076. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  1077. await tick();
  1078. expect(searchBar.find('textarea').props().value).toEqual(
  1079. 'predefined:"predefined tag with spaces" '
  1080. );
  1081. });
  1082. it('escapes quotes in predefined values properly when autocompleting', async function () {
  1083. jest.useRealTimers();
  1084. const onSearch = jest.fn();
  1085. supportedTags.predefined = {
  1086. key: 'predefined',
  1087. name: 'predefined',
  1088. predefined: true,
  1089. values: ['"predefined" "tag" "with" "quotes"'],
  1090. };
  1091. const props = {
  1092. orgId: 'org-slug',
  1093. projectId: '0',
  1094. query: '',
  1095. location,
  1096. organization,
  1097. supportedTags,
  1098. onSearch,
  1099. };
  1100. const searchBar = mountWithTheme(
  1101. <SmartSearchBar {...props} api={new Client()} />,
  1102. options
  1103. );
  1104. searchBar.find('textarea').simulate('focus');
  1105. searchBar
  1106. .find('textarea')
  1107. .simulate('change', {target: {value: 'predefined:predefined'}});
  1108. await tick();
  1109. const preventDefault = jest.fn();
  1110. searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'});
  1111. searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault});
  1112. await tick();
  1113. expect(searchBar.find('textarea').props().value).toEqual(
  1114. 'predefined:"\\"predefined\\" \\"tag\\" \\"with\\" \\"quotes\\"" '
  1115. );
  1116. });
  1117. describe('quick actions', () => {
  1118. it('delete first token', async () => {
  1119. const props = {
  1120. query: 'is:unresolved sdk.name:sentry-cocoa has:key',
  1121. organization,
  1122. location,
  1123. supportedTags,
  1124. };
  1125. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  1126. searchBar.updateAutoCompleteItems();
  1127. mockCursorPosition(searchBar, 1);
  1128. await tick();
  1129. const deleteAction = shortcuts.find(a => a.shortcutType === ShortcutType.Delete);
  1130. expect(deleteAction).toBeDefined();
  1131. if (deleteAction) {
  1132. searchBar.runShortcut(deleteAction);
  1133. await tick();
  1134. expect(searchBar.state.query).toEqual('sdk.name:sentry-cocoa has:key');
  1135. }
  1136. });
  1137. it('delete middle token', async () => {
  1138. const props = {
  1139. query: 'is:unresolved sdk.name:sentry-cocoa has:key',
  1140. organization,
  1141. location,
  1142. supportedTags,
  1143. };
  1144. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  1145. searchBar.updateAutoCompleteItems();
  1146. mockCursorPosition(searchBar, 18);
  1147. await tick();
  1148. const deleteAction = shortcuts.find(a => a.shortcutType === ShortcutType.Delete);
  1149. expect(deleteAction).toBeDefined();
  1150. if (deleteAction) {
  1151. searchBar.runShortcut(deleteAction);
  1152. await tick();
  1153. expect(searchBar.state.query).toEqual('is:unresolved has:key');
  1154. }
  1155. });
  1156. it('exclude token', async () => {
  1157. const props = {
  1158. query: 'is:unresolved sdk.name:sentry-cocoa has:key',
  1159. organization,
  1160. location,
  1161. supportedTags,
  1162. };
  1163. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  1164. searchBar.updateAutoCompleteItems();
  1165. mockCursorPosition(searchBar, 18);
  1166. await tick();
  1167. const excludeAction = shortcuts.find(shortcut => shortcut.text === 'Exclude');
  1168. expect(excludeAction).toBeDefined();
  1169. if (excludeAction) {
  1170. searchBar.runShortcut(excludeAction);
  1171. await tick();
  1172. expect(searchBar.state.query).toEqual(
  1173. 'is:unresolved !sdk.name:sentry-cocoa has:key '
  1174. );
  1175. }
  1176. });
  1177. it('include token', async () => {
  1178. const props = {
  1179. query: 'is:unresolved !sdk.name:sentry-cocoa has:key',
  1180. organization,
  1181. location,
  1182. supportedTags,
  1183. };
  1184. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options).instance();
  1185. searchBar.updateAutoCompleteItems();
  1186. mockCursorPosition(searchBar, 18);
  1187. await tick();
  1188. const includeAction = shortcuts.find(shortcut => shortcut.text === 'Include');
  1189. expect(includeAction).toBeDefined();
  1190. if (includeAction) {
  1191. searchBar.runShortcut(includeAction);
  1192. await tick();
  1193. expect(searchBar.state.query).toEqual(
  1194. 'is:unresolved sdk.name:sentry-cocoa has:key '
  1195. );
  1196. }
  1197. });
  1198. it('replaces the correct word', async function () {
  1199. const props = {
  1200. query: '',
  1201. organization,
  1202. location,
  1203. supportedTags,
  1204. };
  1205. const smartSearchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  1206. const searchBar = smartSearchBar.instance();
  1207. const textarea = smartSearchBar.find('textarea');
  1208. textarea.simulate('focus');
  1209. mockCursorPosition(searchBar, 4);
  1210. textarea.simulate('change', {target: {value: 'typ ti err'}});
  1211. await tick();
  1212. // Expect the correct search term to be selected
  1213. const cursorSearchTerm = searchBar.cursorSearchTerm;
  1214. expect(cursorSearchTerm.searchTerm).toEqual('ti');
  1215. expect(cursorSearchTerm.start).toBe(4);
  1216. expect(cursorSearchTerm.end).toBe(6);
  1217. // use autocompletion to do the rest
  1218. searchBar.onAutoComplete('title:', {});
  1219. expect(searchBar.state.query).toEqual('typ title: err');
  1220. });
  1221. });
  1222. describe('Invalid field state', () => {
  1223. it('Shows invalid field state when invalid field is used', async () => {
  1224. const props = {
  1225. query: 'invalid:',
  1226. organization,
  1227. location,
  1228. supportedTags,
  1229. };
  1230. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  1231. const searchBarInst = searchBar.instance();
  1232. mockCursorPosition(searchBarInst, 8);
  1233. searchBar.find('textarea').simulate('focus');
  1234. searchBarInst.updateAutoCompleteItems();
  1235. await tick();
  1236. expect(searchBarInst.state.searchGroups).toHaveLength(1);
  1237. expect(searchBarInst.state.searchGroups[0].title).toEqual('Keys');
  1238. expect(searchBarInst.state.searchGroups[0].type).toEqual('invalid-tag');
  1239. expect(searchBar.text()).toContain("The field invalid isn't supported here");
  1240. });
  1241. it('Does not show invalid field state when valid field is used', async () => {
  1242. const props = {
  1243. query: 'is:',
  1244. organization,
  1245. location,
  1246. supportedTags,
  1247. };
  1248. const searchBar = mountWithTheme(<SmartSearchBar {...props} />, options);
  1249. const searchBarInst = searchBar.instance();
  1250. mockCursorPosition(searchBarInst, 3);
  1251. searchBarInst.updateAutoCompleteItems();
  1252. await tick();
  1253. expect(searchBar.text()).not.toContain("isn't supported here");
  1254. });
  1255. });
  1256. describe('date fields', () => {
  1257. const props = {
  1258. query: '',
  1259. organization,
  1260. location,
  1261. supportedTags,
  1262. };
  1263. it('displays date picker dropdown when appropriate', () => {
  1264. render(<SmartSearchBar {...props} />);
  1265. const textbox = screen.getByRole('textbox');
  1266. userEvent.click(textbox);
  1267. expect(screen.queryByTestId('search-bar-date-picker')).not.toBeInTheDocument();
  1268. // Just lastSeen: will display relative and absolute options, not the datepicker
  1269. userEvent.type(textbox, 'lastSeen:');
  1270. expect(screen.queryByTestId('search-bar-date-picker')).not.toBeInTheDocument();
  1271. expect(screen.getByText('Last hour')).toBeInTheDocument();
  1272. expect(screen.getByText('After a custom datetime')).toBeInTheDocument();
  1273. // lastSeen:> should open the date picker
  1274. userEvent.type(textbox, '>');
  1275. expect(screen.getByTestId('search-bar-date-picker')).toBeInTheDocument();
  1276. // Continues to display with date typed out
  1277. userEvent.type(textbox, '2022-01-01');
  1278. expect(screen.getByTestId('search-bar-date-picker')).toBeInTheDocument();
  1279. // Goes away when on next term
  1280. userEvent.type(textbox, ' ');
  1281. expect(screen.queryByTestId('search-bar-date-picker')).not.toBeInTheDocument();
  1282. // Pops back up when cursor is back in date token
  1283. userEvent.keyboard('{arrowleft}');
  1284. expect(screen.getByTestId('search-bar-date-picker')).toBeInTheDocument();
  1285. // Moving cursor inside the `lastSeen` token hides the date picker
  1286. textbox.setSelectionRange(1, 1);
  1287. userEvent.click(textbox);
  1288. expect(screen.queryByTestId('search-bar-date-picker')).not.toBeInTheDocument();
  1289. });
  1290. it('can select a suggested relative time value', () => {
  1291. render(<SmartSearchBar {...props} />);
  1292. userEvent.type(screen.getByRole('textbox'), 'lastSeen:');
  1293. userEvent.click(screen.getByText('Last hour'));
  1294. expect(screen.getByRole('textbox')).toHaveValue('lastSeen:-1h ');
  1295. });
  1296. it('can select a specific date/time', async () => {
  1297. render(<SmartSearchBar {...props} />);
  1298. userEvent.type(screen.getByRole('textbox'), 'lastSeen:');
  1299. userEvent.click(screen.getByText('After a custom datetime'));
  1300. // Should have added '>' to query and show a date picker
  1301. expect(screen.getByRole('textbox')).toHaveValue('lastSeen:>');
  1302. expect(screen.getByTestId('search-bar-date-picker')).toBeInTheDocument();
  1303. // For whatever reason, need this line to get the lazily-loaded datepicker
  1304. // to show up in this test. Without it, the datepicker never shows up
  1305. // no matter how long the timeout is set to.
  1306. await tick();
  1307. // Select a day on the calendar
  1308. const dateInput = await screen.findByTestId('date-picker');
  1309. fireEvent.change(dateInput, {target: {value: '2022-01-02'}});
  1310. expect(screen.getByRole('textbox')).toHaveValue(
  1311. // -05:00 because our tests run in EST
  1312. 'lastSeen:>2022-01-02T00:00:00-05:00'
  1313. );
  1314. const timeInput = screen.getByLabelText('Time');
  1315. // Simulate changing time input one bit at a time
  1316. userEvent.click(timeInput);
  1317. fireEvent.change(timeInput, {target: {value: '01:00:00'}});
  1318. fireEvent.change(timeInput, {target: {value: '01:02:00'}});
  1319. fireEvent.change(timeInput, {target: {value: '01:02:03'}});
  1320. // Time input should have retained focus this whole time
  1321. expect(timeInput).toHaveFocus();
  1322. fireEvent.blur(timeInput);
  1323. expect(screen.getByRole('textbox')).toHaveValue(
  1324. 'lastSeen:>2022-01-02T01:02:03-05:00'
  1325. );
  1326. // Toggle UTC on, which should remove the timezone (-05:00) from the query
  1327. userEvent.click(screen.getByLabelText('Use UTC'));
  1328. expect(screen.getByRole('textbox')).toHaveValue('lastSeen:>2022-01-02T01:02:03');
  1329. });
  1330. it('can change an existing datetime', async () => {
  1331. render(<SmartSearchBar {...props} />);
  1332. const textbox = screen.getByRole('textbox');
  1333. fireEvent.change(textbox, {
  1334. target: {value: 'lastSeen:2022-01-02 firstSeen:2022-01-01'},
  1335. });
  1336. // Move cursor to the lastSeen date
  1337. userEvent.click(textbox);
  1338. textbox.setSelectionRange(10, 10);
  1339. fireEvent.focus(textbox);
  1340. await tick();
  1341. const dateInput = await screen.findByTestId('date-picker');
  1342. expect(dateInput).toHaveValue('2022-01-02');
  1343. expect(screen.getByLabelText('Time')).toHaveValue('00:00:00');
  1344. expect(screen.getByLabelText('Use UTC')).toBeChecked();
  1345. fireEvent.change(dateInput, {target: {value: '2022-01-03'}});
  1346. expect(textbox).toHaveValue('lastSeen:2022-01-03T00:00:00 firstSeen:2022-01-01');
  1347. // Cursor should be at end of the value we just replaced
  1348. expect(textbox.selectionStart).toBe('lastSeen:2022-01-03T00:00:00'.length);
  1349. });
  1350. it('populates the date picker correctly for date without time', async () => {
  1351. render(<SmartSearchBar {...props} query="lastSeen:2022-01-01" />);
  1352. const textbox = screen.getByRole('textbox');
  1353. // Move cursor to the timestamp
  1354. userEvent.click(textbox);
  1355. textbox.setSelectionRange(10, 10);
  1356. fireEvent.focus(textbox);
  1357. userEvent.click(screen.getByRole('textbox'));
  1358. await tick();
  1359. const dateInput = await screen.findByTestId('date-picker');
  1360. expect(dateInput).toHaveValue('2022-01-01');
  1361. // No time provided, so time input should be the default value
  1362. expect(screen.getByLabelText('Time')).toHaveValue('00:00:00');
  1363. // UTC is checked because there is no timezone
  1364. expect(screen.getByLabelText('Use UTC')).toBeChecked();
  1365. });
  1366. it('populates the date picker correctly for date with time and no timezone', async () => {
  1367. render(<SmartSearchBar {...props} query="lastSeen:2022-01-01T09:45:12" />);
  1368. const textbox = screen.getByRole('textbox');
  1369. // Move cursor to the timestamp
  1370. userEvent.click(textbox);
  1371. textbox.setSelectionRange(10, 10);
  1372. fireEvent.focus(textbox);
  1373. await tick();
  1374. const dateInput = await screen.findByTestId('date-picker');
  1375. expect(dateInput).toHaveValue('2022-01-01');
  1376. expect(screen.getByLabelText('Time')).toHaveValue('09:45:12');
  1377. expect(screen.getByLabelText('Use UTC')).toBeChecked();
  1378. });
  1379. it('populates the date picker correctly for date with time and timezone', async () => {
  1380. render(<SmartSearchBar {...props} query="lastSeen:2022-01-01T09:45:12-05:00" />);
  1381. const textbox = screen.getByRole('textbox');
  1382. // Move cursor to the timestamp
  1383. userEvent.click(textbox);
  1384. textbox.setSelectionRange(10, 10);
  1385. fireEvent.focus(textbox);
  1386. await tick();
  1387. const dateInput = await screen.findByTestId('date-picker');
  1388. expect(dateInput).toHaveValue('2022-01-01');
  1389. expect(screen.getByLabelText('Time')).toHaveValue('09:45:12');
  1390. expect(screen.getByLabelText('Use UTC')).not.toBeChecked();
  1391. });
  1392. });
  1393. });