solutionsSection.spec.tsx 10 KB

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