import {Fragment} from 'react'; import {EventStacktraceExceptionFixture} from 'sentry-fixture/eventStacktraceException'; 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 ConfigStore from 'sentry/stores/configStore'; import ModalStore from 'sentry/stores/modalStore'; import {GroupStatus, IssueCategory} from 'sentry/types/group'; import * as analytics from 'sentry/utils/analytics'; import {browserHistory} from 'sentry/utils/browserHistory'; import GroupActions from 'sentry/views/issueDetails/actions'; const project = ProjectFixture({ id: '2448', name: 'project name', slug: 'project', }); const group = GroupFixture({ id: '1337', pluginActions: [], pluginIssues: [], issueCategory: IssueCategory.ERROR, project, }); const issuePlatformGroup = GroupFixture({ id: '1338', issueCategory: IssueCategory.PERFORMANCE, project, }); const organization = OrganizationFixture({ id: '4660', slug: 'org', }); describe('GroupActions', function () { const analyticsSpy = jest.spyOn(analytics, 'trackAnalytics'); beforeEach(function () { ConfigStore.init(); }); afterEach(function () { MockApiClient.clearMockResponses(); jest.clearAllMocks(); }); describe('render()', function () { it('renders correctly', async function () { render( ); expect(await screen.findByRole('button', {name: 'Resolve'})).toBeInTheDocument(); }); }); describe('subscribing', function () { let issuesApi: any; beforeEach(function () { issuesApi = MockApiClient.addMockResponse({ url: '/projects/org/project/issues/', method: 'PUT', body: GroupFixture({isSubscribed: false}), }); }); it('can subscribe', async function () { render( ); await userEvent.click(screen.getByRole('button', {name: 'Subscribe'})); expect(issuesApi).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ data: {isSubscribed: true}, }) ); }); }); describe('bookmarking', function () { let issuesApi: any; beforeEach(function () { issuesApi = MockApiClient.addMockResponse({ url: '/projects/org/project/issues/', method: 'PUT', body: GroupFixture({isBookmarked: false}), }); }); it('can bookmark', async function () { render( ); await userEvent.click(screen.getByLabelText('More Actions')); const bookmark = await screen.findByTestId('bookmark'); await userEvent.click(bookmark); expect(issuesApi).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ data: {isBookmarked: true}, }) ); }); }); describe('reprocessing', function () { it('renders ReprocessAction component if org has native exception event', async function () { const event = EventStacktraceExceptionFixture({ platform: 'native', }); render( ); await userEvent.click(screen.getByLabelText('More Actions')); const reprocessActionButton = await screen.findByTestId('reprocess'); expect(reprocessActionButton).toBeInTheDocument(); }); it('open dialog by clicking on the ReprocessAction component', async function () { const event = EventStacktraceExceptionFixture({ platform: 'native', }); render( ); const onReprocessEventFunc = jest.spyOn(ModalStore, 'openModal'); await userEvent.click(screen.getByLabelText('More Actions')); const reprocessActionButton = await screen.findByTestId('reprocess'); expect(reprocessActionButton).toBeInTheDocument(); await userEvent.click(reprocessActionButton); await waitFor(() => expect(onReprocessEventFunc).toHaveBeenCalled()); }); }); it('opens share modal from more actions dropdown', async () => { const org = { ...organization, features: ['shared-issues'], }; const updateMock = MockApiClient.addMockResponse({ url: `/projects/${org.slug}/${project.slug}/issues/`, method: 'PUT', body: {}, }); render( , {organization: org} ); await userEvent.click(screen.getByLabelText('More Actions')); await userEvent.click(await screen.findByText('Share')); const modal = screen.getByRole('dialog'); expect(within(modal).getByText('Share Issue')).toBeInTheDocument(); expect(updateMock).toHaveBeenCalled(); }); describe('delete', function () { it('opens delete confirm modal from more actions dropdown', async () => { const org = OrganizationFixture({ ...organization, access: [...organization.access, 'event:admin'], }); MockApiClient.addMockResponse({ url: `/projects/${org.slug}/${project.slug}/issues/`, method: 'PUT', body: {}, }); const deleteMock = MockApiClient.addMockResponse({ url: `/projects/${org.slug}/${project.slug}/issues/`, method: 'DELETE', body: {}, }); render( , {organization: org} ); await userEvent.click(screen.getByLabelText('More Actions')); await userEvent.click(await screen.findByRole('menuitemradio', {name: 'Delete'})); const modal = screen.getByRole('dialog'); expect( within(modal).getByText(/Deleting this issue is permanent/) ).toBeInTheDocument(); await userEvent.click(within(modal).getByRole('button', {name: 'Delete'})); expect(deleteMock).toHaveBeenCalled(); expect(browserHistory.push).toHaveBeenCalledWith({ pathname: `/organizations/${org.slug}/issues/`, query: {project: project.id}, }); }); it('delete for issue platform', async () => { const org = OrganizationFixture({ access: ['event:admin'], // Delete is only shown if this is present }); render( , {organization: org} ); await userEvent.click(screen.getByLabelText('More Actions')); expect(await screen.findByTestId('delete-issue')).toHaveAttribute( 'aria-disabled', 'true' ); expect(await screen.findByTestId('delete-and-discard')).toHaveAttribute( 'aria-disabled', 'true' ); }); it('delete for issue platform is enabled with feature flag', async () => { const org = OrganizationFixture({ access: ['event:admin'], features: ['issue-platform-deletion-ui'], }); render( , {organization: org} ); await userEvent.click(screen.getByLabelText('More Actions')); expect(await screen.findByTestId('delete-issue')).not.toHaveAttribute( 'aria-disabled' ); expect(await screen.findByTestId('delete-and-discard')).toHaveAttribute( 'aria-disabled', 'true' ); }); }); it('resolves and unresolves issue', async () => { const issuesApi = MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/project/issues/`, method: 'PUT', body: {...group, status: 'resolved'}, }); const {rerender} = render( , {organization} ); await userEvent.click(screen.getByRole('button', {name: 'Resolve'})); expect(issuesApi).toHaveBeenCalledWith( `/projects/${organization.slug}/project/issues/`, expect.objectContaining({ data: {status: 'resolved', statusDetails: {}, substatus: null}, }) ); expect(analyticsSpy).toHaveBeenCalledWith( 'issue_details.action_clicked', expect.objectContaining({ action_type: 'resolved', }) ); rerender( ); await userEvent.click(screen.getByRole('button', {name: 'Resolved'})); expect(issuesApi).toHaveBeenCalledWith( `/projects/${organization.slug}/project/issues/`, expect.objectContaining({ data: {status: 'unresolved', statusDetails: {}, substatus: 'ongoing'}, }) ); }); it('can archive issue', async () => { const issuesApi = MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/project/issues/`, method: 'PUT', body: {...group, status: 'resolved'}, }); render( , {organization} ); await userEvent.click(await screen.findByRole('button', {name: 'Archive'})); expect(issuesApi).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ data: { status: 'ignored', statusDetails: {}, substatus: 'archived_until_escalating', }, }) ); expect(analyticsSpy).toHaveBeenCalledWith( 'issue_details.action_clicked', expect.objectContaining({ action_substatus: 'archived_until_escalating', action_type: 'ignored', }) ); }); });