import {ComponentProps} from 'react'; import omit from 'lodash/omit'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {render, waitFor} from 'sentry-test/reactTestingLibrary'; import {ApiSource} from 'sentry/components/search/sources/apiSource'; describe('ApiSource', function () { const {org, router} = initializeOrg(); let orgsMock; let projectsMock; let teamsMock; let membersMock; let shortIdMock; let eventIdMock; const defaultProps: ComponentProps = { query: '', organization: org, router, location: router.location, routes: [], params: {orgId: org.slug}, children: jest.fn().mockReturnValue(null), }; beforeEach(function () { MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: '/organizations/', body: [TestStubs.Organization({slug: 'test-org'})], }); orgsMock = MockApiClient.addMockResponse({ url: '/organizations/', body: [TestStubs.Organization({slug: 'foo-org'})], }); projectsMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/projects/', body: [TestStubs.Project({slug: 'foo-project'})], }); teamsMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/teams/', body: [TestStubs.Team({slug: 'foo-team'})], }); membersMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/members/', body: TestStubs.Members(), }); shortIdMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/shortids/test-1/', body: TestStubs.ShortIdQueryResult(), }); eventIdMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/eventids/12345678901234567890123456789012/', body: TestStubs.EventIdQueryResult(), }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/plugins/?plugins=_all', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/plugins/configs/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/config/integrations/', body: [], }); MockApiClient.addMockResponse({ url: '/sentry-apps/?status=published', body: [], }); MockApiClient.addMockResponse({ url: '/doc-integrations/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/shortids/foo-t/', body: [], }); }); it('queries all API endpoints', function () { const mock = jest.fn().mockReturnValue(null); render( {mock} ); expect(orgsMock).toHaveBeenCalled(); expect(projectsMock).toHaveBeenCalled(); expect(teamsMock).toHaveBeenCalled(); expect(membersMock).toHaveBeenCalled(); expect(shortIdMock).not.toHaveBeenCalled(); expect(eventIdMock).not.toHaveBeenCalled(); }); it('only queries for shortids when query matches shortid format', async function () { const mock = jest.fn().mockReturnValue(null); const {rerender} = render( {mock} ); expect(shortIdMock).not.toHaveBeenCalled(); rerender( {mock} ); expect(shortIdMock).toHaveBeenCalled(); // These may not be desired behavior in future, but lets specify the expectation regardless expect(orgsMock).toHaveBeenCalled(); expect(projectsMock).toHaveBeenCalled(); expect(teamsMock).toHaveBeenCalled(); expect(membersMock).toHaveBeenCalled(); expect(eventIdMock).not.toHaveBeenCalled(); await waitFor(() => { expect(mock).toHaveBeenLastCalledWith( expect.objectContaining({ results: [ { item: expect.objectContaining({ title: 'group type', description: 'group description', sourceType: 'issue', resultType: 'issue', to: '/org-slug/project-slug/issues/1/', }), score: 1, refIndex: 0, }, ], }) ); }); }); it('only queries for eventids when query matches eventid format of 32 chars', async function () { const mock = jest.fn().mockReturnValue(null); const {rerender} = render( {mock} ); expect(eventIdMock).not.toHaveBeenCalled(); rerender( // This is a valid short id now {mock} ); await waitFor(() => { expect(eventIdMock).toHaveBeenCalled(); }); // These may not be desired behavior in future, but lets specify the expectation regardless expect(orgsMock).toHaveBeenCalled(); expect(projectsMock).toHaveBeenCalled(); expect(teamsMock).toHaveBeenCalled(); expect(membersMock).toHaveBeenCalled(); expect(shortIdMock).not.toHaveBeenCalled(); expect(mock).toHaveBeenLastCalledWith( expect.objectContaining({ results: [ { item: expect.objectContaining({ title: 'event type', description: 'event description', sourceType: 'event', resultType: 'event', to: '/org-slug/project-slug/issues/1/events/12345678901234567890123456789012/', }), score: 1, refIndex: 0, }, ], }) ); }); it('only queries org endpoint if there is no org in context', function () { const mock = jest.fn().mockReturnValue(null); render( {mock} ); expect(orgsMock).toHaveBeenCalled(); expect(projectsMock).not.toHaveBeenCalled(); expect(teamsMock).not.toHaveBeenCalled(); expect(membersMock).not.toHaveBeenCalled(); }); it('render function is called with correct results', async function () { const mock = jest.fn().mockReturnValue(null); render( {mock} ); await waitFor(() => { expect(mock).toHaveBeenLastCalledWith({ isLoading: false, results: expect.arrayContaining([ expect.objectContaining({ item: expect.objectContaining({ model: expect.objectContaining({ slug: 'foo-org', }), sourceType: 'organization', resultType: 'settings', to: '/settings/foo-org/', }), matches: expect.anything(), score: expect.anything(), }), expect.objectContaining({ item: expect.objectContaining({ model: expect.objectContaining({ slug: 'foo-org', }), sourceType: 'organization', resultType: 'route', to: '/foo-org/', }), matches: expect.anything(), score: expect.anything(), }), expect.objectContaining({ item: expect.objectContaining({ model: expect.objectContaining({ slug: 'foo-project', }), sourceType: 'project', resultType: 'route', to: '/organizations/org-slug/projects/foo-project/?project=2', }), matches: expect.anything(), score: expect.anything(), }), expect.objectContaining({ item: expect.objectContaining({ model: expect.objectContaining({ slug: 'foo-project', }), sourceType: 'project', resultType: 'route', to: '/organizations/org-slug/alerts/rules/?project=2', }), matches: expect.anything(), score: expect.anything(), }), expect.objectContaining({ item: expect.objectContaining({ model: expect.objectContaining({ slug: 'foo-project', }), sourceType: 'project', resultType: 'settings', to: '/settings/org-slug/projects/foo-project/', }), matches: expect.anything(), score: expect.anything(), }), expect.objectContaining({ item: expect.objectContaining({ model: expect.objectContaining({ slug: 'foo-team', }), sourceType: 'team', resultType: 'settings', to: '/settings/org-slug/teams/foo-team/', }), matches: expect.anything(), score: expect.anything(), }), ]), }); }); // The return values here are because of fuzzy search matching. // There are no members that match expect(mock.mock.calls[1][0].results).toHaveLength(6); }); it('render function is called with correct results when API requests partially succeed', async function () { const mock = jest.fn().mockReturnValue(null); MockApiClient.addMockResponse({ url: '/organizations/org-slug/projects/', statusCode: 500, }); render( {mock} ); await waitFor(() => { expect(mock).toHaveBeenLastCalledWith({ isLoading: false, results: expect.arrayContaining([ expect.objectContaining({ item: expect.objectContaining({ model: expect.objectContaining({ slug: 'foo-org', }), }), }), expect.objectContaining({ item: expect.objectContaining({ model: expect.objectContaining({ slug: 'foo-org', }), }), }), expect.objectContaining({ item: expect.objectContaining({ model: expect.objectContaining({ slug: 'foo-team', }), }), }), ]), }); }); // The return values here are because of fuzzy search matching. // There are no members that match expect(mock.mock.calls[1][0].results).toHaveLength(3); }); it('render function is updated as query changes', async function () { const mock = jest.fn().mockReturnValue(null); const {rerender} = render( {mock} ); await waitFor(() => { // The return values here are because of fuzzy search matching. // There are no members that match expect(mock.mock.calls[1][0].results).toHaveLength(6); expect(mock.mock.calls[1][0].results[0].item.model.slug).toBe('foo-org'); }); mock.mockClear(); rerender( {mock} ); await waitFor(() => { // Still have 4 results, but is re-ordered expect(mock.mock.calls[0][0].results).toHaveLength(6); expect(mock.mock.calls[0][0].results[0].item.model.slug).toBe('foo-team'); }); }); describe('API queries', function () { it('calls API based on query string', function () { const {rerender} = render(); expect(projectsMock).toHaveBeenCalledTimes(1); rerender(); // calls API when query string length is 1 char expect(projectsMock).toHaveBeenCalledTimes(2); rerender(); // calls API when query string length increases from 1 -> 2 expect(projectsMock).toHaveBeenCalledTimes(3); rerender(); // Should not query API when query is > 2 chars expect(projectsMock).toHaveBeenCalledTimes(3); // re-queries API if first 2 characters are different rerender(); expect(projectsMock).toHaveBeenCalledTimes(4); // Does not requery when query stays the same rerender(); expect(projectsMock).toHaveBeenCalledTimes(4); // queries if we go from 2 chars -> 1 char rerender(); expect(projectsMock).toHaveBeenCalledTimes(5); }); }); });