create.spec.jsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  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 ProjectsStore from 'sentry/stores/projectsStore';
  5. import TeamStore from 'sentry/stores/teamStore';
  6. import {metric} from 'sentry/utils/analytics';
  7. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  8. import AlertsContainer from 'sentry/views/alerts';
  9. import AlertBuilderProjectProvider from 'sentry/views/alerts/builder/projectProvider';
  10. import ProjectAlertsCreate from 'sentry/views/alerts/create';
  11. jest.unmock('sentry/utils/recreateRoute');
  12. jest.mock('sentry/actionCreators/members');
  13. jest.mock('react-router');
  14. jest.mock('sentry/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. trackAdvancedAnalyticsEvent: jest.fn(),
  25. }));
  26. jest.mock('sentry/utils/analytics/trackAdvancedAnalyticsEvent');
  27. describe('ProjectAlertsCreate', function () {
  28. beforeEach(function () {
  29. TeamStore.init();
  30. TeamStore.loadInitialData([], false, null);
  31. MockApiClient.addMockResponse({
  32. url: '/projects/org-slug/project-slug/rules/configuration/',
  33. body: TestStubs.ProjectAlertRuleConfiguration(),
  34. });
  35. MockApiClient.addMockResponse({
  36. url: '/projects/org-slug/project-slug/rules/1/',
  37. body: TestStubs.ProjectAlertRule(),
  38. });
  39. MockApiClient.addMockResponse({
  40. url: '/projects/org-slug/project-slug/environments/',
  41. body: TestStubs.Environments(),
  42. });
  43. MockApiClient.addMockResponse({
  44. url: `/projects/org-slug/project-slug/?expand=hasAlertIntegration`,
  45. body: {},
  46. });
  47. MockApiClient.addMockResponse({
  48. url: `/projects/org-slug/project-slug/ownership/`,
  49. method: 'GET',
  50. body: {
  51. fallthrough: false,
  52. autoAssignment: false,
  53. },
  54. });
  55. });
  56. afterEach(function () {
  57. MockApiClient.clearMockResponses();
  58. jest.clearAllMocks();
  59. });
  60. const createWrapper = (props = {}, location = {}) => {
  61. const {organization, project, router} = initializeOrg(props);
  62. ProjectsStore.loadInitialData([project]);
  63. const params = {orgId: organization.slug, projectId: project.slug};
  64. const wrapper = render(
  65. <AlertsContainer>
  66. <AlertBuilderProjectProvider params={params}>
  67. <ProjectAlertsCreate
  68. params={params}
  69. location={{
  70. pathname: `/organizations/org-slug/alerts/rules/${project.slug}/new/`,
  71. query: {createFromWizard: true},
  72. ...location,
  73. }}
  74. router={router}
  75. />
  76. </AlertBuilderProjectProvider>
  77. </AlertsContainer>,
  78. {organization}
  79. );
  80. return {
  81. wrapper,
  82. organization,
  83. project,
  84. router,
  85. };
  86. };
  87. it('adds default parameters if wizard was skipped', function () {
  88. const location = {query: {}};
  89. const wrapper = createWrapper(undefined, location);
  90. expect(wrapper.router.replace).toHaveBeenCalledWith({
  91. pathname: '/organizations/org-slug/alerts/new/metric',
  92. query: {
  93. aggregate: 'count()',
  94. dataset: 'events',
  95. eventTypes: 'error',
  96. project: 'project-slug',
  97. },
  98. });
  99. });
  100. describe('Issue Alert', function () {
  101. it('loads default values', function () {
  102. createWrapper();
  103. expect(screen.getByText('All Environments')).toBeInTheDocument();
  104. expect(screen.getAllByDisplayValue('all')).toHaveLength(2);
  105. expect(screen.getByText('30 minutes')).toBeInTheDocument();
  106. });
  107. it('can remove filters', async function () {
  108. createWrapper();
  109. const mock = MockApiClient.addMockResponse({
  110. url: '/projects/org-slug/project-slug/rules/',
  111. method: 'POST',
  112. body: TestStubs.ProjectAlertRule(),
  113. });
  114. // Change name of alert rule
  115. userEvent.paste(screen.getByPlaceholderText('Enter Alert Name'), 'My Rule Name');
  116. // Add a filter and remove it
  117. await selectEvent.select(screen.getByText('Add optional filter...'), [
  118. 'The issue is older or newer than...',
  119. ]);
  120. userEvent.click(screen.getByLabelText('Delete Node'));
  121. userEvent.click(screen.getByText('Save Rule'));
  122. expect(mock).toHaveBeenCalledWith(
  123. expect.any(String),
  124. expect.objectContaining({
  125. data: {
  126. actionMatch: 'all',
  127. actions: [],
  128. conditions: [],
  129. filterMatch: 'all',
  130. filters: [],
  131. frequency: 30,
  132. name: 'My Rule Name',
  133. owner: null,
  134. },
  135. })
  136. );
  137. // updateOnboardingTask triggers an out of band state update
  138. await act(tick);
  139. });
  140. it('can remove triggers', async function () {
  141. const {organization} = createWrapper();
  142. const mock = MockApiClient.addMockResponse({
  143. url: '/projects/org-slug/project-slug/rules/',
  144. method: 'POST',
  145. body: TestStubs.ProjectAlertRule(),
  146. });
  147. // Change name of alert rule
  148. userEvent.paste(screen.getByPlaceholderText('Enter Alert Name'), 'My Rule Name');
  149. // Add a trigger and remove it
  150. await selectEvent.select(screen.getByText('Add optional trigger...'), [
  151. 'A new issue is created',
  152. ]);
  153. userEvent.click(screen.getByLabelText('Delete Node'));
  154. expect(trackAdvancedAnalyticsEvent).toHaveBeenCalledWith(
  155. 'edit_alert_rule.add_row',
  156. {
  157. name: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  158. organization,
  159. project_id: '2',
  160. type: 'conditions',
  161. }
  162. );
  163. userEvent.click(screen.getByText('Save Rule'));
  164. expect(mock).toHaveBeenCalledWith(
  165. expect.any(String),
  166. expect.objectContaining({
  167. data: {
  168. actionMatch: 'all',
  169. actions: [],
  170. conditions: [],
  171. filterMatch: 'all',
  172. filters: [],
  173. frequency: 30,
  174. name: 'My Rule Name',
  175. owner: null,
  176. },
  177. })
  178. );
  179. // updateOnboardingTask triggers an out of band state update
  180. await act(tick);
  181. });
  182. it('can remove actions', async function () {
  183. createWrapper();
  184. const mock = MockApiClient.addMockResponse({
  185. url: '/projects/org-slug/project-slug/rules/',
  186. method: 'POST',
  187. body: TestStubs.ProjectAlertRule(),
  188. });
  189. // Change name of alert rule
  190. userEvent.paste(screen.getByPlaceholderText('Enter Alert Name'), 'My Rule Name');
  191. // Add an action and remove it
  192. await selectEvent.select(screen.getByText('Add action...'), [
  193. 'Send a notification to all legacy integrations',
  194. ]);
  195. userEvent.click(screen.getByLabelText('Delete Node'));
  196. userEvent.click(screen.getByText('Save Rule'));
  197. expect(mock).toHaveBeenCalledWith(
  198. expect.any(String),
  199. expect.objectContaining({
  200. data: {
  201. actionMatch: 'all',
  202. actions: [],
  203. conditions: [],
  204. filterMatch: 'all',
  205. filters: [],
  206. frequency: 30,
  207. name: 'My Rule Name',
  208. owner: null,
  209. },
  210. })
  211. );
  212. // updateOnboardingTask triggers an out of band state update
  213. await act(tick);
  214. });
  215. describe('updates and saves', function () {
  216. let mock;
  217. beforeEach(function () {
  218. mock = MockApiClient.addMockResponse({
  219. url: '/projects/org-slug/project-slug/rules/',
  220. method: 'POST',
  221. body: TestStubs.ProjectAlertRule(),
  222. });
  223. });
  224. afterEach(function () {
  225. jest.clearAllMocks();
  226. });
  227. it('environment, action and filter match', async function () {
  228. const wrapper = createWrapper();
  229. // Change target environment
  230. await selectEvent.select(screen.getByText('All Environments'), ['production']);
  231. // Change actionMatch and filterMatch dropdown
  232. const allDropdowns = screen.getAllByText('all');
  233. expect(allDropdowns).toHaveLength(2);
  234. await selectEvent.select(allDropdowns[0], ['any']);
  235. await selectEvent.select(allDropdowns[1], ['any']);
  236. // Change name of alert rule
  237. userEvent.paste(screen.getByPlaceholderText('Enter Alert Name'), 'My Rule Name');
  238. userEvent.click(screen.getByText('Save Rule'));
  239. expect(mock).toHaveBeenCalledWith(
  240. expect.any(String),
  241. expect.objectContaining({
  242. data: {
  243. actionMatch: 'any',
  244. filterMatch: 'any',
  245. conditions: [],
  246. actions: [],
  247. filters: [],
  248. environment: 'production',
  249. frequency: 30,
  250. name: 'My Rule Name',
  251. owner: null,
  252. },
  253. })
  254. );
  255. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  256. await waitFor(() => {
  257. expect(wrapper.router.push).toHaveBeenCalledWith({
  258. pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
  259. });
  260. });
  261. });
  262. it('new condition', async function () {
  263. const wrapper = createWrapper();
  264. // Change name of alert rule
  265. userEvent.paste(screen.getByPlaceholderText('Enter Alert Name'), 'My Rule Name');
  266. // Add another condition
  267. await selectEvent.select(screen.getByText('Add optional filter...'), [
  268. "The event's tags match {key} {match} {value}",
  269. ]);
  270. // Edit new Condition
  271. userEvent.paste(screen.getByPlaceholderText('key'), 'conditionKey');
  272. userEvent.paste(screen.getByPlaceholderText('value'), 'conditionValue');
  273. await selectEvent.select(screen.getByText('contains'), ['does not equal']);
  274. userEvent.click(screen.getByText('Save Rule'));
  275. expect(mock).toHaveBeenCalledWith(
  276. expect.any(String),
  277. expect.objectContaining({
  278. data: {
  279. actionMatch: 'all',
  280. actions: [],
  281. conditions: [],
  282. filterMatch: 'all',
  283. filters: [
  284. {
  285. id: 'sentry.rules.filters.tagged_event.TaggedEventFilter',
  286. key: 'conditionKey',
  287. match: 'ne',
  288. value: 'conditionValue',
  289. },
  290. ],
  291. frequency: 30,
  292. name: 'My Rule Name',
  293. owner: null,
  294. },
  295. })
  296. );
  297. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  298. await waitFor(() => {
  299. expect(wrapper.router.push).toHaveBeenCalledWith({
  300. pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
  301. });
  302. });
  303. });
  304. it('new filter', async function () {
  305. const wrapper = createWrapper();
  306. // Change name of alert rule
  307. userEvent.paste(screen.getByPlaceholderText('Enter Alert Name'), 'My Rule Name');
  308. // Add a new filter
  309. await selectEvent.select(screen.getByText('Add optional filter...'), [
  310. 'The issue is older or newer than...',
  311. ]);
  312. userEvent.paste(screen.getByPlaceholderText('10'), '12');
  313. userEvent.click(screen.getByText('Save Rule'));
  314. expect(mock).toHaveBeenCalledWith(
  315. expect.any(String),
  316. expect.objectContaining({
  317. data: {
  318. actionMatch: 'all',
  319. filterMatch: 'all',
  320. filters: [
  321. {
  322. id: 'sentry.rules.filters.age_comparison.AgeComparisonFilter',
  323. comparison_type: 'older',
  324. time: 'minute',
  325. value: '12',
  326. },
  327. ],
  328. actions: [],
  329. conditions: [],
  330. frequency: 30,
  331. name: 'My Rule Name',
  332. owner: null,
  333. },
  334. })
  335. );
  336. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  337. await waitFor(() => {
  338. expect(wrapper.router.push).toHaveBeenCalledWith({
  339. pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
  340. });
  341. });
  342. });
  343. it('new action', async function () {
  344. const wrapper = createWrapper();
  345. // Change name of alert rule
  346. userEvent.paste(screen.getByPlaceholderText('Enter Alert Name'), 'My Rule Name');
  347. // Add a new action
  348. await selectEvent.select(screen.getByText('Add action...'), [
  349. 'Issue Owners, Team, or Member',
  350. ]);
  351. // Update action interval
  352. await selectEvent.select(screen.getByText('30 minutes'), ['60 minutes']);
  353. userEvent.click(screen.getByText('Save Rule'));
  354. expect(mock).toHaveBeenCalledWith(
  355. expect.any(String),
  356. expect.objectContaining({
  357. data: {
  358. actionMatch: 'all',
  359. actions: [
  360. {id: 'sentry.mail.actions.NotifyEmailAction', targetType: 'IssueOwners'},
  361. ],
  362. conditions: [],
  363. filterMatch: 'all',
  364. filters: [],
  365. frequency: '60',
  366. name: 'My Rule Name',
  367. owner: null,
  368. },
  369. })
  370. );
  371. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  372. await waitFor(() => {
  373. expect(wrapper.router.push).toHaveBeenCalledWith({
  374. pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
  375. });
  376. });
  377. });
  378. });
  379. });
  380. });