ruleNode.spec.tsx 9.1 KB

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