create.spec.tsx 24 KB

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