index.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. import type {ComponentProps} from 'react';
  2. import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
  3. import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder';
  4. import type {TagCollection} from 'sentry/types';
  5. import {FieldKey, FieldKind} from 'sentry/utils/fields';
  6. const MOCK_SUPPORTED_KEYS: TagCollection = {
  7. [FieldKey.AGE]: {key: FieldKey.AGE, name: 'Age', kind: FieldKind.FIELD},
  8. [FieldKey.ASSIGNED]: {
  9. key: FieldKey.ASSIGNED,
  10. name: 'Assigned To',
  11. kind: FieldKind.FIELD,
  12. predefined: true,
  13. values: ['me', 'unassigned', 'person@sentry.io'],
  14. },
  15. [FieldKey.BROWSER_NAME]: {
  16. key: FieldKey.BROWSER_NAME,
  17. name: 'Browser Name',
  18. kind: FieldKind.FIELD,
  19. predefined: true,
  20. values: ['Chrome', 'Firefox', 'Safari', 'Edge'],
  21. },
  22. custom_tag_name: {key: 'custom_tag_name', name: 'Custom_Tag_Name', kind: FieldKind.TAG},
  23. };
  24. describe('SearchQueryBuilder', function () {
  25. afterEach(function () {
  26. jest.restoreAllMocks();
  27. });
  28. const defaultProps: ComponentProps<typeof SearchQueryBuilder> = {
  29. getTagValues: jest.fn(),
  30. initialQuery: '',
  31. supportedKeys: MOCK_SUPPORTED_KEYS,
  32. label: 'Query Builder',
  33. };
  34. describe('mouse interactions', function () {
  35. it('can remove a token by clicking the delete button', async function () {
  36. render(
  37. <SearchQueryBuilder
  38. {...defaultProps}
  39. initialQuery="browser.name:firefox custom_tag_name:123"
  40. />
  41. );
  42. expect(screen.getByRole('row', {name: 'browser.name:firefox'})).toBeInTheDocument();
  43. expect(screen.getByRole('row', {name: 'custom_tag_name:123'})).toBeInTheDocument();
  44. await userEvent.click(
  45. within(screen.getByRole('row', {name: 'browser.name:firefox'})).getByRole(
  46. 'button',
  47. {name: 'Remove filter: browser.name'}
  48. )
  49. );
  50. // Browser name token should be removed
  51. expect(
  52. screen.queryByRole('row', {name: 'browser.name:firefox'})
  53. ).not.toBeInTheDocument();
  54. // Custom tag token should still be present
  55. expect(screen.getByRole('row', {name: 'custom_tag_name:123'})).toBeInTheDocument();
  56. });
  57. it('can modify the operator by clicking into it', async function () {
  58. render(
  59. <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
  60. );
  61. // Should display as "is" to start
  62. expect(
  63. within(
  64. screen.getByRole('button', {name: 'Edit operator for filter: browser.name'})
  65. ).getByText('is')
  66. ).toBeInTheDocument();
  67. await userEvent.click(
  68. screen.getByRole('button', {name: 'Edit operator for filter: browser.name'})
  69. );
  70. await userEvent.click(screen.getByRole('menuitemradio', {name: 'is not'}));
  71. // Token should be modified to be negated
  72. expect(
  73. screen.getByRole('row', {name: '!browser.name:firefox'})
  74. ).toBeInTheDocument();
  75. // Should now have "is not" label
  76. expect(
  77. within(
  78. screen.getByRole('button', {name: 'Edit operator for filter: browser.name'})
  79. ).getByText('is not')
  80. ).toBeInTheDocument();
  81. });
  82. it('can modify the value by clicking into it', async function () {
  83. render(
  84. <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
  85. );
  86. // Should display as "firefox" to start
  87. expect(
  88. within(
  89. screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
  90. ).getByText('firefox')
  91. ).toBeInTheDocument();
  92. await userEvent.click(
  93. screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
  94. );
  95. // Should have placeholder text of previous value
  96. expect(screen.getByRole('combobox', {name: 'Edit filter value'})).toHaveAttribute(
  97. 'placeholder',
  98. 'firefox'
  99. );
  100. await userEvent.click(screen.getByRole('combobox', {name: 'Edit filter value'}));
  101. // Clicking the "Chrome option should update the value"
  102. await userEvent.click(screen.getByRole('option', {name: 'Chrome'}));
  103. expect(screen.getByRole('row', {name: 'browser.name:Chrome'})).toBeInTheDocument();
  104. expect(
  105. within(
  106. screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
  107. ).getByText('Chrome')
  108. ).toBeInTheDocument();
  109. });
  110. it('escapes values with spaces and reserved characters', async function () {
  111. render(
  112. <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
  113. );
  114. await userEvent.click(
  115. screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
  116. );
  117. await userEvent.keyboard('some" value{enter}');
  118. // Value should be surrounded by quotes and escaped
  119. expect(
  120. screen.getByRole('row', {name: 'browser.name:"some\\" value"'})
  121. ).toBeInTheDocument();
  122. // Display text should be display the original value
  123. expect(
  124. within(
  125. screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
  126. ).getByText('some" value')
  127. ).toBeInTheDocument();
  128. });
  129. });
  130. describe('new search tokens', function () {
  131. it('can add a new token by clicking a key suggestion', async function () {
  132. render(<SearchQueryBuilder {...defaultProps} />);
  133. await userEvent.click(screen.getByRole('combobox', {name: 'Add a search term'}));
  134. await userEvent.click(screen.getByRole('option', {name: 'Browser Name'}));
  135. // New token should be added with the correct key
  136. expect(screen.getByRole('row', {name: 'browser.name:'})).toBeInTheDocument();
  137. await userEvent.click(screen.getByRole('combobox', {name: 'Edit filter value'}));
  138. await userEvent.click(screen.getByRole('option', {name: 'Firefox'}));
  139. // New token should have a value
  140. expect(screen.getByRole('row', {name: 'browser.name:Firefox'})).toBeInTheDocument();
  141. });
  142. it('can add free text by typing', async function () {
  143. render(<SearchQueryBuilder {...defaultProps} />);
  144. await userEvent.click(screen.getByRole('grid'));
  145. await userEvent.type(screen.getByRole('combobox'), 'some free text{enter}');
  146. expect(screen.getByRole('combobox')).toHaveValue('some free text');
  147. });
  148. it('can add a filter after some free text', async function () {
  149. render(<SearchQueryBuilder {...defaultProps} />);
  150. await userEvent.click(screen.getByRole('grid'));
  151. await userEvent.type(
  152. screen.getByRole('combobox'),
  153. 'some free text brow{ArrowDown}'
  154. );
  155. // XXX(malwilley): SearchQueryBuilderInput updates state in the render
  156. // function which causes an act warning despite using userEvent.click.
  157. // Cannot find a way to avoid this warning.
  158. jest.spyOn(console, 'error').mockImplementation(jest.fn());
  159. await userEvent.click(screen.getByRole('option', {name: 'Browser Name'}));
  160. jest.restoreAllMocks();
  161. // Should have a free text token "some free text"
  162. expect(screen.getByRole('row', {name: 'some free text'})).toBeInTheDocument();
  163. // Should have a filter token with key "browser.name"
  164. expect(screen.getByRole('row', {name: 'browser.name:'})).toBeInTheDocument();
  165. // Filter value should have focus
  166. expect(screen.getByRole('combobox', {name: 'Edit filter value'})).toHaveFocus();
  167. });
  168. });
  169. describe('keyboard interactions', function () {
  170. it('can remove a previous token by pressing backspace', async function () {
  171. render(
  172. <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
  173. );
  174. // Focus into search (cursor be at end of the query)
  175. await userEvent.click(screen.getByRole('grid'));
  176. // Pressing backspace once should focus the previous token
  177. await userEvent.keyboard('{backspace}');
  178. expect(screen.queryByRole('row', {name: 'browser.name:firefox'})).toHaveFocus();
  179. // Pressing backspace again should remove the token
  180. await userEvent.keyboard('{backspace}');
  181. expect(
  182. screen.queryByRole('row', {name: 'browser.name:firefox'})
  183. ).not.toBeInTheDocument();
  184. });
  185. it('can remove a subsequent token by pressing delete', async function () {
  186. render(
  187. <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
  188. );
  189. // Put focus into the first input (before the token)
  190. await userEvent.click(
  191. screen.getAllByRole('combobox', {name: 'Add a search term'})[0]
  192. );
  193. // Pressing delete once should focus the previous token
  194. await userEvent.keyboard('{delete}');
  195. expect(screen.queryByRole('row', {name: 'browser.name:firefox'})).toHaveFocus();
  196. // Pressing delete again should remove the token
  197. await userEvent.keyboard('{delete}');
  198. expect(
  199. screen.queryByRole('row', {name: 'browser.name:firefox'})
  200. ).not.toBeInTheDocument();
  201. });
  202. it('can navigate between tokens with arrow keys', async function () {
  203. render(
  204. <SearchQueryBuilder
  205. {...defaultProps}
  206. initialQuery="browser.name:firefox abc assigned:me"
  207. />
  208. );
  209. await userEvent.click(screen.getByRole('grid'));
  210. // Focus should be in the last text input
  211. expect(
  212. screen.getAllByRole('combobox', {name: 'Add a search term'}).at(-1)
  213. ).toHaveFocus();
  214. // Left once focuses the assigned remove button
  215. await userEvent.keyboard('{arrowleft}');
  216. expect(screen.getByRole('button', {name: 'Remove filter: assigned'})).toHaveFocus();
  217. // Left again focuses the assigned filter value
  218. await userEvent.keyboard('{arrowleft}');
  219. expect(
  220. screen.getByRole('button', {name: 'Edit value for filter: assigned'})
  221. ).toHaveFocus();
  222. // Left again focuses the assigned operator
  223. await userEvent.keyboard('{arrowleft}');
  224. expect(
  225. screen.getByRole('button', {name: 'Edit operator for filter: assigned'})
  226. ).toHaveFocus();
  227. // Left again focuses the assigned key
  228. await userEvent.keyboard('{arrowleft}');
  229. expect(
  230. screen.getByRole('button', {name: 'Edit filter key: assigned'})
  231. ).toHaveFocus();
  232. // Left again goes to the next text input between tokens
  233. await userEvent.keyboard('{arrowleft}');
  234. expect(
  235. screen.getAllByRole('combobox', {name: 'Add a search term'}).at(-2)
  236. ).toHaveFocus();
  237. // 4 more lefts go through the input text "abc" and to the next token
  238. await userEvent.keyboard('{arrowleft}{arrowleft}{arrowleft}{arrowleft}');
  239. expect(
  240. screen.getByRole('button', {name: 'Remove filter: browser.name'})
  241. ).toHaveFocus();
  242. // 1 right goes back to the text input
  243. await userEvent.keyboard('{arrowright}');
  244. expect(
  245. screen.getAllByRole('combobox', {name: 'Add a search term'}).at(-2)
  246. ).toHaveFocus();
  247. });
  248. it('has a single tab stop', async function () {
  249. render(
  250. <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
  251. );
  252. expect(document.body).toHaveFocus();
  253. // Tabbing in should focus the last input
  254. await userEvent.keyboard('{Tab}');
  255. expect(
  256. screen.getAllByRole('combobox', {name: 'Add a search term'}).at(-1)
  257. ).toHaveFocus();
  258. // Shift-tabbing should exit the component
  259. await userEvent.keyboard('{Shift>}{Tab}{/Shift}');
  260. expect(document.body).toHaveFocus();
  261. });
  262. });
  263. });