create.spec.tsx 25 KB

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