import {Fragment} from 'react'; import {GroupFixture} from 'sentry-fixture/group'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {ProjectFixture} from 'sentry-fixture/project'; import { render, screen, userEvent, waitFor, within, } from 'sentry-test/reactTestingLibrary'; import GlobalModal from 'sentry/components/globalModal'; import {DEFAULT_QUERY} from 'sentry/constants'; import GroupStore from 'sentry/stores/groupStore'; import SelectedGroupStore from 'sentry/stores/selectedGroupStore'; import {IssueCategory} from 'sentry/types/group'; import * as analytics from 'sentry/utils/analytics'; import {IssueListActions} from 'sentry/views/issueList/actions'; const organization = OrganizationFixture(); 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: [ProjectFixture({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', {name: 'Select all'})); }); it('can bulk select', async function () { render(); await userEvent.click(screen.getByRole('checkbox', {name: 'Select all'})); 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', {name: 'Select all'})); await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link')); await userEvent.click(await screen.findByRole('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}, }) ); }); it('bulk sets priority', async function () { const apiMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/', method: 'PUT', }); render(); await userEvent.click(screen.getByRole('checkbox', {name: 'Select all'})); await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link')); await userEvent.click(await screen.findByRole('button', {name: 'Set Priority'})); await userEvent.click(screen.getByRole('menuitemradio', {name: 'High'})); expect( within(screen.getByRole('dialog')).getByText( 'Are you sure you want to reprioritize to high the first 1,000 issues that match the search?' ) ).toBeInTheDocument(); await userEvent.click( screen.getByRole('button', {name: 'Bulk reprioritize issues'}) ); expect(apiMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ query: { project: [1], }, data: {priority: 'high'}, }) ); }); }); describe('Total results less than bulk limit', function () { it('after checking "Select all" checkbox, displays bulk select message', async function () { render(); const checkbox = screen.getByRole('checkbox', {name: 'Select all'}); await userEvent.click(checkbox); expect(checkbox).toBeChecked(); 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', {name: 'Select all'})); await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link')); await userEvent.click(await screen.findByRole('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 set priority', async function () { 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: 'Set Priority'})); await userEvent.click(screen.getByRole('menuitemradio', {name: 'High'})); expect(apiMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ query: { id: ['1'], project: [1], }, data: {priority: 'high'}, }) ); }); it('can archive an issue until escalating', async () => { const analyticsSpy = jest.spyOn(analytics, 'trackAnalytics'); const apiMock = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/`, method: 'PUT', }); jest.spyOn(SelectedGroupStore, 'getSelectedIds').mockReturnValue(new Set(['1'])); render(, {organization}); 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 apiMock = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/`, method: 'PUT', }); jest.spyOn(SelectedGroupStore, 'getSelectedIds').mockReturnValue(new Set(['1'])); render(, { organization, }); 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', statusDetails: {}}, }) ); }); it('can resolve but not merge issues from different projects', async function () { jest .spyOn(SelectedGroupStore, 'getSelectedIds') .mockImplementation(() => new Set(['1', '2', '3'])); jest.spyOn(GroupStore, 'get').mockImplementation(id => { switch (id) { case '1': return GroupFixture({project: ProjectFixture({slug: 'project-1'})}); default: return GroupFixture({project: ProjectFixture({slug: 'project-2'})}); } }); render(); // Can resolve but not merge issues from multiple projects expect(await screen.findByRole('button', {name: 'Resolve'})).toBeEnabled(); await userEvent.click(screen.getByRole('button', {name: 'More issue actions'})); expect(screen.getByRole('menuitemradio', {name: 'Merge'})).toHaveAttribute( 'aria-disabled', 'true' ); }); it('sets the project ID when My Projects is selected', async function () { jest .spyOn(SelectedGroupStore, 'getSelectedIds') .mockImplementation(() => new Set(['1'])); jest .spyOn(GroupStore, 'get') .mockImplementation(id => GroupFixture({id, project: ProjectFixture({id: '123', slug: 'project-1'})}) ); const apiMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/', method: 'PUT', }); render( My Projects projects: [], environments: [], datetime: {start: null, end: null, period: null, utc: true}, }} /> ); await userEvent.click(await screen.findByRole('button', {name: 'Resolve'})); // API request should have project ID set to 123 await waitFor(() => { expect(apiMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ query: { id: ['1'], project: ['123'], }, }) ); }); }); describe('mark reviewed', function () { it('acknowledges group', async function () { const mockOnActionTaken = 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 GroupFixture({ id, inbox: { date_added: '2020-11-24T13:17:42.248751Z', reason: 0, reason_details: null, }, }); }); render(); await userEvent.click(screen.getByRole('button', {name: 'More issue actions'})); const reviewButton = screen.getByRole('menuitemradio', {name: 'Mark Reviewed'}); await userEvent.click(reviewButton); expect(mockOnActionTaken).toHaveBeenCalledWith(['1', '2', '3'], {inbox: false}); }); it('mark reviewed disabled for group that is already reviewed', async function () { SelectedGroupStore.add(['1']); SelectedGroupStore.toggleSelectAll(); GroupStore.loadInitialData([GroupFixture({id: '1', inbox: null})]); render(); await userEvent.click(screen.getByRole('button', {name: 'More issue actions'})); expect( await screen.findByRole('menuitemradio', {name: 'Mark Reviewed'}) ).toHaveAttribute('aria-disabled', 'true'); }); }); 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 GroupFixture({ issueCategory: IssueCategory.ERROR, }); default: return GroupFixture({ issueCategory: IssueCategory.PERFORMANCE, }); } }); render(); // Resolve and ignore are supported expect(screen.getByRole('button', {name: 'Resolve'})).toBeEnabled(); expect(screen.getByRole('button', {name: 'Archive'})).toBeEnabled(); // Open overflow menu await userEvent.click(screen.getByRole('button', {name: 'More issue actions'})); // Merge is not supported and should be disabled expect(screen.getByRole('menuitemradio', {name: 'Merge'})).toHaveAttribute( 'aria-disabled', 'true' ); // '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 = OrganizationFixture({ 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', {name: 'Select all'})); await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link')); await userEvent.click( await screen.findByRole('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: DEFAULT_QUERY + ' 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(GroupFixture({project: ProjectFixture({slug: 'project-1'})})); render( , {organization: orgWithPerformanceIssues} ); await userEvent.click(screen.getByRole('checkbox', {name: 'Select all'})); await userEvent.click(screen.getByTestId('issue-list-select-all-notice-link')); await userEvent.click( await screen.findByRole('button', {name: 'More issue actions'}) ); await userEvent.click(screen.getByRole('menuitemradio', {name: 'Merge'})); 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: DEFAULT_QUERY + ' issue.category:error', }), }) ); }); }); }); });