import {GroupFixture} from 'sentry-fixture/group';
import {GroupStatsFixture} from 'sentry-fixture/groupStats';
import {LocationFixture} from 'sentry-fixture/locationFixture';
import {MemberFixture} from 'sentry-fixture/member';
import {SearchFixture} from 'sentry-fixture/search';
import {TagsFixture} from 'sentry-fixture/tags';
import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
import {textWithMarkupMatcher} from 'sentry-test/utils';
import StreamGroup from 'sentry/components/stream/group';
import TagStore from 'sentry/stores/tagStore';
import IssueList from 'sentry/views/issueList/overview';
jest.mock('sentry/views/issueList/filters', () => jest.fn(() => null));
jest.mock('sentry/components/stream/group', () =>
jest.fn(({id}) =>
)
);
jest.mock('js-cookie', () => ({
get: jest.fn(),
set: jest.fn(),
}));
const PREVIOUS_PAGE_CURSOR = '1443575731';
const DEFAULT_LINKS_HEADER =
`; rel="previous"; results="false"; cursor="${PREVIOUS_PAGE_CURSOR}:0:1", ` +
'; rel="next"; results="true"; cursor="1443575000:0:0"';
describe('IssueList -> Polling', function () {
let issuesRequest: jest.Mock;
let pollRequest: jest.Mock;
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
const {organization, project, routerProps, routerContext} = initializeOrg({
organization: {
access: ['project:releases'],
},
});
const savedSearch = SearchFixture({
id: '789',
query: 'is:unresolved',
name: 'Unresolved Issues',
});
const group = GroupFixture({project});
const group2 = GroupFixture({project, id: '2'});
const defaultProps = {
location: LocationFixture({
query: {query: 'is:unresolved'},
search: 'query=is:unresolved',
}),
params: {},
organization,
};
/* helpers */
const renderComponent = async () => {
render(, {
context: routerContext,
});
await Promise.resolve();
jest.runAllTimers();
};
beforeEach(function () {
// The tests fail because we have a "component update was not wrapped in act" error.
// It should be safe to ignore this error, but we should remove the mock once we move to react testing library
// eslint-disable-next-line no-console
jest.spyOn(console, 'error').mockImplementation(jest.fn());
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: '/organizations/org-slug/searches/',
body: [savedSearch],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/recent-searches/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/issues-count/',
method: 'GET',
body: [{}],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/processingissues/',
method: 'GET',
body: [
{
project: 'test-project',
numIssues: 1,
hasIssues: true,
lastSeen: '2019-01-16T15:39:11.081Z',
},
],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/tags/',
method: 'GET',
body: TagsFixture(),
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/users/',
method: 'GET',
body: [MemberFixture({projects: [project.slug]})],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/recent-searches/',
method: 'GET',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/searches/',
body: [savedSearch],
});
issuesRequest = MockApiClient.addMockResponse({
url: '/organizations/org-slug/issues/',
body: [group],
headers: {
Link: DEFAULT_LINKS_HEADER,
'X-Hits': '1',
},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/issues-stats/',
body: [GroupStatsFixture()],
});
pollRequest = MockApiClient.addMockResponse({
url: `/api/0/organizations/org-slug/issues/?cursor=${PREVIOUS_PAGE_CURSOR}:0:1`,
body: [],
headers: {
Link: DEFAULT_LINKS_HEADER,
'X-Hits': '1',
},
});
jest.mocked(StreamGroup).mockClear();
TagStore.init();
});
afterEach(function () {
MockApiClient.clearMockResponses();
});
it('toggles polling for new issues', async function () {
await renderComponent();
await waitFor(() => {
expect(issuesRequest).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
// Should be called with default query
data: expect.stringContaining('is%3Aunresolved'),
})
);
});
// Enable realtime updates
await userEvent.click(
screen.getByRole('button', {name: 'Enable real-time updates'}),
{delay: null}
);
// Each poll request gets delayed by additional 3s, up to max of 60s
jest.advanceTimersByTime(3001);
expect(pollRequest).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(6001);
expect(pollRequest).toHaveBeenCalledTimes(2);
// Pauses
await userEvent.click(screen.getByRole('button', {name: 'Pause real-time updates'}), {
delay: null,
});
jest.advanceTimersByTime(12001);
expect(pollRequest).toHaveBeenCalledTimes(2);
});
it('displays new group and pagination caption correctly', async function () {
pollRequest = MockApiClient.addMockResponse({
url: `/api/0/organizations/org-slug/issues/?cursor=${PREVIOUS_PAGE_CURSOR}:0:1`,
body: [group2],
headers: {
Link: DEFAULT_LINKS_HEADER,
'X-Hits': '2',
},
});
await renderComponent();
expect(
await screen.findByText(textWithMarkupMatcher('1-1 of 1'))
).toBeInTheDocument();
// Enable realtime updates
await userEvent.click(
screen.getByRole('button', {name: 'Enable real-time updates'}),
{delay: null}
);
jest.advanceTimersByTime(3001);
expect(pollRequest).toHaveBeenCalledTimes(1);
// We mock out the stream group component and only render the ID as a testid
await screen.findByTestId('2');
expect(screen.getByText(textWithMarkupMatcher('1-2 of 2'))).toBeInTheDocument();
});
it('stops polling for new issues when endpoint returns a 401', async function () {
pollRequest = MockApiClient.addMockResponse({
url: `/api/0/organizations/org-slug/issues/?cursor=${PREVIOUS_PAGE_CURSOR}:0:1`,
body: [],
statusCode: 401,
});
await renderComponent();
// Enable real time control
await userEvent.click(
await screen.findByRole('button', {name: 'Enable real-time updates'}),
{delay: null}
);
// Each poll request gets delayed by additional 3s, up to max of 60s
jest.advanceTimersByTime(3001);
expect(pollRequest).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(9001);
expect(pollRequest).toHaveBeenCalledTimes(1);
});
it('stops polling for new issues when endpoint returns a 403', async function () {
pollRequest = MockApiClient.addMockResponse({
url: `/api/0/organizations/org-slug/issues/?cursor=${PREVIOUS_PAGE_CURSOR}:0:1`,
body: [],
statusCode: 403,
});
await renderComponent();
// Enable real time control
await userEvent.click(
await screen.findByRole('button', {name: 'Enable real-time updates'}),
{delay: null}
);
// Each poll request gets delayed by additional 3s, up to max of 60s
jest.advanceTimersByTime(3001);
expect(pollRequest).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(9001);
expect(pollRequest).toHaveBeenCalledTimes(1);
});
it('stops polling for new issues when endpoint returns a 404', async function () {
pollRequest = MockApiClient.addMockResponse({
url: `/api/0/organizations/org-slug/issues/?cursor=${PREVIOUS_PAGE_CURSOR}:0:1`,
body: [],
statusCode: 404,
});
await renderComponent();
// Enable real time control
await userEvent.click(
await screen.findByRole('button', {name: 'Enable real-time updates'}),
{delay: null}
);
// Each poll request gets delayed by additional 3s, up to max of 60s
jest.advanceTimersByTime(3001);
expect(pollRequest).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(9001);
expect(pollRequest).toHaveBeenCalledTimes(1);
});
});