index.spec.tsx 8.7 KB

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