index.spec.jsx 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  1. import {
  2. act,
  3. fireEvent,
  4. render,
  5. screen,
  6. userEvent,
  7. waitFor,
  8. } from 'sentry-test/reactTestingLibrary';
  9. import {SmartSearchBar} from 'sentry/components/smartSearchBar';
  10. import TagStore from 'sentry/stores/tagStore';
  11. import {FieldKey} from 'sentry/utils/fields';
  12. describe('SmartSearchBar', function () {
  13. let defaultProps;
  14. beforeEach(function () {
  15. TagStore.reset();
  16. TagStore.loadTagsSuccess(TestStubs.Tags());
  17. const supportedTags = TagStore.getState();
  18. supportedTags.firstRelease = {
  19. key: 'firstRelease',
  20. name: 'firstRelease',
  21. };
  22. supportedTags.is = {
  23. key: 'is',
  24. name: 'is',
  25. };
  26. const organization = TestStubs.Organization({id: '123'});
  27. const location = {
  28. pathname: '/organizations/org-slug/recent-searches/',
  29. query: {
  30. projectId: '0',
  31. },
  32. };
  33. MockApiClient.clearMockResponses();
  34. MockApiClient.addMockResponse({
  35. url: '/organizations/org-slug/recent-searches/',
  36. body: [],
  37. });
  38. defaultProps = {
  39. query: '',
  40. organization,
  41. location,
  42. supportedTags,
  43. onGetTagValues: jest.fn().mockResolvedValue([]),
  44. onSearch: jest.fn(),
  45. };
  46. });
  47. afterEach(function () {
  48. MockApiClient.clearMockResponses();
  49. });
  50. it('quotes in values with spaces when autocompleting', async function () {
  51. const onGetTagValuesMock = jest
  52. .fn()
  53. .mockResolvedValue(['this is filled with spaces']);
  54. render(<SmartSearchBar {...defaultProps} onGetTagValues={onGetTagValuesMock} />);
  55. const textbox = screen.getByRole('textbox');
  56. userEvent.click(textbox);
  57. userEvent.type(textbox, 'device:this');
  58. const option = await screen.findByText(/this is filled with spaces/);
  59. userEvent.click(option);
  60. expect(textbox).toHaveValue('device:"this is filled with spaces" ');
  61. });
  62. it('escapes quotes in values properly when autocompleting', async function () {
  63. const onGetTagValuesMock = jest
  64. .fn()
  65. .mockResolvedValue(['this " is " filled " with " quotes']);
  66. render(<SmartSearchBar {...defaultProps} onGetTagValues={onGetTagValuesMock} />);
  67. const textbox = screen.getByRole('textbox');
  68. userEvent.click(textbox);
  69. userEvent.type(textbox, 'device:this');
  70. const option = await screen.findByText(/this \\" is \\" filled \\" with \\" quotes/);
  71. userEvent.click(option);
  72. expect(textbox).toHaveValue('device:"this \\" is \\" filled \\" with \\" quotes" ');
  73. });
  74. it('does not search when pressing enter on a tag without a value', function () {
  75. const onSearchMock = jest.fn();
  76. render(<SmartSearchBar {...defaultProps} onSearch={onSearchMock} />);
  77. const textbox = screen.getByRole('textbox');
  78. userEvent.type(textbox, 'browser:{enter}');
  79. expect(onSearchMock).not.toHaveBeenCalled();
  80. });
  81. it('autocompletes value with tab', async function () {
  82. const onSearchMock = jest.fn();
  83. render(<SmartSearchBar {...defaultProps} onSearch={onSearchMock} />);
  84. const textbox = screen.getByRole('textbox');
  85. userEvent.type(textbox, 'bro');
  86. expect(
  87. await screen.findByRole('option', {name: 'bro wser - field'})
  88. ).toBeInTheDocument();
  89. // down once to 'browser' dropdown item
  90. userEvent.keyboard('{ArrowDown}{Tab}');
  91. await waitFor(() => {
  92. expect(textbox).toHaveValue('browser:');
  93. });
  94. expect(textbox).toHaveFocus();
  95. // Should not have executed the search
  96. expect(onSearchMock).not.toHaveBeenCalled();
  97. });
  98. it('autocompletes value with enter', async function () {
  99. const onSearchMock = jest.fn();
  100. render(<SmartSearchBar {...defaultProps} onSearch={onSearchMock} />);
  101. const textbox = screen.getByRole('textbox');
  102. userEvent.type(textbox, 'bro');
  103. expect(
  104. await screen.findByRole('option', {name: 'bro wser - field'})
  105. ).toBeInTheDocument();
  106. // down once to 'browser' dropdown item
  107. userEvent.keyboard('{ArrowDown}{Enter}');
  108. await waitFor(() => {
  109. expect(textbox).toHaveValue('browser:');
  110. });
  111. expect(textbox).toHaveFocus();
  112. // Should not have executed the search
  113. expect(onSearchMock).not.toHaveBeenCalled();
  114. });
  115. it('searches and completes tags with negation operator', async function () {
  116. render(<SmartSearchBar {...defaultProps} />);
  117. const textbox = screen.getByRole('textbox');
  118. userEvent.type(textbox, '!bro');
  119. const field = await screen.findByRole('option', {name: 'bro wser - field'});
  120. userEvent.click(field);
  121. expect(textbox).toHaveValue('!browser:');
  122. });
  123. describe('componentWillReceiveProps()', function () {
  124. it('should add a space when setting query', function () {
  125. render(<SmartSearchBar {...defaultProps} query="one" />);
  126. expect(screen.getByRole('textbox')).toHaveValue('one ');
  127. });
  128. it('updates query when prop changes', function () {
  129. const {rerender} = render(<SmartSearchBar {...defaultProps} query="one" />);
  130. rerender(<SmartSearchBar {...defaultProps} query="two" />);
  131. expect(screen.getByRole('textbox')).toHaveValue('two ');
  132. });
  133. it('updates query when prop set to falsey value', function () {
  134. const {rerender} = render(<SmartSearchBar {...defaultProps} query="one" />);
  135. rerender(<SmartSearchBar {...defaultProps} query={null} />);
  136. expect(screen.getByRole('textbox')).toHaveValue('');
  137. });
  138. it('should not reset user textarea if a noop props change happens', function () {
  139. const {rerender} = render(<SmartSearchBar {...defaultProps} query="one" />);
  140. userEvent.type(screen.getByRole('textbox'), 'two');
  141. rerender(<SmartSearchBar {...defaultProps} query="one" />);
  142. expect(screen.getByRole('textbox')).toHaveValue('one two');
  143. });
  144. it('should reset user textarea if a meaningful props change happens', function () {
  145. const {rerender} = render(<SmartSearchBar {...defaultProps} query="one" />);
  146. userEvent.type(screen.getByRole('textbox'), 'two');
  147. rerender(<SmartSearchBar {...defaultProps} query="blah" />);
  148. expect(screen.getByRole('textbox')).toHaveValue('blah ');
  149. });
  150. });
  151. describe('clear search', function () {
  152. it('clicking the clear search button clears the query and calls onSearch', function () {
  153. const mockOnSearch = jest.fn();
  154. render(
  155. <SmartSearchBar {...defaultProps} onSearch={mockOnSearch} query="is:unresolved" />
  156. );
  157. expect(screen.getByRole('textbox')).toHaveValue('is:unresolved ');
  158. userEvent.click(screen.getByRole('button', {name: 'Clear search'}));
  159. expect(screen.getByRole('textbox')).toHaveValue('');
  160. expect(mockOnSearch).toHaveBeenCalledTimes(1);
  161. expect(mockOnSearch).toHaveBeenCalledWith('');
  162. });
  163. });
  164. describe('dropdown open state', function () {
  165. it('opens the dropdown when the search box is clicked', function () {
  166. render(<SmartSearchBar {...defaultProps} />);
  167. const textbox = screen.getByRole('textbox');
  168. userEvent.click(textbox);
  169. expect(screen.getByTestId('smart-search-dropdown')).toBeInTheDocument();
  170. });
  171. it('opens the dropdown when the search box gains focus', function () {
  172. render(<SmartSearchBar {...defaultProps} />);
  173. const textbox = screen.getByRole('textbox');
  174. fireEvent.focus(textbox);
  175. expect(screen.getByTestId('smart-search-dropdown')).toBeInTheDocument();
  176. });
  177. it('hides the drop down when clicking outside', function () {
  178. render(
  179. <div data-test-id="test-container">
  180. <SmartSearchBar {...defaultProps} />
  181. </div>
  182. );
  183. const textbox = screen.getByRole('textbox');
  184. // Open the dropdown
  185. fireEvent.focus(textbox);
  186. userEvent.click(screen.getByTestId('test-container'));
  187. expect(screen.queryByTestId('smart-search-dropdown')).not.toBeInTheDocument();
  188. });
  189. it('hides the drop down when pressing escape', function () {
  190. render(<SmartSearchBar {...defaultProps} />);
  191. const textbox = screen.getByRole('textbox');
  192. // Open the dropdown
  193. fireEvent.focus(textbox);
  194. userEvent.type(textbox, '{Escape}');
  195. expect(screen.queryByTestId('smart-search-dropdown')).not.toBeInTheDocument();
  196. });
  197. });
  198. describe('pasting', function () {
  199. it('trims pasted content', function () {
  200. const mockOnChange = jest.fn();
  201. render(<SmartSearchBar {...defaultProps} onChange={mockOnChange} />);
  202. const textbox = screen.getByRole('textbox');
  203. fireEvent.paste(textbox, {clipboardData: {getData: () => ' something'}});
  204. expect(textbox).toHaveValue('something');
  205. expect(mockOnChange).toHaveBeenCalledWith('something', expect.anything());
  206. });
  207. });
  208. it('invokes onSearch() on enter', function () {
  209. const mockOnSearch = jest.fn();
  210. render(<SmartSearchBar {...defaultProps} query="test" onSearch={mockOnSearch} />);
  211. userEvent.type(screen.getByRole('textbox'), '{Enter}');
  212. expect(mockOnSearch).toHaveBeenCalledWith('test');
  213. });
  214. it('handles an empty query', function () {
  215. render(<SmartSearchBar {...defaultProps} query="" />);
  216. expect(screen.getByRole('textbox')).toHaveValue('');
  217. });
  218. it('does not fetch tag values with environment tag and excludeEnvironment', function () {
  219. jest.useFakeTimers('modern');
  220. const getTagValuesMock = jest.fn().mockResolvedValue([]);
  221. render(
  222. <SmartSearchBar
  223. {...defaultProps}
  224. onGetTagValues={getTagValuesMock}
  225. excludedTags={['environment']}
  226. />
  227. );
  228. const textbox = screen.getByRole('textbox');
  229. userEvent.type(textbox, 'environment:');
  230. act(() => {
  231. jest.runOnlyPendingTimers();
  232. });
  233. expect(getTagValuesMock).not.toHaveBeenCalled();
  234. });
  235. it('does not fetch tag values with timesSeen tag', function () {
  236. jest.useFakeTimers('modern');
  237. const getTagValuesMock = jest.fn().mockResolvedValue([]);
  238. render(
  239. <SmartSearchBar
  240. {...defaultProps}
  241. onGetTagValues={getTagValuesMock}
  242. excludedTags={['environment']}
  243. />
  244. );
  245. const textbox = screen.getByRole('textbox');
  246. userEvent.type(textbox, 'timesSeen:');
  247. act(() => {
  248. jest.runOnlyPendingTimers();
  249. });
  250. expect(getTagValuesMock).not.toHaveBeenCalled();
  251. });
  252. it('fetches and displays tag values with other tags', function () {
  253. jest.useFakeTimers();
  254. const getTagValuesMock = jest.fn().mockResolvedValue([]);
  255. render(
  256. <SmartSearchBar
  257. {...defaultProps}
  258. onGetTagValues={getTagValuesMock}
  259. excludedTags={['environment']}
  260. />
  261. );
  262. const textbox = screen.getByRole('textbox');
  263. userEvent.type(textbox, 'browser:');
  264. act(() => {
  265. jest.runOnlyPendingTimers();
  266. });
  267. expect(getTagValuesMock).toHaveBeenCalledTimes(1);
  268. });
  269. it('shows correct options on cursor changes for keys and values', async function () {
  270. jest.useFakeTimers();
  271. const getTagValuesMock = jest.fn().mockResolvedValue([]);
  272. render(
  273. <SmartSearchBar
  274. {...defaultProps}
  275. query="is:unresolved"
  276. onGetTagValues={getTagValuesMock}
  277. onGetRecentSearches={jest.fn().mockReturnValue([])}
  278. />
  279. );
  280. const textbox = screen.getByRole('textbox');
  281. // Set cursor to beginning of "is" tag
  282. textbox.setSelectionRange(0, 0);
  283. userEvent.click(textbox);
  284. act(() => {
  285. jest.runAllTimers();
  286. });
  287. // Should show "Keys" section
  288. expect(await screen.findByText('Keys')).toBeInTheDocument();
  289. // Set cursor to middle of "is" tag
  290. userEvent.keyboard('{ArrowRight}');
  291. act(() => {
  292. jest.runAllTimers();
  293. });
  294. // Should show "Keys" and NOT "Operator Helpers" or "Values"
  295. expect(await screen.findByText('Keys')).toBeInTheDocument();
  296. expect(screen.queryByText('Operator Helpers')).not.toBeInTheDocument();
  297. expect(screen.queryByText('Values')).not.toBeInTheDocument();
  298. // Set cursor to end of "is" tag
  299. userEvent.keyboard('{ArrowRight}');
  300. act(() => {
  301. jest.runAllTimers();
  302. });
  303. // Should show "Tags" and "Operator Helpers" but NOT "Values"
  304. expect(await screen.findByText('Keys')).toBeInTheDocument();
  305. expect(screen.getByText('Operator Helpers')).toBeInTheDocument();
  306. expect(screen.queryByText('Values')).not.toBeInTheDocument();
  307. // Set cursor after the ":"
  308. userEvent.keyboard('{ArrowRight}');
  309. act(() => {
  310. jest.runAllTimers();
  311. });
  312. // Should show "Values" and "Operator Helpers" but NOT "Keys"
  313. expect(await screen.findByText('Values')).toBeInTheDocument();
  314. expect(await screen.findByText('Operator Helpers')).toBeInTheDocument();
  315. expect(screen.queryByText('Keys')).not.toBeInTheDocument();
  316. // Set cursor inside value
  317. userEvent.keyboard('{ArrowRight}');
  318. act(() => {
  319. jest.runAllTimers();
  320. });
  321. // Should show "Values" and NOT "Operator Helpers" or "Keys"
  322. expect(await screen.findByText('Values')).toBeInTheDocument();
  323. expect(screen.queryByText('Operator Helpers')).not.toBeInTheDocument();
  324. expect(screen.queryByText('Keys')).not.toBeInTheDocument();
  325. });
  326. it('shows syntax error for incorrect tokens', function () {
  327. render(<SmartSearchBar {...defaultProps} query="tag: is: has:" />);
  328. // Should have three invalid tokens (tag:, is:, and has:)
  329. expect(screen.getAllByTestId('filter-token-invalid')).toHaveLength(3);
  330. });
  331. it('renders nested keys correctly', async function () {
  332. const {container} = render(
  333. <SmartSearchBar
  334. {...defaultProps}
  335. query=""
  336. supportedTags={{
  337. nested: {
  338. key: 'nested',
  339. name: 'nested',
  340. },
  341. 'nested.child': {
  342. key: 'nested.child',
  343. name: 'nested.child',
  344. },
  345. 'nestednoparent.child': {
  346. key: 'nestednoparent.child',
  347. name: 'nestednoparent.child',
  348. },
  349. }}
  350. />
  351. );
  352. const textbox = screen.getByRole('textbox');
  353. userEvent.type(textbox, 'nest');
  354. await screen.findByText('Keys');
  355. expect(container).toSnapshot();
  356. });
  357. it('filters keys on name and description', async function () {
  358. render(
  359. <SmartSearchBar
  360. {...defaultProps}
  361. query=""
  362. supportedTags={{
  363. [FieldKey.DEVICE_CHARGING]: {
  364. key: FieldKey.DEVICE_CHARGING,
  365. },
  366. [FieldKey.EVENT_TYPE]: {
  367. key: FieldKey.EVENT_TYPE,
  368. },
  369. [FieldKey.DEVICE_ARCH]: {
  370. key: FieldKey.DEVICE_ARCH,
  371. },
  372. }}
  373. />
  374. );
  375. const textbox = screen.getByRole('textbox');
  376. userEvent.type(textbox, 'event');
  377. await screen.findByText('Keys');
  378. // Should show event.type (has event in key) and device.charging (has event in description)
  379. expect(screen.getByRole('option', {name: /event . type/})).toBeInTheDocument();
  380. expect(screen.getByRole('option', {name: /charging/})).toBeInTheDocument();
  381. // But not device.arch (not in key or description)
  382. expect(screen.queryByRole('option', {name: /arch/})).not.toBeInTheDocument();
  383. });
  384. it('handles autocomplete race conditions when cursor position changed', async function () {
  385. jest.useFakeTimers();
  386. const mockOnGetTagValues = jest.fn().mockImplementation(
  387. () =>
  388. new Promise(resolve => {
  389. setTimeout(() => {
  390. resolve(['value']);
  391. }, [300]);
  392. })
  393. );
  394. render(
  395. <SmartSearchBar {...defaultProps} onGetTagValues={mockOnGetTagValues} query="" />
  396. );
  397. const textbox = screen.getByRole('textbox');
  398. // Type key and start searching values
  399. userEvent.type(textbox, 'is:');
  400. act(() => {
  401. jest.advanceTimersByTime(200);
  402. });
  403. // Before values have finished searching, clear the textbox
  404. userEvent.clear(textbox);
  405. act(() => {
  406. jest.runAllTimers();
  407. });
  408. // Should show keys, not values in dropdown
  409. expect(await screen.findByText('Keys')).toBeInTheDocument();
  410. expect(screen.queryByText('Values')).not.toBeInTheDocument();
  411. });
  412. it('autocompletes tag values', async function () {
  413. jest.useFakeTimers();
  414. const mockOnChange = jest.fn();
  415. const getTagValuesMock = jest.fn().mockResolvedValue(['Chrome', 'Firefox']);
  416. render(
  417. <SmartSearchBar
  418. {...defaultProps}
  419. onGetTagValues={getTagValuesMock}
  420. query=""
  421. onChange={mockOnChange}
  422. />
  423. );
  424. const textbox = screen.getByRole('textbox');
  425. userEvent.type(textbox, 'browser:');
  426. act(() => {
  427. jest.runOnlyPendingTimers();
  428. });
  429. const option = await screen.findByRole('option', {name: /Firefox/});
  430. userEvent.click(option);
  431. await waitFor(() => {
  432. expect(mockOnChange).toHaveBeenLastCalledWith(
  433. 'browser:Firefox ',
  434. expect.anything()
  435. );
  436. });
  437. });
  438. it('autocompletes tag values when there are other tags', async function () {
  439. jest.useFakeTimers();
  440. const mockOnChange = jest.fn();
  441. const getTagValuesMock = jest.fn().mockResolvedValue(['Chrome', 'Firefox']);
  442. render(
  443. <SmartSearchBar
  444. {...defaultProps}
  445. onGetTagValues={getTagValuesMock}
  446. excludedTags={['environment']}
  447. query="is:unresolved error.handled:true"
  448. onChange={mockOnChange}
  449. />
  450. );
  451. // Type "browser:" in between existing key/values
  452. const textbox = screen.getByRole('textbox');
  453. fireEvent.change(textbox, {
  454. target: {value: 'is:unresolved browser: error.handled:true'},
  455. });
  456. // Make sure cursor is at end of "browser:""
  457. textbox.setSelectionRange(
  458. 'is:unresolved browser:'.length,
  459. 'is:unresolved browser:'.length
  460. );
  461. userEvent.click(textbox);
  462. act(() => {
  463. jest.runOnlyPendingTimers();
  464. });
  465. const option = await screen.findByRole('option', {name: /Firefox/});
  466. userEvent.click(option);
  467. await waitFor(() => {
  468. expect(mockOnChange).toHaveBeenLastCalledWith(
  469. 'is:unresolved browser:Firefox error.handled:true',
  470. expect.anything()
  471. );
  472. });
  473. });
  474. it('autocompletes tag values (user tag)', async function () {
  475. jest.useFakeTimers();
  476. const mockOnChange = jest.fn();
  477. const getTagValuesMock = jest.fn().mockResolvedValue(['id:1']);
  478. render(
  479. <SmartSearchBar
  480. {...defaultProps}
  481. onGetTagValues={getTagValuesMock}
  482. query=""
  483. onChange={mockOnChange}
  484. />
  485. );
  486. const textbox = screen.getByRole('textbox');
  487. userEvent.type(textbox, 'user:');
  488. act(() => {
  489. jest.runOnlyPendingTimers();
  490. });
  491. const option = await screen.findByRole('option', {name: /id:1/});
  492. userEvent.click(option);
  493. await waitFor(() => {
  494. expect(mockOnChange).toHaveBeenLastCalledWith('user:"id:1" ', expect.anything());
  495. });
  496. });
  497. it('autocompletes tag values (predefined values with spaces)', async function () {
  498. jest.useFakeTimers();
  499. const mockOnChange = jest.fn();
  500. render(
  501. <SmartSearchBar
  502. {...defaultProps}
  503. query=""
  504. onChange={mockOnChange}
  505. supportedTags={{
  506. predefined: {
  507. key: 'predefined',
  508. name: 'predefined',
  509. predefined: true,
  510. values: ['predefined tag with spaces'],
  511. },
  512. }}
  513. />
  514. );
  515. const textbox = screen.getByRole('textbox');
  516. userEvent.type(textbox, 'predefined:');
  517. act(() => {
  518. jest.runOnlyPendingTimers();
  519. });
  520. const option = await screen.findByRole('option', {
  521. name: /predefined tag with spaces/,
  522. });
  523. userEvent.click(option);
  524. await waitFor(() => {
  525. expect(mockOnChange).toHaveBeenLastCalledWith(
  526. 'predefined:"predefined tag with spaces" ',
  527. expect.anything()
  528. );
  529. });
  530. });
  531. it('autocompletes tag values (predefined values with quotes)', async function () {
  532. jest.useFakeTimers();
  533. const mockOnChange = jest.fn();
  534. render(
  535. <SmartSearchBar
  536. {...defaultProps}
  537. query=""
  538. onChange={mockOnChange}
  539. supportedTags={{
  540. predefined: {
  541. key: 'predefined',
  542. name: 'predefined',
  543. predefined: true,
  544. values: ['"predefined" "tag" "with" "quotes"'],
  545. },
  546. }}
  547. />
  548. );
  549. const textbox = screen.getByRole('textbox');
  550. userEvent.type(textbox, 'predefined:');
  551. act(() => {
  552. jest.runOnlyPendingTimers();
  553. });
  554. const option = await screen.findByRole('option', {
  555. name: /quotes/,
  556. });
  557. userEvent.click(option);
  558. await waitFor(() => {
  559. expect(mockOnChange).toHaveBeenLastCalledWith(
  560. 'predefined:"\\"predefined\\" \\"tag\\" \\"with\\" \\"quotes\\"" ',
  561. expect.anything()
  562. );
  563. });
  564. });
  565. describe('quick actions', function () {
  566. it('can delete tokens', function () {
  567. render(
  568. <SmartSearchBar
  569. {...defaultProps}
  570. query="is:unresolved sdk.name:sentry-cocoa has:key"
  571. />
  572. );
  573. const textbox = screen.getByRole('textbox');
  574. userEvent.click(textbox);
  575. // Put cursor inside is:resolved
  576. textbox.setSelectionRange(1, 1);
  577. userEvent.click(screen.getByRole('button', {name: /Delete/}));
  578. expect(textbox).toHaveValue('sdk.name:sentry-cocoa has:key');
  579. });
  580. it('can delete a middle token', function () {
  581. render(
  582. <SmartSearchBar
  583. {...defaultProps}
  584. query="is:unresolved sdk.name:sentry-cocoa has:key"
  585. />
  586. );
  587. const textbox = screen.getByRole('textbox');
  588. // Put cursor inside sdk.name
  589. textbox.setSelectionRange('is:unresolved s'.length, 'is:unresolved s'.length);
  590. userEvent.click(textbox);
  591. userEvent.click(screen.getByRole('button', {name: /Delete/}));
  592. expect(textbox).toHaveValue('is:unresolved has:key');
  593. });
  594. it('can exclude a token', function () {
  595. render(
  596. <SmartSearchBar
  597. {...defaultProps}
  598. query="is:unresolved sdk.name:sentry-cocoa has:key"
  599. />
  600. );
  601. const textbox = screen.getByRole('textbox');
  602. // Put cursor inside sdk.name
  603. textbox.setSelectionRange('is:unresolved sd'.length, 'is:unresolved sd'.length);
  604. userEvent.click(textbox);
  605. userEvent.click(screen.getByRole('button', {name: /Exclude/}));
  606. expect(textbox).toHaveValue('is:unresolved !sdk.name:sentry-cocoa has:key ');
  607. });
  608. it('can include a token', async function () {
  609. render(
  610. <SmartSearchBar
  611. {...defaultProps}
  612. query="is:unresolved !sdk.name:sentry-cocoa has:key"
  613. />
  614. );
  615. const textbox = screen.getByRole('textbox');
  616. // Put cursor inside sdk.name
  617. textbox.setSelectionRange('is:unresolved !s'.length, 'is:unresolved !s'.length);
  618. userEvent.click(textbox);
  619. expect(textbox).toHaveValue('is:unresolved !sdk.name:sentry-cocoa has:key ');
  620. await screen.findByRole('button', {name: /Include/});
  621. userEvent.click(screen.getByRole('button', {name: /Include/}));
  622. expect(textbox).toHaveValue('is:unresolved sdk.name:sentry-cocoa has:key ');
  623. });
  624. });
  625. it('displays invalid field message', async function () {
  626. render(<SmartSearchBar {...defaultProps} query="" />);
  627. const textbox = screen.getByRole('textbox');
  628. userEvent.type(textbox, 'invalid:');
  629. act(() => {
  630. jest.runOnlyPendingTimers();
  631. });
  632. expect(
  633. await screen.findByRole('option', {name: /the field invalid isn't supported here/i})
  634. ).toBeInTheDocument();
  635. });
  636. describe('date fields', () => {
  637. // Transpile the lazy-loaded datepicker up front so tests don't flake
  638. beforeAll(async function () {
  639. await import('sentry/components/calendar/datePicker');
  640. });
  641. it('displays date picker dropdown when appropriate', () => {
  642. render(<SmartSearchBar {...defaultProps} query="" />);
  643. const textbox = screen.getByRole('textbox');
  644. userEvent.click(textbox);
  645. expect(screen.queryByTestId('search-bar-date-picker')).not.toBeInTheDocument();
  646. // Just lastSeen: will display relative and absolute options, not the datepicker
  647. userEvent.type(textbox, 'lastSeen:');
  648. expect(screen.queryByTestId('search-bar-date-picker')).not.toBeInTheDocument();
  649. expect(screen.getByText('Last hour')).toBeInTheDocument();
  650. expect(screen.getByText('After a custom datetime')).toBeInTheDocument();
  651. // lastSeen:> should open the date picker
  652. userEvent.type(textbox, '>');
  653. expect(screen.getByTestId('search-bar-date-picker')).toBeInTheDocument();
  654. // Continues to display with date typed out
  655. userEvent.type(textbox, '2022-01-01');
  656. expect(screen.getByTestId('search-bar-date-picker')).toBeInTheDocument();
  657. // Goes away when on next term
  658. userEvent.type(textbox, ' ');
  659. expect(screen.queryByTestId('search-bar-date-picker')).not.toBeInTheDocument();
  660. // Pops back up when cursor is back in date token
  661. userEvent.keyboard('{arrowleft}');
  662. expect(screen.getByTestId('search-bar-date-picker')).toBeInTheDocument();
  663. // Moving cursor inside the `lastSeen` token hides the date picker
  664. textbox.setSelectionRange(1, 1);
  665. userEvent.click(textbox);
  666. expect(screen.queryByTestId('search-bar-date-picker')).not.toBeInTheDocument();
  667. });
  668. it('can select a suggested relative time value', () => {
  669. render(<SmartSearchBar {...defaultProps} query="" />);
  670. userEvent.type(screen.getByRole('textbox'), 'lastSeen:');
  671. userEvent.click(screen.getByText('Last hour'));
  672. expect(screen.getByRole('textbox')).toHaveValue('lastSeen:-1h ');
  673. });
  674. it('can select a specific date/time', async () => {
  675. render(<SmartSearchBar {...defaultProps} query="" />);
  676. userEvent.type(screen.getByRole('textbox'), 'lastSeen:');
  677. userEvent.click(screen.getByText('After a custom datetime'));
  678. // Should have added '>' to query and show a date picker
  679. expect(screen.getByRole('textbox')).toHaveValue('lastSeen:>');
  680. expect(screen.getByTestId('search-bar-date-picker')).toBeInTheDocument();
  681. // Select a day on the calendar
  682. const dateInput = await screen.findByTestId('date-picker');
  683. fireEvent.change(dateInput, {target: {value: '2022-01-02'}});
  684. expect(screen.getByRole('textbox')).toHaveValue(
  685. // -05:00 because our tests run in EST
  686. 'lastSeen:>2022-01-02T00:00:00-05:00'
  687. );
  688. const timeInput = screen.getByLabelText('Time');
  689. // Simulate changing time input one bit at a time
  690. userEvent.click(timeInput);
  691. fireEvent.change(timeInput, {target: {value: '01:00:00'}});
  692. fireEvent.change(timeInput, {target: {value: '01:02:00'}});
  693. fireEvent.change(timeInput, {target: {value: '01:02:03'}});
  694. // Time input should have retained focus this whole time
  695. expect(timeInput).toHaveFocus();
  696. fireEvent.blur(timeInput);
  697. expect(screen.getByRole('textbox')).toHaveValue(
  698. 'lastSeen:>2022-01-02T01:02:03-05:00'
  699. );
  700. // Toggle UTC on, which should remove the timezone (-05:00) from the query
  701. userEvent.click(screen.getByLabelText('Use UTC'));
  702. expect(screen.getByRole('textbox')).toHaveValue('lastSeen:>2022-01-02T01:02:03');
  703. });
  704. it('can change an existing datetime', async () => {
  705. render(<SmartSearchBar {...defaultProps} query="" />);
  706. const textbox = screen.getByRole('textbox');
  707. fireEvent.change(textbox, {
  708. target: {value: 'lastSeen:2022-01-02 firstSeen:2022-01-01'},
  709. });
  710. // Move cursor to the lastSeen date
  711. userEvent.click(textbox);
  712. textbox.setSelectionRange(10, 10);
  713. fireEvent.focus(textbox);
  714. const dateInput = await screen.findByTestId('date-picker');
  715. expect(dateInput).toHaveValue('2022-01-02');
  716. expect(screen.getByLabelText('Time')).toHaveValue('00:00:00');
  717. expect(screen.getByLabelText('Use UTC')).toBeChecked();
  718. fireEvent.change(dateInput, {target: {value: '2022-01-03'}});
  719. expect(textbox).toHaveValue('lastSeen:2022-01-03T00:00:00 firstSeen:2022-01-01');
  720. // Cursor should be at end of the value we just replaced
  721. expect(textbox.selectionStart).toBe('lastSeen:2022-01-03T00:00:00'.length);
  722. });
  723. it('populates the date picker correctly for date without time', async () => {
  724. render(<SmartSearchBar {...defaultProps} query="lastSeen:2022-01-01" />);
  725. const textbox = screen.getByRole('textbox');
  726. // Move cursor to the timestamp
  727. userEvent.click(textbox);
  728. textbox.setSelectionRange(10, 10);
  729. fireEvent.focus(textbox);
  730. userEvent.click(screen.getByRole('textbox'));
  731. const dateInput = await screen.findByTestId('date-picker');
  732. expect(dateInput).toHaveValue('2022-01-01');
  733. // No time provided, so time input should be the default value
  734. expect(screen.getByLabelText('Time')).toHaveValue('00:00:00');
  735. // UTC is checked because there is no timezone
  736. expect(screen.getByLabelText('Use UTC')).toBeChecked();
  737. });
  738. it('populates the date picker correctly for date with time and no timezone', async () => {
  739. render(<SmartSearchBar {...defaultProps} query="lastSeen:2022-01-01T09:45:12" />);
  740. const textbox = screen.getByRole('textbox');
  741. // Move cursor to the timestamp
  742. userEvent.click(textbox);
  743. textbox.setSelectionRange(10, 10);
  744. fireEvent.focus(textbox);
  745. const dateInput = await screen.findByTestId('date-picker');
  746. expect(dateInput).toHaveValue('2022-01-01');
  747. expect(screen.getByLabelText('Time')).toHaveValue('09:45:12');
  748. expect(screen.getByLabelText('Use UTC')).toBeChecked();
  749. });
  750. it('populates the date picker correctly for date with time and timezone', async () => {
  751. render(
  752. <SmartSearchBar {...defaultProps} query="lastSeen:2022-01-01T09:45:12-05:00" />
  753. );
  754. const textbox = screen.getByRole('textbox');
  755. // Move cursor to the timestamp
  756. userEvent.click(textbox);
  757. textbox.setSelectionRange(10, 10);
  758. fireEvent.focus(textbox);
  759. const dateInput = await screen.findByTestId('date-picker');
  760. expect(dateInput).toHaveValue('2022-01-01');
  761. expect(screen.getByLabelText('Time')).toHaveValue('09:45:12');
  762. expect(screen.getByLabelText('Use UTC')).not.toBeChecked();
  763. });
  764. });
  765. describe('custom performance metric filters', () => {
  766. it('raises Invalid file size when parsed filter unit is not a valid size unit', () => {
  767. render(
  768. <SmartSearchBar
  769. {...defaultProps}
  770. customPerformanceMetrics={{
  771. 'measurements.custom.kibibyte': {
  772. fieldType: 'size',
  773. },
  774. }}
  775. />
  776. );
  777. const textbox = screen.getByRole('textbox');
  778. userEvent.click(textbox);
  779. userEvent.type(textbox, 'measurements.custom.kibibyte:10ms ');
  780. userEvent.keyboard('{arrowleft}');
  781. expect(
  782. screen.getByText(
  783. 'Invalid file size. Expected number followed by file size unit suffix'
  784. )
  785. ).toBeInTheDocument();
  786. });
  787. it('raises Invalid duration when parsed filter unit is not a valid duration unit', () => {
  788. render(
  789. <SmartSearchBar
  790. {...defaultProps}
  791. customPerformanceMetrics={{
  792. 'measurements.custom.minute': {
  793. fieldType: 'duration',
  794. },
  795. }}
  796. />
  797. );
  798. const textbox = screen.getByRole('textbox');
  799. userEvent.click(textbox);
  800. userEvent.type(textbox, 'measurements.custom.minute:10kb ');
  801. userEvent.keyboard('{arrowleft}');
  802. expect(
  803. screen.getByText(
  804. 'Invalid duration. Expected number followed by duration unit suffix'
  805. )
  806. ).toBeInTheDocument();
  807. });
  808. });
  809. });