actions.spec.jsx 15 KB

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