create.spec.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  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('sentry/utils/analytics', () => ({
  27. metric: {
  28. startSpan: jest.fn(() => ({
  29. setTag: jest.fn(),
  30. setData: jest.fn(),
  31. })),
  32. endSpan: 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: ProjectAlertRuleConfigurationFixture(),
  45. });
  46. MockApiClient.addMockResponse({
  47. url: '/projects/org-slug/project-slug/rules/1/',
  48. body: ProjectAlertRuleFixture(),
  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} = initializeOrg(props);
  78. ProjectsStore.loadInitialData([project]);
  79. const params = {orgId: organization.slug, projectId: project.slug};
  80. const wrapper = render(
  81. <AlertsContainer>
  82. <AlertBuilderProjectProvider
  83. {...RouteComponentPropsFixture()}
  84. params={params}
  85. organization={organization}
  86. hasMetricAlerts={false}
  87. >
  88. <ProjectAlertsCreate
  89. {...RouteComponentPropsFixture()}
  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, router}
  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: ProjectAlertRuleFixture(),
  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: ProjectAlertRuleFixture(),
  181. });
  182. // delete node
  183. await userEvent.click(screen.getAllByLabelText('Delete Node')[0]);
  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 waitFor(() => {
  200. expect(trackAnalytics).toHaveBeenCalledWith('edit_alert_rule.delete_row', {
  201. name: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  202. organization,
  203. project_id: '2',
  204. type: 'conditions',
  205. });
  206. });
  207. await userEvent.click(screen.getByText('Save Rule'));
  208. await waitFor(() => {
  209. expect(mock).toHaveBeenCalledWith(
  210. expect.any(String),
  211. expect.objectContaining({
  212. data: {
  213. actionMatch: 'any',
  214. actions: [],
  215. conditions: [],
  216. filterMatch: 'all',
  217. filters: [],
  218. frequency: 60 * 24,
  219. name: 'myname',
  220. owner: null,
  221. },
  222. })
  223. );
  224. });
  225. });
  226. it('can remove actions', async function () {
  227. createWrapper();
  228. const mock = MockApiClient.addMockResponse({
  229. url: '/projects/org-slug/project-slug/rules/',
  230. method: 'POST',
  231. body: ProjectAlertRuleFixture(),
  232. });
  233. // Change name of alert rule
  234. await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'myname');
  235. // Add an action and remove it
  236. await selectEvent.select(screen.getByText('Add action...'), [
  237. 'Send a notification to all legacy integrations',
  238. ]);
  239. await userEvent.click(screen.getAllByLabelText('Delete Node')[1]);
  240. await userEvent.click(screen.getByText('Save Rule'));
  241. await waitFor(() => {
  242. expect(mock).toHaveBeenCalledWith(
  243. expect.any(String),
  244. expect.objectContaining({
  245. data: {
  246. actionMatch: 'any',
  247. actions: [],
  248. conditions: [
  249. expect.objectContaining({
  250. id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  251. }),
  252. ],
  253. filterMatch: 'all',
  254. filters: [],
  255. frequency: 60 * 24,
  256. name: 'myname',
  257. owner: null,
  258. },
  259. })
  260. );
  261. });
  262. });
  263. describe('updates and saves', function () {
  264. let mock;
  265. beforeEach(function () {
  266. mock = MockApiClient.addMockResponse({
  267. url: '/projects/org-slug/project-slug/rules/',
  268. method: 'POST',
  269. body: ProjectAlertRuleFixture(),
  270. });
  271. });
  272. afterEach(function () {
  273. jest.clearAllMocks();
  274. });
  275. it('environment, async action and filter match', async function () {
  276. const wrapper = createWrapper();
  277. // Change target environment
  278. await selectEvent.select(screen.getByText('All Environments'), ['production']);
  279. // Change actionMatch and filterMatch dropdown
  280. const anyDropdown = screen.getByText('any');
  281. expect(anyDropdown).toBeInTheDocument();
  282. const allDropdown = screen.getByText('all');
  283. expect(allDropdown).toBeInTheDocument();
  284. await selectEvent.select(anyDropdown, ['all']);
  285. await selectEvent.select(allDropdown, ['any']);
  286. // Change name of alert rule
  287. await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'myname');
  288. await userEvent.click(screen.getByText('Save Rule'));
  289. expect(mock).toHaveBeenCalledWith(
  290. expect.any(String),
  291. expect.objectContaining({
  292. data: {
  293. actionMatch: 'all',
  294. filterMatch: 'any',
  295. conditions: [
  296. expect.objectContaining({
  297. id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  298. }),
  299. ],
  300. actions: [],
  301. filters: [],
  302. environment: 'production',
  303. frequency: 60 * 24,
  304. name: 'myname',
  305. owner: null,
  306. },
  307. })
  308. );
  309. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  310. await waitFor(() => {
  311. expect(wrapper.router.push).toHaveBeenCalledWith({
  312. pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
  313. });
  314. });
  315. });
  316. it('new condition', async function () {
  317. const wrapper = createWrapper();
  318. // Change name of alert rule
  319. await userEvent.click(screen.getByPlaceholderText('Enter Alert Name'));
  320. await userEvent.paste('myname');
  321. // Add another condition
  322. await selectEvent.select(screen.getByText('Add optional filter...'), [
  323. "The event's tags match {key} {match} {value}",
  324. ]);
  325. // Edit new Condition
  326. await userEvent.click(screen.getByPlaceholderText('key'));
  327. await userEvent.paste('conditionKey');
  328. await userEvent.click(screen.getByPlaceholderText('value'));
  329. await userEvent.paste('conditionValue');
  330. await selectEvent.select(screen.getByText('contains'), ['does not equal']);
  331. await userEvent.click(screen.getByText('Save Rule'));
  332. expect(mock).toHaveBeenCalledWith(
  333. expect.any(String),
  334. expect.objectContaining({
  335. data: {
  336. actionMatch: 'any',
  337. actions: [],
  338. conditions: [
  339. expect.objectContaining({
  340. id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  341. }),
  342. ],
  343. filterMatch: 'all',
  344. filters: [
  345. {
  346. id: 'sentry.rules.filters.tagged_event.TaggedEventFilter',
  347. key: 'conditionKey',
  348. match: 'ne',
  349. value: 'conditionValue',
  350. },
  351. ],
  352. frequency: 60 * 24,
  353. name: 'myname',
  354. owner: null,
  355. },
  356. })
  357. );
  358. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  359. await waitFor(() => {
  360. expect(wrapper.router.push).toHaveBeenCalledWith({
  361. pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
  362. });
  363. });
  364. });
  365. it('new filter', async function () {
  366. const wrapper = createWrapper();
  367. // Change name of alert rule
  368. await userEvent.click(screen.getByPlaceholderText('Enter Alert Name'));
  369. await userEvent.paste('myname');
  370. // delete one condition
  371. await userEvent.click(screen.getAllByLabelText('Delete Node')[0]);
  372. // Add a new filter
  373. await selectEvent.select(screen.getByText('Add optional filter...'), [
  374. 'The issue is older or newer than...',
  375. ]);
  376. await userEvent.click(screen.getByPlaceholderText('10'));
  377. await userEvent.paste('12');
  378. await userEvent.click(screen.getByText('Save Rule'));
  379. expect(mock).toHaveBeenCalledWith(
  380. expect.any(String),
  381. expect.objectContaining({
  382. data: {
  383. actionMatch: 'any',
  384. filterMatch: 'all',
  385. filters: [
  386. {
  387. id: 'sentry.rules.filters.age_comparison.AgeComparisonFilter',
  388. comparison_type: 'older',
  389. time: 'minute',
  390. value: '12',
  391. },
  392. ],
  393. actions: [],
  394. conditions: [],
  395. frequency: 60 * 24,
  396. name: 'myname',
  397. owner: null,
  398. },
  399. })
  400. );
  401. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  402. await waitFor(() => {
  403. expect(wrapper.router.push).toHaveBeenCalledWith({
  404. pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
  405. });
  406. });
  407. });
  408. it('new action', async function () {
  409. const wrapper = createWrapper();
  410. // Change name of alert rule
  411. await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'myname');
  412. // Add a new action
  413. await selectEvent.select(screen.getByText('Add action...'), [
  414. 'Suggested Assignees, Team, or Member',
  415. ]);
  416. // Update action interval
  417. await selectEvent.select(screen.getByText('24 hours'), ['60 minutes']);
  418. await userEvent.click(screen.getByText('Save Rule'));
  419. expect(mock).toHaveBeenCalledWith(
  420. expect.any(String),
  421. expect.objectContaining({
  422. data: {
  423. actionMatch: 'any',
  424. actions: [
  425. {id: 'sentry.mail.actions.NotifyEmailAction', targetType: 'IssueOwners'},
  426. ],
  427. conditions: [
  428. expect.objectContaining({
  429. id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  430. }),
  431. ],
  432. filterMatch: 'all',
  433. filters: [],
  434. frequency: '60',
  435. name: 'myname',
  436. owner: null,
  437. },
  438. })
  439. );
  440. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  441. await waitFor(() => {
  442. expect(wrapper.router.push).toHaveBeenCalledWith({
  443. pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
  444. });
  445. });
  446. });
  447. });
  448. });
  449. describe('test preview chart', () => {
  450. it('valid preview table', async () => {
  451. const groups = GroupsFixture();
  452. const date = new Date();
  453. for (let i = 0; i < groups.length; i++) {
  454. groups[i].lastTriggered = String(date);
  455. }
  456. const mock = MockApiClient.addMockResponse({
  457. url: '/projects/org-slug/project-slug/rules/preview/',
  458. method: 'POST',
  459. body: groups,
  460. headers: {
  461. 'X-Hits': String(groups.length),
  462. Endpoint: 'endpoint',
  463. },
  464. });
  465. createWrapper();
  466. await waitFor(() => {
  467. expect(mock).toHaveBeenCalledWith(
  468. expect.any(String),
  469. expect.objectContaining({
  470. data: {
  471. actionMatch: 'any',
  472. conditions: [
  473. expect.objectContaining({
  474. id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
  475. }),
  476. ],
  477. filterMatch: 'all',
  478. filters: [],
  479. frequency: 60 * 24,
  480. endpoint: null,
  481. },
  482. })
  483. );
  484. });
  485. expect(
  486. screen.getByText('4 issues would have triggered this rule in the past 14 days', {
  487. exact: false,
  488. })
  489. ).toBeInTheDocument();
  490. for (const group of groups) {
  491. expect(screen.getByText(group.shortId)).toBeInTheDocument();
  492. }
  493. expect(screen.getAllByText('3mo ago')[0]).toBeInTheDocument();
  494. });
  495. it('invalid preview alert', async () => {
  496. const mock = MockApiClient.addMockResponse({
  497. url: '/projects/org-slug/project-slug/rules/preview/',
  498. method: 'POST',
  499. statusCode: 400,
  500. });
  501. createWrapper();
  502. // delete existion conditions
  503. await userEvent.click(screen.getAllByLabelText('Delete Node')[0]);
  504. await waitFor(() => {
  505. expect(mock).toHaveBeenCalled();
  506. });
  507. expect(
  508. screen.getByText('Select a condition to generate a preview')
  509. ).toBeInTheDocument();
  510. await selectEvent.select(screen.getByText('Add optional trigger...'), [
  511. 'A new issue is created',
  512. ]);
  513. expect(
  514. await screen.findByText('Preview is not supported for these conditions')
  515. ).toBeInTheDocument();
  516. });
  517. it('empty preview table', async () => {
  518. const mock = MockApiClient.addMockResponse({
  519. url: '/projects/org-slug/project-slug/rules/preview/',
  520. method: 'POST',
  521. body: [],
  522. headers: {
  523. 'X-Hits': '0',
  524. Endpoint: 'endpoint',
  525. },
  526. });
  527. createWrapper();
  528. await waitFor(() => {
  529. expect(mock).toHaveBeenCalled();
  530. });
  531. expect(
  532. screen.getByText("We couldn't find any issues that would've triggered your rule")
  533. ).toBeInTheDocument();
  534. });
  535. });
  536. describe('test incompatible conditions', () => {
  537. const errorText =
  538. 'The conditions highlighted in red are in conflict. They may prevent the alert from ever being triggered.';
  539. it('shows error for incompatible conditions', async () => {
  540. createWrapper();
  541. await userEvent.click(screen.getAllByLabelText('Delete Node')[0]);
  542. await selectEvent.select(screen.getByText('Add optional trigger...'), [
  543. 'A new issue is created',
  544. ]);
  545. const anyDropdown = screen.getByText('any');
  546. expect(anyDropdown).toBeInTheDocument();
  547. await selectEvent.select(anyDropdown, ['all']);
  548. await selectEvent.select(screen.getByText('Add optional trigger...'), [
  549. 'The issue changes state from resolved to unresolved',
  550. ]);
  551. expect(screen.getByText(errorText)).toBeInTheDocument();
  552. expect(screen.getByRole('button', {name: 'Save Rule'})).toHaveAttribute(
  553. 'aria-disabled',
  554. 'true'
  555. );
  556. await userEvent.click(screen.getAllByLabelText('Delete Node')[0]);
  557. expect(screen.queryByText(errorText)).not.toBeInTheDocument();
  558. });
  559. it('test any filterMatch', async () => {
  560. createWrapper();
  561. await userEvent.click(screen.getAllByLabelText('Delete Node')[0]);
  562. await selectEvent.select(screen.getByText('Add optional trigger...'), [
  563. 'A new issue is created',
  564. ]);
  565. const allDropdown = screen.getByText('all');
  566. await selectEvent.select(allDropdown, ['any']);
  567. await selectEvent.select(screen.getByText('Add optional filter...'), [
  568. 'The issue is older or newer than...',
  569. ]);
  570. await userEvent.type(screen.getByPlaceholderText('10'), '10');
  571. await userEvent.click(document.body);
  572. await selectEvent.select(screen.getByText('Add optional filter...'), [
  573. 'The issue has happened at least {x} times (Note: this is approximate)',
  574. ]);
  575. expect(screen.getByText(errorText)).toBeInTheDocument();
  576. await userEvent.click(screen.getAllByLabelText('Delete Node')[1]);
  577. await userEvent.clear(screen.getByDisplayValue('10'));
  578. await userEvent.click(document.body);
  579. expect(screen.queryByText(errorText)).not.toBeInTheDocument();
  580. });
  581. });
  582. it('shows archived to escalating instead of ignored to unresolved', async () => {
  583. createWrapper({
  584. organization: OrganizationFixture(),
  585. });
  586. await selectEvent.select(screen.getByText('Add optional trigger...'), [
  587. 'The issue changes state from archived to escalating',
  588. ]);
  589. expect(
  590. screen.getByText('The issue changes state from archived to escalating')
  591. ).toBeInTheDocument();
  592. });
  593. it('displays noisy alert checkbox for no conditions + filters', async function () {
  594. const mock = MockApiClient.addMockResponse({
  595. url: '/projects/org-slug/project-slug/rules/',
  596. method: 'POST',
  597. body: ProjectAlertRuleFixture(),
  598. });
  599. createWrapper();
  600. await userEvent.click((await screen.findAllByLabelText('Delete Node'))[0]);
  601. await selectEvent.select(screen.getByText('Add action...'), [
  602. 'Suggested Assignees, Team, or Member',
  603. ]);
  604. expect(
  605. screen.getByText(/Alerts without conditions can fire too frequently/)
  606. ).toBeInTheDocument();
  607. expect(trackAnalytics).toHaveBeenCalledWith(
  608. 'alert_builder.noisy_warning_viewed',
  609. expect.anything()
  610. );
  611. await userEvent.click(screen.getByText('Save Rule'));
  612. expect(mock).not.toHaveBeenCalled();
  613. await userEvent.click(
  614. screen.getByRole('checkbox', {name: 'Yes, I don’t mind if this alert gets noisy'})
  615. );
  616. await userEvent.click(screen.getByText('Save Rule'));
  617. expect(mock).toHaveBeenCalled();
  618. expect(trackAnalytics).toHaveBeenCalledWith(
  619. 'alert_builder.noisy_warning_agreed',
  620. expect.anything()
  621. );
  622. });
  623. it('does not display noisy alert banner for legacy integrations', async function () {
  624. createWrapper();
  625. await userEvent.click((await screen.findAllByLabelText('Delete Node'))[0]);
  626. await selectEvent.select(screen.getByText('Add action...'), [
  627. 'Send a notification to all legacy integrations',
  628. ]);
  629. expect(
  630. screen.queryByText(/Alerts without conditions can fire too frequently/)
  631. ).not.toBeInTheDocument();
  632. await selectEvent.select(screen.getByText('Add action...'), [
  633. 'Suggested Assignees, Team, or Member',
  634. ]);
  635. expect(
  636. screen.getByText(/Alerts without conditions can fire too frequently/)
  637. ).toBeInTheDocument();
  638. });
  639. it('displays duplicate error banner with link', async function () {
  640. MockApiClient.addMockResponse({
  641. url: '/projects/org-slug/project-slug/rules/',
  642. method: 'POST',
  643. statusCode: 400,
  644. body: {
  645. name: [
  646. "This rule is an exact duplicate of 'test alert' in this project and may not be created.",
  647. ],
  648. ruleId: [1337],
  649. },
  650. });
  651. createWrapper();
  652. await userEvent.click(screen.getByText('Save Rule'));
  653. const bannerLink = await screen.findByRole('link', {
  654. name: /rule fully duplicates "test alert"/,
  655. });
  656. expect(bannerLink).toBeInTheDocument();
  657. expect(bannerLink).toHaveAttribute(
  658. 'href',
  659. '/organizations/org-slug/alerts/rules/project-slug/1337/details/'
  660. );
  661. });
  662. });