arithmeticInput.spec.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  2. import type {Column} from 'sentry/utils/discover/fields';
  3. import {generateFieldAsString} from 'sentry/utils/discover/fields';
  4. import ArithmeticInput from 'sentry/views/discover/table/arithmeticInput';
  5. describe('ArithmeticInput', function () {
  6. const operators = ['+', '-', '*', '/', '(', ')'];
  7. const numericColumns: Column[] = [
  8. {kind: 'field', field: 'transaction.duration'},
  9. {kind: 'field', field: 'measurements.lcp'},
  10. {kind: 'field', field: 'spans.http'},
  11. {kind: 'function', function: ['p50', '', undefined, undefined]},
  12. {
  13. kind: 'function',
  14. function: ['percentile', 'transaction.duration', '0.25', undefined],
  15. },
  16. {kind: 'function', function: ['count', '', undefined, undefined]},
  17. ];
  18. const columns: Column[] = [
  19. ...numericColumns,
  20. // these columns will not be rendered in the dropdown
  21. {kind: 'function', function: ['any', 'transaction.duration', undefined, undefined]},
  22. {kind: 'field', field: 'transaction'},
  23. {kind: 'function', function: ['failure_rate', '', undefined, undefined]},
  24. {kind: 'equation', field: 'transaction.duration+measurements.lcp'},
  25. ];
  26. it('can toggle autocomplete dropdown on focus and blur', async function () {
  27. render(
  28. <ArithmeticInput
  29. name="refinement"
  30. key="parameter:text"
  31. type="text"
  32. required
  33. value=""
  34. onUpdate={jest.fn()}
  35. options={columns}
  36. />
  37. );
  38. expect(screen.queryByText('Fields')).not.toBeInTheDocument();
  39. // focus the input
  40. await userEvent.click(screen.getByRole('textbox'));
  41. expect(screen.getByText('Fields')).toBeInTheDocument();
  42. // moves focus away from the input
  43. await userEvent.tab();
  44. expect(screen.queryByText('Fields')).not.toBeInTheDocument();
  45. });
  46. it('renders only numeric options in autocomplete', async function () {
  47. render(
  48. <ArithmeticInput
  49. name="refinement"
  50. key="parameter:text"
  51. type="text"
  52. required
  53. value=""
  54. onUpdate={jest.fn()}
  55. options={columns}
  56. />
  57. );
  58. // focus the input
  59. await userEvent.click(screen.getByRole('textbox'));
  60. const listItems = screen.getAllByRole('listitem');
  61. // options + headers that are inside listitem
  62. expect(listItems).toHaveLength(numericColumns.length + operators.length + 2);
  63. const options = listItems.filter(
  64. item => item.textContent !== 'Fields' && item.textContent !== 'Operators'
  65. );
  66. options.forEach((option, i) => {
  67. if (i < numericColumns.length) {
  68. expect(option).toHaveTextContent(generateFieldAsString(numericColumns[i]));
  69. return;
  70. }
  71. expect(option).toHaveTextContent(operators[i - numericColumns.length]);
  72. });
  73. });
  74. it('can use keyboard to select an option', async function () {
  75. render(
  76. <ArithmeticInput
  77. name="refinement"
  78. key="parameter:text"
  79. type="text"
  80. required
  81. value=""
  82. onUpdate={jest.fn()}
  83. options={columns}
  84. />
  85. );
  86. // focus the input
  87. await userEvent.click(screen.getByRole('textbox'));
  88. for (const column of numericColumns) {
  89. await userEvent.keyboard('{ArrowDown}');
  90. expect(
  91. screen.getByRole('listitem', {name: generateFieldAsString(column)})
  92. ).toHaveClass('active', {exact: false});
  93. }
  94. for (const operator of operators) {
  95. await userEvent.keyboard('{ArrowDown}');
  96. expect(screen.getByRole('listitem', {name: operator})).toHaveClass('active', {
  97. exact: false,
  98. });
  99. }
  100. // wrap around to the first option again
  101. await userEvent.keyboard('{ArrowDown}');
  102. for (const operator of [...operators].reverse()) {
  103. await userEvent.keyboard('{ArrowUp}');
  104. expect(screen.getByRole('listitem', {name: operator})).toHaveClass('active', {
  105. exact: false,
  106. });
  107. }
  108. for (const column of [...numericColumns].reverse()) {
  109. await userEvent.keyboard('{ArrowUp}');
  110. expect(
  111. screen.getByRole('listitem', {name: generateFieldAsString(column)})
  112. ).toHaveClass('active', {
  113. exact: false,
  114. });
  115. }
  116. // the update is buffered until blur happens
  117. await userEvent.keyboard('{Enter}');
  118. await userEvent.keyboard('{Escape}');
  119. expect(screen.getByRole('textbox')).toHaveValue(
  120. `${generateFieldAsString(numericColumns[0])} `
  121. );
  122. });
  123. it('can use mouse to select an option', async function () {
  124. render(
  125. <ArithmeticInput
  126. name="refinement"
  127. key="parameter:text"
  128. type="text"
  129. required
  130. value=""
  131. onUpdate={jest.fn()}
  132. options={columns}
  133. />
  134. );
  135. await userEvent.click(screen.getByRole('textbox'));
  136. await userEvent.click(screen.getByText(generateFieldAsString(numericColumns[2])));
  137. expect(screen.getByRole('textbox')).toHaveValue(
  138. `${generateFieldAsString(numericColumns[2])} `
  139. );
  140. });
  141. it('autocompletes the current term when it is in the front', async function () {
  142. render(
  143. <ArithmeticInput
  144. name="refinement"
  145. key="parameter:text"
  146. type="text"
  147. required
  148. value=""
  149. onUpdate={jest.fn()}
  150. options={columns}
  151. />
  152. );
  153. const element = screen.getByRole('textbox') as HTMLInputElement;
  154. await userEvent.type(element, 'lcp + transaction.duration');
  155. await userEvent.type(element, '{ArrowLeft>24}');
  156. await userEvent.click(screen.getByText('measurements.lcp'));
  157. expect(screen.getByRole('textbox')).toHaveValue(
  158. 'measurements.lcp + transaction.duration'
  159. );
  160. });
  161. it('autocompletes the current term when it is in the end', async function () {
  162. render(
  163. <ArithmeticInput
  164. name="refinement"
  165. key="parameter:text"
  166. type="text"
  167. required
  168. value=""
  169. onUpdate={jest.fn()}
  170. options={columns}
  171. />
  172. );
  173. await userEvent.type(screen.getByRole('textbox'), 'transaction.duration + lcp');
  174. await userEvent.click(screen.getByText('measurements.lcp'));
  175. expect(screen.getByRole('textbox')).toHaveValue(
  176. 'transaction.duration + measurements.lcp '
  177. );
  178. });
  179. it('handles autocomplete on invalid term', async function () {
  180. render(
  181. <ArithmeticInput
  182. name="refinement"
  183. key="parameter:text"
  184. type="text"
  185. required
  186. value=""
  187. onUpdate={jest.fn()}
  188. options={columns}
  189. />
  190. );
  191. // focus the input
  192. await userEvent.type(screen.getByRole('textbox'), 'foo + bar');
  193. await userEvent.keyboard('{keydown}');
  194. expect(screen.getAllByText('No items found')).toHaveLength(2);
  195. });
  196. it('can hide Fields options', async function () {
  197. render(
  198. <ArithmeticInput
  199. name="refinement"
  200. type="text"
  201. required
  202. value=""
  203. onUpdate={() => {}}
  204. options={[]}
  205. hideFieldOptions
  206. />
  207. );
  208. // focus the input
  209. await userEvent.click(screen.getByRole('textbox'));
  210. expect(screen.getByText('Operators')).toBeInTheDocument();
  211. expect(screen.queryByText('Fields')).not.toBeInTheDocument();
  212. });
  213. });