similarIssues.spec.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. import {GroupsFixture} from 'sentry-fixture/groups';
  2. import {ProjectFixture} from 'sentry-fixture/project';
  3. import {RouterFixture} from 'sentry-fixture/routerFixture';
  4. import {
  5. render,
  6. renderGlobalModal,
  7. screen,
  8. userEvent,
  9. waitFor,
  10. } from 'sentry-test/reactTestingLibrary';
  11. import {trackAnalytics} from 'sentry/utils/analytics';
  12. import GroupSimilarIssues from 'sentry/views/issueDetails/groupSimilarIssues/similarIssues';
  13. const MockNavigate = jest.fn();
  14. jest.mock('sentry/utils/useNavigate', () => ({
  15. useNavigate: () => MockNavigate,
  16. }));
  17. jest.mock('sentry/utils/analytics');
  18. describe('Issues Similar View', function () {
  19. let mock: jest.Mock;
  20. const project = ProjectFixture({
  21. features: ['similarity-view'],
  22. });
  23. const router = RouterFixture({
  24. params: {orgId: 'org-slug', projectId: 'project-slug', groupId: 'group-id'},
  25. });
  26. const scores = [
  27. {'exception:stacktrace:pairs': 0.375},
  28. {'exception:stacktrace:pairs': 0.01264},
  29. {'exception:stacktrace:pairs': 0.875},
  30. {'exception:stacktrace:pairs': 0.001488},
  31. ];
  32. const mockData = {
  33. similar: GroupsFixture().map((issue, i) => [issue, scores[i]]),
  34. };
  35. beforeEach(function () {
  36. mock = MockApiClient.addMockResponse({
  37. url: '/organizations/org-slug/issues/group-id/similar/?limit=50',
  38. body: mockData.similar,
  39. });
  40. });
  41. afterEach(() => {
  42. MockApiClient.clearMockResponses();
  43. jest.clearAllMocks();
  44. });
  45. const selectNthSimilarItem = async (index: number) => {
  46. const items = await screen.findAllByTestId('similar-item-row');
  47. const item = items.at(index);
  48. expect(item).toBeDefined();
  49. await userEvent.click(item!);
  50. };
  51. it('renders with mocked data', async function () {
  52. render(
  53. <GroupSimilarIssues
  54. project={project}
  55. params={{orgId: 'org-slug', groupId: 'group-id'}}
  56. location={router.location}
  57. />,
  58. {router}
  59. );
  60. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  61. await waitFor(() => expect(mock).toHaveBeenCalled());
  62. expect(await screen.findByText('Show 3 issues below threshold')).toBeInTheDocument();
  63. });
  64. it('can merge and redirect to new parent', async function () {
  65. const merge = MockApiClient.addMockResponse({
  66. method: 'PUT',
  67. url: '/projects/org-slug/project-slug/issues/',
  68. body: {
  69. merge: {children: ['123'], parent: '321'},
  70. },
  71. });
  72. render(
  73. <GroupSimilarIssues
  74. project={project}
  75. params={{orgId: 'org-slug', groupId: 'group-id'}}
  76. location={router.location}
  77. />,
  78. {router}
  79. );
  80. renderGlobalModal();
  81. await selectNthSimilarItem(0);
  82. await userEvent.click(await screen.findByRole('button', {name: 'Merge (1)'}));
  83. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  84. await waitFor(() => {
  85. expect(merge).toHaveBeenCalledWith(
  86. '/projects/org-slug/project-slug/issues/',
  87. expect.objectContaining({
  88. data: {merge: 1},
  89. })
  90. );
  91. });
  92. expect(MockNavigate).toHaveBeenCalledWith(
  93. '/organizations/org-slug/issues/321/similar/'
  94. );
  95. });
  96. it('correctly shows merge count', async function () {
  97. render(
  98. <GroupSimilarIssues
  99. project={project}
  100. params={{orgId: 'org-slug', groupId: 'group-id'}}
  101. location={router.location}
  102. />,
  103. {router}
  104. );
  105. renderGlobalModal();
  106. await selectNthSimilarItem(0);
  107. expect(screen.getByText('Merge (1)')).toBeInTheDocument();
  108. // Correctly show "Merge (0)" when the item is un-clicked
  109. await selectNthSimilarItem(0);
  110. expect(screen.getByText('Merge (0)')).toBeInTheDocument();
  111. });
  112. it('shows empty message', async function () {
  113. // Manually clear responses and add an empty response
  114. MockApiClient.clearMockResponses();
  115. jest.clearAllMocks();
  116. mock = MockApiClient.addMockResponse({
  117. url: '/organizations/org-slug/issues/group-id/similar/?limit=50',
  118. body: [],
  119. });
  120. render(
  121. <GroupSimilarIssues
  122. project={project}
  123. params={{orgId: 'org-slug', groupId: 'group-id'}}
  124. location={router.location}
  125. />,
  126. {router}
  127. );
  128. renderGlobalModal();
  129. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  130. await waitFor(() => expect(mock).toHaveBeenCalled());
  131. expect(
  132. await screen.findByText("There don't seem to be any similar issues.")
  133. ).toBeInTheDocument();
  134. expect(
  135. screen.queryByText(
  136. 'This can occur when the issue has no stacktrace or in-app frames.'
  137. )
  138. ).not.toBeInTheDocument();
  139. });
  140. });
  141. describe('Issues Similar Embeddings View', function () {
  142. let mock;
  143. const project = ProjectFixture({
  144. features: ['similarity-view', 'similarity-embeddings'],
  145. });
  146. const router = RouterFixture({
  147. params: {orgId: 'org-slug', projectId: 'project-slug', groupId: 'group-id'},
  148. });
  149. const similarEmbeddingsScores = [
  150. {exception: 0.01, message: 0.3748, shouldBeGrouped: 'Yes'},
  151. {exception: 0.005, message: 0.3738, shouldBeGrouped: 'Yes'},
  152. {exception: 0.7384, message: 0.3743, shouldBeGrouped: 'No'},
  153. {exception: 0.3849, message: 0.4738, shouldBeGrouped: 'No'},
  154. ];
  155. const mockData = {
  156. simlarEmbeddings: GroupsFixture().map((issue, i) => [
  157. issue,
  158. similarEmbeddingsScores[i],
  159. ]),
  160. };
  161. beforeEach(function () {
  162. mock = MockApiClient.addMockResponse({
  163. url: '/organizations/org-slug/issues/group-id/similar-issues-embeddings/?k=10&threshold=0.01&useReranking=true',
  164. body: mockData.simlarEmbeddings,
  165. });
  166. });
  167. afterEach(() => {
  168. MockApiClient.clearMockResponses();
  169. jest.clearAllMocks();
  170. });
  171. const selectNthSimilarItem = async (index: number) => {
  172. const items = await screen.findAllByTestId('similar-item-row');
  173. const item = items.at(index);
  174. expect(item).toBeDefined();
  175. await userEvent.click(item!);
  176. };
  177. it('renders with mocked data', async function () {
  178. render(
  179. <GroupSimilarIssues
  180. project={project}
  181. params={{orgId: 'org-slug', groupId: 'group-id'}}
  182. location={router.location}
  183. />,
  184. {router}
  185. );
  186. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  187. await waitFor(() => expect(mock).toHaveBeenCalled());
  188. expect(await screen.findByText('Would Group')).toBeInTheDocument();
  189. expect(screen.queryByText('Show 3 issues below threshold')).not.toBeInTheDocument();
  190. });
  191. it('can merge and redirect to new parent', async function () {
  192. const merge = MockApiClient.addMockResponse({
  193. method: 'PUT',
  194. url: '/projects/org-slug/project-slug/issues/',
  195. body: {
  196. merge: {children: ['123'], parent: '321'},
  197. },
  198. });
  199. render(
  200. <GroupSimilarIssues
  201. project={project}
  202. params={{orgId: 'org-slug', groupId: 'group-id'}}
  203. location={router.location}
  204. />,
  205. {router}
  206. );
  207. renderGlobalModal();
  208. await selectNthSimilarItem(0);
  209. await userEvent.click(await screen.findByRole('button', {name: 'Merge (1)'}));
  210. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  211. await waitFor(() => {
  212. expect(merge).toHaveBeenCalledWith(
  213. '/projects/org-slug/project-slug/issues/',
  214. expect.objectContaining({
  215. data: {merge: 1},
  216. })
  217. );
  218. });
  219. expect(MockNavigate).toHaveBeenCalledWith(
  220. '/organizations/org-slug/issues/321/similar/'
  221. );
  222. });
  223. it('correctly shows merge count', async function () {
  224. render(
  225. <GroupSimilarIssues
  226. project={project}
  227. params={{orgId: 'org-slug', groupId: 'group-id'}}
  228. location={router.location}
  229. />,
  230. {router}
  231. );
  232. renderGlobalModal();
  233. await selectNthSimilarItem(0);
  234. expect(screen.getByText('Merge (1)')).toBeInTheDocument();
  235. // Correctly show "Merge (0)" when the item is un-clicked
  236. await selectNthSimilarItem(0);
  237. expect(screen.getByText('Merge (0)')).toBeInTheDocument();
  238. });
  239. it('sends issue similarity embeddings agree analytics', async function () {
  240. render(
  241. <GroupSimilarIssues
  242. project={project}
  243. params={{orgId: 'org-slug', groupId: 'group-id'}}
  244. location={router.location}
  245. />,
  246. {router}
  247. );
  248. renderGlobalModal();
  249. await selectNthSimilarItem(0);
  250. await userEvent.click(await screen.findByRole('button', {name: 'Agree (1)'}));
  251. expect(trackAnalytics).toHaveBeenCalledTimes(1);
  252. expect(trackAnalytics).toHaveBeenCalledWith(
  253. 'issue_details.similar_issues.similarity_embeddings_feedback_recieved',
  254. expect.objectContaining({
  255. projectId: project.id,
  256. groupId: 'group-id',
  257. value: 'Yes',
  258. wouldGroup: similarEmbeddingsScores[0].shouldBeGrouped,
  259. })
  260. );
  261. });
  262. it('shows empty message', async function () {
  263. // Manually clear responses and add an empty response
  264. MockApiClient.clearMockResponses();
  265. jest.clearAllMocks();
  266. mock = MockApiClient.addMockResponse({
  267. url: '/organizations/org-slug/issues/group-id/similar-issues-embeddings/?k=10&threshold=0.01&useReranking=true',
  268. body: [],
  269. });
  270. render(
  271. <GroupSimilarIssues
  272. project={project}
  273. params={{orgId: 'org-slug', groupId: 'group-id'}}
  274. location={router.location}
  275. />,
  276. {router}
  277. );
  278. renderGlobalModal();
  279. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  280. await waitFor(() => expect(mock).toHaveBeenCalled());
  281. expect(
  282. await screen.findByText(
  283. "There don't seem to be any similar issues. This can occur when the issue has no stacktrace or in-app frames."
  284. )
  285. ).toBeInTheDocument();
  286. });
  287. });