solutionsSection.spec.tsx 11 KB

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