solutionsSection.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. import {EventFixture} from 'sentry-fixture/event';
  2. import {FrameFixture} from 'sentry-fixture/frame';
  3. import {GroupFixture} from 'sentry-fixture/group';
  4. import {OrganizationFixture} from 'sentry-fixture/organization';
  5. import {ProjectFixture} from 'sentry-fixture/project';
  6. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  7. import {EntryType} from 'sentry/types/event';
  8. import {IssueCategory} from 'sentry/types/group';
  9. import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
  10. import type {IssueTypeConfig} from 'sentry/utils/issueTypeConfig/types';
  11. import * as RegionUtils from 'sentry/utils/regions';
  12. import SolutionsSection from 'sentry/views/issueDetails/streamline/sidebar/solutionsSection';
  13. import {Tab} from 'sentry/views/issueDetails/types';
  14. jest.mock('sentry/utils/issueTypeConfig');
  15. jest.mock('sentry/utils/regions');
  16. describe('SolutionsSection', () => {
  17. const mockEvent = EventFixture({
  18. entries: [
  19. {
  20. type: EntryType.EXCEPTION,
  21. data: {values: [{stacktrace: {frames: [FrameFixture()]}}]},
  22. },
  23. ],
  24. });
  25. const mockGroup = GroupFixture();
  26. const mockProject = ProjectFixture();
  27. const organization = OrganizationFixture({
  28. genAIConsent: true,
  29. hideAiFeatures: false,
  30. features: ['gen-ai-features'],
  31. });
  32. const mockIssueTypeConfig: IssueTypeConfig = {
  33. issueSummary: {
  34. enabled: true,
  35. },
  36. resources: {
  37. description: 'Test Resource',
  38. links: [{link: 'https://example.com', text: 'Test Link'}],
  39. linksByPlatform: {},
  40. },
  41. actions: {
  42. archiveUntilOccurrence: {enabled: false},
  43. delete: {enabled: false},
  44. deleteAndDiscard: {enabled: false},
  45. ignore: {enabled: false},
  46. merge: {enabled: false},
  47. resolve: {enabled: true},
  48. resolveInRelease: {enabled: false},
  49. share: {enabled: false},
  50. },
  51. customCopy: {
  52. resolution: 'Resolved',
  53. eventUnits: 'Events',
  54. },
  55. pages: {
  56. landingPage: Tab.DETAILS,
  57. events: {enabled: false},
  58. openPeriods: {enabled: false},
  59. checkIns: {enabled: false},
  60. uptimeChecks: {enabled: false},
  61. attachments: {enabled: false},
  62. userFeedback: {enabled: false},
  63. replays: {enabled: false},
  64. tagsTab: {enabled: false},
  65. },
  66. detector: {enabled: false},
  67. autofix: true,
  68. discover: {enabled: false},
  69. eventAndUserCounts: {enabled: true},
  70. evidence: null,
  71. header: {
  72. filterBar: {enabled: true, fixedEnvironment: false},
  73. graph: {enabled: true, type: 'discover-events'},
  74. tagDistribution: {enabled: false},
  75. occurrenceSummary: {enabled: false},
  76. },
  77. logLevel: {enabled: true},
  78. mergedIssues: {enabled: false},
  79. performanceDurationRegression: {enabled: false},
  80. profilingDurationRegression: {enabled: false},
  81. regression: {enabled: false},
  82. showFeedbackWidget: false,
  83. similarIssues: {enabled: false},
  84. spanEvidence: {enabled: false},
  85. stacktrace: {enabled: false},
  86. stats: {enabled: false},
  87. tags: {enabled: false},
  88. useOpenPeriodChecks: false,
  89. usesIssuePlatform: false,
  90. };
  91. beforeEach(() => {
  92. MockApiClient.clearMockResponses();
  93. MockApiClient.addMockResponse({
  94. url: `/issues/${mockGroup.id}/autofix/setup/`,
  95. body: {
  96. genAIConsent: {ok: true},
  97. integration: {ok: true},
  98. githubWriteIntegration: {ok: true},
  99. },
  100. });
  101. MockApiClient.addMockResponse({
  102. url: `/issues/${mockGroup.id}/autofix/`,
  103. body: {
  104. steps: [],
  105. },
  106. });
  107. jest.mocked(getConfigForIssueType).mockReturnValue(mockIssueTypeConfig);
  108. });
  109. it('renders loading state when summary is pending', () => {
  110. // Use a delayed response to simulate loading state
  111. MockApiClient.addMockResponse({
  112. url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/summarize/`,
  113. method: 'POST',
  114. statusCode: 200,
  115. body: new Promise(() => {}), // Never resolves, keeping the loading state
  116. });
  117. jest.mocked(getConfigForIssueType).mockReturnValue({
  118. ...mockIssueTypeConfig,
  119. autofix: false,
  120. });
  121. render(
  122. <SolutionsSection event={mockEvent} group={mockGroup} project={mockProject} />,
  123. {
  124. organization,
  125. }
  126. );
  127. expect(screen.getByText('Solutions Hub')).toBeInTheDocument();
  128. expect(screen.getAllByTestId('loading-placeholder')).toHaveLength(3); // what's wrong, possible cause, and CTA button
  129. });
  130. it('renders summary when AI features are enabled and data is available', async () => {
  131. const mockSummary = 'This is a test summary';
  132. MockApiClient.addMockResponse({
  133. url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/summarize/`,
  134. method: 'POST',
  135. body: {
  136. whatsWrong: mockSummary,
  137. },
  138. });
  139. render(
  140. <SolutionsSection event={mockEvent} group={mockGroup} project={mockProject} />,
  141. {
  142. organization,
  143. }
  144. );
  145. await waitFor(() => {
  146. expect(screen.getByText(mockSummary)).toBeInTheDocument();
  147. });
  148. });
  149. it('renders resources section when AI features are disabled', () => {
  150. const customOrganization = OrganizationFixture({
  151. hideAiFeatures: true,
  152. genAIConsent: false,
  153. features: ['gen-ai-features'],
  154. });
  155. render(
  156. <SolutionsSection event={mockEvent} group={mockGroup} project={mockProject} />,
  157. {
  158. organization: customOrganization,
  159. }
  160. );
  161. expect(screen.getByText('Test Link')).toBeInTheDocument();
  162. expect(screen.getByRole('button', {name: 'READ MORE'})).toBeInTheDocument();
  163. });
  164. it('toggles resources content when clicking Read More/Show Less', async () => {
  165. const customOrganization = OrganizationFixture({
  166. hideAiFeatures: true,
  167. genAIConsent: false,
  168. });
  169. render(
  170. <SolutionsSection event={mockEvent} group={mockGroup} project={mockProject} />,
  171. {
  172. organization: customOrganization,
  173. }
  174. );
  175. const readMoreButton = screen.getByRole('button', {name: 'READ MORE'});
  176. await userEvent.click(readMoreButton);
  177. expect(screen.getByRole('button', {name: 'SHOW LESS'})).toBeInTheDocument();
  178. const showLessButton = screen.getByRole('button', {name: 'SHOW LESS'});
  179. await userEvent.click(showLessButton);
  180. expect(screen.queryByRole('button', {name: 'SHOW LESS'})).not.toBeInTheDocument();
  181. expect(screen.getByRole('button', {name: 'READ MORE'})).toBeInTheDocument();
  182. });
  183. describe('Solutions Hub button text', () => {
  184. it('shows "Set Up Autofix" when AI needs setup', async () => {
  185. const customOrganization = OrganizationFixture({
  186. genAIConsent: false,
  187. hideAiFeatures: false,
  188. features: ['gen-ai-features'],
  189. });
  190. MockApiClient.addMockResponse({
  191. url: `/issues/${mockGroup.id}/autofix/setup/`,
  192. body: {
  193. genAIConsent: {ok: false},
  194. integration: {ok: false},
  195. githubWriteIntegration: {ok: false},
  196. },
  197. });
  198. render(
  199. <SolutionsSection event={mockEvent} group={mockGroup} project={mockProject} />,
  200. {
  201. organization: customOrganization,
  202. }
  203. );
  204. await waitFor(() => {
  205. expect(screen.queryByTestId('loading-placeholder')).not.toBeInTheDocument();
  206. });
  207. expect(
  208. screen.getByText('Explore potential root causes and solutions with Autofix.')
  209. ).toBeInTheDocument();
  210. expect(screen.getByRole('button', {name: 'Set Up Autofix'})).toBeInTheDocument();
  211. });
  212. it('shows "Find Root Cause" even when autofix needs setup', async () => {
  213. MockApiClient.addMockResponse({
  214. url: `/issues/${mockGroup.id}/autofix/setup/`,
  215. body: {
  216. genAIConsent: {ok: true},
  217. integration: {ok: false},
  218. githubWriteIntegration: {ok: false},
  219. },
  220. });
  221. MockApiClient.addMockResponse({
  222. url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/summarize/`,
  223. method: 'POST',
  224. body: {
  225. whatsWrong: 'Test summary',
  226. },
  227. });
  228. render(
  229. <SolutionsSection event={mockEvent} group={mockGroup} project={mockProject} />,
  230. {
  231. organization,
  232. }
  233. );
  234. await waitFor(() => {
  235. expect(screen.queryByTestId('loading-placeholder')).not.toBeInTheDocument();
  236. });
  237. expect(screen.getByRole('button', {name: 'Find Root Cause'})).toBeInTheDocument();
  238. });
  239. it('shows "Find Root Cause" when autofix is available', async () => {
  240. // Mock successful autofix setup but disable resources
  241. MockApiClient.addMockResponse({
  242. url: `/issues/${mockGroup.id}/autofix/setup/`,
  243. body: {
  244. genAIConsent: {ok: true},
  245. integration: {ok: true},
  246. githubWriteIntegration: {ok: true},
  247. },
  248. });
  249. MockApiClient.addMockResponse({
  250. url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/summarize/`,
  251. method: 'POST',
  252. body: {
  253. whatsWrong: 'Test summary',
  254. },
  255. });
  256. jest.mocked(getConfigForIssueType).mockReturnValue({
  257. ...jest.mocked(getConfigForIssueType)(mockGroup, mockGroup.project),
  258. resources: null,
  259. });
  260. render(
  261. <SolutionsSection event={mockEvent} group={mockGroup} project={mockProject} />,
  262. {
  263. organization,
  264. }
  265. );
  266. await waitFor(() => {
  267. expect(screen.getByRole('button', {name: 'Find Root Cause'})).toBeInTheDocument();
  268. });
  269. });
  270. it('shows "READ MORE" when only resources are available', async () => {
  271. mockGroup.issueCategory = IssueCategory.UPTIME;
  272. // Mock config with autofix disabled
  273. MockApiClient.addMockResponse({
  274. url: `/issues/${mockGroup.id}/autofix/setup/`,
  275. body: {
  276. genAIConsent: {ok: true},
  277. integration: {ok: true},
  278. githubWriteIntegration: {ok: true},
  279. },
  280. });
  281. jest.mocked(getConfigForIssueType).mockReturnValue({
  282. ...jest.mocked(getConfigForIssueType)(mockGroup, mockGroup.project),
  283. autofix: false,
  284. issueSummary: {enabled: false},
  285. resources: {
  286. description: '',
  287. links: [],
  288. linksByPlatform: {},
  289. },
  290. });
  291. render(
  292. <SolutionsSection event={mockEvent} group={mockGroup} project={mockProject} />,
  293. {
  294. organization,
  295. }
  296. );
  297. await waitFor(() => {
  298. expect(screen.queryByTestId('loading-placeholder')).not.toBeInTheDocument();
  299. });
  300. expect(screen.getByRole('button', {name: 'READ MORE'})).toBeInTheDocument();
  301. });
  302. it('does not show CTA button when region is de', () => {
  303. jest.mock('sentry/utils/regions');
  304. jest.mocked(RegionUtils.getRegionDataFromOrganization).mockImplementation(() => ({
  305. name: 'de',
  306. displayName: 'Europe (Frankfurt)',
  307. url: 'https://sentry.de.example.com',
  308. }));
  309. MockApiClient.addMockResponse({
  310. url: `/issues/${mockGroup.id}/autofix/setup/`,
  311. body: {
  312. genAIConsent: {ok: true},
  313. integration: {ok: true},
  314. githubWriteIntegration: {ok: true},
  315. },
  316. });
  317. MockApiClient.addMockResponse({
  318. url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/summarize/`,
  319. method: 'POST',
  320. body: {
  321. whatsWrong: 'Test summary',
  322. },
  323. });
  324. jest.mocked(getConfigForIssueType).mockReturnValue({
  325. ...jest.mocked(getConfigForIssueType)(mockGroup, mockGroup.project),
  326. resources: null,
  327. });
  328. render(
  329. <SolutionsSection event={mockEvent} group={mockGroup} project={mockProject} />,
  330. {
  331. organization,
  332. }
  333. );
  334. expect(screen.queryByTestId('loading-placeholder')).not.toBeInTheDocument();
  335. expect(
  336. screen.queryByRole('button', {name: 'Set Up Autofix'})
  337. ).not.toBeInTheDocument();
  338. expect(
  339. screen.queryByRole('button', {name: 'Find Root Cause'})
  340. ).not.toBeInTheDocument();
  341. });
  342. });
  343. });