arithmeticInput.spec.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  2. import {Column, generateFieldAsString} from 'sentry/utils/discover/fields';
  3. import ArithmeticInput from 'sentry/views/discover/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', async 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. await userEvent.click(screen.getByRole('textbox'));
  40. expect(screen.getByText('Fields')).toBeInTheDocument();
  41. // moves focus away from the input
  42. await userEvent.tab();
  43. expect(screen.queryByText('Fields')).not.toBeInTheDocument();
  44. });
  45. it('renders only numeric options in autocomplete', async 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. await 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', async 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. await userEvent.click(screen.getByRole('textbox'));
  87. for (const column of numericColumns) {
  88. await 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. await 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. await userEvent.keyboard('{ArrowDown}');
  101. for (const operator of [...operators].reverse()) {
  102. await 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. await 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. await userEvent.keyboard('{Enter}');
  117. await userEvent.keyboard('{Escape}');
  118. expect(screen.getByRole('textbox')).toHaveValue(
  119. `${generateFieldAsString(numericColumns[0])} `
  120. );
  121. });
  122. it('can use mouse to select an option', async 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. await userEvent.click(screen.getByRole('textbox'));
  135. await 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', async 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. await userEvent.type(element, 'lcp + transaction.duration');
  154. await userEvent.type(element, '{ArrowLeft>24}');
  155. await userEvent.click(screen.getByText('measurements.lcp'));
  156. expect(screen.getByRole('textbox')).toHaveValue(
  157. 'measurements.lcp + transaction.duration'
  158. );
  159. });
  160. it('autocompletes the current term when it is in the end', async function () {
  161. render(
  162. <ArithmeticInput
  163. name="refinement"
  164. key="parameter:text"
  165. type="text"
  166. required
  167. value=""
  168. onUpdate={jest.fn()}
  169. options={columns}
  170. />
  171. );
  172. await userEvent.type(screen.getByRole('textbox'), 'transaction.duration + lcp');
  173. await userEvent.click(screen.getByText('measurements.lcp'));
  174. expect(screen.getByRole('textbox')).toHaveValue(
  175. 'transaction.duration + measurements.lcp '
  176. );
  177. });
  178. it('handles autocomplete on invalid term', async function () {
  179. render(
  180. <ArithmeticInput
  181. name="refinement"
  182. key="parameter:text"
  183. type="text"
  184. required
  185. value=""
  186. onUpdate={jest.fn()}
  187. options={columns}
  188. />
  189. );
  190. // focus the input
  191. await userEvent.type(screen.getByRole('textbox'), 'foo + bar');
  192. await userEvent.keyboard('{keydown}');
  193. expect(screen.getAllByText('No items found')).toHaveLength(2);
  194. });
  195. it('can hide Fields options', async function () {
  196. render(
  197. <ArithmeticInput
  198. name="refinement"
  199. type="text"
  200. required
  201. value=""
  202. onUpdate={() => {}}
  203. options={[]}
  204. hideFieldOptions
  205. />
  206. );
  207. // focus the input
  208. await userEvent.click(screen.getByRole('textbox'));
  209. expect(screen.getByText('Operators')).toBeInTheDocument();
  210. expect(screen.queryByText('Fields')).not.toBeInTheDocument();
  211. });
  212. });