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(); }); }); });