import {EventFixture} from 'sentry-fixture/event';
import {FrameFixture} from 'sentry-fixture/frame';
import {GroupFixture} from 'sentry-fixture/group';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectFixture} from 'sentry-fixture/project';
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
import {EntryType} from 'sentry/types/event';
import {IssueCategory} from 'sentry/types/group';
import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
import type {IssueTypeConfig} from 'sentry/utils/issueTypeConfig/types';
import * as RegionUtils from 'sentry/utils/regions';
import SolutionsSection from 'sentry/views/issueDetails/streamline/sidebar/solutionsSection';
import {Tab} from 'sentry/views/issueDetails/types';
jest.mock('sentry/utils/issueTypeConfig');
jest.mock('sentry/utils/regions');
describe('SolutionsSection', () => {
const mockEvent = EventFixture({
entries: [
{
type: EntryType.EXCEPTION,
data: {values: [{stacktrace: {frames: [FrameFixture()]}}]},
},
],
});
const mockGroup = GroupFixture();
const mockProject = ProjectFixture();
const organization = OrganizationFixture({
genAIConsent: true,
hideAiFeatures: false,
features: ['gen-ai-features'],
});
const mockIssueTypeConfig: IssueTypeConfig = {
issueSummary: {
enabled: true,
},
resources: {
description: 'Test Resource',
links: [{link: 'https://example.com', text: 'Test Link'}],
linksByPlatform: {},
},
actions: {
archiveUntilOccurrence: {enabled: false},
delete: {enabled: false},
deleteAndDiscard: {enabled: false},
ignore: {enabled: false},
merge: {enabled: false},
resolve: {enabled: true},
resolveInRelease: {enabled: false},
share: {enabled: false},
},
customCopy: {
resolution: 'Resolved',
eventUnits: 'Events',
},
pages: {
landingPage: Tab.DETAILS,
events: {enabled: false},
openPeriods: {enabled: false},
checkIns: {enabled: false},
uptimeChecks: {enabled: false},
attachments: {enabled: false},
userFeedback: {enabled: false},
replays: {enabled: false},
tagsTab: {enabled: false},
},
detector: {enabled: false},
autofix: true,
discover: {enabled: false},
eventAndUserCounts: {enabled: true},
evidence: null,
header: {
filterBar: {enabled: true, fixedEnvironment: false},
graph: {enabled: true, type: 'discover-events'},
tagDistribution: {enabled: false},
occurrenceSummary: {enabled: false},
},
logLevel: {enabled: true},
mergedIssues: {enabled: false},
performanceDurationRegression: {enabled: false},
profilingDurationRegression: {enabled: false},
regression: {enabled: false},
showFeedbackWidget: false,
similarIssues: {enabled: false},
spanEvidence: {enabled: false},
stacktrace: {enabled: false},
stats: {enabled: false},
tags: {enabled: false},
useOpenPeriodChecks: false,
usesIssuePlatform: false,
};
beforeEach(() => {
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: `/issues/${mockGroup.id}/autofix/setup/`,
body: {
genAIConsent: {ok: true},
integration: {ok: true},
githubWriteIntegration: {ok: true},
},
});
MockApiClient.addMockResponse({
url: `/issues/${mockGroup.id}/autofix/`,
body: {
steps: [],
},
});
jest.mocked(getConfigForIssueType).mockReturnValue(mockIssueTypeConfig);
});
it('renders loading state when summary is pending', () => {
// Use a delayed response to simulate loading state
MockApiClient.addMockResponse({
url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/summarize/`,
method: 'POST',
statusCode: 200,
body: new Promise(() => {}), // Never resolves, keeping the loading state
});
jest.mocked(getConfigForIssueType).mockReturnValue({
...mockIssueTypeConfig,
autofix: false,
});
render(
,
{
organization,
}
);
expect(screen.getByText('Solutions Hub')).toBeInTheDocument();
expect(screen.getAllByTestId('loading-placeholder')).toHaveLength(3); // what's wrong, possible cause, and CTA button
});
it('renders summary when AI features are enabled and data is available', async () => {
const mockSummary = 'This is a test summary';
MockApiClient.addMockResponse({
url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/summarize/`,
method: 'POST',
body: {
whatsWrong: mockSummary,
},
});
render(
,
{
organization,
}
);
await waitFor(() => {
expect(screen.getByText(mockSummary)).toBeInTheDocument();
});
});
it('renders resources section when AI features are disabled', () => {
const customOrganization = OrganizationFixture({
hideAiFeatures: true,
genAIConsent: false,
features: ['gen-ai-features'],
});
render(
,
{
organization: customOrganization,
}
);
expect(screen.getByText('Test Link')).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'READ MORE'})).toBeInTheDocument();
});
it('toggles resources content when clicking Read More/Show Less', async () => {
const customOrganization = OrganizationFixture({
hideAiFeatures: true,
genAIConsent: false,
});
render(
,
{
organization: customOrganization,
}
);
const readMoreButton = screen.getByRole('button', {name: 'READ MORE'});
await userEvent.click(readMoreButton);
expect(screen.getByRole('button', {name: 'SHOW LESS'})).toBeInTheDocument();
const showLessButton = screen.getByRole('button', {name: 'SHOW LESS'});
await userEvent.click(showLessButton);
expect(screen.queryByRole('button', {name: 'SHOW LESS'})).not.toBeInTheDocument();
expect(screen.getByRole('button', {name: 'READ MORE'})).toBeInTheDocument();
});
describe('Solutions Hub button text', () => {
it('shows "Set Up Autofix" when AI needs setup', async () => {
const customOrganization = OrganizationFixture({
genAIConsent: false,
hideAiFeatures: false,
features: ['gen-ai-features'],
});
MockApiClient.addMockResponse({
url: `/issues/${mockGroup.id}/autofix/setup/`,
body: {
genAIConsent: {ok: false},
integration: {ok: false},
githubWriteIntegration: {ok: false},
},
});
render(
,
{
organization: customOrganization,
}
);
await waitFor(() => {
expect(screen.queryByTestId('loading-placeholder')).not.toBeInTheDocument();
});
expect(
screen.getByText('Explore potential root causes and solutions with Autofix.')
).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Set Up Autofix'})).toBeInTheDocument();
});
it('shows "Find Root Cause" even when autofix needs setup', async () => {
MockApiClient.addMockResponse({
url: `/issues/${mockGroup.id}/autofix/setup/`,
body: {
genAIConsent: {ok: true},
integration: {ok: false},
githubWriteIntegration: {ok: false},
},
});
MockApiClient.addMockResponse({
url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/summarize/`,
method: 'POST',
body: {
whatsWrong: 'Test summary',
},
});
render(
,
{
organization,
}
);
await waitFor(() => {
expect(screen.queryByTestId('loading-placeholder')).not.toBeInTheDocument();
});
expect(screen.getByRole('button', {name: 'Find Root Cause'})).toBeInTheDocument();
});
it('shows "Find Root Cause" when autofix is available', async () => {
// Mock successful autofix setup but disable resources
MockApiClient.addMockResponse({
url: `/issues/${mockGroup.id}/autofix/setup/`,
body: {
genAIConsent: {ok: true},
integration: {ok: true},
githubWriteIntegration: {ok: true},
},
});
MockApiClient.addMockResponse({
url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/summarize/`,
method: 'POST',
body: {
whatsWrong: 'Test summary',
},
});
jest.mocked(getConfigForIssueType).mockReturnValue({
...jest.mocked(getConfigForIssueType)(mockGroup, mockGroup.project),
resources: null,
});
render(
,
{
organization,
}
);
await waitFor(() => {
expect(screen.getByRole('button', {name: 'Find Root Cause'})).toBeInTheDocument();
});
});
it('shows "READ MORE" when only resources are available', async () => {
mockGroup.issueCategory = IssueCategory.UPTIME;
// Mock config with autofix disabled
MockApiClient.addMockResponse({
url: `/issues/${mockGroup.id}/autofix/setup/`,
body: {
genAIConsent: {ok: true},
integration: {ok: true},
githubWriteIntegration: {ok: true},
},
});
jest.mocked(getConfigForIssueType).mockReturnValue({
...jest.mocked(getConfigForIssueType)(mockGroup, mockGroup.project),
autofix: false,
issueSummary: {enabled: false},
resources: {
description: '',
links: [],
linksByPlatform: {},
},
});
render(
,
{
organization,
}
);
await waitFor(() => {
expect(screen.queryByTestId('loading-placeholder')).not.toBeInTheDocument();
});
expect(screen.getByRole('button', {name: 'READ MORE'})).toBeInTheDocument();
});
it('does not show CTA button when region is de', () => {
jest.mock('sentry/utils/regions');
jest.mocked(RegionUtils.getRegionDataFromOrganization).mockImplementation(() => ({
name: 'de',
displayName: 'Europe (Frankfurt)',
url: 'https://sentry.de.example.com',
}));
MockApiClient.addMockResponse({
url: `/issues/${mockGroup.id}/autofix/setup/`,
body: {
genAIConsent: {ok: true},
integration: {ok: true},
githubWriteIntegration: {ok: true},
},
});
MockApiClient.addMockResponse({
url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/summarize/`,
method: 'POST',
body: {
whatsWrong: 'Test summary',
},
});
jest.mocked(getConfigForIssueType).mockReturnValue({
...jest.mocked(getConfigForIssueType)(mockGroup, mockGroup.project),
resources: null,
});
render(
,
{
organization,
}
);
expect(screen.queryByTestId('loading-placeholder')).not.toBeInTheDocument();
expect(
screen.queryByRole('button', {name: 'Set Up Autofix'})
).not.toBeInTheDocument();
expect(
screen.queryByRole('button', {name: 'Find Root Cause'})
).not.toBeInTheDocument();
});
});
});