ruleForm.spec.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  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 {act, 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;
  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. });
  91. afterEach(() => {
  92. MockApiClient.clearMockResponses();
  93. jest.clearAllMocks();
  94. });
  95. describe('Viewing the rule', () => {
  96. const rule = MetricRuleFixture();
  97. it('is enabled without org-level alerts:write', async () => {
  98. organization.access = [];
  99. project.access = [];
  100. createWrapper({rule});
  101. expect(await screen.findByText(permissionAlertText)).toBeInTheDocument();
  102. expect(screen.queryByLabelText('Save Rule')).toBeDisabled();
  103. });
  104. it('is enabled with org-level alerts:write', async () => {
  105. organization.access = ['alerts:write'];
  106. project.access = [];
  107. createWrapper({rule});
  108. expect(await screen.findByLabelText('Save Rule')).toBeEnabled();
  109. expect(screen.queryByText(permissionAlertText)).not.toBeInTheDocument();
  110. });
  111. it('is enabled with project-level alerts:write', async () => {
  112. organization.access = [];
  113. project.access = ['alerts:write'];
  114. createWrapper({rule});
  115. expect(await screen.findByLabelText('Save Rule')).toBeEnabled();
  116. expect(screen.queryByText(permissionAlertText)).not.toBeInTheDocument();
  117. });
  118. it('renders time window', async () => {
  119. createWrapper({rule});
  120. expect(await screen.findByText('1 hour interval')).toBeInTheDocument();
  121. });
  122. it('renders time window for activated alerts', async () => {
  123. createWrapper({
  124. rule: {
  125. ...rule,
  126. monitorType: MonitorType.CONTINUOUS,
  127. },
  128. });
  129. expect(await screen.findByText('1 hour interval')).toBeInTheDocument();
  130. });
  131. });
  132. describe('Creating a new rule', () => {
  133. let createRule;
  134. beforeEach(() => {
  135. ProjectsStore.loadInitialData([
  136. project,
  137. {
  138. ...project,
  139. id: '10',
  140. slug: 'project-slug-2',
  141. },
  142. ]);
  143. createRule = MockApiClient.addMockResponse({
  144. url: '/organizations/org-slug/alert-rules/',
  145. method: 'POST',
  146. });
  147. MockApiClient.addMockResponse({
  148. url: '/projects/org-slug/project-slug-2/environments/',
  149. body: [],
  150. });
  151. });
  152. /**
  153. * Note this isn't necessarily the desired behavior, as it is just documenting the behavior
  154. */
  155. it('creates a rule', async () => {
  156. const rule = MetricRuleFixture();
  157. createWrapper({
  158. rule: {
  159. ...rule,
  160. id: undefined,
  161. eventTypes: ['default'],
  162. },
  163. });
  164. // Clear field
  165. await userEvent.clear(screen.getByPlaceholderText('Enter Alert Name'));
  166. // Enter in name so we can submit
  167. await userEvent.type(
  168. screen.getByPlaceholderText('Enter Alert Name'),
  169. 'Incident Rule'
  170. );
  171. // Set thresholdPeriod
  172. await selectEvent.select(screen.getAllByText('For 1 minute')[0], 'For 10 minutes');
  173. await userEvent.click(screen.getByLabelText('Save Rule'));
  174. expect(createRule).toHaveBeenCalledWith(
  175. expect.anything(),
  176. expect.objectContaining({
  177. data: expect.objectContaining({
  178. name: 'Incident Rule',
  179. projects: ['project-slug'],
  180. eventTypes: ['default'],
  181. thresholdPeriod: 10,
  182. }),
  183. })
  184. );
  185. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  186. });
  187. it('can create a rule for a different project', async () => {
  188. const rule = MetricRuleFixture();
  189. createWrapper({
  190. rule: {
  191. ...rule,
  192. id: undefined,
  193. eventTypes: ['default'],
  194. },
  195. });
  196. // Clear field
  197. await userEvent.clear(screen.getByPlaceholderText('Enter Alert Name'));
  198. // Enter in name so we can submit
  199. await userEvent.type(
  200. screen.getByPlaceholderText('Enter Alert Name'),
  201. 'Incident Rule'
  202. );
  203. // Change project
  204. await userEvent.click(screen.getByText('project-slug'));
  205. await userEvent.click(screen.getByText('project-slug-2'));
  206. await userEvent.click(screen.getByLabelText('Save Rule'));
  207. expect(createRule).toHaveBeenCalledWith(
  208. expect.anything(),
  209. expect.objectContaining({
  210. data: expect.objectContaining({
  211. name: 'Incident Rule',
  212. projects: ['project-slug-2'],
  213. }),
  214. })
  215. );
  216. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  217. });
  218. it('creates a rule with generic_metrics dataset', async () => {
  219. organization.features = [...organization.features, 'mep-rollout-flag'];
  220. const rule = MetricRuleFixture();
  221. createWrapper({
  222. rule: {
  223. ...rule,
  224. id: undefined,
  225. aggregate: 'count()',
  226. eventTypes: ['transaction'],
  227. dataset: 'transactions',
  228. },
  229. });
  230. expect(await screen.findByTestId('alert-total-events')).toHaveTextContent('Total5');
  231. await userEvent.click(screen.getByLabelText('Save Rule'));
  232. expect(createRule).toHaveBeenCalledWith(
  233. expect.anything(),
  234. expect.objectContaining({
  235. data: expect.objectContaining({
  236. name: 'My Incident Rule',
  237. projects: ['project-slug'],
  238. aggregate: 'count()',
  239. eventTypes: ['transaction'],
  240. dataset: 'generic_metrics',
  241. thresholdPeriod: 1,
  242. }),
  243. })
  244. );
  245. });
  246. // Activation condition
  247. it('creates a rule with an activation condition', async () => {
  248. organization.features = [
  249. ...organization.features,
  250. 'mep-rollout-flag',
  251. 'activated-alert-rules',
  252. ];
  253. const rule = MetricRuleFixture({
  254. monitorType: MonitorType.ACTIVATED,
  255. activationCondition: ActivationConditionType.RELEASE_CREATION,
  256. });
  257. createWrapper({
  258. rule: {
  259. ...rule,
  260. id: undefined,
  261. aggregate: 'count()',
  262. eventTypes: ['transaction'],
  263. dataset: 'transactions',
  264. },
  265. });
  266. expect(await screen.findByTestId('alert-total-events')).toHaveTextContent('Total5');
  267. await userEvent.click(screen.getByLabelText('Save Rule'));
  268. expect(createRule).toHaveBeenCalledWith(
  269. expect.anything(),
  270. expect.objectContaining({
  271. data: expect.objectContaining({
  272. name: 'My Incident Rule',
  273. projects: ['project-slug'],
  274. aggregate: 'count()',
  275. eventTypes: ['transaction'],
  276. dataset: 'generic_metrics',
  277. thresholdPeriod: 1,
  278. }),
  279. })
  280. );
  281. });
  282. it('creates a continuous rule with activated rules enabled', async () => {
  283. organization.features = [
  284. ...organization.features,
  285. 'mep-rollout-flag',
  286. 'activated-alert-rules',
  287. ];
  288. const rule = MetricRuleFixture({
  289. monitorType: MonitorType.CONTINUOUS,
  290. });
  291. createWrapper({
  292. rule: {
  293. ...rule,
  294. id: undefined,
  295. aggregate: 'count()',
  296. eventTypes: ['transaction'],
  297. dataset: 'transactions',
  298. },
  299. });
  300. expect(await screen.findByTestId('alert-total-events')).toHaveTextContent('Total5');
  301. await userEvent.click(screen.getByLabelText('Save Rule'));
  302. expect(createRule).toHaveBeenCalledWith(
  303. expect.anything(),
  304. expect.objectContaining({
  305. data: expect.objectContaining({
  306. name: 'My Incident Rule',
  307. projects: ['project-slug'],
  308. aggregate: 'count()',
  309. eventTypes: ['transaction'],
  310. dataset: 'generic_metrics',
  311. thresholdPeriod: 1,
  312. }),
  313. })
  314. );
  315. });
  316. it('creates an anomaly detection rule', async () => {
  317. organization.features = [...organization.features, 'anomaly-detection-alerts'];
  318. const rule = MetricRuleFixture({
  319. detectionType: AlertRuleComparisonType.PERCENT,
  320. sensitivity: AlertRuleSensitivity.MEDIUM,
  321. seasonality: AlertRuleSeasonality.AUTO,
  322. });
  323. createWrapper({
  324. rule: {
  325. ...rule,
  326. id: undefined,
  327. aggregate: 'count()',
  328. eventTypes: ['error'],
  329. dataset: 'events',
  330. },
  331. });
  332. await userEvent.click(
  333. screen.getByText('Anomaly: when evaluated values are outside of expected bounds')
  334. );
  335. expect(
  336. await screen.findByLabelText(
  337. 'Anomaly: when evaluated values are outside of expected bounds'
  338. )
  339. ).toBeChecked();
  340. expect(
  341. await screen.findByRole('textbox', {name: 'Sensitivity'})
  342. ).toBeInTheDocument();
  343. await userEvent.click(screen.getByLabelText('Save Rule'));
  344. expect(createRule).toHaveBeenLastCalledWith(
  345. expect.anything(),
  346. expect.objectContaining({
  347. data: expect.objectContaining({
  348. aggregate: 'count()',
  349. dataset: 'events',
  350. environment: null,
  351. eventTypes: ['error'],
  352. detectionType: AlertRuleComparisonType.DYNAMIC,
  353. sensitivity: AlertRuleSensitivity.MEDIUM,
  354. seasonality: AlertRuleSeasonality.AUTO,
  355. }),
  356. })
  357. );
  358. });
  359. it('switches to custom metric and selects event.type:error', async () => {
  360. organization.features = [...organization.features, 'performance-view'];
  361. const rule = MetricRuleFixture();
  362. createWrapper({
  363. rule: {
  364. ...rule,
  365. id: undefined,
  366. eventTypes: ['default'],
  367. },
  368. });
  369. await userEvent.click(screen.getAllByText('Number of Errors').at(1)!);
  370. await userEvent.click(await screen.findByText('Custom Measurement'));
  371. await userEvent.click(screen.getAllByText('event.type:transaction').at(1)!);
  372. await userEvent.click(await screen.findByText('event.type:error'));
  373. expect(screen.getAllByText('Custom Measurement')).toHaveLength(2);
  374. await userEvent.click(screen.getByLabelText('Save Rule'));
  375. expect(createRule).toHaveBeenLastCalledWith(
  376. expect.anything(),
  377. expect.objectContaining({
  378. data: expect.objectContaining({
  379. aggregate: 'count()',
  380. alertType: 'custom_transactions',
  381. dataset: 'events',
  382. datasource: 'error',
  383. environment: null,
  384. eventTypes: ['error'],
  385. name: 'My Incident Rule',
  386. projectId: '2',
  387. projects: ['project-slug'],
  388. query: '',
  389. }),
  390. })
  391. );
  392. });
  393. });
  394. describe('Editing a rule', () => {
  395. let editRule;
  396. let editTrigger;
  397. const rule = MetricRuleFixture();
  398. beforeEach(() => {
  399. editRule = MockApiClient.addMockResponse({
  400. url: `/organizations/org-slug/alert-rules/${rule.id}/`,
  401. method: 'PUT',
  402. body: rule,
  403. });
  404. editTrigger = MockApiClient.addMockResponse({
  405. url: `/organizations/org-slug/alert-rules/${rule.id}/triggers/1/`,
  406. method: 'PUT',
  407. body: IncidentTriggerFixture({id: '1'}),
  408. });
  409. });
  410. afterEach(() => {
  411. editRule.mockReset();
  412. editTrigger.mockReset();
  413. });
  414. it('edits metric', async () => {
  415. createWrapper({
  416. ruleId: rule.id,
  417. rule,
  418. });
  419. // Clear field
  420. await userEvent.clear(screen.getByPlaceholderText('Enter Alert Name'));
  421. await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'new name');
  422. await userEvent.click(screen.getByLabelText('Save Rule'));
  423. expect(editRule).toHaveBeenLastCalledWith(
  424. expect.anything(),
  425. expect.objectContaining({
  426. data: expect.objectContaining({
  427. name: 'new name',
  428. }),
  429. })
  430. );
  431. });
  432. it('switches from percent change to count', async () => {
  433. createWrapper({
  434. ruleId: rule.id,
  435. rule: {
  436. ...rule,
  437. timeWindow: 60,
  438. comparisonDelta: 100,
  439. eventTypes: ['error'],
  440. resolution: 2,
  441. },
  442. });
  443. expect(screen.getByLabelText('Static: above or below {x}')).not.toBeChecked();
  444. await userEvent.click(screen.getByText('Static: above or below {x}'));
  445. await waitFor(() =>
  446. expect(screen.getByLabelText('Static: above or below {x}')).toBeChecked()
  447. );
  448. await userEvent.click(screen.getByLabelText('Save Rule'));
  449. expect(editRule).toHaveBeenLastCalledWith(
  450. expect.anything(),
  451. expect.objectContaining({
  452. data: expect.objectContaining({
  453. // Comparison delta is reset
  454. comparisonDelta: null,
  455. }),
  456. })
  457. );
  458. });
  459. it('switches to anomaly detection threshold', async () => {
  460. organization.features = [...organization.features, 'anomaly-detection-alerts'];
  461. createWrapper({
  462. rule: {
  463. ...rule,
  464. id: undefined,
  465. eventTypes: ['default'],
  466. },
  467. });
  468. const anomaly_option = await screen.findByText(
  469. 'Anomaly: when evaluated values are outside of expected bounds'
  470. );
  471. expect(anomaly_option).toBeInTheDocument();
  472. });
  473. it('switches event type from error to default', async () => {
  474. createWrapper({
  475. ruleId: rule.id,
  476. rule: {
  477. ...rule,
  478. eventTypes: ['error', 'default'],
  479. },
  480. });
  481. await userEvent.click(screen.getByText('event.type:error OR event.type:default'));
  482. await userEvent.click(await screen.findByText('event.type:default'));
  483. expect(screen.getAllByText('Number of Errors')).toHaveLength(2);
  484. await userEvent.click(screen.getByLabelText('Save Rule'));
  485. expect(editRule).toHaveBeenLastCalledWith(
  486. expect.anything(),
  487. expect.objectContaining({
  488. data: expect.objectContaining({
  489. eventTypes: ['default'],
  490. }),
  491. })
  492. );
  493. });
  494. it('saves a valid on demand metric rule', async () => {
  495. const validOnDemandMetricRule = MetricRuleFixture({
  496. query: 'transaction.duration:<1s',
  497. });
  498. const onSubmitSuccess = jest.fn();
  499. createWrapper({
  500. ruleId: validOnDemandMetricRule.id,
  501. rule: {
  502. ...validOnDemandMetricRule,
  503. eventTypes: ['transaction'],
  504. },
  505. onSubmitSuccess,
  506. });
  507. await userEvent.click(screen.getByLabelText('Save Rule'), {delay: null});
  508. expect(onSubmitSuccess).toHaveBeenCalled();
  509. });
  510. it('hides fields when migrating error metric alerts to filter archived issues', async () => {
  511. const errorAlert = MetricRuleFixture({
  512. dataset: Dataset.ERRORS,
  513. query: 'example-error',
  514. });
  515. location = {...location, query: {migration: '1'}};
  516. const onSubmitSuccess = jest.fn();
  517. createWrapper({
  518. ruleId: errorAlert.id,
  519. rule: {
  520. ...errorAlert,
  521. eventTypes: ['transaction'],
  522. },
  523. onSubmitSuccess,
  524. });
  525. expect(
  526. await screen.findByText(/please make sure the current thresholds are still valid/)
  527. ).toBeInTheDocument();
  528. await userEvent.click(screen.getByLabelText('Looks good to me!'), {delay: null});
  529. expect(onSubmitSuccess).toHaveBeenCalled();
  530. const formModel = onSubmitSuccess.mock.calls[0][1] as FormModel;
  531. expect(formModel.getData()).toEqual(
  532. expect.objectContaining({query: 'is:unresolved example-error'})
  533. );
  534. });
  535. });
  536. describe('Slack async lookup', () => {
  537. const uuid = 'xxxx-xxxx-xxxx';
  538. beforeEach(() => {
  539. jest.useFakeTimers();
  540. });
  541. afterEach(() => {
  542. jest.useRealTimers();
  543. });
  544. it('success status updates the rule', async () => {
  545. const alertRule = MetricRuleFixture({name: 'Slack Alert Rule'});
  546. MockApiClient.addMockResponse({
  547. url: `/organizations/org-slug/alert-rules/${alertRule.id}/`,
  548. method: 'PUT',
  549. body: {uuid},
  550. statusCode: 202,
  551. });
  552. MockApiClient.addMockResponse({
  553. url: `/projects/org-slug/project-slug/alert-rule-task/${uuid}/`,
  554. body: {
  555. status: 'success',
  556. alertRule,
  557. },
  558. });
  559. const onSubmitSuccess = jest.fn();
  560. createWrapper({
  561. ruleId: alertRule.id,
  562. rule: alertRule,
  563. onSubmitSuccess,
  564. });
  565. act(jest.runAllTimers);
  566. await userEvent.type(
  567. await screen.findByPlaceholderText('Enter Alert Name'),
  568. 'Slack Alert Rule',
  569. {delay: null}
  570. );
  571. await userEvent.click(screen.getByLabelText('Save Rule'), {delay: null});
  572. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  573. act(jest.runAllTimers);
  574. await waitFor(
  575. () => {
  576. expect(onSubmitSuccess).toHaveBeenCalledWith(
  577. expect.objectContaining({
  578. id: alertRule.id,
  579. name: alertRule.name,
  580. }),
  581. expect.anything()
  582. );
  583. },
  584. {timeout: 2000, interval: 10}
  585. );
  586. });
  587. it('pending status keeps loading true', async () => {
  588. const alertRule = MetricRuleFixture({name: 'Slack Alert Rule'});
  589. MockApiClient.addMockResponse({
  590. url: `/organizations/org-slug/alert-rules/${alertRule.id}/`,
  591. method: 'PUT',
  592. body: {uuid},
  593. statusCode: 202,
  594. });
  595. MockApiClient.addMockResponse({
  596. url: `/projects/org-slug/project-slug/alert-rule-task/${uuid}/`,
  597. body: {
  598. status: 'pending',
  599. },
  600. });
  601. const onSubmitSuccess = jest.fn();
  602. createWrapper({
  603. ruleId: alertRule.id,
  604. rule: alertRule,
  605. onSubmitSuccess,
  606. });
  607. act(jest.runAllTimers);
  608. expect(await screen.findByTestId('loading-indicator')).toBeInTheDocument();
  609. expect(onSubmitSuccess).not.toHaveBeenCalled();
  610. });
  611. it('failed status renders error message', async () => {
  612. const alertRule = MetricRuleFixture({name: 'Slack Alert Rule'});
  613. MockApiClient.addMockResponse({
  614. url: `/organizations/org-slug/alert-rules/${alertRule.id}/`,
  615. method: 'PUT',
  616. body: {uuid},
  617. statusCode: 202,
  618. });
  619. MockApiClient.addMockResponse({
  620. url: `/projects/org-slug/project-slug/alert-rule-task/${uuid}/`,
  621. body: {
  622. status: 'failed',
  623. error: 'An error occurred',
  624. },
  625. });
  626. const onSubmitSuccess = jest.fn();
  627. createWrapper({
  628. ruleId: alertRule.id,
  629. rule: alertRule,
  630. onSubmitSuccess,
  631. });
  632. act(jest.runAllTimers);
  633. await userEvent.type(
  634. await screen.findByPlaceholderText('Enter Alert Name'),
  635. 'Slack Alert Rule',
  636. {delay: null}
  637. );
  638. await userEvent.click(screen.getByLabelText('Save Rule'), {delay: null});
  639. act(jest.runAllTimers);
  640. await waitFor(
  641. () => {
  642. expect(addErrorMessage).toHaveBeenCalledWith('An error occurred');
  643. },
  644. {timeout: 2000, interval: 10}
  645. );
  646. expect(onSubmitSuccess).not.toHaveBeenCalled();
  647. });
  648. });
  649. });