create.spec.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. import {browserHistory} from 'react-router';
  2. import selectEvent from 'react-select-event';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {mockRouterPush} from 'sentry-test/mockRouterPush';
  5. import {fireEvent, mountWithTheme, waitFor} from 'sentry-test/reactTestingLibrary';
  6. import * as memberActionCreators from 'app/actionCreators/members';
  7. import ProjectsStore from 'app/stores/projectsStore';
  8. import {metric, trackAnalyticsEvent} from 'app/utils/analytics';
  9. import AlertsContainer from 'app/views/alerts';
  10. import AlertBuilderProjectProvider from 'app/views/alerts/builder/projectProvider';
  11. import ProjectAlertsCreate from 'app/views/alerts/create';
  12. jest.unmock('app/utils/recreateRoute');
  13. jest.mock('react-router');
  14. jest.mock('app/utils/analytics', () => ({
  15. metric: {
  16. startTransaction: jest.fn(() => ({
  17. setTag: jest.fn(),
  18. setData: jest.fn(),
  19. })),
  20. endTransaction: jest.fn(),
  21. mark: jest.fn(),
  22. measure: jest.fn(),
  23. },
  24. trackAnalyticsEvent: jest.fn(),
  25. }));
  26. describe('ProjectAlertsCreate', function () {
  27. const projectAlertRuleDetailsRoutes = [
  28. {
  29. path: '/organizations/:orgId/alerts/',
  30. name: 'Organization Alerts',
  31. indexRoute: {},
  32. childRoutes: [
  33. {
  34. path: 'rules/',
  35. name: 'Rules',
  36. childRoutes: [
  37. {
  38. name: 'Project',
  39. path: ':projectId/',
  40. childRoutes: [
  41. {
  42. name: 'New Alert Rule',
  43. path: 'new/',
  44. },
  45. {
  46. name: 'Edit Alert Rule',
  47. path: ':ruleId/',
  48. },
  49. ],
  50. },
  51. ],
  52. },
  53. {
  54. path: 'metric-rules',
  55. name: 'Metric Rules',
  56. childRoutes: [
  57. {
  58. name: 'Project',
  59. path: ':projectId/',
  60. childRoutes: [
  61. {
  62. name: 'New Alert Rule',
  63. path: 'new/',
  64. },
  65. {
  66. name: 'Edit Alert Rule',
  67. path: ':ruleId/',
  68. },
  69. ],
  70. },
  71. ],
  72. },
  73. ],
  74. },
  75. {
  76. name: 'Project',
  77. path: ':projectId/',
  78. },
  79. {
  80. name: 'New Alert Rule',
  81. path: 'new/',
  82. },
  83. ];
  84. beforeEach(function () {
  85. memberActionCreators.fetchOrgMembers = jest.fn();
  86. MockApiClient.addMockResponse({
  87. url: '/projects/org-slug/project-slug/rules/configuration/',
  88. body: TestStubs.ProjectAlertRuleConfiguration(),
  89. });
  90. MockApiClient.addMockResponse({
  91. url: '/projects/org-slug/project-slug/rules/1/',
  92. body: TestStubs.ProjectAlertRule(),
  93. });
  94. MockApiClient.addMockResponse({
  95. url: '/projects/org-slug/project-slug/environments/',
  96. body: TestStubs.Environments(),
  97. });
  98. MockApiClient.addMockResponse({
  99. url: '/organizations/org-slug/users/',
  100. body: [TestStubs.User()],
  101. });
  102. metric.startTransaction.mockClear();
  103. });
  104. afterEach(function () {
  105. MockApiClient.clearMockResponses();
  106. trackAnalyticsEvent.mockClear();
  107. });
  108. const createWrapper = (props = {}, location = {}) => {
  109. const {organization, project, routerContext, router} = initializeOrg(props);
  110. ProjectsStore.loadInitialData([project]);
  111. const params = {orgId: organization.slug, projectId: project.slug};
  112. const wrapper = mountWithTheme(
  113. <AlertsContainer organization={organization} params={params}>
  114. <AlertBuilderProjectProvider params={params}>
  115. <ProjectAlertsCreate
  116. params={params}
  117. location={{
  118. pathname: `/organizations/org-slug/alerts/rules/${project.slug}/new/`,
  119. query: {createFromWizard: true},
  120. ...location,
  121. }}
  122. routes={projectAlertRuleDetailsRoutes}
  123. router={router}
  124. />
  125. </AlertBuilderProjectProvider>
  126. </AlertsContainer>,
  127. {context: routerContext}
  128. );
  129. mockRouterPush(wrapper, router);
  130. return {
  131. wrapper,
  132. organization,
  133. project,
  134. router,
  135. };
  136. };
  137. it('redirects to wizard', async function () {
  138. const location = {query: {}};
  139. createWrapper(undefined, location);
  140. await waitFor(() => {
  141. expect(browserHistory.replace).toHaveBeenCalledWith(
  142. '/organizations/org-slug/alerts/project-slug/wizard'
  143. );
  144. });
  145. });
  146. describe('Issue Alert', function () {
  147. it('loads default values', async function () {
  148. const {
  149. wrapper: {getByDisplayValue},
  150. } = createWrapper();
  151. await waitFor(() => {
  152. expect(getByDisplayValue('__all_environments__')).toBeInTheDocument();
  153. });
  154. expect(getByDisplayValue('all')).toBeInTheDocument();
  155. expect(getByDisplayValue('30')).toBeInTheDocument();
  156. });
  157. it('can remove filters, conditions and actions', async function () {
  158. const {
  159. wrapper: {getByLabelText, getByPlaceholderText, getByText},
  160. } = createWrapper({
  161. organization: {
  162. features: ['alert-filters'],
  163. },
  164. });
  165. const mock = MockApiClient.addMockResponse({
  166. url: '/projects/org-slug/project-slug/rules/',
  167. method: 'POST',
  168. body: TestStubs.ProjectAlertRule(),
  169. });
  170. await waitFor(() => {
  171. expect(memberActionCreators.fetchOrgMembers).toHaveBeenCalled();
  172. });
  173. // Change name of alert rule
  174. fireEvent.change(getByPlaceholderText('My Rule Name'), {
  175. target: {value: 'My Rule Name'},
  176. });
  177. // Add a condition and remove it
  178. await selectEvent.select(getByText('Add optional condition...'), [
  179. 'A new issue is created',
  180. ]);
  181. fireEvent.click(getByLabelText('Delete Node'));
  182. expect(trackAnalyticsEvent).toHaveBeenCalledWith({
  183. eventKey: 'edit_alert_rule.add_row',
  184. eventName: 'Edit Alert Rule: Add Row',
  185. name: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  186. organization_id: '3',
  187. project_id: '2',
  188. type: 'conditions',
  189. });
  190. // Add a filter and remove it
  191. await selectEvent.select(getByText('Add optional filter...'), [
  192. 'The issue is {comparison_type} than {value} {time}',
  193. ]);
  194. fireEvent.click(getByLabelText('Delete Node'));
  195. // Add an action and remove it
  196. await selectEvent.select(getByText('Add action...'), [
  197. 'Send a notification (for all legacy integrations)',
  198. ]);
  199. fireEvent.click(getByLabelText('Delete Node'));
  200. fireEvent.click(getByText('Save Rule'));
  201. await waitFor(() => {
  202. expect(mock).toHaveBeenCalledWith(
  203. expect.any(String),
  204. expect.objectContaining({
  205. data: {
  206. actionMatch: 'all',
  207. actions: [],
  208. conditions: [],
  209. filterMatch: 'all',
  210. filters: [],
  211. frequency: 30,
  212. name: 'My Rule Name',
  213. owner: null,
  214. },
  215. })
  216. );
  217. });
  218. });
  219. it('updates values and saves', async function () {
  220. const {
  221. wrapper: {getAllByText, getByPlaceholderText, getByText},
  222. router,
  223. } = createWrapper({
  224. organization: {
  225. features: ['alert-filters'],
  226. },
  227. });
  228. const mock = MockApiClient.addMockResponse({
  229. url: '/projects/org-slug/project-slug/rules/',
  230. method: 'POST',
  231. body: TestStubs.ProjectAlertRule(),
  232. });
  233. await waitFor(() => {
  234. expect(memberActionCreators.fetchOrgMembers).toHaveBeenCalled();
  235. });
  236. // Change target environment
  237. await selectEvent.select(getByText('All Environments'), ['production']);
  238. // Change actionMatch and filterMatch dropdown
  239. await selectEvent.select(getAllByText('all')[0], ['any']);
  240. await selectEvent.select(getAllByText('all')[0], ['any']);
  241. // Change name of alert rule
  242. fireEvent.change(getByPlaceholderText('My Rule Name'), {
  243. target: {value: 'My Rule Name'},
  244. });
  245. // Add another condition
  246. await selectEvent.select(getByText('Add optional condition...'), [
  247. "An event's tags match {key} {match} {value}",
  248. ]);
  249. // Edit new Condition
  250. fireEvent.change(getByPlaceholderText('key'), {
  251. target: {value: 'conditionKey'},
  252. });
  253. fireEvent.change(getByPlaceholderText('value'), {
  254. target: {value: 'conditionValue'},
  255. });
  256. await selectEvent.select(getByText('equals'), ['does not equal']);
  257. // Add a new filter
  258. await selectEvent.select(getByText('Add optional filter...'), [
  259. 'The issue is {comparison_type} than {value} {time}',
  260. ]);
  261. fireEvent.change(getByPlaceholderText('10'), {
  262. target: {value: '12'},
  263. });
  264. // Add a new action
  265. await selectEvent.select(getByText('Add action...'), [
  266. 'Send a notification via {service}',
  267. ]);
  268. // Update action interval
  269. await selectEvent.select(getByText('30 minutes'), ['60 minutes']);
  270. fireEvent.click(getByText('Save Rule'));
  271. await waitFor(() => {
  272. expect(mock).toHaveBeenCalledWith(
  273. expect.any(String),
  274. expect.objectContaining({
  275. data: {
  276. actionMatch: 'any',
  277. filterMatch: 'any',
  278. actions: [
  279. {
  280. id: 'sentry.rules.actions.notify_event_service.NotifyEventServiceAction',
  281. service: 'mail',
  282. },
  283. ],
  284. conditions: [
  285. {
  286. id: 'sentry.rules.conditions.tagged_event.TaggedEventCondition',
  287. key: 'conditionKey',
  288. match: 'ne',
  289. value: 'conditionValue',
  290. },
  291. ],
  292. filters: [
  293. {
  294. id: 'sentry.rules.filters.age_comparison.AgeComparisonFilter',
  295. comparison_type: 'older',
  296. time: 'minute',
  297. value: '12',
  298. },
  299. ],
  300. environment: 'production',
  301. frequency: '60',
  302. name: 'My Rule Name',
  303. owner: null,
  304. },
  305. })
  306. );
  307. });
  308. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  309. expect(router.push).toHaveBeenCalledWith('/organizations/org-slug/alerts/rules/');
  310. });
  311. });
  312. });