issueRuleEditor.spec.jsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. import {browserHistory} from 'react-router';
  2. import selectEvent from 'react-select-event';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {
  5. render,
  6. renderGlobalModal,
  7. screen,
  8. userEvent,
  9. waitFor,
  10. } from 'sentry-test/reactTestingLibrary';
  11. import {
  12. addErrorMessage,
  13. addLoadingMessage,
  14. addSuccessMessage,
  15. } from 'sentry/actionCreators/indicator';
  16. import {updateOnboardingTask} from 'sentry/actionCreators/onboardingTasks';
  17. import {metric} from 'sentry/utils/analytics';
  18. import IssueRuleEditor from 'sentry/views/alerts/rules/issue';
  19. import ProjectAlerts from 'sentry/views/settings/projectAlerts';
  20. jest.unmock('sentry/utils/recreateRoute');
  21. jest.mock('sentry/actionCreators/onboardingTasks');
  22. jest.mock('sentry/actionCreators/indicator', () => ({
  23. addSuccessMessage: jest.fn(),
  24. addErrorMessage: jest.fn(),
  25. addLoadingMessage: jest.fn(),
  26. }));
  27. jest.mock('sentry/utils/analytics', () => ({
  28. metric: {
  29. startTransaction: jest.fn(() => ({
  30. setTag: jest.fn(),
  31. setData: jest.fn(),
  32. })),
  33. endTransaction: jest.fn(),
  34. mark: jest.fn(),
  35. measure: jest.fn(),
  36. },
  37. }));
  38. const projectAlertRuleDetailsRoutes = [
  39. {
  40. path: '/',
  41. },
  42. {
  43. path: '/settings/',
  44. name: 'Settings',
  45. indexRoute: {},
  46. },
  47. {
  48. name: 'Organization',
  49. path: ':orgId/',
  50. },
  51. {
  52. name: 'Project',
  53. path: 'projects/:projectId/',
  54. },
  55. {},
  56. {
  57. indexRoute: {name: 'General'},
  58. },
  59. {
  60. name: 'Alert Rules',
  61. path: 'alerts/',
  62. indexRoute: {},
  63. },
  64. {
  65. path: 'rules/',
  66. name: 'Rules',
  67. component: null,
  68. indexRoute: {},
  69. childRoutes: [
  70. {path: 'new/', name: 'New'},
  71. {path: ':ruleId/', name: 'Edit'},
  72. ],
  73. },
  74. {path: ':ruleId/', name: 'Edit Alert Rule'},
  75. ];
  76. const createWrapper = (props = {}) => {
  77. const {organization, project, routerContext, router} = initializeOrg(props);
  78. const params = {
  79. orgId: organization.slug,
  80. projectId: project.slug,
  81. ruleId: router.location.query.createFromDuplicate ? undefined : '1',
  82. };
  83. const onChangeTitleMock = jest.fn();
  84. const wrapper = render(
  85. <ProjectAlerts organization={organization} params={params}>
  86. <IssueRuleEditor
  87. params={params}
  88. location={router.location}
  89. routes={projectAlertRuleDetailsRoutes}
  90. router={router}
  91. onChangeTitle={onChangeTitleMock}
  92. project={project}
  93. userTeamIds={[]}
  94. />
  95. </ProjectAlerts>,
  96. {context: routerContext}
  97. );
  98. return {
  99. wrapper,
  100. organization,
  101. project,
  102. onChangeTitleMock,
  103. };
  104. };
  105. describe('ProjectAlerts -> IssueRuleEditor', function () {
  106. beforeEach(function () {
  107. browserHistory.replace = jest.fn();
  108. MockApiClient.addMockResponse({
  109. url: '/projects/org-slug/project-slug/rules/configuration/',
  110. body: TestStubs.ProjectAlertRuleConfiguration(),
  111. });
  112. MockApiClient.addMockResponse({
  113. url: '/projects/org-slug/project-slug/rules/1/',
  114. body: TestStubs.ProjectAlertRule(),
  115. });
  116. MockApiClient.addMockResponse({
  117. url: '/projects/org-slug/project-slug/environments/',
  118. body: TestStubs.Environments(),
  119. });
  120. MockApiClient.addMockResponse({
  121. url: `/projects/org-slug/project-slug/?expand=hasAlertIntegration`,
  122. body: {},
  123. });
  124. });
  125. afterEach(function () {
  126. MockApiClient.clearMockResponses();
  127. jest.clearAllMocks();
  128. });
  129. describe('Edit Rule', function () {
  130. let mock;
  131. const endpoint = '/projects/org-slug/project-slug/rules/1/';
  132. beforeEach(function () {
  133. mock = MockApiClient.addMockResponse({
  134. url: endpoint,
  135. method: 'PUT',
  136. body: TestStubs.ProjectAlertRule(),
  137. });
  138. });
  139. it('gets correct rule name', function () {
  140. const rule = TestStubs.ProjectAlertRule();
  141. mock = MockApiClient.addMockResponse({
  142. url: endpoint,
  143. method: 'GET',
  144. body: rule,
  145. });
  146. const {onChangeTitleMock} = createWrapper();
  147. expect(mock).toHaveBeenCalled();
  148. expect(onChangeTitleMock).toHaveBeenCalledWith(rule.name);
  149. });
  150. it('deletes rule', async function () {
  151. const deleteMock = MockApiClient.addMockResponse({
  152. url: endpoint,
  153. method: 'DELETE',
  154. body: {},
  155. });
  156. createWrapper();
  157. renderGlobalModal();
  158. userEvent.click(screen.getByLabelText('Delete Rule'));
  159. expect(
  160. await screen.findByText('Are you sure you want to delete this rule?')
  161. ).toBeInTheDocument();
  162. userEvent.click(screen.getByTestId('confirm-button'));
  163. await waitFor(() => expect(deleteMock).toHaveBeenCalled());
  164. expect(browserHistory.replace).toHaveBeenCalledWith(
  165. '/settings/org-slug/projects/project-slug/alerts/'
  166. );
  167. });
  168. it('sends correct environment value', async function () {
  169. createWrapper();
  170. await selectEvent.select(screen.getByText('staging'), 'production');
  171. userEvent.click(screen.getByText('Save Rule'));
  172. await waitFor(() =>
  173. expect(mock).toHaveBeenCalledWith(
  174. endpoint,
  175. expect.objectContaining({
  176. data: expect.objectContaining({environment: 'production'}),
  177. })
  178. )
  179. );
  180. expect(metric.startTransaction).toHaveBeenCalledTimes(1);
  181. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  182. });
  183. it('strips environment value if "All environments" is selected', async function () {
  184. createWrapper();
  185. await selectEvent.select(screen.getByText('staging'), 'All Environments');
  186. userEvent.click(screen.getByText('Save Rule'));
  187. await waitFor(() => expect(mock).toHaveBeenCalledTimes(1));
  188. expect(mock).not.toHaveBeenCalledWith(
  189. endpoint,
  190. expect.objectContaining({
  191. data: expect.objectContaining({environment: '__all_environments__'}),
  192. })
  193. );
  194. expect(metric.startTransaction).toHaveBeenCalledTimes(1);
  195. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  196. });
  197. it('updates the alert onboarding task', async function () {
  198. createWrapper();
  199. userEvent.click(screen.getByText('Save Rule'));
  200. await waitFor(() => expect(updateOnboardingTask).toHaveBeenCalledTimes(1));
  201. expect(metric.startTransaction).toHaveBeenCalledTimes(1);
  202. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  203. });
  204. });
  205. describe('Edit Rule: Slack Channel Look Up', function () {
  206. const uuid = 'xxxx-xxxx-xxxx';
  207. beforeEach(function () {
  208. jest.useFakeTimers();
  209. });
  210. afterEach(function () {
  211. jest.clearAllTimers();
  212. MockApiClient.clearMockResponses();
  213. });
  214. it('success status updates the rule', async function () {
  215. const mockSuccess = MockApiClient.addMockResponse({
  216. url: `/projects/org-slug/project-slug/rule-task/${uuid}/`,
  217. body: {status: 'success', rule: TestStubs.ProjectAlertRule({name: 'Slack Rule'})},
  218. });
  219. MockApiClient.addMockResponse({
  220. url: '/projects/org-slug/project-slug/rules/1/',
  221. method: 'PUT',
  222. statusCode: 202,
  223. body: {uuid},
  224. });
  225. createWrapper();
  226. userEvent.click(screen.getByText('Save Rule'));
  227. await waitFor(() => expect(addLoadingMessage).toHaveBeenCalledTimes(2));
  228. jest.advanceTimersByTime(1000);
  229. await waitFor(() => expect(mockSuccess).toHaveBeenCalledTimes(1));
  230. jest.advanceTimersByTime(1000);
  231. await waitFor(() => expect(addSuccessMessage).toHaveBeenCalledTimes(1));
  232. expect(screen.getByDisplayValue('Slack Rule')).toBeInTheDocument();
  233. });
  234. it('pending status keeps loading true', async function () {
  235. const pollingMock = MockApiClient.addMockResponse({
  236. url: `/projects/org-slug/project-slug/rule-task/${uuid}/`,
  237. body: {status: 'pending'},
  238. });
  239. MockApiClient.addMockResponse({
  240. url: '/projects/org-slug/project-slug/rules/1/',
  241. method: 'PUT',
  242. statusCode: 202,
  243. body: {uuid},
  244. });
  245. createWrapper();
  246. userEvent.click(screen.getByText('Save Rule'));
  247. await waitFor(() => expect(addLoadingMessage).toHaveBeenCalledTimes(2));
  248. jest.advanceTimersByTime(1000);
  249. await waitFor(() => expect(pollingMock).toHaveBeenCalledTimes(1));
  250. expect(screen.getByTestId('loading-mask')).toBeInTheDocument();
  251. });
  252. it('failed status renders error message', async function () {
  253. const mockFailed = MockApiClient.addMockResponse({
  254. url: `/projects/org-slug/project-slug/rule-task/${uuid}/`,
  255. body: {status: 'failed'},
  256. });
  257. MockApiClient.addMockResponse({
  258. url: '/projects/org-slug/project-slug/rules/1/',
  259. method: 'PUT',
  260. statusCode: 202,
  261. body: {uuid},
  262. });
  263. createWrapper();
  264. userEvent.click(screen.getByText('Save Rule'));
  265. await waitFor(() => expect(addLoadingMessage).toHaveBeenCalledTimes(2));
  266. jest.advanceTimersByTime(1000);
  267. await waitFor(() => expect(mockFailed).toHaveBeenCalledTimes(1));
  268. expect(screen.getByText('An error occurred')).toBeInTheDocument();
  269. expect(addErrorMessage).toHaveBeenCalledTimes(1);
  270. });
  271. });
  272. describe('Duplicate Rule', function () {
  273. let mock;
  274. const rule = TestStubs.ProjectAlertRule();
  275. const endpoint = `/projects/org-slug/project-slug/rules/${rule.id}/`;
  276. beforeEach(function () {
  277. mock = MockApiClient.addMockResponse({
  278. url: endpoint,
  279. method: 'GET',
  280. body: rule,
  281. });
  282. });
  283. it('gets correct rule to duplicate and renders fields correctly', function () {
  284. createWrapper({
  285. organization: {
  286. access: ['alerts:write'],
  287. features: ['alert-wizard-v3', 'duplicate-alert-rule'],
  288. },
  289. router: {
  290. location: {
  291. query: {
  292. createFromDuplicate: true,
  293. duplicateRuleId: `${rule.id}`,
  294. },
  295. },
  296. },
  297. });
  298. expect(mock).toHaveBeenCalled();
  299. expect(screen.getByTestId('alert-name')).toHaveValue(`${rule.name} copy`);
  300. });
  301. });
  302. });