ruleNode.spec.tsx 9.0 KB

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