arithmeticInput.spec.tsx 6.8 KB

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