alertRulesList.spec.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. import {IncidentFixture} from 'sentry-fixture/incident';
  2. import {LocationFixture} from 'sentry-fixture/locationFixture';
  3. import {MetricRuleFixture} from 'sentry-fixture/metricRule';
  4. import {OrganizationFixture} from 'sentry-fixture/organization';
  5. import {ProjectFixture} from 'sentry-fixture/project';
  6. import {ProjectAlertRuleFixture} from 'sentry-fixture/projectAlertRule';
  7. import {TeamFixture} from 'sentry-fixture/team';
  8. import {UptimeRuleFixture} from 'sentry-fixture/uptimeRule';
  9. import {initializeOrg} from 'sentry-test/initializeOrg';
  10. import {
  11. act,
  12. render,
  13. renderGlobalModal,
  14. screen,
  15. userEvent,
  16. within,
  17. } from 'sentry-test/reactTestingLibrary';
  18. import OrganizationStore from 'sentry/stores/organizationStore';
  19. import ProjectsStore from 'sentry/stores/projectsStore';
  20. import TeamStore from 'sentry/stores/teamStore';
  21. import {CombinedAlertType, IncidentStatus} from 'sentry/views/alerts/types';
  22. import AlertRulesList from './alertRulesList';
  23. jest.mock('sentry/utils/analytics');
  24. describe('AlertRulesList', () => {
  25. const defaultOrg = OrganizationFixture({
  26. access: ['alerts:write'],
  27. });
  28. TeamStore.loadInitialData([TeamFixture()], false, null);
  29. let rulesMock!: jest.Mock;
  30. let projectMock!: jest.Mock;
  31. const pageLinks =
  32. '<https://sentry.io/api/0/organizations/org-slug/combined-rules/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1", ' +
  33. '<https://sentry.io/api/0/organizations/org-slug/combined-rules/?cursor=0:100:0>; rel="next"; results="true"; cursor="0:100:0"';
  34. beforeEach(() => {
  35. rulesMock = MockApiClient.addMockResponse({
  36. url: '/organizations/org-slug/combined-rules/',
  37. headers: {Link: pageLinks},
  38. body: [
  39. {
  40. ...ProjectAlertRuleFixture({
  41. id: '123',
  42. name: 'First Issue Alert',
  43. projects: ['earth'],
  44. createdBy: {name: 'Samwise', id: 1, email: ''},
  45. }),
  46. type: CombinedAlertType.ISSUE,
  47. },
  48. {
  49. ...MetricRuleFixture({
  50. id: '345',
  51. projects: ['earth'],
  52. latestIncident: IncidentFixture({
  53. status: IncidentStatus.CRITICAL,
  54. }),
  55. }),
  56. type: CombinedAlertType.METRIC,
  57. },
  58. {
  59. ...MetricRuleFixture({
  60. id: '678',
  61. projects: ['earth'],
  62. latestIncident: null,
  63. }),
  64. type: CombinedAlertType.METRIC,
  65. },
  66. ],
  67. });
  68. projectMock = MockApiClient.addMockResponse({
  69. url: '/organizations/org-slug/projects/',
  70. body: [
  71. ProjectFixture({
  72. slug: 'earth',
  73. platform: 'javascript',
  74. teams: [TeamFixture()],
  75. }),
  76. ],
  77. });
  78. act(() => OrganizationStore.onUpdate(defaultOrg, {replace: true}));
  79. act(() => ProjectsStore.loadInitialData([]));
  80. });
  81. afterEach(() => {
  82. act(() => ProjectsStore.reset());
  83. MockApiClient.clearMockResponses();
  84. jest.clearAllMocks();
  85. });
  86. it('displays list', async () => {
  87. const {router, organization} = initializeOrg({organization: defaultOrg});
  88. render(<AlertRulesList />, {router, organization});
  89. expect(await screen.findByText('First Issue Alert')).toBeInTheDocument();
  90. expect(projectMock).toHaveBeenLastCalledWith(
  91. expect.anything(),
  92. expect.objectContaining({
  93. query: expect.objectContaining({query: 'slug:earth'}),
  94. })
  95. );
  96. expect(screen.getAllByTestId('badge-display-name')[0]).toHaveTextContent('earth');
  97. });
  98. it('displays empty state', async () => {
  99. MockApiClient.addMockResponse({
  100. url: '/organizations/org-slug/combined-rules/',
  101. body: [],
  102. });
  103. const {router, organization} = initializeOrg({organization: defaultOrg});
  104. render(<AlertRulesList />, {router, organization});
  105. expect(
  106. await screen.findByText('No alert rules found for the current query.')
  107. ).toBeInTheDocument();
  108. expect(rulesMock).toHaveBeenCalledTimes(0);
  109. });
  110. it('displays team dropdown context if unassigned', async () => {
  111. const {router, organization} = initializeOrg({organization: defaultOrg});
  112. render(<AlertRulesList />, {router, organization});
  113. const assignee = (await screen.findAllByTestId('alert-row-assignee'))[0];
  114. const btn = within(assignee).getAllByRole('button')[0];
  115. expect(assignee).toBeInTheDocument();
  116. expect(btn).toBeInTheDocument();
  117. await userEvent.click(btn, {skipHover: true});
  118. expect(screen.getByText('#team-slug')).toBeInTheDocument();
  119. expect(within(assignee).getByText('Unassigned')).toBeInTheDocument();
  120. });
  121. it('assigns rule to team from unassigned', async () => {
  122. const assignMock = MockApiClient.addMockResponse({
  123. method: 'PUT',
  124. url: '/projects/org-slug/earth/rules/123/',
  125. body: [],
  126. });
  127. const {router, organization} = initializeOrg({organization: defaultOrg});
  128. render(<AlertRulesList />, {router, organization});
  129. const assignee = (await screen.findAllByTestId('alert-row-assignee'))[0];
  130. const btn = within(assignee).getAllByRole('button')[0];
  131. expect(assignee).toBeInTheDocument();
  132. expect(btn).toBeInTheDocument();
  133. await userEvent.click(btn, {skipHover: true});
  134. await userEvent.click(screen.getByText('#team-slug'));
  135. expect(assignMock).toHaveBeenCalledWith(
  136. '/projects/org-slug/earth/rules/123/',
  137. expect.objectContaining({
  138. data: expect.objectContaining({owner: 'team:1'}),
  139. })
  140. );
  141. });
  142. it('displays dropdown context menu with actions', async () => {
  143. const {router, organization} = initializeOrg({organization: defaultOrg});
  144. render(<AlertRulesList />, {router, organization});
  145. const actions = (await screen.findAllByRole('button', {name: 'Actions'}))[0];
  146. expect(actions).toBeInTheDocument();
  147. await userEvent.click(actions);
  148. expect(screen.getByText('Edit')).toBeInTheDocument();
  149. expect(screen.getByText('Delete')).toBeInTheDocument();
  150. expect(screen.getByText('Duplicate')).toBeInTheDocument();
  151. });
  152. it('deletes an issue rule', async () => {
  153. const deletedRuleName = 'Issue Rule';
  154. const issueRule = ProjectAlertRuleFixture({
  155. name: deletedRuleName,
  156. projects: ['project-slug'],
  157. });
  158. MockApiClient.addMockResponse({
  159. url: '/organizations/org-slug/combined-rules/',
  160. headers: {Link: pageLinks},
  161. body: [{...issueRule, type: CombinedAlertType.ISSUE}],
  162. });
  163. const {router, project, organization} = initializeOrg({organization: defaultOrg});
  164. render(<AlertRulesList />, {router, organization});
  165. renderGlobalModal();
  166. const deleteMock = MockApiClient.addMockResponse({
  167. url: `/projects/${organization.slug}/${project.slug}/rules/${issueRule.id}/`,
  168. method: 'DELETE',
  169. body: {},
  170. });
  171. const actions = (await screen.findAllByRole('button', {name: 'Actions'}))[0];
  172. // Add a new response to the mock with no rules
  173. const emptyListMock = MockApiClient.addMockResponse({
  174. url: '/organizations/org-slug/combined-rules/',
  175. headers: {Link: pageLinks},
  176. body: [],
  177. });
  178. expect(screen.queryByText(deletedRuleName)).toBeInTheDocument();
  179. await userEvent.click(actions);
  180. await userEvent.click(screen.getByRole('menuitemradio', {name: 'Delete'}));
  181. await userEvent.click(screen.getByRole('button', {name: 'Delete Rule'}));
  182. expect(deleteMock).toHaveBeenCalledTimes(1);
  183. expect(emptyListMock).toHaveBeenCalledTimes(1);
  184. expect(screen.queryByText(deletedRuleName)).not.toBeInTheDocument();
  185. });
  186. it('deletes a metric rule', async () => {
  187. const deletedRuleName = 'Metric Rule';
  188. const metricRule = MetricRuleFixture({
  189. name: deletedRuleName,
  190. });
  191. MockApiClient.addMockResponse({
  192. url: '/organizations/org-slug/combined-rules/',
  193. headers: {Link: pageLinks},
  194. body: [{...metricRule, type: CombinedAlertType.METRIC}],
  195. });
  196. const {router, organization} = initializeOrg({organization: defaultOrg});
  197. render(<AlertRulesList />, {router, organization});
  198. renderGlobalModal();
  199. const deleteMock = MockApiClient.addMockResponse({
  200. url: `/organizations/${organization.slug}/alert-rules/${metricRule.id}/`,
  201. method: 'DELETE',
  202. body: {},
  203. });
  204. const actions = (await screen.findAllByRole('button', {name: 'Actions'}))[0];
  205. // Add a new response to the mock with no rules
  206. const emptyListMock = MockApiClient.addMockResponse({
  207. url: '/organizations/org-slug/combined-rules/',
  208. headers: {Link: pageLinks},
  209. body: [],
  210. });
  211. expect(screen.queryByText(deletedRuleName)).toBeInTheDocument();
  212. await userEvent.click(actions);
  213. await userEvent.click(screen.getByRole('menuitemradio', {name: 'Delete'}));
  214. await userEvent.click(screen.getByRole('button', {name: 'Delete Rule'}));
  215. expect(deleteMock).toHaveBeenCalledTimes(1);
  216. expect(emptyListMock).toHaveBeenCalledTimes(1);
  217. expect(screen.queryByText(deletedRuleName)).not.toBeInTheDocument();
  218. });
  219. it('sends user to new alert page on duplicate action', async () => {
  220. const {organization, router} = initializeOrg({
  221. organization: defaultOrg,
  222. });
  223. render(<AlertRulesList />, {router, organization});
  224. const actions = (await screen.findAllByRole('button', {name: 'Actions'}))[0];
  225. expect(actions).toBeInTheDocument();
  226. await userEvent.click(actions);
  227. const duplicate = await screen.findByText('Duplicate');
  228. expect(duplicate).toBeInTheDocument();
  229. await userEvent.click(duplicate);
  230. expect(router.push).toHaveBeenCalledWith({
  231. pathname: '/organizations/org-slug/alerts/new/issue/',
  232. query: {
  233. createFromDuplicate: 'true',
  234. duplicateRuleId: '123',
  235. project: 'earth',
  236. referrer: 'alert_stream',
  237. },
  238. });
  239. });
  240. it('sorts by name', async () => {
  241. const {router, organization} = initializeOrg({
  242. organization: defaultOrg,
  243. router: {
  244. location: LocationFixture({
  245. query: {asc: '1', sort: 'name'},
  246. // Sort by the name column
  247. search: '?asc=1&sort=name`',
  248. }),
  249. },
  250. });
  251. render(<AlertRulesList />, {router, organization});
  252. expect(await screen.findByText('Alert Rule')).toHaveAttribute(
  253. 'aria-sort',
  254. 'ascending'
  255. );
  256. expect(rulesMock).toHaveBeenCalledTimes(1);
  257. expect(rulesMock).toHaveBeenCalledWith(
  258. '/organizations/org-slug/combined-rules/',
  259. expect.objectContaining({
  260. query: expect.objectContaining({sort: 'name', asc: '1'}),
  261. })
  262. );
  263. });
  264. it('disables the new alert button for members', async () => {
  265. const noAccessOrg = {
  266. ...defaultOrg,
  267. access: [],
  268. };
  269. const {router, organization} = initializeOrg({organization: noAccessOrg});
  270. render(<AlertRulesList />, {router, organization});
  271. expect(await screen.findByLabelText('Create Alert')).toBeDisabled();
  272. });
  273. it('searches by name', async () => {
  274. const {organization, router} = initializeOrg();
  275. render(<AlertRulesList />, {router, organization});
  276. const search = await screen.findByPlaceholderText('Search by name');
  277. expect(search).toBeInTheDocument();
  278. const testQuery = 'test name';
  279. await userEvent.type(search, `${testQuery}{enter}`);
  280. expect(router.push).toHaveBeenCalledWith(
  281. expect.objectContaining({
  282. query: {
  283. name: testQuery,
  284. },
  285. })
  286. );
  287. });
  288. it('uses empty team query parameter when removing all teams', async () => {
  289. const {organization, router} = initializeOrg({
  290. router: {
  291. location: LocationFixture({
  292. query: {team: 'myteams'},
  293. search: '?team=myteams`',
  294. }),
  295. },
  296. });
  297. render(<AlertRulesList />, {router, organization});
  298. expect(await screen.findByText('First Issue Alert')).toBeInTheDocument();
  299. await userEvent.click(await screen.findByRole('button', {name: 'My Teams'}));
  300. // Uncheck myteams
  301. const myTeams = await screen.findAllByText('My Teams');
  302. await userEvent.click(myTeams[1]);
  303. expect(router.push).toHaveBeenCalledWith(
  304. expect.objectContaining({
  305. query: {
  306. team: '',
  307. },
  308. })
  309. );
  310. });
  311. it('displays metric alert status', async () => {
  312. const {router, organization} = initializeOrg({organization: defaultOrg});
  313. render(<AlertRulesList />, {router, organization});
  314. const rules = await screen.findAllByText('My Incident Rule');
  315. expect(rules[0]).toBeInTheDocument();
  316. expect(screen.getByText('Triggered')).toBeInTheDocument();
  317. expect(screen.getByText('Above 70')).toBeInTheDocument(); // the fixture trigger threshold
  318. expect(screen.getByText('Below 36')).toBeInTheDocument(); // the fixture resolved threshold
  319. expect(screen.getAllByTestId('alert-badge')[0]).toBeInTheDocument();
  320. });
  321. it('displays activated metric alert status', async () => {
  322. rulesMock = MockApiClient.addMockResponse({
  323. url: '/organizations/org-slug/combined-rules/',
  324. headers: {Link: pageLinks},
  325. body: [
  326. {
  327. ...MetricRuleFixture({
  328. id: '1',
  329. projects: ['earth'],
  330. name: 'Active Activated Alert',
  331. monitorType: 1,
  332. activationCondition: 0,
  333. activations: [
  334. {
  335. alertRuleId: '1',
  336. dateCreated: '2021-08-01T00:00:00Z',
  337. finishedAt: '',
  338. id: '1',
  339. isComplete: false,
  340. querySubscriptionId: '1',
  341. activator: '123',
  342. conditionType: '0',
  343. },
  344. ],
  345. latestIncident: IncidentFixture({
  346. status: IncidentStatus.CRITICAL,
  347. }),
  348. }),
  349. type: CombinedAlertType.METRIC,
  350. },
  351. {
  352. ...MetricRuleFixture({
  353. id: '2',
  354. projects: ['earth'],
  355. name: 'Ready Activated Alert',
  356. monitorType: 1,
  357. activationCondition: 0,
  358. }),
  359. type: CombinedAlertType.METRIC,
  360. },
  361. ],
  362. });
  363. const {router, organization} = initializeOrg({organization: defaultOrg});
  364. render(<AlertRulesList />, {router, organization});
  365. expect(await screen.findByText('Active Activated Alert')).toBeInTheDocument();
  366. expect(await screen.findByText('Ready Activated Alert')).toBeInTheDocument();
  367. expect(screen.getByText('Last activated')).toBeInTheDocument();
  368. expect(screen.getByText('Alert has not been activated yet')).toBeInTheDocument();
  369. expect(screen.getByText('Above 70')).toBeInTheDocument(); // the fixture trigger threshold
  370. expect(screen.getByText('Below 70')).toBeInTheDocument(); // Alert has never fired, so no resolved threshold
  371. expect(screen.getAllByTestId('alert-badge')[0]).toBeInTheDocument();
  372. });
  373. it('displays issue alert disabled', async () => {
  374. MockApiClient.addMockResponse({
  375. url: '/organizations/org-slug/combined-rules/',
  376. headers: {Link: pageLinks},
  377. body: [
  378. {
  379. ...ProjectAlertRuleFixture({
  380. name: 'First Issue Alert',
  381. projects: ['earth'],
  382. status: 'disabled',
  383. }),
  384. type: CombinedAlertType.ISSUE,
  385. },
  386. ],
  387. });
  388. const {router, organization} = initializeOrg({organization: defaultOrg});
  389. render(<AlertRulesList />, {router, organization});
  390. expect(await screen.findByText('First Issue Alert')).toBeInTheDocument();
  391. expect(screen.getByText('Disabled')).toBeInTheDocument();
  392. });
  393. it('displays issue alert disabled instead of muted', async () => {
  394. MockApiClient.addMockResponse({
  395. url: '/organizations/org-slug/combined-rules/',
  396. headers: {Link: pageLinks},
  397. body: [
  398. {
  399. ...ProjectAlertRuleFixture({
  400. name: 'First Issue Alert',
  401. projects: ['earth'],
  402. // both disabled and muted
  403. status: 'disabled',
  404. snooze: true,
  405. }),
  406. type: CombinedAlertType.ISSUE,
  407. },
  408. ],
  409. });
  410. const {router, organization} = initializeOrg({organization: defaultOrg});
  411. render(<AlertRulesList />, {router, organization});
  412. expect(await screen.findByText('First Issue Alert')).toBeInTheDocument();
  413. expect(screen.getByText('Disabled')).toBeInTheDocument();
  414. expect(screen.queryByText('Muted')).not.toBeInTheDocument();
  415. });
  416. it('displays issue alert muted', async () => {
  417. MockApiClient.addMockResponse({
  418. url: '/organizations/org-slug/combined-rules/',
  419. headers: {Link: pageLinks},
  420. body: [
  421. {
  422. ...ProjectAlertRuleFixture({
  423. name: 'First Issue Alert',
  424. projects: ['earth'],
  425. snooze: true,
  426. }),
  427. type: CombinedAlertType.ISSUE,
  428. },
  429. ],
  430. });
  431. const {router, organization} = initializeOrg({organization: defaultOrg});
  432. render(<AlertRulesList />, {router, organization});
  433. expect(await screen.findByText('First Issue Alert')).toBeInTheDocument();
  434. expect(screen.getByText('Muted')).toBeInTheDocument();
  435. });
  436. it('displays metric alert muted', async () => {
  437. MockApiClient.addMockResponse({
  438. url: '/organizations/org-slug/combined-rules/',
  439. headers: {Link: pageLinks},
  440. body: [
  441. {
  442. ...MetricRuleFixture({
  443. projects: ['earth'],
  444. snooze: true,
  445. }),
  446. type: CombinedAlertType.METRIC,
  447. },
  448. ],
  449. });
  450. const {router, organization} = initializeOrg({organization: defaultOrg});
  451. render(<AlertRulesList />, {router, organization});
  452. expect(await screen.findByText('My Incident Rule')).toBeInTheDocument();
  453. expect(screen.getByText('Muted')).toBeInTheDocument();
  454. });
  455. it('sorts by alert rule', async () => {
  456. const {router, organization} = initializeOrg({organization: defaultOrg});
  457. render(<AlertRulesList />, {router, organization});
  458. expect(await screen.findByText('First Issue Alert')).toBeInTheDocument();
  459. expect(rulesMock).toHaveBeenCalledWith(
  460. '/organizations/org-slug/combined-rules/',
  461. expect.objectContaining({
  462. query: {
  463. expand: ['latestIncident', 'lastTriggered'],
  464. sort: ['incident_status', 'date_triggered'],
  465. team: ['myteams', 'unassigned'],
  466. },
  467. })
  468. );
  469. });
  470. it('preserves empty team query parameter on pagination', async () => {
  471. const {organization, router} = initializeOrg({
  472. organization: defaultOrg,
  473. });
  474. render(<AlertRulesList />, {router, organization});
  475. expect(await screen.findByText('First Issue Alert')).toBeInTheDocument();
  476. await userEvent.click(screen.getByLabelText('Next'));
  477. expect(router.push).toHaveBeenCalledWith(
  478. expect.objectContaining({
  479. query: {
  480. team: '',
  481. cursor: '0:100:0',
  482. },
  483. })
  484. );
  485. });
  486. it('renders ACTIVATED Metric Alerts', async () => {
  487. rulesMock = MockApiClient.addMockResponse({
  488. url: '/organizations/org-slug/combined-rules/',
  489. headers: {Link: pageLinks},
  490. body: [
  491. {
  492. ...ProjectAlertRuleFixture({
  493. id: '123',
  494. name: 'First Issue Alert',
  495. projects: ['earth'],
  496. createdBy: {name: 'Samwise', id: 1, email: ''},
  497. }),
  498. type: CombinedAlertType.ISSUE,
  499. },
  500. {
  501. ...MetricRuleFixture({
  502. id: '345',
  503. projects: ['earth'],
  504. name: 'activated Test Metric Alert',
  505. monitorType: 1,
  506. latestIncident: IncidentFixture({
  507. status: IncidentStatus.CRITICAL,
  508. }),
  509. }),
  510. type: CombinedAlertType.METRIC,
  511. },
  512. {
  513. ...MetricRuleFixture({
  514. id: '678',
  515. name: 'Test Metric Alert 2',
  516. monitorType: 0,
  517. projects: ['earth'],
  518. latestIncident: null,
  519. }),
  520. type: CombinedAlertType.METRIC,
  521. },
  522. ],
  523. });
  524. const {router, organization} = initializeOrg({organization: defaultOrg});
  525. render(<AlertRulesList />, {router, organization});
  526. expect(await screen.findByText('Test Metric Alert 2')).toBeInTheDocument();
  527. expect(await screen.findByText('First Issue Alert')).toBeInTheDocument();
  528. expect(await screen.findByText('activated Test Metric Alert')).toBeInTheDocument();
  529. });
  530. it('renders uptime alert rules', async () => {
  531. rulesMock = MockApiClient.addMockResponse({
  532. url: '/organizations/org-slug/combined-rules/',
  533. headers: {Link: pageLinks},
  534. body: [
  535. {
  536. ...UptimeRuleFixture({owner: undefined}),
  537. type: CombinedAlertType.UPTIME,
  538. },
  539. ],
  540. });
  541. const {router, organization} = initializeOrg({organization: defaultOrg});
  542. render(<AlertRulesList />, {router, organization});
  543. expect(await screen.findByText('Uptime Rule')).toBeInTheDocument();
  544. expect(await screen.findByText('Auto Detected')).toBeInTheDocument();
  545. expect(await screen.findByText('Up')).toBeInTheDocument();
  546. });
  547. it('deletes an uptime rule', async () => {
  548. const deletedRuleName = 'Uptime Rule';
  549. const uptimeRule = UptimeRuleFixture({owner: undefined});
  550. MockApiClient.addMockResponse({
  551. url: '/organizations/org-slug/combined-rules/',
  552. headers: {Link: pageLinks},
  553. body: [{...uptimeRule, type: CombinedAlertType.UPTIME}],
  554. });
  555. const {router, project, organization} = initializeOrg({organization: defaultOrg});
  556. render(<AlertRulesList />, {router, organization});
  557. renderGlobalModal();
  558. const deleteMock = MockApiClient.addMockResponse({
  559. url: `/projects/${organization.slug}/${project.slug}/uptime/${uptimeRule.id}/`,
  560. method: 'DELETE',
  561. body: {},
  562. });
  563. const actions = (await screen.findAllByRole('button', {name: 'Actions'}))[0];
  564. // Add a new response to the mock with no rules
  565. const emptyListMock = MockApiClient.addMockResponse({
  566. url: '/organizations/org-slug/combined-rules/',
  567. headers: {Link: pageLinks},
  568. body: [],
  569. });
  570. expect(
  571. screen.queryByRole('link', {name: 'Uptime Rule Auto Detected'})
  572. ).toBeInTheDocument();
  573. await userEvent.click(actions);
  574. await userEvent.click(screen.getByRole('menuitemradio', {name: 'Delete'}));
  575. await userEvent.click(screen.getByRole('button', {name: 'Delete Rule'}));
  576. expect(deleteMock).toHaveBeenCalledTimes(1);
  577. expect(emptyListMock).toHaveBeenCalledTimes(1);
  578. expect(screen.queryByText(deletedRuleName)).not.toBeInTheDocument();
  579. });
  580. });