create.spec.tsx 23 KB

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