index.spec.jsx 45 KB

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