index.spec.jsx 15 KB

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