index.spec.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. import {Fragment} from 'react';
  2. import {GroupFixture} from 'sentry-fixture/group';
  3. import {OrganizationFixture} from 'sentry-fixture/organization';
  4. import {ProjectFixture} from 'sentry-fixture/project';
  5. import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
  6. import GlobalModal from 'sentry/components/globalModal';
  7. import GroupStore from 'sentry/stores/groupStore';
  8. import SelectedGroupStore from 'sentry/stores/selectedGroupStore';
  9. import {IssueCategory} from 'sentry/types';
  10. import * as analytics from 'sentry/utils/analytics';
  11. import {IssueListActions} from 'sentry/views/issueList/actions';
  12. const organization = OrganizationFixture();
  13. const defaultProps = {
  14. allResultsVisible: false,
  15. query: '',
  16. queryCount: 15,
  17. projectId: 'project-slug',
  18. selection: {
  19. projects: [1],
  20. environments: [],
  21. datetime: {start: null, end: null, period: null, utc: true},
  22. },
  23. groupIds: ['1', '2', '3'],
  24. onRealtimeChange: jest.fn(),
  25. onSelectStatsPeriod: jest.fn(),
  26. realtimeActive: false,
  27. statsPeriod: '24h',
  28. onDelete: jest.fn(),
  29. displayReprocessingActions: false,
  30. onSortChange: jest.fn(),
  31. sort: '',
  32. };
  33. function WrappedComponent(props) {
  34. return (
  35. <Fragment>
  36. <GlobalModal />
  37. <IssueListActions {...defaultProps} {...props} />
  38. </Fragment>
  39. );
  40. }
  41. describe('IssueListActions', function () {
  42. afterEach(() => {
  43. jest.restoreAllMocks();
  44. });
  45. beforeEach(() => {
  46. GroupStore.reset();
  47. SelectedGroupStore.reset();
  48. SelectedGroupStore.add(['1', '2', '3']);
  49. MockApiClient.addMockResponse({
  50. url: `/organizations/${organization.slug}/projects/`,
  51. body: [ProjectFixture({id: '1'})],
  52. });
  53. });
  54. describe('Bulk', function () {
  55. describe('Total results greater than bulk limit', function () {
  56. it('after checking "Select all" checkbox, displays bulk select message', async function () {
  57. render(<WrappedComponent queryCount={1500} />);
  58. await userEvent.click(screen.getByRole('checkbox'));
  59. });
  60. it('can bulk select', async function () {
  61. render(<WrappedComponent queryCount={1500} />);
  62. await userEvent.click(screen.getByRole('checkbox'));
  63. await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link'));
  64. });
  65. it('bulk resolves', async function () {
  66. const apiMock = MockApiClient.addMockResponse({
  67. url: '/organizations/org-slug/issues/',
  68. method: 'PUT',
  69. });
  70. render(<WrappedComponent queryCount={1500} />);
  71. await userEvent.click(screen.getByRole('checkbox'));
  72. await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link'));
  73. await userEvent.click(screen.getByRole('button', {name: 'Resolve'}));
  74. await screen.findByRole('dialog');
  75. await userEvent.click(screen.getByRole('button', {name: 'Bulk resolve issues'}));
  76. expect(apiMock).toHaveBeenCalledWith(
  77. expect.anything(),
  78. expect.objectContaining({
  79. query: {
  80. project: [1],
  81. },
  82. data: {status: 'resolved', statusDetails: {}, substatus: null},
  83. })
  84. );
  85. });
  86. });
  87. describe('Total results less than bulk limit', function () {
  88. it('after checking "Select all" checkbox, displays bulk select message', async function () {
  89. render(<WrappedComponent queryCount={15} />);
  90. await userEvent.click(screen.getByRole('checkbox'));
  91. });
  92. it('can bulk select', async function () {
  93. render(<WrappedComponent queryCount={15} />);
  94. await userEvent.click(screen.getByRole('checkbox'));
  95. await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link'));
  96. });
  97. it('bulk resolves', async function () {
  98. const apiMock = MockApiClient.addMockResponse({
  99. url: '/organizations/org-slug/issues/',
  100. method: 'PUT',
  101. });
  102. render(<WrappedComponent queryCount={15} />);
  103. await userEvent.click(screen.getByRole('checkbox'));
  104. await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link'));
  105. await userEvent.click(screen.getByRole('button', {name: 'Resolve'}));
  106. const modal = screen.getByRole('dialog');
  107. await userEvent.click(
  108. within(modal).getByRole('button', {name: 'Bulk resolve issues'})
  109. );
  110. expect(apiMock).toHaveBeenCalledWith(
  111. expect.anything(),
  112. expect.objectContaining({
  113. query: {
  114. project: [1],
  115. },
  116. data: {status: 'resolved', statusDetails: {}, substatus: null},
  117. })
  118. );
  119. });
  120. });
  121. describe('Selected on page', function () {
  122. it('resolves selected items', async function () {
  123. const apiMock = MockApiClient.addMockResponse({
  124. url: '/organizations/org-slug/issues/',
  125. method: 'PUT',
  126. });
  127. jest.spyOn(SelectedGroupStore, 'getSelectedIds').mockReturnValue(new Set(['1']));
  128. render(<WrappedComponent groupIds={['1', '2', '3', '6', '9']} />);
  129. const resolveButton = screen.getByRole('button', {name: 'Resolve'});
  130. expect(resolveButton).toBeEnabled();
  131. await userEvent.click(resolveButton);
  132. expect(apiMock).toHaveBeenCalledWith(
  133. expect.anything(),
  134. expect.objectContaining({
  135. query: {
  136. id: ['1'],
  137. project: [1],
  138. },
  139. data: {status: 'resolved', statusDetails: {}, substatus: null},
  140. })
  141. );
  142. });
  143. });
  144. });
  145. it('can archive an issue until escalating', async () => {
  146. const analyticsSpy = jest.spyOn(analytics, 'trackAnalytics');
  147. const apiMock = MockApiClient.addMockResponse({
  148. url: `/organizations/${organization.slug}/issues/`,
  149. method: 'PUT',
  150. });
  151. jest.spyOn(SelectedGroupStore, 'getSelectedIds').mockReturnValue(new Set(['1']));
  152. render(<WrappedComponent {...defaultProps} />, {organization});
  153. await userEvent.click(screen.getByRole('button', {name: 'Archive'}));
  154. expect(apiMock).toHaveBeenCalledWith(
  155. expect.anything(),
  156. expect.objectContaining({
  157. query: {
  158. id: ['1'],
  159. project: [1],
  160. },
  161. data: {
  162. status: 'ignored',
  163. statusDetails: {},
  164. substatus: 'archived_until_escalating',
  165. },
  166. })
  167. );
  168. expect(analyticsSpy).toHaveBeenCalledWith(
  169. 'issues_stream.archived',
  170. expect.objectContaining({
  171. action_substatus: 'archived_until_escalating',
  172. })
  173. );
  174. });
  175. it('can unarchive an issue when the query contains is:archived', async () => {
  176. const apiMock = MockApiClient.addMockResponse({
  177. url: `/organizations/${organization.slug}/issues/`,
  178. method: 'PUT',
  179. });
  180. jest.spyOn(SelectedGroupStore, 'getSelectedIds').mockReturnValue(new Set(['1']));
  181. render(<WrappedComponent {...defaultProps} query="is:archived" />, {
  182. organization,
  183. });
  184. await userEvent.click(screen.getByRole('button', {name: 'Unarchive'}));
  185. expect(apiMock).toHaveBeenCalledWith(
  186. expect.anything(),
  187. expect.objectContaining({
  188. query: expect.objectContaining({id: ['1'], project: [1]}),
  189. data: {status: 'unresolved'},
  190. })
  191. );
  192. });
  193. it('can resolve but not merge issues from different projects', function () {
  194. jest
  195. .spyOn(SelectedGroupStore, 'getSelectedIds')
  196. .mockImplementation(() => new Set(['1', '2', '3']));
  197. jest.spyOn(GroupStore, 'get').mockImplementation(id => {
  198. switch (id) {
  199. case '1':
  200. return GroupFixture({project: ProjectFixture({slug: 'project-1'})});
  201. default:
  202. return GroupFixture({project: ProjectFixture({slug: 'project-2'})});
  203. }
  204. });
  205. render(<WrappedComponent />);
  206. // Can resolve but not merge issues from multiple projects
  207. expect(screen.getByRole('button', {name: 'Resolve'})).toBeEnabled();
  208. expect(screen.getByRole('button', {name: 'Merge Selected Issues'})).toBeDisabled();
  209. });
  210. describe('mark reviewed', function () {
  211. it('acknowledges group', async function () {
  212. const mockOnMarkReviewed = jest.fn();
  213. MockApiClient.addMockResponse({
  214. url: '/organizations/org-slug/issues/',
  215. method: 'PUT',
  216. });
  217. jest
  218. .spyOn(SelectedGroupStore, 'getSelectedIds')
  219. .mockImplementation(() => new Set(['1', '2', '3']));
  220. jest.spyOn(GroupStore, 'get').mockImplementation(id => {
  221. return GroupFixture({
  222. id,
  223. inbox: {
  224. date_added: '2020-11-24T13:17:42.248751Z',
  225. reason: 0,
  226. reason_details: null,
  227. },
  228. });
  229. });
  230. render(<WrappedComponent onMarkReviewed={mockOnMarkReviewed} />);
  231. const reviewButton = screen.getByRole('button', {name: 'Mark Reviewed'});
  232. expect(reviewButton).toBeEnabled();
  233. await userEvent.click(reviewButton);
  234. expect(mockOnMarkReviewed).toHaveBeenCalledWith(['1', '2', '3']);
  235. });
  236. it('mark reviewed disabled for group that is already reviewed', function () {
  237. SelectedGroupStore.add(['1']);
  238. SelectedGroupStore.toggleSelectAll();
  239. GroupStore.loadInitialData([GroupFixture({id: '1', inbox: null})]);
  240. render(<WrappedComponent {...defaultProps} />);
  241. expect(screen.getByRole('button', {name: 'Mark Reviewed'})).toBeDisabled();
  242. });
  243. });
  244. describe('sort', function () {
  245. it('calls onSortChange with new sort value', async function () {
  246. const mockOnSortChange = jest.fn();
  247. render(<WrappedComponent onSortChange={mockOnSortChange} />);
  248. await userEvent.click(screen.getByRole('button', {name: 'Last Seen'}));
  249. await userEvent.click(screen.getByText(/Number of events/));
  250. expect(mockOnSortChange).toHaveBeenCalledWith('freq');
  251. });
  252. });
  253. describe('performance issues', function () {
  254. it('disables options that are not supported for performance issues', async () => {
  255. jest
  256. .spyOn(SelectedGroupStore, 'getSelectedIds')
  257. .mockImplementation(() => new Set(['1', '2']));
  258. jest.spyOn(GroupStore, 'get').mockImplementation(id => {
  259. switch (id) {
  260. case '1':
  261. return GroupFixture({
  262. issueCategory: IssueCategory.ERROR,
  263. });
  264. default:
  265. return GroupFixture({
  266. issueCategory: IssueCategory.PERFORMANCE,
  267. });
  268. }
  269. });
  270. render(<WrappedComponent />);
  271. // Resolve and ignore are supported
  272. expect(screen.getByRole('button', {name: 'Resolve'})).toBeEnabled();
  273. expect(screen.getByRole('button', {name: 'Archive'})).toBeEnabled();
  274. // Merge is not supported and should be disabled
  275. expect(screen.getByRole('button', {name: 'Merge Selected Issues'})).toBeDisabled();
  276. // Open overflow menu
  277. await userEvent.click(screen.getByRole('button', {name: 'More issue actions'}));
  278. // 'Add to Bookmarks' is supported
  279. expect(
  280. screen.getByRole('menuitemradio', {name: 'Add to Bookmarks'})
  281. ).not.toHaveAttribute('aria-disabled');
  282. // Deleting is not supported and menu item should be disabled
  283. expect(screen.getByRole('menuitemradio', {name: 'Delete'})).toHaveAttribute(
  284. 'aria-disabled',
  285. 'true'
  286. );
  287. });
  288. describe('bulk action performance issues', function () {
  289. const orgWithPerformanceIssues = OrganizationFixture({
  290. features: ['performance-issues'],
  291. });
  292. it('silently filters out performance issues when bulk deleting', async function () {
  293. const bulkDeleteMock = MockApiClient.addMockResponse({
  294. url: '/organizations/org-slug/issues/',
  295. method: 'DELETE',
  296. });
  297. render(
  298. <Fragment>
  299. <GlobalModal />
  300. <IssueListActions {...defaultProps} query="is:unresolved" queryCount={100} />
  301. </Fragment>,
  302. {organization: orgWithPerformanceIssues}
  303. );
  304. await userEvent.click(screen.getByRole('checkbox'));
  305. await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link'));
  306. await userEvent.click(screen.getByRole('button', {name: 'More issue actions'}));
  307. await userEvent.click(screen.getByRole('menuitemradio', {name: 'Delete'}));
  308. const modal = screen.getByRole('dialog');
  309. expect(
  310. within(modal).getByText(/deleting performance issues is not yet supported/i)
  311. ).toBeInTheDocument();
  312. await userEvent.click(
  313. within(modal).getByRole('button', {name: 'Bulk delete issues'})
  314. );
  315. expect(bulkDeleteMock).toHaveBeenCalledWith(
  316. expect.anything(),
  317. expect.objectContaining({
  318. query: expect.objectContaining({
  319. query: 'is:unresolved issue.category:error',
  320. }),
  321. })
  322. );
  323. });
  324. it('silently filters out performance issues when bulk merging', async function () {
  325. const bulkMergeMock = MockApiClient.addMockResponse({
  326. url: '/organizations/org-slug/issues/',
  327. method: 'PUT',
  328. });
  329. // Ensure that all issues have the same project so we can merge
  330. jest
  331. .spyOn(GroupStore, 'get')
  332. .mockReturnValue(GroupFixture({project: ProjectFixture({slug: 'project-1'})}));
  333. render(
  334. <Fragment>
  335. <GlobalModal />
  336. <IssueListActions {...defaultProps} query="is:unresolved" queryCount={100} />
  337. </Fragment>,
  338. {organization: orgWithPerformanceIssues}
  339. );
  340. await userEvent.click(screen.getByRole('checkbox'));
  341. await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link'));
  342. await userEvent.click(
  343. screen.getByRole('button', {name: 'Merge Selected Issues'})
  344. );
  345. const modal = screen.getByRole('dialog');
  346. expect(
  347. within(modal).getByText(/merging performance issues is not yet supported/i)
  348. ).toBeInTheDocument();
  349. await userEvent.click(
  350. within(modal).getByRole('button', {name: 'Bulk merge issues'})
  351. );
  352. expect(bulkMergeMock).toHaveBeenCalledWith(
  353. expect.anything(),
  354. expect.objectContaining({
  355. query: expect.objectContaining({
  356. query: 'is:unresolved issue.category:error',
  357. }),
  358. })
  359. );
  360. });
  361. });
  362. });
  363. });