similarIssues.spec.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import {GroupFixture} from 'sentry-fixture/group';
  2. import {GroupsFixture} from 'sentry-fixture/groups';
  3. import {ProjectFixture} from 'sentry-fixture/project';
  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 ProjectsStore from 'sentry/stores/projectsStore';
  13. import GroupSimilarIssues from 'sentry/views/issueDetails/groupSimilarIssues/similarIssues';
  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: jest.Mock;
  21. const project = ProjectFixture({
  22. features: ['similarity-view'],
  23. });
  24. const group = GroupFixture();
  25. const router = RouterFixture({
  26. params: {orgId: 'org-slug', groupId: group.id},
  27. });
  28. const scores = [
  29. {'exception:stacktrace:pairs': 0.375},
  30. {'exception:stacktrace:pairs': 0.01264},
  31. {'exception:stacktrace:pairs': 0.875},
  32. {'exception:stacktrace:pairs': 0.001488},
  33. ];
  34. const mockData = {
  35. similar: GroupsFixture().map((issue, i) => [issue, scores[i]]),
  36. };
  37. beforeEach(function () {
  38. mock = MockApiClient.addMockResponse({
  39. url: `/organizations/org-slug/issues/${group.id}/similar/?limit=50`,
  40. body: mockData.similar,
  41. });
  42. MockApiClient.addMockResponse({
  43. url: `/organizations/org-slug/issues/${group.id}/`,
  44. body: group,
  45. });
  46. MockApiClient.addMockResponse({
  47. url: `/projects/org-slug/project-slug/`,
  48. body: {features: ['similarity-view']},
  49. });
  50. ProjectsStore.init();
  51. ProjectsStore.loadInitialData([project]);
  52. });
  53. afterEach(() => {
  54. MockApiClient.clearMockResponses();
  55. jest.clearAllMocks();
  56. });
  57. const selectNthSimilarItem = async (index: number) => {
  58. const items = await screen.findAllByTestId('similar-item-row');
  59. const item = items.at(index);
  60. expect(item).toBeDefined();
  61. await userEvent.click(item!);
  62. };
  63. it('renders with mocked data', async function () {
  64. render(<GroupSimilarIssues />, {router});
  65. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  66. await waitFor(() => expect(mock).toHaveBeenCalled());
  67. expect(await screen.findByText('Show 3 issues below threshold')).toBeInTheDocument();
  68. });
  69. it('can merge and redirect to new parent', async function () {
  70. const merge = MockApiClient.addMockResponse({
  71. method: 'PUT',
  72. url: '/projects/org-slug/project-slug/issues/',
  73. body: {
  74. merge: {children: ['123'], parent: '321'},
  75. },
  76. });
  77. render(<GroupSimilarIssues />, {router});
  78. renderGlobalModal();
  79. await selectNthSimilarItem(0);
  80. await userEvent.click(await screen.findByRole('button', {name: 'Merge (1)'}));
  81. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  82. await waitFor(() => {
  83. expect(merge).toHaveBeenCalledWith(
  84. '/projects/org-slug/project-slug/issues/',
  85. expect.objectContaining({
  86. data: {merge: 1},
  87. })
  88. );
  89. });
  90. expect(MockNavigate).toHaveBeenCalledWith(
  91. '/organizations/org-slug/issues/321/similar/'
  92. );
  93. });
  94. it('correctly shows merge count', async function () {
  95. render(<GroupSimilarIssues />, {router});
  96. renderGlobalModal();
  97. await selectNthSimilarItem(0);
  98. expect(screen.getByText('Merge (1)')).toBeInTheDocument();
  99. // Correctly show "Merge (0)" when the item is un-clicked
  100. await selectNthSimilarItem(0);
  101. expect(screen.getByText('Merge (0)')).toBeInTheDocument();
  102. });
  103. it('shows empty message', async function () {
  104. mock = MockApiClient.addMockResponse({
  105. url: `/organizations/org-slug/issues/${group.id}/similar/?limit=50`,
  106. body: [],
  107. });
  108. render(<GroupSimilarIssues />, {router});
  109. renderGlobalModal();
  110. await waitFor(() => expect(mock).toHaveBeenCalled());
  111. expect(
  112. await screen.findByText("There don't seem to be any similar issues.")
  113. ).toBeInTheDocument();
  114. expect(
  115. screen.queryByText(
  116. 'This can occur when the issue has no stacktrace or in-app frames.'
  117. )
  118. ).not.toBeInTheDocument();
  119. });
  120. });
  121. describe('Issues Similar Embeddings View', function () {
  122. let mock: jest.Mock;
  123. const group = GroupFixture();
  124. const project = ProjectFixture({
  125. features: ['similarity-view'],
  126. });
  127. const router = RouterFixture({
  128. params: {orgId: 'org-slug', groupId: group.id},
  129. });
  130. const similarEmbeddingsScores = [
  131. {exception: 0.01, shouldBeGrouped: 'Yes'},
  132. {exception: 0.005, shouldBeGrouped: 'Yes'},
  133. {exception: 0.7384, shouldBeGrouped: 'No'},
  134. {exception: 0.3849, shouldBeGrouped: 'No'},
  135. ];
  136. const mockData = {
  137. similarEmbeddings: GroupsFixture().map((issue, i) => [
  138. issue,
  139. similarEmbeddingsScores[i],
  140. ]),
  141. };
  142. beforeEach(function () {
  143. mock = MockApiClient.addMockResponse({
  144. url: `/organizations/org-slug/issues/${group.id}/similar-issues-embeddings/?k=10&threshold=0.01&useReranking=true`,
  145. body: mockData.similarEmbeddings,
  146. });
  147. MockApiClient.addMockResponse({
  148. url: `/organizations/org-slug/issues/${group.id}/`,
  149. body: group,
  150. });
  151. MockApiClient.addMockResponse({
  152. url: `/projects/org-slug/project-slug/`,
  153. body: {features: ['similarity-embeddings']},
  154. });
  155. ProjectsStore.init();
  156. ProjectsStore.loadInitialData([project]);
  157. });
  158. afterEach(() => {
  159. MockApiClient.clearMockResponses();
  160. jest.clearAllMocks();
  161. });
  162. const selectNthSimilarItem = async (index: number) => {
  163. const items = await screen.findAllByTestId('similar-item-row');
  164. const item = items.at(index);
  165. expect(item).toBeDefined();
  166. await userEvent.click(item!);
  167. };
  168. it('renders with mocked data', async function () {
  169. render(<GroupSimilarIssues />, {router});
  170. await waitFor(() => expect(mock).toHaveBeenCalled());
  171. expect(screen.queryByText('Show 3 issues below threshold')).not.toBeInTheDocument();
  172. });
  173. it('can merge and redirect to new parent', async function () {
  174. const merge = MockApiClient.addMockResponse({
  175. method: 'PUT',
  176. url: '/projects/org-slug/project-slug/issues/',
  177. body: {
  178. merge: {children: ['123'], parent: '321'},
  179. },
  180. });
  181. render(<GroupSimilarIssues />, {router});
  182. renderGlobalModal();
  183. await selectNthSimilarItem(0);
  184. await userEvent.click(await screen.findByRole('button', {name: 'Merge (1)'}));
  185. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  186. await waitFor(() => {
  187. expect(merge).toHaveBeenCalledWith(
  188. '/projects/org-slug/project-slug/issues/',
  189. expect.objectContaining({
  190. data: {merge: 1},
  191. })
  192. );
  193. });
  194. expect(MockNavigate).toHaveBeenCalledWith(
  195. '/organizations/org-slug/issues/321/similar/'
  196. );
  197. });
  198. it('correctly shows merge count', async function () {
  199. render(<GroupSimilarIssues />, {router});
  200. renderGlobalModal();
  201. await selectNthSimilarItem(0);
  202. expect(screen.getByText('Merge (1)')).toBeInTheDocument();
  203. // Correctly show "Merge (0)" when the item is un-clicked
  204. await selectNthSimilarItem(0);
  205. expect(screen.getByText('Merge (0)')).toBeInTheDocument();
  206. });
  207. it('shows empty message', async function () {
  208. mock = MockApiClient.addMockResponse({
  209. url: `/organizations/org-slug/issues/${group.id}/similar-issues-embeddings/?k=10&threshold=0.01&useReranking=true`,
  210. body: [],
  211. });
  212. render(<GroupSimilarIssues />, {router});
  213. renderGlobalModal();
  214. await waitFor(() => expect(mock).toHaveBeenCalled());
  215. expect(
  216. await screen.findByText(
  217. "There don't seem to be any similar issues. This can occur when the issue has no stacktrace or in-app frames."
  218. )
  219. ).toBeInTheDocument();
  220. });
  221. });