index.spec.tsx 6.8 KB

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