ruleForm.spec.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. import {EventsStatsFixture} from 'sentry-fixture/events';
  2. import {IncidentTriggerFixture} from 'sentry-fixture/incidentTrigger';
  3. import {MetricRuleFixture} from 'sentry-fixture/metricRule';
  4. import {initializeOrg} from 'sentry-test/initializeOrg';
  5. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  6. import selectEvent from 'sentry-test/selectEvent';
  7. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  8. import type FormModel from 'sentry/components/forms/model';
  9. import ProjectsStore from 'sentry/stores/projectsStore';
  10. import {ActivationConditionType, MonitorType} from 'sentry/types/alerts';
  11. import {metric} from 'sentry/utils/analytics';
  12. import RuleFormContainer from 'sentry/views/alerts/rules/metric/ruleForm';
  13. import {
  14. AlertRuleComparisonType,
  15. AlertRuleSeasonality,
  16. AlertRuleSensitivity,
  17. Dataset,
  18. } from 'sentry/views/alerts/rules/metric/types';
  19. import {permissionAlertText} from 'sentry/views/settings/project/permissionAlert';
  20. jest.mock('sentry/actionCreators/indicator');
  21. jest.mock('sentry/utils/analytics', () => ({
  22. metric: {
  23. startSpan: jest.fn(() => ({
  24. setTag: jest.fn(),
  25. setData: jest.fn(),
  26. })),
  27. endSpan: jest.fn(),
  28. },
  29. }));
  30. describe('Incident Rules Form', () => {
  31. let organization, project, router, location, anomalies;
  32. // create wrapper
  33. const createWrapper = props =>
  34. render(
  35. <RuleFormContainer
  36. params={{orgId: organization.slug, projectId: project.slug}}
  37. organization={organization}
  38. location={location}
  39. project={project}
  40. {...props}
  41. />,
  42. {router, organization}
  43. );
  44. beforeEach(() => {
  45. const initialData = initializeOrg({
  46. organization: {features: ['metric-alert-threshold-period', 'change-alerts']},
  47. });
  48. organization = initialData.organization;
  49. project = initialData.project;
  50. location = initialData.router.location;
  51. ProjectsStore.loadInitialData([project]);
  52. router = initialData.router;
  53. MockApiClient.addMockResponse({
  54. url: '/organizations/org-slug/tags/',
  55. body: [],
  56. });
  57. MockApiClient.addMockResponse({
  58. url: '/organizations/org-slug/users/',
  59. body: [],
  60. });
  61. MockApiClient.addMockResponse({
  62. url: '/projects/org-slug/project-slug/environments/',
  63. body: [],
  64. });
  65. MockApiClient.addMockResponse({
  66. url: '/organizations/org-slug/events-stats/',
  67. body: EventsStatsFixture({
  68. isMetricsData: true,
  69. }),
  70. });
  71. MockApiClient.addMockResponse({
  72. url: '/organizations/org-slug/events-meta/',
  73. body: {count: 5},
  74. });
  75. MockApiClient.addMockResponse({
  76. url: '/organizations/org-slug/alert-rules/available-actions/',
  77. body: [
  78. {
  79. allowedTargetTypes: ['user', 'team'],
  80. integrationName: null,
  81. type: 'email',
  82. integrationId: null,
  83. },
  84. ],
  85. });
  86. MockApiClient.addMockResponse({
  87. url: '/organizations/org-slug/metrics-estimation-stats/',
  88. body: EventsStatsFixture(),
  89. });
  90. MockApiClient.addMockResponse({
  91. url: '/organizations/org-slug/metrics/meta/',
  92. body: [],
  93. });
  94. MockApiClient.addMockResponse({
  95. url: '/organizations/org-slug/metrics/tags/',
  96. body: [],
  97. });
  98. MockApiClient.addMockResponse({
  99. method: 'GET',
  100. url: '/organizations/org-slug/recent-searches/',
  101. body: [],
  102. });
  103. anomalies = MockApiClient.addMockResponse({
  104. method: 'POST',
  105. url: '/organizations/org-slug/events/anomalies/',
  106. body: [],
  107. });
  108. });
  109. afterEach(() => {
  110. MockApiClient.clearMockResponses();
  111. jest.clearAllMocks();
  112. });
  113. describe('Viewing the rule', () => {
  114. const rule = MetricRuleFixture();
  115. it('is enabled without org-level alerts:write', async () => {
  116. organization.access = [];
  117. project.access = [];
  118. createWrapper({rule});
  119. expect(await screen.findByText(permissionAlertText)).toBeInTheDocument();
  120. expect(screen.queryByLabelText('Save Rule')).toBeDisabled();
  121. });
  122. it('is enabled with org-level alerts:write', async () => {
  123. organization.access = ['alerts:write'];
  124. project.access = [];
  125. createWrapper({rule});
  126. expect(await screen.findByLabelText('Save Rule')).toBeEnabled();
  127. expect(screen.queryByText(permissionAlertText)).not.toBeInTheDocument();
  128. });
  129. it('is enabled with project-level alerts:write', async () => {
  130. organization.access = [];
  131. project.access = ['alerts:write'];
  132. createWrapper({rule});
  133. expect(await screen.findByLabelText('Save Rule')).toBeEnabled();
  134. expect(screen.queryByText(permissionAlertText)).not.toBeInTheDocument();
  135. });
  136. it('renders time window', async () => {
  137. createWrapper({rule});
  138. expect(await screen.findByText('1 hour interval')).toBeInTheDocument();
  139. });
  140. it('renders time window for activated alerts', async () => {
  141. createWrapper({
  142. rule: {
  143. ...rule,
  144. monitorType: MonitorType.CONTINUOUS,
  145. },
  146. });
  147. expect(await screen.findByText('1 hour interval')).toBeInTheDocument();
  148. });
  149. });
  150. describe('Creating a new rule', () => {
  151. let createRule;
  152. beforeEach(() => {
  153. ProjectsStore.loadInitialData([
  154. project,
  155. {
  156. ...project,
  157. id: '10',
  158. slug: 'project-slug-2',
  159. },
  160. ]);
  161. createRule = MockApiClient.addMockResponse({
  162. url: '/organizations/org-slug/alert-rules/',
  163. method: 'POST',
  164. });
  165. MockApiClient.addMockResponse({
  166. url: '/projects/org-slug/project-slug-2/environments/',
  167. body: [],
  168. });
  169. });
  170. /**
  171. * Note this isn't necessarily the desired behavior, as it is just documenting the behavior
  172. */
  173. it('creates a rule', async () => {
  174. const rule = MetricRuleFixture();
  175. createWrapper({
  176. rule: {
  177. ...rule,
  178. id: undefined,
  179. eventTypes: ['default'],
  180. },
  181. });
  182. // Clear field
  183. await userEvent.clear(screen.getByPlaceholderText('Enter Alert Name'));
  184. // Enter in name so we can submit
  185. await userEvent.type(
  186. screen.getByPlaceholderText('Enter Alert Name'),
  187. 'Incident Rule'
  188. );
  189. // Set thresholdPeriod
  190. await selectEvent.select(screen.getAllByText('For 1 minute')[0], 'For 10 minutes');
  191. await userEvent.click(screen.getByLabelText('Save Rule'));
  192. expect(createRule).toHaveBeenCalledWith(
  193. expect.anything(),
  194. expect.objectContaining({
  195. data: expect.objectContaining({
  196. name: 'Incident Rule',
  197. projects: ['project-slug'],
  198. eventTypes: ['default'],
  199. thresholdPeriod: 10,
  200. }),
  201. })
  202. );
  203. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  204. });
  205. it('can create a rule for a different project', async () => {
  206. const rule = MetricRuleFixture();
  207. createWrapper({
  208. rule: {
  209. ...rule,
  210. id: undefined,
  211. eventTypes: ['default'],
  212. },
  213. });
  214. // Clear field
  215. await userEvent.clear(screen.getByPlaceholderText('Enter Alert Name'));
  216. // Enter in name so we can submit
  217. await userEvent.type(
  218. screen.getByPlaceholderText('Enter Alert Name'),
  219. 'Incident Rule'
  220. );
  221. // Change project
  222. await userEvent.click(screen.getByText('project-slug'));
  223. await userEvent.click(screen.getByText('project-slug-2'));
  224. await userEvent.click(screen.getByLabelText('Save Rule'));
  225. expect(createRule).toHaveBeenCalledWith(
  226. expect.anything(),
  227. expect.objectContaining({
  228. data: expect.objectContaining({
  229. name: 'Incident Rule',
  230. projects: ['project-slug-2'],
  231. }),
  232. })
  233. );
  234. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  235. });
  236. it('creates a rule with generic_metrics dataset', async () => {
  237. organization.features = [...organization.features, 'mep-rollout-flag'];
  238. const rule = MetricRuleFixture();
  239. createWrapper({
  240. rule: {
  241. ...rule,
  242. id: undefined,
  243. aggregate: 'count()',
  244. eventTypes: ['transaction'],
  245. dataset: 'transactions',
  246. },
  247. });
  248. expect(await screen.findByTestId('alert-total-events')).toHaveTextContent('Total5');
  249. await userEvent.click(screen.getByLabelText('Save Rule'));
  250. expect(createRule).toHaveBeenCalledWith(
  251. expect.anything(),
  252. expect.objectContaining({
  253. data: expect.objectContaining({
  254. name: 'My Incident Rule',
  255. projects: ['project-slug'],
  256. aggregate: 'count()',
  257. eventTypes: ['transaction'],
  258. dataset: 'generic_metrics',
  259. thresholdPeriod: 1,
  260. }),
  261. })
  262. );
  263. });
  264. // Activation condition
  265. it('creates a rule with an activation condition', async () => {
  266. organization.features = [
  267. ...organization.features,
  268. 'mep-rollout-flag',
  269. 'activated-alert-rules',
  270. ];
  271. const rule = MetricRuleFixture({
  272. monitorType: MonitorType.ACTIVATED,
  273. activationCondition: ActivationConditionType.RELEASE_CREATION,
  274. });
  275. createWrapper({
  276. rule: {
  277. ...rule,
  278. id: undefined,
  279. aggregate: 'count()',
  280. eventTypes: ['transaction'],
  281. dataset: 'transactions',
  282. },
  283. });
  284. expect(await screen.findByTestId('alert-total-events')).toHaveTextContent('Total5');
  285. await userEvent.click(screen.getByLabelText('Save Rule'));
  286. expect(createRule).toHaveBeenCalledWith(
  287. expect.anything(),
  288. expect.objectContaining({
  289. data: expect.objectContaining({
  290. name: 'My Incident Rule',
  291. projects: ['project-slug'],
  292. aggregate: 'count()',
  293. eventTypes: ['transaction'],
  294. dataset: 'generic_metrics',
  295. thresholdPeriod: 1,
  296. }),
  297. })
  298. );
  299. });
  300. it('creates a continuous rule with activated rules enabled', async () => {
  301. organization.features = [
  302. ...organization.features,
  303. 'mep-rollout-flag',
  304. 'activated-alert-rules',
  305. ];
  306. const rule = MetricRuleFixture({
  307. monitorType: MonitorType.CONTINUOUS,
  308. });
  309. createWrapper({
  310. rule: {
  311. ...rule,
  312. id: undefined,
  313. aggregate: 'count()',
  314. eventTypes: ['transaction'],
  315. dataset: 'transactions',
  316. },
  317. });
  318. expect(await screen.findByTestId('alert-total-events')).toHaveTextContent('Total5');
  319. await userEvent.click(screen.getByLabelText('Save Rule'));
  320. expect(createRule).toHaveBeenCalledWith(
  321. expect.anything(),
  322. expect.objectContaining({
  323. data: expect.objectContaining({
  324. name: 'My Incident Rule',
  325. projects: ['project-slug'],
  326. aggregate: 'count()',
  327. eventTypes: ['transaction'],
  328. dataset: 'generic_metrics',
  329. thresholdPeriod: 1,
  330. }),
  331. })
  332. );
  333. });
  334. it('creates an anomaly detection rule', async () => {
  335. organization.features = [
  336. ...organization.features,
  337. 'anomaly-detection-alerts',
  338. 'anomaly-detection-rollout',
  339. ];
  340. const rule = MetricRuleFixture({
  341. sensitivity: AlertRuleSensitivity.MEDIUM,
  342. seasonality: AlertRuleSeasonality.AUTO,
  343. });
  344. createWrapper({
  345. rule: {
  346. ...rule,
  347. id: undefined,
  348. aggregate: 'count()',
  349. eventTypes: ['error'],
  350. dataset: 'events',
  351. },
  352. });
  353. expect(
  354. await screen.findByRole('textbox', {name: 'Level of responsiveness'})
  355. ).toBeInTheDocument();
  356. expect(anomalies).toHaveBeenLastCalledWith(
  357. expect.anything(),
  358. expect.objectContaining({
  359. data: expect.objectContaining({
  360. config: {
  361. direction: 'up',
  362. sensitivity: AlertRuleSensitivity.MEDIUM,
  363. expected_seasonality: AlertRuleSeasonality.AUTO,
  364. time_period: 60,
  365. },
  366. }),
  367. })
  368. );
  369. await userEvent.click(screen.getByLabelText('Save Rule'));
  370. expect(createRule).toHaveBeenLastCalledWith(
  371. expect.anything(),
  372. expect.objectContaining({
  373. data: expect.objectContaining({
  374. aggregate: 'count()',
  375. dataset: 'events',
  376. environment: null,
  377. eventTypes: ['error'],
  378. detectionType: AlertRuleComparisonType.DYNAMIC,
  379. sensitivity: AlertRuleSensitivity.MEDIUM,
  380. seasonality: AlertRuleSeasonality.AUTO,
  381. }),
  382. })
  383. );
  384. });
  385. it('switches to custom metric and selects event.type:error', async () => {
  386. organization.features = [...organization.features, 'performance-view'];
  387. const rule = MetricRuleFixture();
  388. createWrapper({
  389. rule: {
  390. ...rule,
  391. id: undefined,
  392. eventTypes: ['default'],
  393. },
  394. });
  395. await userEvent.click(screen.getAllByText('Number of Errors').at(1)!);
  396. await userEvent.click(await screen.findByText('Custom Measurement'));
  397. await userEvent.click(screen.getAllByText('event.type:transaction').at(1)!);
  398. await userEvent.click(await screen.findByText('event.type:error'));
  399. expect(screen.getAllByText('Custom Measurement')).toHaveLength(2);
  400. await userEvent.click(screen.getByLabelText('Save Rule'));
  401. expect(createRule).toHaveBeenLastCalledWith(
  402. expect.anything(),
  403. expect.objectContaining({
  404. data: expect.objectContaining({
  405. aggregate: 'count()',
  406. alertType: 'custom_transactions',
  407. dataset: 'events',
  408. datasource: 'error',
  409. environment: null,
  410. eventTypes: ['error'],
  411. name: 'My Incident Rule',
  412. projectId: '2',
  413. projects: ['project-slug'],
  414. query: '',
  415. }),
  416. })
  417. );
  418. });
  419. it('creates an insights metric rule', async () => {
  420. const rule = MetricRuleFixture();
  421. createWrapper({
  422. rule: {
  423. ...rule,
  424. id: undefined,
  425. eventTypes: ['transaction'],
  426. aggregate: 'avg(d:spans/exclusive_time@millisecond)',
  427. dataset: Dataset.GENERIC_METRICS,
  428. },
  429. });
  430. // Clear field
  431. await userEvent.clear(screen.getByPlaceholderText('Enter Alert Name'));
  432. // Enter in name so we can submit
  433. await userEvent.type(
  434. screen.getByPlaceholderText('Enter Alert Name'),
  435. 'Insights Incident Rule'
  436. );
  437. // Set thresholdPeriod
  438. await selectEvent.select(screen.getAllByText('For 1 minute')[0], 'For 10 minutes');
  439. await userEvent.click(screen.getByLabelText('Save Rule'));
  440. expect(createRule).toHaveBeenCalledWith(
  441. expect.anything(),
  442. expect.objectContaining({
  443. data: expect.objectContaining({
  444. name: 'Insights Incident Rule',
  445. projects: ['project-slug'],
  446. eventTypes: ['transaction'],
  447. thresholdPeriod: 10,
  448. alertType: 'insights_metrics',
  449. dataset: 'generic_metrics',
  450. }),
  451. })
  452. );
  453. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  454. });
  455. it('creates an EAP metric rule', async () => {
  456. const rule = MetricRuleFixture();
  457. createWrapper({
  458. rule: {
  459. ...rule,
  460. id: undefined,
  461. eventTypes: [],
  462. aggregate: 'count(span.duration)',
  463. dataset: Dataset.EVENTS_ANALYTICS_PLATFORM,
  464. },
  465. });
  466. // Clear field
  467. await userEvent.clear(screen.getByPlaceholderText('Enter Alert Name'));
  468. // Enter in name so we can submit
  469. await userEvent.type(
  470. screen.getByPlaceholderText('Enter Alert Name'),
  471. 'EAP Incident Rule'
  472. );
  473. // Set thresholdPeriod
  474. await selectEvent.select(screen.getAllByText('For 1 minute')[0], 'For 10 minutes');
  475. await userEvent.click(screen.getByLabelText('Save Rule'));
  476. expect(createRule).toHaveBeenCalledWith(
  477. expect.anything(),
  478. expect.objectContaining({
  479. data: expect.objectContaining({
  480. name: 'EAP Incident Rule',
  481. projects: ['project-slug'],
  482. eventTypes: [],
  483. thresholdPeriod: 10,
  484. alertType: 'eap_metrics',
  485. dataset: 'events_analytics_platform',
  486. }),
  487. })
  488. );
  489. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  490. });
  491. });
  492. describe('Editing a rule', () => {
  493. let editRule;
  494. let editTrigger;
  495. const rule = MetricRuleFixture();
  496. beforeEach(() => {
  497. editRule = MockApiClient.addMockResponse({
  498. url: `/organizations/org-slug/alert-rules/${rule.id}/`,
  499. method: 'PUT',
  500. body: rule,
  501. });
  502. editTrigger = MockApiClient.addMockResponse({
  503. url: `/organizations/org-slug/alert-rules/${rule.id}/triggers/1/`,
  504. method: 'PUT',
  505. body: IncidentTriggerFixture({id: '1'}),
  506. });
  507. });
  508. afterEach(() => {
  509. editRule.mockReset();
  510. editTrigger.mockReset();
  511. });
  512. it('edits metric', async () => {
  513. createWrapper({
  514. ruleId: rule.id,
  515. rule,
  516. });
  517. // Clear field
  518. await userEvent.clear(screen.getByPlaceholderText('Enter Alert Name'));
  519. await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'new name');
  520. await userEvent.click(screen.getByLabelText('Save Rule'));
  521. expect(editRule).toHaveBeenLastCalledWith(
  522. expect.anything(),
  523. expect.objectContaining({
  524. data: expect.objectContaining({
  525. name: 'new name',
  526. }),
  527. })
  528. );
  529. });
  530. it('switches from percent change to count', async () => {
  531. createWrapper({
  532. ruleId: rule.id,
  533. rule: {
  534. ...rule,
  535. timeWindow: 60,
  536. comparisonDelta: 100,
  537. eventTypes: ['error'],
  538. resolution: 2,
  539. },
  540. });
  541. expect(screen.getByLabelText('Static: above or below {x}')).not.toBeChecked();
  542. await userEvent.click(screen.getByText('Static: above or below {x}'));
  543. await waitFor(() =>
  544. expect(screen.getByLabelText('Static: above or below {x}')).toBeChecked()
  545. );
  546. await userEvent.click(screen.getByLabelText('Save Rule'));
  547. expect(editRule).toHaveBeenLastCalledWith(
  548. expect.anything(),
  549. expect.objectContaining({
  550. data: expect.objectContaining({
  551. // Comparison delta is reset
  552. comparisonDelta: null,
  553. }),
  554. })
  555. );
  556. });
  557. it('switches to anomaly detection threshold', async () => {
  558. organization.features = [
  559. ...organization.features,
  560. 'anomaly-detection-alerts',
  561. 'anomaly-detection-rollout',
  562. ];
  563. createWrapper({
  564. rule: {
  565. ...rule,
  566. id: undefined,
  567. eventTypes: ['default'],
  568. },
  569. });
  570. const anomaly_option = await screen.findByText(
  571. 'Anomaly: whenever values are outside of expected bounds'
  572. );
  573. expect(anomaly_option).toBeInTheDocument();
  574. });
  575. it('switches event type from error to default', async () => {
  576. createWrapper({
  577. ruleId: rule.id,
  578. rule: {
  579. ...rule,
  580. eventTypes: ['error', 'default'],
  581. },
  582. });
  583. await userEvent.click(screen.getByText('event.type:error OR event.type:default'));
  584. await userEvent.click(await screen.findByText('event.type:default'));
  585. expect(screen.getAllByText('Number of Errors')).toHaveLength(2);
  586. await userEvent.click(screen.getByLabelText('Save Rule'));
  587. expect(editRule).toHaveBeenLastCalledWith(
  588. expect.anything(),
  589. expect.objectContaining({
  590. data: expect.objectContaining({
  591. eventTypes: ['default'],
  592. }),
  593. })
  594. );
  595. });
  596. it('saves a valid on demand metric rule', async () => {
  597. const validOnDemandMetricRule = MetricRuleFixture({
  598. query: 'transaction.duration:<1s',
  599. });
  600. const onSubmitSuccess = jest.fn();
  601. createWrapper({
  602. ruleId: validOnDemandMetricRule.id,
  603. rule: {
  604. ...validOnDemandMetricRule,
  605. eventTypes: ['transaction'],
  606. },
  607. onSubmitSuccess,
  608. });
  609. await userEvent.click(screen.getByLabelText('Save Rule'), {delay: null});
  610. expect(onSubmitSuccess).toHaveBeenCalled();
  611. });
  612. it('hides fields when migrating error metric alerts to filter archived issues', async () => {
  613. const errorAlert = MetricRuleFixture({
  614. dataset: Dataset.ERRORS,
  615. query: 'example-error',
  616. });
  617. location = {...location, query: {migration: '1'}};
  618. const onSubmitSuccess = jest.fn();
  619. createWrapper({
  620. ruleId: errorAlert.id,
  621. rule: {
  622. ...errorAlert,
  623. eventTypes: ['transaction'],
  624. },
  625. onSubmitSuccess,
  626. });
  627. expect(
  628. await screen.findByText(/please make sure the current thresholds are still valid/)
  629. ).toBeInTheDocument();
  630. await userEvent.click(screen.getByLabelText('Looks good to me!'), {delay: null});
  631. expect(onSubmitSuccess).toHaveBeenCalled();
  632. const formModel = onSubmitSuccess.mock.calls[0][1] as FormModel;
  633. expect(formModel.getData()).toEqual(
  634. expect.objectContaining({query: 'is:unresolved example-error'})
  635. );
  636. });
  637. });
  638. describe('Slack async lookup', () => {
  639. const uuid = 'xxxx-xxxx-xxxx';
  640. it('success status updates the rule', async () => {
  641. const alertRule = MetricRuleFixture({name: 'Slack Alert Rule'});
  642. MockApiClient.addMockResponse({
  643. url: `/organizations/org-slug/alert-rules/${alertRule.id}/`,
  644. method: 'PUT',
  645. body: {uuid},
  646. statusCode: 202,
  647. });
  648. MockApiClient.addMockResponse({
  649. url: `/projects/org-slug/project-slug/alert-rule-task/${uuid}/`,
  650. body: {
  651. status: 'success',
  652. alertRule,
  653. },
  654. });
  655. const onSubmitSuccess = jest.fn();
  656. createWrapper({
  657. ruleId: alertRule.id,
  658. rule: alertRule,
  659. onSubmitSuccess,
  660. });
  661. await screen.findByTestId('loading-indicator');
  662. await userEvent.type(
  663. await screen.findByPlaceholderText('Enter Alert Name'),
  664. 'Slack Alert Rule',
  665. {delay: null}
  666. );
  667. await userEvent.click(await screen.findByLabelText('Save Rule'), {delay: null});
  668. expect(await screen.findByTestId('loading-indicator')).toBeInTheDocument();
  669. await waitFor(
  670. () => {
  671. expect(onSubmitSuccess).toHaveBeenCalledWith(
  672. expect.objectContaining({
  673. id: alertRule.id,
  674. name: alertRule.name,
  675. }),
  676. expect.anything()
  677. );
  678. },
  679. {timeout: 2000, interval: 10}
  680. );
  681. });
  682. it('pending status keeps loading true', async () => {
  683. const alertRule = MetricRuleFixture({name: 'Slack Alert Rule'});
  684. MockApiClient.addMockResponse({
  685. url: `/organizations/org-slug/alert-rules/${alertRule.id}/`,
  686. method: 'PUT',
  687. body: {uuid},
  688. statusCode: 202,
  689. });
  690. MockApiClient.addMockResponse({
  691. url: `/projects/org-slug/project-slug/alert-rule-task/${uuid}/`,
  692. body: {
  693. status: 'pending',
  694. },
  695. });
  696. const onSubmitSuccess = jest.fn();
  697. createWrapper({
  698. ruleId: alertRule.id,
  699. rule: alertRule,
  700. onSubmitSuccess,
  701. });
  702. expect(await screen.findByTestId('loading-indicator')).toBeInTheDocument();
  703. expect(onSubmitSuccess).not.toHaveBeenCalled();
  704. });
  705. it('failed status renders error message', async () => {
  706. const alertRule = MetricRuleFixture({name: 'Slack Alert Rule'});
  707. MockApiClient.addMockResponse({
  708. url: `/organizations/org-slug/alert-rules/${alertRule.id}/`,
  709. method: 'PUT',
  710. body: {uuid},
  711. statusCode: 202,
  712. });
  713. MockApiClient.addMockResponse({
  714. url: `/projects/org-slug/project-slug/alert-rule-task/${uuid}/`,
  715. body: {
  716. status: 'failed',
  717. error: 'An error occurred',
  718. },
  719. });
  720. const onSubmitSuccess = jest.fn();
  721. createWrapper({
  722. ruleId: alertRule.id,
  723. rule: alertRule,
  724. onSubmitSuccess,
  725. });
  726. await userEvent.type(
  727. await screen.findByPlaceholderText('Enter Alert Name'),
  728. 'Slack Alert Rule',
  729. {delay: null}
  730. );
  731. await userEvent.click(await screen.findByLabelText('Save Rule'), {delay: null});
  732. await waitFor(
  733. () => {
  734. expect(addErrorMessage).toHaveBeenCalledWith('An error occurred');
  735. },
  736. {timeout: 2000, interval: 10}
  737. );
  738. expect(onSubmitSuccess).not.toHaveBeenCalled();
  739. });
  740. });
  741. });