solutionsSection.spec.tsx 11 KB

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