index.spec.tsx 11 KB

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