create.spec.tsx 23 KB

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