import {mountWithTheme} from 'sentry-test/enzyme';
import {ApiSource} from 'sentry/components/search/sources/apiSource';
describe('ApiSource', function () {
let wrapper;
const org = TestStubs.Organization();
let orgsMock;
let projectsMock;
let teamsMock;
let membersMock;
let shortIdMock;
let eventIdMock;
let allMocks;
beforeEach(function () {
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: '/organizations/',
query: 'test-1',
body: [TestStubs.Organization({slug: 'test-org'})],
});
orgsMock = MockApiClient.addMockResponse({
url: '/organizations/',
query: 'foo',
body: [TestStubs.Organization({slug: 'foo-org'})],
});
projectsMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/projects/',
query: 'foo',
body: [TestStubs.Project({slug: 'foo-project'})],
});
teamsMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/teams/',
query: 'foo',
body: [TestStubs.Team({slug: 'foo-team'})],
});
membersMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/members/',
query: 'foo',
body: TestStubs.Members(),
});
shortIdMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/shortids/test-1/',
query: 'TEST-1',
body: TestStubs.ShortIdQueryResult(),
});
eventIdMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/eventids/12345678901234567890123456789012/',
query: '12345678901234567890123456789012',
body: TestStubs.EventIdQueryResult(),
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/plugins/?plugins=_all',
query: {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: [],
});
allMocks = {orgsMock, projectsMock, teamsMock, membersMock, shortIdMock, eventIdMock};
});
it('queries all API endpoints', function () {
const mock = jest.fn().mockReturnValue(null);
wrapper = mountWithTheme(
{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);
wrapper = mountWithTheme(
{mock}
);
await tick();
expect(shortIdMock).not.toHaveBeenCalled();
// Reset all mocks
Object.values(allMocks).forEach(m => m.mockReset);
// This is a valid short id now
wrapper.setProps({query: 'test-1'});
await tick();
wrapper.update();
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();
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);
wrapper = mountWithTheme(
{mock}
);
await tick();
expect(eventIdMock).not.toHaveBeenCalled();
// Reset all mocks
Object.values(allMocks).forEach(m => m.mockReset);
// This is a valid short id now
wrapper.setProps({query: '12345678901234567890123456789012'});
wrapper.update();
await tick();
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);
wrapper = mountWithTheme(
{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);
wrapper = mountWithTheme(
{mock}
);
await tick();
wrapper.update();
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/',
query: 'foo',
statusCode: 500,
});
wrapper = mountWithTheme(
{mock}
);
await tick();
wrapper.update();
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);
wrapper = mountWithTheme(
{mock}
);
await tick();
wrapper.update();
// 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();
wrapper.setProps({query: 'foo-t'});
await tick();
wrapper.update();
// 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 () {
let mock;
beforeAll(function () {
mock = jest.fn().mockReturnValue(null);
wrapper = mountWithTheme(
{mock}
);
});
it('does not call API with empty query string', function () {
expect(projectsMock).not.toHaveBeenCalled();
});
it('calls API when query string length is 1 char', function () {
wrapper.setProps({query: 'f'});
wrapper.update();
expect(projectsMock).toHaveBeenCalledTimes(1);
});
it('calls API when query string length increases from 1 -> 2', function () {
wrapper.setProps({query: 'fo'});
wrapper.update();
expect(projectsMock).toHaveBeenCalledTimes(1);
});
it('does not query API when query string > 2 chars', function () {
// Should not query API when query is > 2 chars
wrapper.setProps({query: 'foo'});
wrapper.update();
expect(projectsMock).toHaveBeenCalledTimes(0);
});
it('does not query API when query string 3 -> 4 chars', function () {
wrapper.setProps({query: 'foob'});
wrapper.update();
expect(projectsMock).toHaveBeenCalledTimes(0);
});
it('re-queries API if first 2 characters are different', function () {
wrapper.setProps({query: 'ba'});
wrapper.update();
expect(projectsMock).toHaveBeenCalledTimes(1);
});
it('does not requery if query string is the same', function () {
wrapper.setProps({query: 'ba'});
wrapper.update();
expect(projectsMock).toHaveBeenCalledTimes(0);
});
it('queries if we go from 2 chars -> 1 char', function () {
wrapper.setProps({query: 'b'});
wrapper.update();
expect(projectsMock).toHaveBeenCalledTimes(1);
});
});
});