actions.spec.jsx 14 KB

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