create.spec.jsx 11 KB

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