index.spec.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import {IncidentFixture} from 'sentry-fixture/incident';
  2. import {IncidentStatsFixture} from 'sentry-fixture/incidentStats';
  3. import {MetricRuleFixture} from 'sentry-fixture/metricRule';
  4. import {ProjectFixture} from 'sentry-fixture/project';
  5. import {TeamFixture} from 'sentry-fixture/team';
  6. import {initializeOrg} from 'sentry-test/initializeOrg';
  7. import {act, render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
  8. import selectEvent from 'sentry-test/selectEvent';
  9. import ProjectsStore from 'sentry/stores/projectsStore';
  10. import TeamStore from 'sentry/stores/teamStore';
  11. import type {Organization} from 'sentry/types/organization';
  12. import AlertsContainer from 'sentry/views/alerts';
  13. import IncidentsList from 'sentry/views/alerts/list/incidents';
  14. describe('IncidentsList', () => {
  15. const projects1 = ['a', 'b', 'c'];
  16. const projects2 = ['c', 'd'];
  17. interface Props {
  18. orgOverride?: Partial<Organization>;
  19. }
  20. const renderComponent = ({orgOverride}: Props = {}) => {
  21. const {organization, routerContext, routerProps, router} = initializeOrg({
  22. organization: {features: ['incidents'], ...orgOverride},
  23. });
  24. return {
  25. component: render(
  26. <AlertsContainer>
  27. <IncidentsList {...routerProps} organization={organization} />
  28. </AlertsContainer>,
  29. {context: routerContext, organization}
  30. ),
  31. router,
  32. };
  33. };
  34. beforeEach(() => {
  35. MockApiClient.addMockResponse({
  36. url: '/organizations/org-slug/incidents/',
  37. body: [
  38. IncidentFixture({
  39. id: '123',
  40. identifier: '1',
  41. title: 'First incident',
  42. projects: projects1,
  43. }),
  44. IncidentFixture({
  45. id: '342',
  46. identifier: '2',
  47. title: 'Second incident',
  48. projects: projects2,
  49. }),
  50. ],
  51. });
  52. MockApiClient.addMockResponse({
  53. url: '/organizations/org-slug/incidents/2/stats/',
  54. body: IncidentStatsFixture({
  55. totalEvents: 1000,
  56. uniqueUsers: 32,
  57. eventStats: {
  58. data: [[1591390293327, [{count: 42}]]],
  59. },
  60. }),
  61. });
  62. const projects = [
  63. ProjectFixture({slug: 'a', platform: 'javascript'}),
  64. ProjectFixture({slug: 'b'}),
  65. ProjectFixture({slug: 'c'}),
  66. ProjectFixture({slug: 'd'}),
  67. ];
  68. act(() => ProjectsStore.loadInitialData(projects));
  69. });
  70. afterEach(() => {
  71. act(() => ProjectsStore.reset());
  72. MockApiClient.clearMockResponses();
  73. });
  74. it('displays list', async () => {
  75. renderComponent();
  76. const items = await screen.findAllByTestId('alert-title');
  77. expect(items).toHaveLength(2);
  78. expect(within(items[0]).getByText('First incident')).toBeInTheDocument();
  79. expect(within(items[1]).getByText('Second incident')).toBeInTheDocument();
  80. const projectBadges = screen.getAllByTestId('badge-display-name');
  81. expect(within(projectBadges[0]).getByText('a')).toBeInTheDocument();
  82. });
  83. it('displays empty state (first time experience)', async () => {
  84. MockApiClient.addMockResponse({
  85. url: '/organizations/org-slug/incidents/',
  86. body: [],
  87. });
  88. const rulesMock = MockApiClient.addMockResponse({
  89. url: '/organizations/org-slug/alert-rules/',
  90. body: [],
  91. });
  92. const promptsMock = MockApiClient.addMockResponse({
  93. url: '/organizations/org-slug/prompts-activity/',
  94. body: {data: {dismissed_ts: null}},
  95. });
  96. const promptsUpdateMock = MockApiClient.addMockResponse({
  97. url: '/organizations/org-slug/prompts-activity/',
  98. method: 'PUT',
  99. });
  100. renderComponent();
  101. expect(await screen.findByText('More signal, less noise')).toBeInTheDocument();
  102. expect(rulesMock).toHaveBeenCalledTimes(1);
  103. expect(promptsMock).toHaveBeenCalledTimes(1);
  104. expect(promptsUpdateMock).toHaveBeenCalledTimes(1);
  105. });
  106. it('displays empty state (rules not yet created)', async () => {
  107. MockApiClient.addMockResponse({
  108. url: '/organizations/org-slug/incidents/',
  109. body: [],
  110. });
  111. const rulesMock = MockApiClient.addMockResponse({
  112. url: '/organizations/org-slug/alert-rules/',
  113. body: [],
  114. });
  115. const promptsMock = MockApiClient.addMockResponse({
  116. url: '/organizations/org-slug/prompts-activity/',
  117. body: {data: {dismissed_ts: Math.floor(Date.now() / 1000)}},
  118. });
  119. renderComponent();
  120. expect(
  121. await screen.findByText('No incidents exist for the current query.')
  122. ).toBeInTheDocument();
  123. expect(rulesMock).toHaveBeenCalledTimes(1);
  124. expect(promptsMock).toHaveBeenCalledTimes(1);
  125. });
  126. it('displays empty state (rules created)', async () => {
  127. MockApiClient.addMockResponse({
  128. url: '/organizations/org-slug/incidents/',
  129. body: [],
  130. });
  131. const rulesMock = MockApiClient.addMockResponse({
  132. url: '/organizations/org-slug/alert-rules/',
  133. body: [{id: 1}],
  134. });
  135. const promptsMock = MockApiClient.addMockResponse({
  136. url: '/organizations/org-slug/prompts-activity/',
  137. body: {data: {dismissed_ts: Math.floor(Date.now() / 1000)}},
  138. });
  139. renderComponent();
  140. expect(
  141. await screen.findByText('No incidents exist for the current query.')
  142. ).toBeInTheDocument();
  143. expect(rulesMock).toHaveBeenCalledTimes(1);
  144. expect(promptsMock).toHaveBeenCalledTimes(0);
  145. });
  146. it('filters by status', async () => {
  147. const {router} = renderComponent();
  148. await selectEvent.select(await screen.findByText('Status'), 'Active');
  149. expect(router.push).toHaveBeenCalledWith(
  150. expect.objectContaining({
  151. query: {
  152. status: 'open',
  153. },
  154. })
  155. );
  156. });
  157. it('disables the new alert button for those without alert:write', async () => {
  158. const noAccessOrg = {
  159. access: [],
  160. };
  161. renderComponent({orgOverride: noAccessOrg});
  162. expect(await screen.findByLabelText('Create Alert')).toHaveAttribute(
  163. 'aria-disabled',
  164. 'true'
  165. );
  166. });
  167. it('does not disable the new alert button for those with alert:write', async () => {
  168. // Enabled with access
  169. renderComponent();
  170. expect(await screen.findByLabelText('Create Alert')).toHaveAttribute(
  171. 'aria-disabled',
  172. 'false'
  173. );
  174. });
  175. it('searches by name', async () => {
  176. const {router} = renderComponent();
  177. const input = await screen.findByPlaceholderText('Search by name');
  178. expect(input).toBeInTheDocument();
  179. const testQuery = 'test name';
  180. await userEvent.type(input, `${testQuery}{enter}`);
  181. expect(router.push).toHaveBeenCalledWith(
  182. expect.objectContaining({
  183. query: {
  184. title: testQuery,
  185. },
  186. })
  187. );
  188. });
  189. it('displays owner from alert rule', async () => {
  190. const team = TeamFixture();
  191. MockApiClient.addMockResponse({
  192. url: '/organizations/org-slug/incidents/',
  193. body: [
  194. IncidentFixture({
  195. id: '123',
  196. identifier: '1',
  197. title: 'First incident',
  198. projects: projects1,
  199. alertRule: MetricRuleFixture({owner: `team:${team.id}`}),
  200. }),
  201. ],
  202. });
  203. TeamStore.getById = jest.fn().mockReturnValue(team);
  204. renderComponent();
  205. expect(await screen.findByText(team.name)).toBeInTheDocument();
  206. });
  207. });