import {EventFixture} from 'sentry-fixture/event'; import {GitHubIntegrationFixture} from 'sentry-fixture/githubIntegration'; import {GroupFixture} from 'sentry-fixture/group'; import {JiraIntegrationFixture} from 'sentry-fixture/jiraIntegration'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {PlatformExternalIssueFixture} from 'sentry-fixture/platformExternalIssue'; import {ProjectFixture} from 'sentry-fixture/project'; import {SentryAppComponentFixture} from 'sentry-fixture/sentryAppComponent'; import {SentryAppInstallationFixture} from 'sentry-fixture/sentryAppInstallation'; import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; import SentryAppComponentsStore from 'sentry/stores/sentryAppComponentsStore'; import SentryAppInstallationStore from 'sentry/stores/sentryAppInstallationsStore'; import {ExternalIssueList} from './externalIssueList'; describe('ExternalIssueList', () => { const organization = OrganizationFixture(); const event = EventFixture(); const group = GroupFixture(); const project = ProjectFixture(); beforeEach(() => { MockApiClient.clearMockResponses(); SentryAppComponentsStore.init(); }); it('should allow unlinking integration external issues', async () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/1/external-issues/`, body: [], }); const issueKey = 'Test-Sentry/github-test#13'; MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/${group.id}/integrations/`, body: [ GitHubIntegrationFixture({ status: 'active', externalIssues: [ { id: '321', key: issueKey, url: 'https://github.com/Test-Sentry/github-test/issues/13', title: 'SyntaxError: XYZ', description: 'something else, sorry', displayName: '', }, ], }), ], }); const unlinkMock = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/${group.id}/integrations/1/`, query: {externalIssue: '321'}, method: 'DELETE', }); render(); expect(await screen.findByRole('button', {name: issueKey})).toBeInTheDocument(); await userEvent.hover(screen.getByRole('button', {name: issueKey})); // Integrations are refetched, remove the external issue from the object const refetchMock = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/${group.id}/integrations/`, body: [ GitHubIntegrationFixture({ status: 'active', externalIssues: [], }), ], }); await userEvent.click(await screen.findByRole('button', {name: 'Unlink issue'})); await waitFor(() => { expect(screen.queryByRole('button', {name: issueKey})).not.toBeInTheDocument(); }); expect(unlinkMock).toHaveBeenCalledTimes(1); expect(refetchMock).toHaveBeenCalledTimes(1); }); it('should allow unlinking sentry app issues', async () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/${group.id}/integrations/`, body: [], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/1/external-issues/`, body: [ PlatformExternalIssueFixture({ id: '1', issueId: '1', serviceType: 'clickup', displayName: 'ClickUp: hello#1', webUrl: 'https://app.clickup.com/t/1', }), ], }); const unlinkMock = MockApiClient.addMockResponse({ url: `/issues/1/external-issues/1/`, method: 'DELETE', }); const component = SentryAppComponentFixture({ sentryApp: { ...SentryAppComponentFixture().sentryApp, slug: 'clickup', name: 'Clickup', }, }); SentryAppComponentsStore.loadComponents([component]); SentryAppInstallationStore.load([ SentryAppInstallationFixture({ app: component.sentryApp, }), ]); render(); expect( await screen.findByRole('button', {name: 'ClickUp: hello#1'}) ).toBeInTheDocument(); await userEvent.hover(screen.getByRole('button', {name: 'ClickUp: hello#1'})); await userEvent.click(await screen.findByRole('button', {name: 'Unlink issue'})); await waitFor(() => { expect( screen.queryByRole('button', {name: 'ClickUp: hello#1'}) ).not.toBeInTheDocument(); }); expect(unlinkMock).toHaveBeenCalledTimes(1); }); it('should combine multiple integration configurations into a single dropdown', async () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/${group.id}/external-issues/`, body: [], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/${group.id}/integrations/`, body: [ GitHubIntegrationFixture({ status: 'active', externalIssues: [], name: 'GitHub sentry', }), GitHubIntegrationFixture({ id: '2', status: 'active', externalIssues: [], name: 'GitHub codecov', }), ], }); render(); expect(await screen.findByRole('button', {name: 'GitHub'})).toBeInTheDocument(); await userEvent.click(await screen.findByRole('button', {name: 'GitHub'})); // Both items are listed inside the dropdown expect( await screen.findByRole('menuitemradio', {name: /GitHub sentry/}) ).toBeInTheDocument(); expect( await screen.findByRole('menuitemradio', {name: /GitHub codecov/}) ).toBeInTheDocument(); }); it('should render empty state when no integrations', async () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/${group.id}/integrations/`, body: [], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/${group.id}/external-issues/`, body: [], }); render(); expect( await screen.findByText('Track this issue in Jira, GitHub, etc.') ).toBeInTheDocument(); }); it('should render dropdown items with subtext correctly', async () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/${group.id}/external-issues/`, body: [], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/issues/${group.id}/integrations/`, body: [ JiraIntegrationFixture({ id: '1', status: 'active', externalIssues: [], name: 'Jira Integration 1', domainName: 'hello.com', }), JiraIntegrationFixture({ id: '2', status: 'active', externalIssues: [], name: 'Jira', domainName: 'example.com', }), ], }); render(); expect(await screen.findByRole('button', {name: 'Jira'})).toBeInTheDocument(); await userEvent.click(await screen.findByRole('button', {name: 'Jira'})); // Item with different name and subtext should show both const menuItem = await screen.findByRole('menuitemradio', { name: /Jira Integration 1/, }); expect(menuItem).toHaveTextContent('hello.com'); // Item with name matching integration name should only show subtext expect(screen.getByRole('menuitemradio', {name: 'example.com'})).toBeInTheDocument(); }); });