import {Fragment} from 'react'; import { fireEvent, render, screen, userEvent, within, } from 'sentry-test/reactTestingLibrary'; import GlobalModal from 'sentry/components/globalModal'; import GroupStore from 'sentry/stores/groupStore'; import SelectedGroupStore from 'sentry/stores/selectedGroupStore'; import {IssueCategory} from 'sentry/types'; import * as analytics from 'sentry/utils/analytics'; import {IssueListActions} from 'sentry/views/issueList/actions'; const organization = TestStubs.Organization(); const defaultProps = { allResultsVisible: false, query: '', queryCount: 15, projectId: 'project-slug', selection: { projects: [1], environments: [], datetime: {start: null, end: null, period: null, utc: true}, }, groupIds: ['1', '2', '3'], onRealtimeChange: jest.fn(), onSelectStatsPeriod: jest.fn(), realtimeActive: false, statsPeriod: '24h', onDelete: jest.fn(), displayReprocessingActions: false, onSortChange: jest.fn(), sort: '', }; function WrappedComponent(props) { return ( ); } describe('IssueListActions', function () { afterEach(() => { jest.restoreAllMocks(); }); beforeEach(() => { GroupStore.reset(); SelectedGroupStore.reset(); SelectedGroupStore.add(['1', '2', '3']); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/projects/`, body: [TestStubs.Project({id: 1})], }); }); describe('Bulk', function () { describe('Total results greater than bulk limit', function () { it('after checking "Select all" checkbox, displays bulk select message', async function () { render(); await userEvent.click(screen.getByRole('checkbox')); }); it('can bulk select', async function () { render(); await userEvent.click(screen.getByRole('checkbox')); await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link')); }); it('bulk resolves', async function () { const apiMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/', method: 'PUT', }); render(); await userEvent.click(screen.getByRole('checkbox')); await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link')); await userEvent.click(screen.getByRole('button', {name: 'Resolve'})); await screen.findByRole('dialog'); await userEvent.click(screen.getByRole('button', {name: 'Bulk resolve issues'})); expect(apiMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ query: { project: [1], }, data: {status: 'resolved', statusDetails: {}, substatus: null}, }) ); }); }); describe('Total results less than bulk limit', function () { it('after checking "Select all" checkbox, displays bulk select message', async function () { render(); await userEvent.click(screen.getByRole('checkbox')); }); it('can bulk select', async function () { render(); await userEvent.click(screen.getByRole('checkbox')); await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link')); }); it('bulk resolves', async function () { const apiMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/', method: 'PUT', }); render(); await userEvent.click(screen.getByRole('checkbox')); await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link')); await userEvent.click(screen.getByRole('button', {name: 'Resolve'})); const modal = screen.getByRole('dialog'); await userEvent.click( within(modal).getByRole('button', {name: 'Bulk resolve issues'}) ); expect(apiMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ query: { project: [1], }, data: {status: 'resolved', statusDetails: {}, substatus: null}, }) ); }); }); describe('Selected on page', function () { it('resolves selected items', async function () { const apiMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/', method: 'PUT', }); jest.spyOn(SelectedGroupStore, 'getSelectedIds').mockReturnValue(new Set(['1'])); render(); const resolveButton = screen.getByRole('button', {name: 'Resolve'}); expect(resolveButton).toBeEnabled(); await userEvent.click(resolveButton); expect(apiMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ query: { id: ['1'], project: [1], }, data: {status: 'resolved', statusDetails: {}, substatus: null}, }) ); }); it('can ignore selected items (custom)', async function () { const analyticsSpy = jest.spyOn(analytics, 'trackAnalytics'); const apiMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/', method: 'PUT', }); jest.spyOn(SelectedGroupStore, 'getSelectedIds').mockReturnValue(new Set(['1'])); render(); await userEvent.click(screen.getByRole('button', {name: 'Ignore options'})); fireEvent.click(screen.getByText(/Until this affects an additional/)); await screen.findByTestId('until-affect-custom'); await userEvent.click(screen.getByTestId('until-affect-custom')); const modal = screen.getByRole('dialog'); await userEvent.clear( within(modal).getByRole('spinbutton', {name: 'Number of users'}) ); await userEvent.type( within(modal).getByRole('spinbutton', {name: 'Number of users'}), '300' ); await userEvent.click(within(modal).getByRole('textbox')); await userEvent.click(within(modal).getByText('per week')); await userEvent.click(within(modal).getByRole('button', {name: 'Ignore'})); expect(apiMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ query: { id: ['1'], project: [1], }, data: { status: 'ignored', statusDetails: { ignoreUserCount: 300, ignoreUserWindow: 10080, }, substatus: 'archived_until_condition_met', }, }) ); expect(analyticsSpy).toHaveBeenCalledWith( 'issues_stream.archived', expect.objectContaining({ action_status_details: 'ignoreUserCount', }) ); }); }); }); it('can archive an issue until escalating', async () => { const analyticsSpy = jest.spyOn(analytics, 'trackAnalytics'); const org_escalating = {...organization, features: ['escalating-issues']}; const apiMock = MockApiClient.addMockResponse({ url: `/organizations/${org_escalating.slug}/issues/`, method: 'PUT', }); jest.spyOn(SelectedGroupStore, 'getSelectedIds').mockReturnValue(new Set(['1'])); render(, {organization: org_escalating}); await userEvent.click(screen.getByRole('button', {name: 'Archive'})); expect(apiMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ query: { id: ['1'], project: [1], }, data: { status: 'ignored', statusDetails: {}, substatus: 'archived_until_escalating', }, }) ); expect(analyticsSpy).toHaveBeenCalledWith( 'issues_stream.archived', expect.objectContaining({ action_substatus: 'archived_until_escalating', }) ); }); it('can unarchive an issue when the query contains is:archived', async () => { const org_escalating = {...organization, features: ['escalating-issues']}; const apiMock = MockApiClient.addMockResponse({ url: `/organizations/${org_escalating.slug}/issues/`, method: 'PUT', }); jest.spyOn(SelectedGroupStore, 'getSelectedIds').mockReturnValue(new Set(['1'])); render(, { organization: org_escalating, }); await userEvent.click(screen.getByRole('button', {name: 'Unarchive'})); expect(apiMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ query: expect.objectContaining({id: ['1'], project: [1]}), data: {status: 'unresolved'}, }) ); }); it('can resolve but not merge issues from different projects', function () { jest .spyOn(SelectedGroupStore, 'getSelectedIds') .mockImplementation(() => new Set(['1', '2', '3'])); jest.spyOn(GroupStore, 'get').mockImplementation(id => { switch (id) { case '1': return TestStubs.Group({project: TestStubs.Project({slug: 'project-1'})}); default: return TestStubs.Group({project: TestStubs.Project({slug: 'project-2'})}); } }); render(); // Can resolve but not merge issues from multiple projects expect(screen.getByRole('button', {name: 'Resolve'})).toBeEnabled(); expect(screen.getByRole('button', {name: 'Merge Selected Issues'})).toBeDisabled(); }); describe('mark reviewed', function () { it('acknowledges group', async function () { const mockOnMarkReviewed = jest.fn(); MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/', method: 'PUT', }); jest .spyOn(SelectedGroupStore, 'getSelectedIds') .mockImplementation(() => new Set(['1', '2', '3'])); jest.spyOn(GroupStore, 'get').mockImplementation(id => { return TestStubs.Group({ id, inbox: { date_added: '2020-11-24T13:17:42.248751Z', reason: 0, reason_details: null, }, }); }); render(); const reviewButton = screen.getByRole('button', {name: 'Mark Reviewed'}); expect(reviewButton).toBeEnabled(); await userEvent.click(reviewButton); expect(mockOnMarkReviewed).toHaveBeenCalledWith(['1', '2', '3']); }); it('mark reviewed disabled for group that is already reviewed', function () { SelectedGroupStore.add(['1']); SelectedGroupStore.toggleSelectAll(); GroupStore.loadInitialData([TestStubs.Group({id: '1', inbox: null})]); render(); expect(screen.getByRole('button', {name: 'Mark Reviewed'})).toBeDisabled(); }); }); describe('sort', function () { it('calls onSortChange with new sort value', async function () { const mockOnSortChange = jest.fn(); render(); await userEvent.click(screen.getByRole('button', {name: 'Last Seen'})); await userEvent.click(screen.getByText(/Number of events/)); expect(mockOnSortChange).toHaveBeenCalledWith('freq'); }); }); describe('performance issues', function () { it('disables options that are not supported for performance issues', async () => { jest .spyOn(SelectedGroupStore, 'getSelectedIds') .mockImplementation(() => new Set(['1', '2'])); jest.spyOn(GroupStore, 'get').mockImplementation(id => { switch (id) { case '1': return TestStubs.Group({ issueCategory: IssueCategory.ERROR, }); default: return TestStubs.Group({ issueCategory: IssueCategory.PERFORMANCE, }); } }); render(); // Resolve and ignore are supported expect(screen.getByRole('button', {name: 'Resolve'})).toBeEnabled(); expect(screen.getByRole('button', {name: 'Ignore'})).toBeEnabled(); // Merge is not supported and should be disabled expect(screen.getByRole('button', {name: 'Merge Selected Issues'})).toBeDisabled(); // Open overflow menu await userEvent.click(screen.getByRole('button', {name: 'More issue actions'})); // 'Add to Bookmarks' is supported expect( screen.getByRole('menuitemradio', {name: 'Add to Bookmarks'}) ).not.toHaveAttribute('aria-disabled'); // Deleting is not supported and menu item should be disabled expect(screen.getByRole('menuitemradio', {name: 'Delete'})).toHaveAttribute( 'aria-disabled', 'true' ); }); describe('bulk action performance issues', function () { const orgWithPerformanceIssues = TestStubs.Organization({ features: ['performance-issues'], }); it('silently filters out performance issues when bulk deleting', async function () { const bulkDeleteMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/', method: 'DELETE', }); render( , {organization: orgWithPerformanceIssues} ); await userEvent.click(screen.getByRole('checkbox')); await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link')); await userEvent.click(screen.getByRole('button', {name: 'More issue actions'})); await userEvent.click(screen.getByRole('menuitemradio', {name: 'Delete'})); const modal = screen.getByRole('dialog'); expect( within(modal).getByText(/deleting performance issues is not yet supported/i) ).toBeInTheDocument(); await userEvent.click( within(modal).getByRole('button', {name: 'Bulk delete issues'}) ); expect(bulkDeleteMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ query: expect.objectContaining({ query: 'is:unresolved issue.category:error', }), }) ); }); it('silently filters out performance issues when bulk merging', async function () { const bulkMergeMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/', method: 'PUT', }); // Ensure that all issues have the same project so we can merge jest .spyOn(GroupStore, 'get') .mockReturnValue( TestStubs.Group({project: TestStubs.Project({slug: 'project-1'})}) ); render( , {organization: orgWithPerformanceIssues} ); await userEvent.click(screen.getByRole('checkbox')); await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link')); await userEvent.click( screen.getByRole('button', {name: 'Merge Selected Issues'}) ); const modal = screen.getByRole('dialog'); expect( within(modal).getByText(/merging performance issues is not yet supported/i) ).toBeInTheDocument(); await userEvent.click( within(modal).getByRole('button', {name: 'Bulk merge issues'}) ); expect(bulkMergeMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ query: expect.objectContaining({ query: 'is:unresolved issue.category:error', }), }) ); }); }); }); });