ruleForm.spec.tsx 21 KB

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