ruleForm.spec.jsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import selectEvent from 'react-select-event';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  4. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  5. import {metric} from 'sentry/utils/analytics';
  6. import RuleFormContainer from 'sentry/views/alerts/rules/metric/ruleForm';
  7. jest.mock('sentry/actionCreators/indicator');
  8. jest.mock('sentry/utils/analytics', () => ({
  9. metric: {
  10. startTransaction: jest.fn(() => ({
  11. setTag: jest.fn(),
  12. setData: jest.fn(),
  13. })),
  14. endTransaction: jest.fn(),
  15. },
  16. }));
  17. describe('Incident Rules Form', () => {
  18. const {organization, project, routerContext} = initializeOrg({
  19. organization: {features: ['metric-alert-threshold-period', 'change-alerts']},
  20. });
  21. const createWrapper = props =>
  22. render(
  23. <RuleFormContainer
  24. params={{orgId: organization.slug, projectId: project.slug}}
  25. organization={organization}
  26. project={project}
  27. {...props}
  28. />,
  29. {context: routerContext}
  30. );
  31. beforeEach(() => {
  32. MockApiClient.clearMockResponses();
  33. MockApiClient.addMockResponse({
  34. url: '/organizations/org-slug/tags/',
  35. body: [],
  36. });
  37. MockApiClient.addMockResponse({
  38. url: '/organizations/org-slug/users/',
  39. body: [],
  40. });
  41. MockApiClient.addMockResponse({
  42. url: '/projects/org-slug/project-slug/environments/',
  43. body: [],
  44. });
  45. MockApiClient.addMockResponse({
  46. url: '/organizations/org-slug/events-stats/',
  47. body: TestStubs.EventsStats(),
  48. });
  49. MockApiClient.addMockResponse({
  50. url: '/organizations/org-slug/events-meta/',
  51. body: {count: 5},
  52. });
  53. MockApiClient.addMockResponse({
  54. url: '/organizations/org-slug/alert-rules/available-actions/',
  55. body: [
  56. {
  57. allowedTargetTypes: ['user', 'team'],
  58. integrationName: null,
  59. type: 'email',
  60. integrationId: null,
  61. },
  62. ],
  63. });
  64. });
  65. describe('Creating a new rule', () => {
  66. let createRule;
  67. beforeEach(() => {
  68. createRule = MockApiClient.addMockResponse({
  69. url: '/projects/org-slug/project-slug/alert-rules/',
  70. method: 'POST',
  71. });
  72. metric.startTransaction.mockClear();
  73. });
  74. /**
  75. * Note this isn't necessarily the desired behavior, as it is just documenting the behavior
  76. */
  77. it('creates a rule', async () => {
  78. const rule = TestStubs.MetricRule();
  79. createWrapper({
  80. rule: {
  81. ...rule,
  82. id: undefined,
  83. eventTypes: ['default'],
  84. },
  85. });
  86. // Clear field
  87. userEvent.clear(screen.getByPlaceholderText('Something really bad happened'));
  88. // Enter in name so we can submit
  89. userEvent.type(
  90. screen.getByPlaceholderText('Something really bad happened'),
  91. 'Incident Rule'
  92. );
  93. // Set thresholdPeriod
  94. await selectEvent.select(screen.getAllByText('For 1 minute')[0], 'For 10 minutes');
  95. userEvent.click(screen.getByLabelText('Save Rule'));
  96. expect(createRule).toHaveBeenCalledWith(
  97. expect.anything(),
  98. expect.objectContaining({
  99. data: expect.objectContaining({
  100. name: 'Incident Rule',
  101. projects: ['project-slug'],
  102. eventTypes: ['default'],
  103. thresholdPeriod: 10,
  104. }),
  105. })
  106. );
  107. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  108. });
  109. });
  110. describe('Editing a rule', () => {
  111. let editRule;
  112. let editTrigger;
  113. const rule = TestStubs.MetricRule();
  114. beforeEach(() => {
  115. editRule = MockApiClient.addMockResponse({
  116. url: `/projects/org-slug/project-slug/alert-rules/${rule.id}/`,
  117. method: 'PUT',
  118. body: rule,
  119. });
  120. editTrigger = MockApiClient.addMockResponse({
  121. url: `/organizations/org-slug/alert-rules/${rule.id}/triggers/1/`,
  122. method: 'PUT',
  123. body: TestStubs.IncidentTrigger({id: 1}),
  124. });
  125. });
  126. afterEach(() => {
  127. editRule.mockReset();
  128. editTrigger.mockReset();
  129. });
  130. it('edits metric', () => {
  131. createWrapper({
  132. ruleId: rule.id,
  133. rule,
  134. });
  135. // Clear field
  136. userEvent.clear(screen.getByPlaceholderText('Something really bad happened'));
  137. userEvent.type(
  138. screen.getByPlaceholderText('Something really bad happened'),
  139. 'new name'
  140. );
  141. userEvent.click(screen.getByLabelText('Save Rule'));
  142. expect(editRule).toHaveBeenLastCalledWith(
  143. expect.anything(),
  144. expect.objectContaining({
  145. data: expect.objectContaining({
  146. name: 'new name',
  147. }),
  148. })
  149. );
  150. });
  151. it('switches from percent change to count', async () => {
  152. createWrapper({
  153. ruleId: rule.id,
  154. rule: {
  155. ...rule,
  156. timeWindow: 60,
  157. comparisonDelta: 100,
  158. eventTypes: ['error'],
  159. resolution: 2,
  160. },
  161. });
  162. expect(screen.getByLabelText('Select Percent Change')).toBeInTheDocument();
  163. expect(screen.getByLabelText('Select Percent Change')).toBeChecked();
  164. userEvent.click(screen.getByLabelText('Select Count'));
  165. await waitFor(() => expect(screen.getByLabelText('Select Count')).toBeChecked());
  166. userEvent.click(screen.getByLabelText('Save Rule'));
  167. expect(editRule).toHaveBeenLastCalledWith(
  168. expect.anything(),
  169. expect.objectContaining({
  170. data: expect.objectContaining({
  171. // Comparison delta is reset
  172. comparisonDelta: null,
  173. }),
  174. })
  175. );
  176. });
  177. });
  178. describe('Slack async lookup', () => {
  179. const uuid = 'xxxx-xxxx-xxxx';
  180. beforeEach(() => {
  181. jest.useFakeTimers();
  182. });
  183. afterEach(() => {
  184. jest.runOnlyPendingTimers();
  185. jest.useRealTimers();
  186. });
  187. it('success status updates the rule', async () => {
  188. const alertRule = TestStubs.MetricRule({name: 'Slack Alert Rule'});
  189. MockApiClient.addMockResponse({
  190. url: `/projects/org-slug/project-slug/alert-rules/${alertRule.id}/`,
  191. method: 'PUT',
  192. body: {uuid},
  193. statusCode: 202,
  194. });
  195. MockApiClient.addMockResponse({
  196. url: `/projects/org-slug/project-slug/alert-rule-task/${uuid}/`,
  197. body: {
  198. status: 'success',
  199. alertRule,
  200. },
  201. });
  202. const onSubmitSuccess = jest.fn();
  203. createWrapper({
  204. ruleId: alertRule.id,
  205. rule: alertRule,
  206. onSubmitSuccess,
  207. });
  208. userEvent.type(
  209. screen.getByPlaceholderText('Something really bad happened'),
  210. 'Slack Alert Rule'
  211. );
  212. userEvent.click(screen.getByLabelText('Save Rule'));
  213. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  214. act(jest.runAllTimers);
  215. await waitFor(
  216. () => {
  217. expect(onSubmitSuccess).toHaveBeenCalledWith(
  218. expect.objectContaining({
  219. id: alertRule.id,
  220. name: alertRule.name,
  221. }),
  222. expect.anything()
  223. );
  224. },
  225. {timeout: 2000, interval: 10}
  226. );
  227. });
  228. it('pending status keeps loading true', () => {
  229. const alertRule = TestStubs.MetricRule({name: 'Slack Alert Rule'});
  230. MockApiClient.addMockResponse({
  231. url: `/projects/org-slug/project-slug/alert-rules/${alertRule.id}/`,
  232. method: 'PUT',
  233. body: {uuid},
  234. statusCode: 202,
  235. });
  236. MockApiClient.addMockResponse({
  237. url: `/projects/org-slug/project-slug/alert-rule-task/${uuid}/`,
  238. body: {
  239. status: 'pending',
  240. },
  241. });
  242. const onSubmitSuccess = jest.fn();
  243. createWrapper({
  244. ruleId: alertRule.id,
  245. rule: alertRule,
  246. onSubmitSuccess,
  247. });
  248. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  249. expect(onSubmitSuccess).not.toHaveBeenCalled();
  250. });
  251. it('failed status renders error message', async () => {
  252. const alertRule = TestStubs.MetricRule({name: 'Slack Alert Rule'});
  253. MockApiClient.addMockResponse({
  254. url: `/projects/org-slug/project-slug/alert-rules/${alertRule.id}/`,
  255. method: 'PUT',
  256. body: {uuid},
  257. statusCode: 202,
  258. });
  259. MockApiClient.addMockResponse({
  260. url: `/projects/org-slug/project-slug/alert-rule-task/${uuid}/`,
  261. body: {
  262. status: 'failed',
  263. error: 'An error occurred',
  264. },
  265. });
  266. const onSubmitSuccess = jest.fn();
  267. createWrapper({
  268. ruleId: alertRule.id,
  269. rule: alertRule,
  270. onSubmitSuccess,
  271. });
  272. userEvent.type(
  273. screen.getByPlaceholderText('Something really bad happened'),
  274. 'Slack Alert Rule'
  275. );
  276. userEvent.click(screen.getByLabelText('Save Rule'));
  277. act(jest.runAllTimers);
  278. await waitFor(
  279. () => {
  280. expect(addErrorMessage).toHaveBeenCalledWith('An error occurred');
  281. },
  282. {timeout: 2000, interval: 10}
  283. );
  284. expect(onSubmitSuccess).not.toHaveBeenCalled();
  285. });
  286. });
  287. });