ruleNode.spec.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import selectEvent from 'react-select-event';
  2. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  3. import ModalStore from 'sentry/stores/modalStore';
  4. import RuleNode from 'sentry/views/alerts/rules/issue/ruleNode';
  5. describe('RuleNode', () => {
  6. const project = TestStubs.Project();
  7. const organization = TestStubs.Organization({projects: [project]});
  8. const index = 0;
  9. const onDelete = jest.fn();
  10. const onReset = jest.fn();
  11. const onPropertyChange = jest.fn();
  12. const simpleNode = {
  13. id: 'sentry.rules.simple_mock',
  14. label: '(mock) A new issue is created',
  15. enabled: true,
  16. };
  17. const formNode = label => ({
  18. label,
  19. id: 'sentry.rules.form_mock',
  20. enabled: true,
  21. formFields: {
  22. exampleStringField: {
  23. type: 'string',
  24. placeholder: 'placeholder',
  25. },
  26. exampleNumberField: {
  27. type: 'number',
  28. placeholder: 100,
  29. },
  30. exampleStringChoiceField: {
  31. type: 'choice',
  32. choices: [
  33. ['value1', 'label1'],
  34. ['value2', 'label2'],
  35. ['value3', 'label3'],
  36. ],
  37. },
  38. exampleResetStringChoiceField: {
  39. type: 'choice',
  40. choices: [
  41. ['value1', 'label1'],
  42. ['value2', 'label2'],
  43. ['value3', 'label3'],
  44. ],
  45. resetsForm: true,
  46. },
  47. exampleNumberChoiceField: {
  48. type: 'choice',
  49. initial: 2,
  50. choices: [
  51. [1, 'label1'],
  52. [2, 'label2'],
  53. [3, 'label3'],
  54. ],
  55. },
  56. exampleMailActionField: {
  57. type: 'mailAction',
  58. choices: [
  59. ['IssueOwners', 'Issue Owners'],
  60. ['Team', 'Team'],
  61. ['Member', 'Member'],
  62. ],
  63. },
  64. exampleAssigneeField: {
  65. type: 'assignee',
  66. choices: [
  67. ['Unassigned', 'Unassigned'],
  68. ['Team', 'Team'],
  69. ['Member', 'Member'],
  70. ],
  71. },
  72. },
  73. });
  74. // TODO: Add this node and test if it implements correctly (e.g. Jira Tickets)
  75. // const ticketNode = {actionType: 'ticket'};
  76. const sentryAppNode = {
  77. id: 'sentry.rules.schema_form_mock',
  78. label: 'Configure SentryApp with these',
  79. enabled: true,
  80. actionType: 'sentryapp',
  81. sentryAppInstallationUuid: '1027',
  82. formFields: {
  83. exampleStringField: {
  84. type: 'string',
  85. placeholder: 'placeholder',
  86. },
  87. exampleNumberField: {
  88. type: 'number',
  89. placeholder: 100,
  90. },
  91. exampleStringChoiceField: {
  92. type: 'choice',
  93. choices: [
  94. ['value1', 'label1'],
  95. ['value2', 'label2'],
  96. ['value3', 'label3'],
  97. ],
  98. },
  99. },
  100. };
  101. const renderRuleNode = (node, data = {}, org = organization) => {
  102. return render(
  103. <RuleNode
  104. index={index}
  105. node={node}
  106. data={{
  107. id: 'sentry.rules.mock',
  108. name: '(mock) A new issue is created',
  109. label: '(mock) A new issue is created',
  110. prompt: '',
  111. ...data,
  112. }}
  113. disabled={false}
  114. organization={org}
  115. project={project}
  116. onDelete={onDelete}
  117. onPropertyChange={onPropertyChange}
  118. onReset={onReset}
  119. />
  120. );
  121. };
  122. const labelReplacer = (label, values) => {
  123. return label.replace(/{\w+}/gm, placeholder => values[placeholder]);
  124. };
  125. afterEach(() => {
  126. jest.resetAllMocks();
  127. });
  128. it('handles being deleted', async () => {
  129. renderRuleNode(simpleNode);
  130. expect(screen.getByText(simpleNode.label)).toBeInTheDocument();
  131. expect(screen.getByRole('button', {name: 'Delete Node'})).toBeInTheDocument();
  132. await userEvent.click(screen.getByRole('button', {name: 'Delete Node'}));
  133. expect(onDelete).toHaveBeenCalledWith(index);
  134. });
  135. it('renders choice string choice fields correctly', async () => {
  136. const fieldName = 'exampleStringChoiceField';
  137. const label = `Here is a string choice field {${fieldName}}`;
  138. renderRuleNode(formNode(label));
  139. // Should render the first option if no initial is provided
  140. expect(
  141. screen.getByText('Here is a string choice field').parentElement
  142. ).toHaveTextContent(labelReplacer(label, {[`{${fieldName}}`]: 'label1'}));
  143. await selectEvent.select(screen.getByText('label1'), 'label3');
  144. expect(onPropertyChange).toHaveBeenCalledWith(index, fieldName, 'value3');
  145. });
  146. it('resets choice fields with resetsForm', async () => {
  147. const fieldName = 'exampleResetStringChoiceField';
  148. const label = `Here is a string reset choice field {${fieldName}}`;
  149. renderRuleNode(formNode(label));
  150. // Should render the first option if no initial is provided
  151. expect(screen.getByText('Here is a string reset choice field')).toBeInTheDocument();
  152. await selectEvent.select(screen.getByText('label1'), 'label3');
  153. expect(onReset).toHaveBeenCalledWith(index, fieldName, 'value3');
  154. });
  155. it('renders choice number choice fields correctly', async () => {
  156. const fieldName = 'exampleNumberChoiceField';
  157. const label = `Here is a number choice field {${fieldName}}`;
  158. renderRuleNode(formNode(label));
  159. // Should render the initial value if one is provided
  160. expect(
  161. screen.getByText('Here is a number choice field').parentElement
  162. ).toHaveTextContent(labelReplacer(label, {[`{${fieldName}}`]: 'label2'}));
  163. selectEvent.openMenu(screen.getByText('label2'));
  164. await userEvent.click(screen.getByText('label3'));
  165. expect(onPropertyChange).toHaveBeenCalledWith(index, fieldName, '3');
  166. });
  167. it('renders number fields correctly', async () => {
  168. const fieldName = 'exampleNumberField';
  169. const label = `Here is a number field {${fieldName}}`;
  170. renderRuleNode(formNode(label));
  171. expect(screen.getByText('Here is a number field')).toBeInTheDocument();
  172. expect(screen.getByPlaceholderText('100')).toBeInTheDocument();
  173. await userEvent.type(screen.getByPlaceholderText('100'), '721');
  174. await userEvent.click(document.body);
  175. expect(onPropertyChange).toHaveBeenCalledWith(index, fieldName, '721');
  176. });
  177. it('sets some number fields by default', () => {
  178. const fieldName = 'exampleNumberField';
  179. const label = `Here is a number field {${fieldName}}`;
  180. renderRuleNode(formNode(label), {
  181. id: 'sentry.rules.filters.issue_occurrences.IssueOccurrencesFilter',
  182. });
  183. expect(onPropertyChange).toHaveBeenCalledTimes(1);
  184. expect(onPropertyChange).toHaveBeenCalledWith(index, fieldName, '100');
  185. });
  186. it('renders text fields correctly', async () => {
  187. const fieldName = 'exampleStringField';
  188. const label = `Here is a text field {${fieldName}}`;
  189. renderRuleNode(formNode(label));
  190. expect(screen.getByText('Here is a text field')).toBeInTheDocument();
  191. const input = screen.getByPlaceholderText('placeholder');
  192. expect(input).toBeInTheDocument();
  193. await userEvent.click(input);
  194. await userEvent.paste('some text');
  195. expect(onPropertyChange).toHaveBeenCalledWith(index, fieldName, 'some text');
  196. });
  197. it('renders sentry apps with schema forms correctly', async () => {
  198. renderRuleNode(sentryAppNode);
  199. const openModal = jest.spyOn(ModalStore, 'openModal');
  200. expect(screen.getByText(sentryAppNode.label)).toBeInTheDocument();
  201. const settingsButton = screen.getByLabelText('Settings');
  202. expect(settingsButton).toBeInTheDocument();
  203. await userEvent.click(settingsButton);
  204. expect(openModal).toHaveBeenCalled();
  205. });
  206. it('renders mail action field', async () => {
  207. const fieldName = 'exampleMailActionField';
  208. const label = `Send a notification to {${fieldName}}`;
  209. renderRuleNode(formNode(label), {targetType: 'IssueOwners'});
  210. expect(screen.getByText('Send a notification to')).toBeInTheDocument();
  211. await selectEvent.select(screen.getByText('Issue Owners'), 'Team');
  212. expect(onPropertyChange).toHaveBeenCalledTimes(2);
  213. expect(onPropertyChange).toHaveBeenCalledWith(index, 'targetType', 'Team');
  214. expect(onPropertyChange).toHaveBeenCalledWith(index, 'targetIdentifier', '');
  215. });
  216. it('renders mail action field with suggested assignees', async () => {
  217. const fieldName = 'exampleMailActionField';
  218. const label = `Send a notification to {${fieldName}}`;
  219. const organizationWithFeat = {
  220. ...organization,
  221. features: ['streamline-targeting-context'],
  222. };
  223. renderRuleNode(formNode(label), {targetType: 'IssueOwners'}, organizationWithFeat);
  224. expect(screen.getByText('Send a notification to')).toBeInTheDocument();
  225. await selectEvent.select(screen.getByText('Suggested Assignees'), 'Team');
  226. });
  227. it('renders assignee field', async () => {
  228. const fieldName = 'exampleAssigneeField';
  229. const label = `The issue is assigned to {${fieldName}}`;
  230. renderRuleNode(formNode(label), {targetType: 'Unassigned'});
  231. expect(screen.getByText('The issue is assigned to')).toBeInTheDocument();
  232. await selectEvent.select(screen.getByText('No One'), 'Team');
  233. expect(onPropertyChange).toHaveBeenCalledTimes(2);
  234. expect(onPropertyChange).toHaveBeenCalledWith(index, 'targetType', 'Team');
  235. expect(onPropertyChange).toHaveBeenCalledWith(index, 'targetIdentifier', '');
  236. });
  237. });