import {GroupFixture} from 'sentry-fixture/group'; import {GroupsFixture} from 'sentry-fixture/groups'; import {ProjectFixture} from 'sentry-fixture/project'; import {RouterFixture} from 'sentry-fixture/routerFixture'; import { render, renderGlobalModal, screen, userEvent, waitFor, } from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; import GroupSimilarIssues from 'sentry/views/issueDetails/groupSimilarIssues/similarIssues'; const MockNavigate = jest.fn(); jest.mock('sentry/utils/useNavigate', () => ({ useNavigate: () => MockNavigate, })); jest.mock('sentry/utils/analytics'); describe('Issues Similar View', function () { let mock: jest.Mock; const project = ProjectFixture({ features: ['similarity-view'], }); const group = GroupFixture(); const router = RouterFixture({ params: {orgId: 'org-slug', groupId: group.id}, }); const scores = [ {'exception:stacktrace:pairs': 0.375}, {'exception:stacktrace:pairs': 0.01264}, {'exception:stacktrace:pairs': 0.875}, {'exception:stacktrace:pairs': 0.001488}, ]; const mockData = { similar: GroupsFixture().map((issue, i) => [issue, scores[i]]), }; beforeEach(function () { mock = MockApiClient.addMockResponse({ url: `/organizations/org-slug/issues/${group.id}/similar/?limit=50`, body: mockData.similar, }); MockApiClient.addMockResponse({ url: `/organizations/org-slug/issues/${group.id}/`, body: group, }); MockApiClient.addMockResponse({ url: `/projects/org-slug/project-slug/`, body: {features: ['similarity-view']}, }); ProjectsStore.init(); ProjectsStore.loadInitialData([project]); }); afterEach(() => { MockApiClient.clearMockResponses(); jest.clearAllMocks(); }); const selectNthSimilarItem = async (index: number) => { const items = await screen.findAllByTestId('similar-item-row'); const item = items.at(index); expect(item).toBeDefined(); await userEvent.click(item!); }; it('renders with mocked data', async function () { render(, {router}); expect(screen.getByTestId('loading-indicator')).toBeInTheDocument(); await waitFor(() => expect(mock).toHaveBeenCalled()); expect(await screen.findByText('Show 3 issues below threshold')).toBeInTheDocument(); }); it('can merge and redirect to new parent', async function () { const merge = MockApiClient.addMockResponse({ method: 'PUT', url: '/projects/org-slug/project-slug/issues/', body: { merge: {children: ['123'], parent: '321'}, }, }); render(, {router}); renderGlobalModal(); await selectNthSimilarItem(0); await userEvent.click(await screen.findByRole('button', {name: 'Merge (1)'})); await userEvent.click(screen.getByRole('button', {name: 'Confirm'})); await waitFor(() => { expect(merge).toHaveBeenCalledWith( '/projects/org-slug/project-slug/issues/', expect.objectContaining({ data: {merge: 1}, }) ); }); expect(MockNavigate).toHaveBeenCalledWith( '/organizations/org-slug/issues/321/similar/' ); }); it('correctly shows merge count', async function () { render(, {router}); renderGlobalModal(); await selectNthSimilarItem(0); expect(screen.getByText('Merge (1)')).toBeInTheDocument(); // Correctly show "Merge (0)" when the item is un-clicked await selectNthSimilarItem(0); expect(screen.getByText('Merge (0)')).toBeInTheDocument(); }); it('shows empty message', async function () { mock = MockApiClient.addMockResponse({ url: `/organizations/org-slug/issues/${group.id}/similar/?limit=50`, body: [], }); render(, {router}); renderGlobalModal(); await waitFor(() => expect(mock).toHaveBeenCalled()); expect( await screen.findByText("There don't seem to be any similar issues.") ).toBeInTheDocument(); expect( screen.queryByText( 'This can occur when the issue has no stacktrace or in-app frames.' ) ).not.toBeInTheDocument(); }); }); describe('Issues Similar Embeddings View', function () { let mock: jest.Mock; const group = GroupFixture(); const project = ProjectFixture({ features: ['similarity-view'], }); const router = RouterFixture({ params: {orgId: 'org-slug', groupId: group.id}, }); const similarEmbeddingsScores = [ {exception: 0.01, shouldBeGrouped: 'Yes'}, {exception: 0.005, shouldBeGrouped: 'Yes'}, {exception: 0.7384, shouldBeGrouped: 'No'}, {exception: 0.3849, shouldBeGrouped: 'No'}, ]; const mockData = { similarEmbeddings: GroupsFixture().map((issue, i) => [ issue, similarEmbeddingsScores[i], ]), }; beforeEach(function () { mock = MockApiClient.addMockResponse({ url: `/organizations/org-slug/issues/${group.id}/similar-issues-embeddings/?k=10&threshold=0.01&useReranking=true`, body: mockData.similarEmbeddings, }); MockApiClient.addMockResponse({ url: `/organizations/org-slug/issues/${group.id}/`, body: group, }); MockApiClient.addMockResponse({ url: `/projects/org-slug/project-slug/`, body: {features: ['similarity-embeddings']}, }); ProjectsStore.init(); ProjectsStore.loadInitialData([project]); }); afterEach(() => { MockApiClient.clearMockResponses(); jest.clearAllMocks(); }); const selectNthSimilarItem = async (index: number) => { const items = await screen.findAllByTestId('similar-item-row'); const item = items.at(index); expect(item).toBeDefined(); await userEvent.click(item!); }; it('renders with mocked data', async function () { render(, {router}); await waitFor(() => expect(mock).toHaveBeenCalled()); expect(screen.queryByText('Show 3 issues below threshold')).not.toBeInTheDocument(); }); it('can merge and redirect to new parent', async function () { const merge = MockApiClient.addMockResponse({ method: 'PUT', url: '/projects/org-slug/project-slug/issues/', body: { merge: {children: ['123'], parent: '321'}, }, }); render(, {router}); renderGlobalModal(); await selectNthSimilarItem(0); await userEvent.click(await screen.findByRole('button', {name: 'Merge (1)'})); await userEvent.click(screen.getByRole('button', {name: 'Confirm'})); await waitFor(() => { expect(merge).toHaveBeenCalledWith( '/projects/org-slug/project-slug/issues/', expect.objectContaining({ data: {merge: 1}, }) ); }); expect(MockNavigate).toHaveBeenCalledWith( '/organizations/org-slug/issues/321/similar/' ); }); it('correctly shows merge count', async function () { render(, {router}); renderGlobalModal(); await selectNthSimilarItem(0); expect(screen.getByText('Merge (1)')).toBeInTheDocument(); // Correctly show "Merge (0)" when the item is un-clicked await selectNthSimilarItem(0); expect(screen.getByText('Merge (0)')).toBeInTheDocument(); }); it('shows empty message', async function () { mock = MockApiClient.addMockResponse({ url: `/organizations/org-slug/issues/${group.id}/similar-issues-embeddings/?k=10&threshold=0.01&useReranking=true`, body: [], }); render(, {router}); renderGlobalModal(); await waitFor(() => expect(mock).toHaveBeenCalled()); expect( await screen.findByText( "There don't seem to be any similar issues. This can occur when the issue has no stacktrace or in-app frames." ) ).toBeInTheDocument(); }); });