index.spec.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  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';
  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;
  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. router={router}
  58. routeParams={router.params}
  59. routes={router.routes}
  60. route={{}}
  61. />,
  62. {router}
  63. );
  64. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  65. await waitFor(() => expect(mock).toHaveBeenCalled());
  66. expect(await screen.findByText('Show 3 issues below threshold')).toBeInTheDocument();
  67. });
  68. it('can merge and redirect to new parent', async function () {
  69. const merge = MockApiClient.addMockResponse({
  70. method: 'PUT',
  71. url: '/projects/org-slug/project-slug/issues/',
  72. body: {
  73. merge: {children: ['123'], parent: '321'},
  74. },
  75. });
  76. render(
  77. <GroupSimilarIssues
  78. project={project}
  79. params={{orgId: 'org-slug', groupId: 'group-id'}}
  80. location={router.location}
  81. router={router}
  82. routeParams={router.params}
  83. routes={router.routes}
  84. route={{}}
  85. />,
  86. {router}
  87. );
  88. renderGlobalModal();
  89. await selectNthSimilarItem(0);
  90. await userEvent.click(await screen.findByRole('button', {name: 'Merge (1)'}));
  91. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  92. await waitFor(() => {
  93. expect(merge).toHaveBeenCalledWith(
  94. '/projects/org-slug/project-slug/issues/',
  95. expect.objectContaining({
  96. data: {merge: 1},
  97. })
  98. );
  99. });
  100. expect(MockNavigate).toHaveBeenCalledWith(
  101. '/organizations/org-slug/issues/321/similar/'
  102. );
  103. });
  104. it('correctly shows merge count', async function () {
  105. render(
  106. <GroupSimilarIssues
  107. project={project}
  108. params={{orgId: 'org-slug', groupId: 'group-id'}}
  109. location={router.location}
  110. router={router}
  111. routeParams={router.params}
  112. routes={router.routes}
  113. route={{}}
  114. />,
  115. {router}
  116. );
  117. renderGlobalModal();
  118. await selectNthSimilarItem(0);
  119. expect(screen.getByText('Merge (1)')).toBeInTheDocument();
  120. // Correctly show "Merge (0)" when the item is un-clicked
  121. await selectNthSimilarItem(0);
  122. expect(screen.getByText('Merge (0)')).toBeInTheDocument();
  123. });
  124. it('shows empty message', async function () {
  125. // Manually clear responses and add an empty response
  126. MockApiClient.clearMockResponses();
  127. jest.clearAllMocks();
  128. mock = MockApiClient.addMockResponse({
  129. url: '/organizations/org-slug/issues/group-id/similar/?limit=50',
  130. body: [],
  131. });
  132. render(
  133. <GroupSimilarIssues
  134. project={project}
  135. params={{orgId: 'org-slug', groupId: 'group-id'}}
  136. location={router.location}
  137. router={router}
  138. routeParams={router.params}
  139. routes={router.routes}
  140. route={{}}
  141. />,
  142. {router}
  143. );
  144. renderGlobalModal();
  145. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  146. await waitFor(() => expect(mock).toHaveBeenCalled());
  147. expect(
  148. await screen.findByText("There don't seem to be any similar issues.")
  149. ).toBeInTheDocument();
  150. expect(
  151. screen.queryByText(
  152. 'This can occur when the issue has no stacktrace or in-app frames.'
  153. )
  154. ).not.toBeInTheDocument();
  155. });
  156. });
  157. describe('Issues Similar Embeddings View', function () {
  158. let mock;
  159. const project = ProjectFixture({
  160. features: ['similarity-view', 'similarity-embeddings'],
  161. });
  162. const router = RouterFixture({
  163. params: {orgId: 'org-slug', projectId: 'project-slug', groupId: 'group-id'},
  164. });
  165. const similarEmbeddingsScores = [
  166. {exception: 0.01, message: 0.3748, shouldBeGrouped: 'Yes'},
  167. {exception: 0.005, message: 0.3738, shouldBeGrouped: 'Yes'},
  168. {exception: 0.7384, message: 0.3743, shouldBeGrouped: 'No'},
  169. {exception: 0.3849, message: 0.4738, shouldBeGrouped: 'No'},
  170. ];
  171. const mockData = {
  172. simlarEmbeddings: GroupsFixture().map((issue, i) => [
  173. issue,
  174. similarEmbeddingsScores[i],
  175. ]),
  176. };
  177. beforeEach(function () {
  178. mock = MockApiClient.addMockResponse({
  179. url: '/organizations/org-slug/issues/group-id/similar-issues-embeddings/?k=10&threshold=0.01',
  180. body: mockData.simlarEmbeddings,
  181. });
  182. });
  183. afterEach(() => {
  184. MockApiClient.clearMockResponses();
  185. jest.clearAllMocks();
  186. });
  187. const selectNthSimilarItem = async (index: number) => {
  188. const items = await screen.findAllByTestId('similar-item-row');
  189. const item = items.at(index);
  190. expect(item).toBeDefined();
  191. await userEvent.click(item!);
  192. };
  193. it('renders with mocked data', async function () {
  194. render(
  195. <GroupSimilarIssues
  196. project={project}
  197. params={{orgId: 'org-slug', groupId: 'group-id'}}
  198. location={router.location}
  199. router={router}
  200. routeParams={router.params}
  201. routes={router.routes}
  202. route={{}}
  203. />,
  204. {router}
  205. );
  206. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  207. await waitFor(() => expect(mock).toHaveBeenCalled());
  208. expect(await screen.findByText('Would Group')).toBeInTheDocument();
  209. expect(screen.queryByText('Show 3 issues below threshold')).not.toBeInTheDocument();
  210. });
  211. it('can merge and redirect to new parent', async function () {
  212. const merge = MockApiClient.addMockResponse({
  213. method: 'PUT',
  214. url: '/projects/org-slug/project-slug/issues/',
  215. body: {
  216. merge: {children: ['123'], parent: '321'},
  217. },
  218. });
  219. render(
  220. <GroupSimilarIssues
  221. project={project}
  222. params={{orgId: 'org-slug', groupId: 'group-id'}}
  223. location={router.location}
  224. router={router}
  225. routeParams={router.params}
  226. routes={router.routes}
  227. route={{}}
  228. />,
  229. {router}
  230. );
  231. renderGlobalModal();
  232. await selectNthSimilarItem(0);
  233. await userEvent.click(await screen.findByRole('button', {name: 'Merge (1)'}));
  234. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  235. await waitFor(() => {
  236. expect(merge).toHaveBeenCalledWith(
  237. '/projects/org-slug/project-slug/issues/',
  238. expect.objectContaining({
  239. data: {merge: 1},
  240. })
  241. );
  242. });
  243. expect(MockNavigate).toHaveBeenCalledWith(
  244. '/organizations/org-slug/issues/321/similar/'
  245. );
  246. });
  247. it('correctly shows merge count', async function () {
  248. render(
  249. <GroupSimilarIssues
  250. project={project}
  251. params={{orgId: 'org-slug', groupId: 'group-id'}}
  252. location={router.location}
  253. router={router}
  254. routeParams={router.params}
  255. routes={router.routes}
  256. route={{}}
  257. />,
  258. {router}
  259. );
  260. renderGlobalModal();
  261. await selectNthSimilarItem(0);
  262. expect(screen.getByText('Merge (1)')).toBeInTheDocument();
  263. // Correctly show "Merge (0)" when the item is un-clicked
  264. await selectNthSimilarItem(0);
  265. expect(screen.getByText('Merge (0)')).toBeInTheDocument();
  266. });
  267. it('sends issue similarity embeddings agree analytics', async function () {
  268. render(
  269. <GroupSimilarIssues
  270. project={project}
  271. params={{orgId: 'org-slug', groupId: 'group-id'}}
  272. location={router.location}
  273. router={router}
  274. routeParams={router.params}
  275. routes={router.routes}
  276. route={{}}
  277. />,
  278. {router}
  279. );
  280. renderGlobalModal();
  281. await selectNthSimilarItem(0);
  282. await userEvent.click(await screen.findByRole('button', {name: 'Agree (1)'}));
  283. expect(trackAnalytics).toHaveBeenCalledTimes(1);
  284. expect(trackAnalytics).toHaveBeenCalledWith(
  285. 'issue_details.similar_issues.similarity_embeddings_feedback_recieved',
  286. expect.objectContaining({
  287. projectId: project.id,
  288. groupId: 'group-id',
  289. value: 'Yes',
  290. wouldGroup: similarEmbeddingsScores[0].shouldBeGrouped,
  291. })
  292. );
  293. });
  294. it('shows empty message', async function () {
  295. // Manually clear responses and add an empty response
  296. MockApiClient.clearMockResponses();
  297. jest.clearAllMocks();
  298. mock = MockApiClient.addMockResponse({
  299. url: '/organizations/org-slug/issues/group-id/similar-issues-embeddings/?k=10&threshold=0.01',
  300. body: [],
  301. });
  302. render(
  303. <GroupSimilarIssues
  304. project={project}
  305. params={{orgId: 'org-slug', groupId: 'group-id'}}
  306. location={router.location}
  307. router={router}
  308. routeParams={router.params}
  309. routes={router.routes}
  310. route={{}}
  311. />,
  312. {router}
  313. );
  314. renderGlobalModal();
  315. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  316. await waitFor(() => expect(mock).toHaveBeenCalled());
  317. expect(
  318. await screen.findByText(
  319. "There don't seem to be any similar issues. This can occur when the issue has no stacktrace or in-app frames."
  320. )
  321. ).toBeInTheDocument();
  322. });
  323. });