create.spec.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. import {EnvironmentsFixture} from 'sentry-fixture/environments';
  2. import {GroupsFixture} from 'sentry-fixture/groups';
  3. import {LocationFixture} from 'sentry-fixture/locationFixture';
  4. import {OrganizationFixture} from 'sentry-fixture/organization';
  5. import {ProjectAlertRuleFixture} from 'sentry-fixture/projectAlertRule';
  6. import {ProjectAlertRuleConfigurationFixture} from 'sentry-fixture/projectAlertRuleConfiguration';
  7. import {RouteComponentPropsFixture} from 'sentry-fixture/routeComponentPropsFixture';
  8. import {initializeOrg} from 'sentry-test/initializeOrg';
  9. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  10. import selectEvent from 'sentry-test/selectEvent';
  11. import ProjectsStore from 'sentry/stores/projectsStore';
  12. import TeamStore from 'sentry/stores/teamStore';
  13. import {metric, trackAnalytics} from 'sentry/utils/analytics';
  14. import AlertsContainer from 'sentry/views/alerts';
  15. import AlertBuilderProjectProvider from 'sentry/views/alerts/builder/projectProvider';
  16. import ProjectAlertsCreate from 'sentry/views/alerts/create';
  17. jest.unmock('sentry/utils/recreateRoute');
  18. // updateOnboardingTask triggers an out of band state update
  19. jest.mock('sentry/actionCreators/onboardingTasks');
  20. jest.mock('sentry/actionCreators/members', () => ({
  21. fetchOrgMembers: jest.fn(() => Promise.resolve([])),
  22. indexMembersByProject: jest.fn(() => {
  23. return {};
  24. }),
  25. }));
  26. jest.mock('react-router');
  27. jest.mock('sentry/utils/analytics', () => ({
  28. metric: {
  29. startSpan: jest.fn(() => ({
  30. setTag: jest.fn(),
  31. setData: jest.fn(),
  32. })),
  33. endSpan: jest.fn(),
  34. mark: jest.fn(),
  35. measure: jest.fn(),
  36. },
  37. trackAnalytics: jest.fn(),
  38. }));
  39. describe('ProjectAlertsCreate', function () {
  40. beforeEach(function () {
  41. TeamStore.init();
  42. TeamStore.loadInitialData([], false, null);
  43. MockApiClient.addMockResponse({
  44. url: '/projects/org-slug/project-slug/rules/configuration/',
  45. body: ProjectAlertRuleConfigurationFixture(),
  46. });
  47. MockApiClient.addMockResponse({
  48. url: '/projects/org-slug/project-slug/rules/1/',
  49. body: ProjectAlertRuleFixture(),
  50. });
  51. MockApiClient.addMockResponse({
  52. url: '/projects/org-slug/project-slug/environments/',
  53. body: EnvironmentsFixture(),
  54. });
  55. MockApiClient.addMockResponse({
  56. url: `/projects/org-slug/project-slug/?expand=hasAlertIntegration`,
  57. body: {},
  58. });
  59. MockApiClient.addMockResponse({
  60. url: `/projects/org-slug/project-slug/ownership/`,
  61. method: 'GET',
  62. body: {
  63. fallthrough: false,
  64. autoAssignment: false,
  65. },
  66. });
  67. MockApiClient.addMockResponse({
  68. url: '/projects/org-slug/project-slug/rules/preview/',
  69. method: 'POST',
  70. body: [],
  71. });
  72. });
  73. afterEach(function () {
  74. MockApiClient.clearMockResponses();
  75. jest.clearAllMocks();
  76. });
  77. const createWrapper = (props = {}, location = {}) => {
  78. const {organization, project, router} = initializeOrg(props);
  79. ProjectsStore.loadInitialData([project]);
  80. const params = {orgId: organization.slug, projectId: project.slug};
  81. const wrapper = render(
  82. <AlertsContainer>
  83. <AlertBuilderProjectProvider
  84. {...RouteComponentPropsFixture()}
  85. params={params}
  86. organization={organization}
  87. hasMetricAlerts={false}
  88. >
  89. <ProjectAlertsCreate
  90. {...RouteComponentPropsFixture()}
  91. hasMetricAlerts={false}
  92. members={[]}
  93. params={params}
  94. organization={organization}
  95. project={project}
  96. location={LocationFixture({
  97. pathname: `/organizations/org-slug/alerts/rules/${project.slug}/new/`,
  98. query: {createFromWizard: 'true'},
  99. ...location,
  100. })}
  101. router={router}
  102. />
  103. </AlertBuilderProjectProvider>
  104. </AlertsContainer>,
  105. {organization, router}
  106. );
  107. return {
  108. wrapper,
  109. organization,
  110. project,
  111. router,
  112. };
  113. };
  114. it('adds default parameters if wizard was skipped', async function () {
  115. const location = {query: {}};
  116. const wrapper = createWrapper(undefined, location);
  117. await waitFor(() => {
  118. expect(wrapper.router.replace).toHaveBeenCalledWith(
  119. expect.objectContaining({
  120. pathname: '/organizations/org-slug/alerts/new/metric',
  121. query: {
  122. aggregate: 'count()',
  123. dataset: 'events',
  124. eventTypes: 'error',
  125. project: 'project-slug',
  126. },
  127. })
  128. );
  129. });
  130. });
  131. describe('Issue Alert', function () {
  132. it('loads default values', async function () {
  133. createWrapper();
  134. expect(await screen.findByText('All Environments')).toBeInTheDocument();
  135. expect(await screen.findByText('any')).toBeInTheDocument();
  136. expect(await screen.findByText('all')).toBeInTheDocument();
  137. expect(await screen.findByText('24 hours')).toBeInTheDocument();
  138. });
  139. it('can remove filters', async function () {
  140. createWrapper();
  141. const mock = MockApiClient.addMockResponse({
  142. url: '/projects/org-slug/project-slug/rules/',
  143. method: 'POST',
  144. body: ProjectAlertRuleFixture(),
  145. });
  146. // Change name of alert rule
  147. await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'myname');
  148. // Add a filter and remove it
  149. await selectEvent.select(screen.getByText('Add optional filter...'), [
  150. 'The issue is older or newer than...',
  151. ]);
  152. await userEvent.click(screen.getAllByLabelText('Delete Node')[1]);
  153. await userEvent.click(screen.getByText('Save Rule'));
  154. await waitFor(() => {
  155. expect(mock).toHaveBeenCalledWith(
  156. expect.any(String),
  157. expect.objectContaining({
  158. data: {
  159. actionMatch: 'any',
  160. actions: [],
  161. conditions: [
  162. expect.objectContaining({
  163. id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  164. }),
  165. ],
  166. filterMatch: 'all',
  167. filters: [],
  168. frequency: 60 * 24,
  169. name: 'myname',
  170. owner: null,
  171. },
  172. })
  173. );
  174. });
  175. });
  176. it('can remove triggers', async function () {
  177. const {organization} = createWrapper();
  178. const mock = MockApiClient.addMockResponse({
  179. url: '/projects/org-slug/project-slug/rules/',
  180. method: 'POST',
  181. body: ProjectAlertRuleFixture(),
  182. });
  183. // delete node
  184. await userEvent.click(screen.getByLabelText('Delete Node'));
  185. // Change name of alert rule
  186. await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'myname');
  187. // Add a trigger and remove it
  188. await selectEvent.select(screen.getByText('Add optional trigger...'), [
  189. 'A new issue is created',
  190. ]);
  191. await userEvent.click(screen.getByLabelText('Delete Node'));
  192. await waitFor(() => {
  193. expect(trackAnalytics).toHaveBeenCalledWith('edit_alert_rule.add_row', {
  194. name: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  195. organization,
  196. project_id: '2',
  197. type: 'conditions',
  198. });
  199. });
  200. await waitFor(() => {
  201. expect(trackAnalytics).toHaveBeenCalledWith('edit_alert_rule.delete_row', {
  202. name: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  203. organization,
  204. project_id: '2',
  205. type: 'conditions',
  206. });
  207. });
  208. await userEvent.click(screen.getByText('Save Rule'));
  209. await waitFor(() => {
  210. expect(mock).toHaveBeenCalledWith(
  211. expect.any(String),
  212. expect.objectContaining({
  213. data: {
  214. actionMatch: 'any',
  215. actions: [],
  216. conditions: [],
  217. filterMatch: 'all',
  218. filters: [],
  219. frequency: 60 * 24,
  220. name: 'myname',
  221. owner: null,
  222. },
  223. })
  224. );
  225. });
  226. });
  227. it('can remove actions', async function () {
  228. createWrapper();
  229. const mock = MockApiClient.addMockResponse({
  230. url: '/projects/org-slug/project-slug/rules/',
  231. method: 'POST',
  232. body: ProjectAlertRuleFixture(),
  233. });
  234. // Change name of alert rule
  235. await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'myname');
  236. // Add an action and remove it
  237. await selectEvent.select(screen.getByText('Add action...'), [
  238. 'Send a notification to all legacy integrations',
  239. ]);
  240. await userEvent.click(screen.getAllByLabelText('Delete Node')[1]);
  241. await userEvent.click(screen.getByText('Save Rule'));
  242. await waitFor(() => {
  243. expect(mock).toHaveBeenCalledWith(
  244. expect.any(String),
  245. expect.objectContaining({
  246. data: {
  247. actionMatch: 'any',
  248. actions: [],
  249. conditions: [
  250. expect.objectContaining({
  251. id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  252. }),
  253. ],
  254. filterMatch: 'all',
  255. filters: [],
  256. frequency: 60 * 24,
  257. name: 'myname',
  258. owner: null,
  259. },
  260. })
  261. );
  262. });
  263. });
  264. describe('updates and saves', function () {
  265. let mock;
  266. beforeEach(function () {
  267. mock = MockApiClient.addMockResponse({
  268. url: '/projects/org-slug/project-slug/rules/',
  269. method: 'POST',
  270. body: ProjectAlertRuleFixture(),
  271. });
  272. });
  273. afterEach(function () {
  274. jest.clearAllMocks();
  275. });
  276. it('environment, async action and filter match', async function () {
  277. const wrapper = createWrapper();
  278. // Change target environment
  279. await selectEvent.select(screen.getByText('All Environments'), ['production']);
  280. // Change actionMatch and filterMatch dropdown
  281. const anyDropdown = screen.getByText('any');
  282. expect(anyDropdown).toBeInTheDocument();
  283. const allDropdown = screen.getByText('all');
  284. expect(allDropdown).toBeInTheDocument();
  285. await selectEvent.select(anyDropdown, ['all']);
  286. await selectEvent.select(allDropdown, ['any']);
  287. // Change name of alert rule
  288. await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'myname');
  289. await userEvent.click(screen.getByText('Save Rule'));
  290. expect(mock).toHaveBeenCalledWith(
  291. expect.any(String),
  292. expect.objectContaining({
  293. data: {
  294. actionMatch: 'all',
  295. filterMatch: 'any',
  296. conditions: [
  297. expect.objectContaining({
  298. id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  299. }),
  300. ],
  301. actions: [],
  302. filters: [],
  303. environment: 'production',
  304. frequency: 60 * 24,
  305. name: 'myname',
  306. owner: null,
  307. },
  308. })
  309. );
  310. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  311. await waitFor(() => {
  312. expect(wrapper.router.push).toHaveBeenCalledWith({
  313. pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
  314. });
  315. });
  316. });
  317. it('new condition', async function () {
  318. const wrapper = createWrapper();
  319. // Change name of alert rule
  320. await userEvent.click(screen.getByPlaceholderText('Enter Alert Name'));
  321. await userEvent.paste('myname');
  322. // Add another condition
  323. await selectEvent.select(screen.getByText('Add optional filter...'), [
  324. "The event's tags match {key} {match} {value}",
  325. ]);
  326. // Edit new Condition
  327. await userEvent.click(screen.getByPlaceholderText('key'));
  328. await userEvent.paste('conditionKey');
  329. await userEvent.click(screen.getByPlaceholderText('value'));
  330. await userEvent.paste('conditionValue');
  331. await selectEvent.select(screen.getByText('contains'), ['does not equal']);
  332. await userEvent.click(screen.getByText('Save Rule'));
  333. expect(mock).toHaveBeenCalledWith(
  334. expect.any(String),
  335. expect.objectContaining({
  336. data: {
  337. actionMatch: 'any',
  338. actions: [],
  339. conditions: [
  340. expect.objectContaining({
  341. id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  342. }),
  343. ],
  344. filterMatch: 'all',
  345. filters: [
  346. {
  347. id: 'sentry.rules.filters.tagged_event.TaggedEventFilter',
  348. key: 'conditionKey',
  349. match: 'ne',
  350. value: 'conditionValue',
  351. },
  352. ],
  353. frequency: 60 * 24,
  354. name: 'myname',
  355. owner: null,
  356. },
  357. })
  358. );
  359. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  360. await waitFor(() => {
  361. expect(wrapper.router.push).toHaveBeenCalledWith({
  362. pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
  363. });
  364. });
  365. });
  366. it('new filter', async function () {
  367. const wrapper = createWrapper();
  368. // Change name of alert rule
  369. await userEvent.click(screen.getByPlaceholderText('Enter Alert Name'));
  370. await userEvent.paste('myname');
  371. // delete one condition
  372. await userEvent.click(screen.getAllByLabelText('Delete Node')[0]);
  373. // Add a new filter
  374. await selectEvent.select(screen.getByText('Add optional filter...'), [
  375. 'The issue is older or newer than...',
  376. ]);
  377. await userEvent.click(screen.getByPlaceholderText('10'));
  378. await userEvent.paste('12');
  379. await userEvent.click(screen.getByText('Save Rule'));
  380. expect(mock).toHaveBeenCalledWith(
  381. expect.any(String),
  382. expect.objectContaining({
  383. data: {
  384. actionMatch: 'any',
  385. filterMatch: 'all',
  386. filters: [
  387. {
  388. id: 'sentry.rules.filters.age_comparison.AgeComparisonFilter',
  389. comparison_type: 'older',
  390. time: 'minute',
  391. value: '12',
  392. },
  393. ],
  394. actions: [],
  395. conditions: [],
  396. frequency: 60 * 24,
  397. name: 'myname',
  398. owner: null,
  399. },
  400. })
  401. );
  402. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  403. await waitFor(() => {
  404. expect(wrapper.router.push).toHaveBeenCalledWith({
  405. pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
  406. });
  407. });
  408. });
  409. it('new action', async function () {
  410. const wrapper = createWrapper();
  411. // Change name of alert rule
  412. await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'myname');
  413. // Add a new action
  414. await selectEvent.select(screen.getByText('Add action...'), [
  415. 'Suggested Assignees, Team, or Member',
  416. ]);
  417. // Update action interval
  418. await selectEvent.select(screen.getByText('24 hours'), ['60 minutes']);
  419. await userEvent.click(screen.getByText('Save Rule'));
  420. expect(mock).toHaveBeenCalledWith(
  421. expect.any(String),
  422. expect.objectContaining({
  423. data: {
  424. actionMatch: 'any',
  425. actions: [
  426. {id: 'sentry.mail.actions.NotifyEmailAction', targetType: 'IssueOwners'},
  427. ],
  428. conditions: [
  429. expect.objectContaining({
  430. id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  431. }),
  432. ],
  433. filterMatch: 'all',
  434. filters: [],
  435. frequency: '60',
  436. name: 'myname',
  437. owner: null,
  438. },
  439. })
  440. );
  441. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  442. await waitFor(() => {
  443. expect(wrapper.router.push).toHaveBeenCalledWith({
  444. pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
  445. });
  446. });
  447. });
  448. });
  449. });
  450. describe('test preview chart', () => {
  451. it('valid preview table', async () => {
  452. const groups = GroupsFixture();
  453. const date = new Date();
  454. for (let i = 0; i < groups.length; i++) {
  455. groups[i].lastTriggered = String(date);
  456. }
  457. const mock = MockApiClient.addMockResponse({
  458. url: '/projects/org-slug/project-slug/rules/preview/',
  459. method: 'POST',
  460. body: groups,
  461. headers: {
  462. 'X-Hits': String(groups.length),
  463. Endpoint: 'endpoint',
  464. },
  465. });
  466. createWrapper();
  467. await waitFor(() => {
  468. expect(mock).toHaveBeenCalledWith(
  469. expect.any(String),
  470. expect.objectContaining({
  471. data: {
  472. actionMatch: 'any',
  473. conditions: [
  474. expect.objectContaining({
  475. id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  476. }),
  477. ],
  478. filterMatch: 'all',
  479. filters: [],
  480. frequency: 60 * 24,
  481. endpoint: null,
  482. },
  483. })
  484. );
  485. });
  486. expect(
  487. screen.getByText('4 issues would have triggered this rule in the past 14 days', {
  488. exact: false,
  489. })
  490. ).toBeInTheDocument();
  491. for (const group of groups) {
  492. expect(screen.getByText(group.shortId)).toBeInTheDocument();
  493. }
  494. expect(screen.getAllByText('3mo ago')[0]).toBeInTheDocument();
  495. });
  496. it('invalid preview alert', async () => {
  497. const mock = MockApiClient.addMockResponse({
  498. url: '/projects/org-slug/project-slug/rules/preview/',
  499. method: 'POST',
  500. statusCode: 400,
  501. });
  502. createWrapper();
  503. // delete existion condition
  504. await userEvent.click(screen.getAllByLabelText('Delete Node')[0]);
  505. await waitFor(() => {
  506. expect(mock).toHaveBeenCalled();
  507. });
  508. expect(
  509. screen.getByText('Select a condition to generate a preview')
  510. ).toBeInTheDocument();
  511. await selectEvent.select(screen.getByText('Add optional trigger...'), [
  512. 'A new issue is created',
  513. ]);
  514. expect(
  515. await screen.findByText('Preview is not supported for these conditions')
  516. ).toBeInTheDocument();
  517. });
  518. it('empty preview table', async () => {
  519. const mock = MockApiClient.addMockResponse({
  520. url: '/projects/org-slug/project-slug/rules/preview/',
  521. method: 'POST',
  522. body: [],
  523. headers: {
  524. 'X-Hits': '0',
  525. Endpoint: 'endpoint',
  526. },
  527. });
  528. createWrapper();
  529. await waitFor(() => {
  530. expect(mock).toHaveBeenCalled();
  531. });
  532. expect(
  533. screen.getByText("We couldn't find any issues that would've triggered your rule")
  534. ).toBeInTheDocument();
  535. });
  536. });
  537. describe('test incompatible conditions', () => {
  538. const errorText =
  539. 'The conditions highlighted in red are in conflict. They may prevent the alert from ever being triggered.';
  540. it('shows error for incompatible conditions', async () => {
  541. createWrapper();
  542. const anyDropdown = screen.getByText('any');
  543. expect(anyDropdown).toBeInTheDocument();
  544. await selectEvent.select(anyDropdown, ['all']);
  545. await selectEvent.select(screen.getByText('Add optional trigger...'), [
  546. 'The issue changes state from resolved to unresolved',
  547. ]);
  548. expect(screen.getByText(errorText)).toBeInTheDocument();
  549. expect(screen.getByRole('button', {name: 'Save Rule'})).toHaveAttribute(
  550. 'aria-disabled',
  551. 'true'
  552. );
  553. await userEvent.click(screen.getAllByLabelText('Delete Node')[0]);
  554. expect(screen.queryByText(errorText)).not.toBeInTheDocument();
  555. });
  556. it('test any filterMatch', async () => {
  557. createWrapper();
  558. const allDropdown = screen.getByText('all');
  559. await selectEvent.select(allDropdown, ['any']);
  560. await selectEvent.select(screen.getByText('Add optional filter...'), [
  561. 'The issue is older or newer than...',
  562. ]);
  563. await userEvent.type(screen.getByPlaceholderText('10'), '10');
  564. await userEvent.click(document.body);
  565. await selectEvent.select(screen.getByText('Add optional filter...'), [
  566. 'The issue has happened at least {x} times (Note: this is approximate)',
  567. ]);
  568. expect(screen.getByText(errorText)).toBeInTheDocument();
  569. await userEvent.click(screen.getAllByLabelText('Delete Node')[1]);
  570. await userEvent.clear(screen.getByDisplayValue('10'));
  571. await userEvent.click(document.body);
  572. expect(screen.queryByText(errorText)).not.toBeInTheDocument();
  573. });
  574. });
  575. it('shows archived to escalating instead of ignored to unresolved', async () => {
  576. createWrapper({
  577. organization: OrganizationFixture(),
  578. });
  579. await selectEvent.select(screen.getByText('Add optional trigger...'), [
  580. 'The issue changes state from archived to escalating',
  581. ]);
  582. expect(
  583. screen.getByText('The issue changes state from archived to escalating')
  584. ).toBeInTheDocument();
  585. });
  586. it('displays noisy alert checkbox for no conditions + filters', async function () {
  587. const mock = MockApiClient.addMockResponse({
  588. url: '/projects/org-slug/project-slug/rules/',
  589. method: 'POST',
  590. body: ProjectAlertRuleFixture(),
  591. });
  592. createWrapper({organization: {features: ['noisy-alert-warning']}});
  593. await userEvent.click((await screen.findAllByLabelText('Delete Node'))[0]);
  594. await selectEvent.select(screen.getByText('Add action...'), [
  595. 'Suggested Assignees, Team, or Member',
  596. ]);
  597. expect(
  598. screen.getByText(/Alerts without conditions can fire too frequently/)
  599. ).toBeInTheDocument();
  600. expect(trackAnalytics).toHaveBeenCalledWith(
  601. 'alert_builder.noisy_warning_viewed',
  602. expect.anything()
  603. );
  604. await userEvent.click(screen.getByText('Save Rule'));
  605. expect(mock).not.toHaveBeenCalled();
  606. await userEvent.click(
  607. screen.getByRole('checkbox', {name: 'Yes, I don’t mind if this alert gets noisy'})
  608. );
  609. await userEvent.click(screen.getByText('Save Rule'));
  610. expect(mock).toHaveBeenCalled();
  611. expect(trackAnalytics).toHaveBeenCalledWith(
  612. 'alert_builder.noisy_warning_agreed',
  613. expect.anything()
  614. );
  615. });
  616. it('does not display noisy alert banner for legacy integrations', async function () {
  617. createWrapper({organization: {features: ['noisy-alert-warning']}});
  618. await userEvent.click((await screen.findAllByLabelText('Delete Node'))[0]);
  619. await selectEvent.select(screen.getByText('Add action...'), [
  620. 'Send a notification to all legacy integrations',
  621. ]);
  622. expect(
  623. screen.queryByText(/Alerts without conditions can fire too frequently/)
  624. ).not.toBeInTheDocument();
  625. await selectEvent.select(screen.getByText('Add action...'), [
  626. 'Suggested Assignees, Team, or Member',
  627. ]);
  628. expect(
  629. screen.getByText(/Alerts without conditions can fire too frequently/)
  630. ).toBeInTheDocument();
  631. });
  632. it('displays duplicate error banner with link', async function () {
  633. MockApiClient.addMockResponse({
  634. url: '/projects/org-slug/project-slug/rules/',
  635. method: 'POST',
  636. statusCode: 400,
  637. body: {
  638. name: [
  639. "This rule is an exact duplicate of 'test alert' in this project and may not be created.",
  640. ],
  641. ruleId: [1337],
  642. },
  643. });
  644. createWrapper();
  645. await userEvent.click(screen.getByText('Save Rule'));
  646. const bannerLink = await screen.findByRole('link', {
  647. name: /rule fully duplicates "test alert"/,
  648. });
  649. expect(bannerLink).toBeInTheDocument();
  650. expect(bannerLink).toHaveAttribute(
  651. 'href',
  652. '/organizations/org-slug/alerts/rules/project-slug/1337/details/'
  653. );
  654. });
  655. });