overview.polling.spec.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import {GroupFixture} from 'sentry-fixture/group';
  2. import {GroupStatsFixture} from 'sentry-fixture/groupStats';
  3. import {LocationFixture} from 'sentry-fixture/locationFixture';
  4. import {MemberFixture} from 'sentry-fixture/member';
  5. import {SearchFixture} from 'sentry-fixture/search';
  6. import {TagsFixture} from 'sentry-fixture/tags';
  7. import {initializeOrg} from 'sentry-test/initializeOrg';
  8. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  9. import {textWithMarkupMatcher} from 'sentry-test/utils';
  10. import StreamGroup from 'sentry/components/stream/group';
  11. import TagStore from 'sentry/stores/tagStore';
  12. import IssueList from 'sentry/views/issueList/overview';
  13. jest.mock('sentry/views/issueList/filters', () => jest.fn(() => null));
  14. jest.mock('sentry/components/stream/group', () =>
  15. jest.fn(({id}) => <div data-test-id={id} />)
  16. );
  17. jest.mock('js-cookie', () => ({
  18. get: jest.fn(),
  19. set: jest.fn(),
  20. }));
  21. const PREVIOUS_PAGE_CURSOR = '1443575731';
  22. const DEFAULT_LINKS_HEADER =
  23. `<http://127.0.0.1:8000/api/0/organizations/org-slug/issues/?cursor=${PREVIOUS_PAGE_CURSOR}:0:1>; rel="previous"; results="false"; cursor="${PREVIOUS_PAGE_CURSOR}:0:1", ` +
  24. '<http://127.0.0.1:8000/api/0/organizations/org-slug/issues/?cursor=1443575000:0:0>; rel="next"; results="true"; cursor="1443575000:0:0"';
  25. describe('IssueList -> Polling', function () {
  26. let issuesRequest: jest.Mock;
  27. let pollRequest: jest.Mock;
  28. beforeEach(() => {
  29. jest.useFakeTimers();
  30. });
  31. afterEach(() => {
  32. jest.useRealTimers();
  33. });
  34. const {organization, project, routerProps, routerContext} = initializeOrg({
  35. organization: {
  36. access: ['project:releases'],
  37. },
  38. });
  39. const savedSearch = SearchFixture({
  40. id: '789',
  41. query: 'is:unresolved',
  42. name: 'Unresolved Issues',
  43. });
  44. const group = GroupFixture({project});
  45. const group2 = GroupFixture({project, id: '2'});
  46. const defaultProps = {
  47. location: LocationFixture({
  48. query: {query: 'is:unresolved'},
  49. search: 'query=is:unresolved',
  50. }),
  51. params: {},
  52. organization,
  53. };
  54. /* helpers */
  55. const renderComponent = async () => {
  56. render(<IssueList {...routerProps} {...defaultProps} />, {
  57. context: routerContext,
  58. });
  59. await Promise.resolve();
  60. jest.runAllTimers();
  61. };
  62. beforeEach(function () {
  63. // The tests fail because we have a "component update was not wrapped in act" error.
  64. // It should be safe to ignore this error, but we should remove the mock once we move to react testing library
  65. // eslint-disable-next-line no-console
  66. jest.spyOn(console, 'error').mockImplementation(jest.fn());
  67. MockApiClient.clearMockResponses();
  68. MockApiClient.addMockResponse({
  69. url: '/organizations/org-slug/searches/',
  70. body: [savedSearch],
  71. });
  72. MockApiClient.addMockResponse({
  73. url: '/organizations/org-slug/recent-searches/',
  74. body: [],
  75. });
  76. MockApiClient.addMockResponse({
  77. url: '/organizations/org-slug/issues-count/',
  78. method: 'GET',
  79. body: [{}],
  80. });
  81. MockApiClient.addMockResponse({
  82. url: '/organizations/org-slug/processingissues/',
  83. method: 'GET',
  84. body: [
  85. {
  86. project: 'test-project',
  87. numIssues: 1,
  88. hasIssues: true,
  89. lastSeen: '2019-01-16T15:39:11.081Z',
  90. },
  91. ],
  92. });
  93. MockApiClient.addMockResponse({
  94. url: '/organizations/org-slug/tags/',
  95. method: 'GET',
  96. body: TagsFixture(),
  97. });
  98. MockApiClient.addMockResponse({
  99. url: '/organizations/org-slug/users/',
  100. method: 'GET',
  101. body: [MemberFixture({projects: [project.slug]})],
  102. });
  103. MockApiClient.addMockResponse({
  104. url: '/organizations/org-slug/recent-searches/',
  105. method: 'GET',
  106. body: [],
  107. });
  108. MockApiClient.addMockResponse({
  109. url: '/organizations/org-slug/searches/',
  110. body: [savedSearch],
  111. });
  112. issuesRequest = MockApiClient.addMockResponse({
  113. url: '/organizations/org-slug/issues/',
  114. body: [group],
  115. headers: {
  116. Link: DEFAULT_LINKS_HEADER,
  117. 'X-Hits': '1',
  118. },
  119. });
  120. MockApiClient.addMockResponse({
  121. url: '/organizations/org-slug/issues-stats/',
  122. body: [GroupStatsFixture()],
  123. });
  124. pollRequest = MockApiClient.addMockResponse({
  125. url: `/api/0/organizations/org-slug/issues/?cursor=${PREVIOUS_PAGE_CURSOR}:0:1`,
  126. body: [],
  127. headers: {
  128. Link: DEFAULT_LINKS_HEADER,
  129. 'X-Hits': '1',
  130. },
  131. });
  132. jest.mocked(StreamGroup).mockClear();
  133. TagStore.init();
  134. });
  135. afterEach(function () {
  136. MockApiClient.clearMockResponses();
  137. });
  138. it('toggles polling for new issues', async function () {
  139. await renderComponent();
  140. await waitFor(() => {
  141. expect(issuesRequest).toHaveBeenCalledWith(
  142. expect.anything(),
  143. expect.objectContaining({
  144. // Should be called with default query
  145. data: expect.stringContaining('is%3Aunresolved'),
  146. })
  147. );
  148. });
  149. // Enable realtime updates
  150. await userEvent.click(
  151. screen.getByRole('button', {name: 'Enable real-time updates'}),
  152. {delay: null}
  153. );
  154. // Each poll request gets delayed by additional 3s, up to max of 60s
  155. jest.advanceTimersByTime(3001);
  156. expect(pollRequest).toHaveBeenCalledTimes(1);
  157. jest.advanceTimersByTime(6001);
  158. expect(pollRequest).toHaveBeenCalledTimes(2);
  159. // Pauses
  160. await userEvent.click(screen.getByRole('button', {name: 'Pause real-time updates'}), {
  161. delay: null,
  162. });
  163. jest.advanceTimersByTime(12001);
  164. expect(pollRequest).toHaveBeenCalledTimes(2);
  165. });
  166. it('displays new group and pagination caption correctly', async function () {
  167. pollRequest = MockApiClient.addMockResponse({
  168. url: `/api/0/organizations/org-slug/issues/?cursor=${PREVIOUS_PAGE_CURSOR}:0:1`,
  169. body: [group2],
  170. headers: {
  171. Link: DEFAULT_LINKS_HEADER,
  172. 'X-Hits': '2',
  173. },
  174. });
  175. await renderComponent();
  176. expect(
  177. await screen.findByText(textWithMarkupMatcher('1-1 of 1'))
  178. ).toBeInTheDocument();
  179. // Enable realtime updates
  180. await userEvent.click(
  181. screen.getByRole('button', {name: 'Enable real-time updates'}),
  182. {delay: null}
  183. );
  184. jest.advanceTimersByTime(3001);
  185. expect(pollRequest).toHaveBeenCalledTimes(1);
  186. // We mock out the stream group component and only render the ID as a testid
  187. await screen.findByTestId('2');
  188. expect(screen.getByText(textWithMarkupMatcher('1-2 of 2'))).toBeInTheDocument();
  189. });
  190. it('stops polling for new issues when endpoint returns a 401', async function () {
  191. pollRequest = MockApiClient.addMockResponse({
  192. url: `/api/0/organizations/org-slug/issues/?cursor=${PREVIOUS_PAGE_CURSOR}:0:1`,
  193. body: [],
  194. statusCode: 401,
  195. });
  196. await renderComponent();
  197. // Enable real time control
  198. await userEvent.click(
  199. await screen.findByRole('button', {name: 'Enable real-time updates'}),
  200. {delay: null}
  201. );
  202. // Each poll request gets delayed by additional 3s, up to max of 60s
  203. jest.advanceTimersByTime(3001);
  204. expect(pollRequest).toHaveBeenCalledTimes(1);
  205. jest.advanceTimersByTime(9001);
  206. expect(pollRequest).toHaveBeenCalledTimes(1);
  207. });
  208. it('stops polling for new issues when endpoint returns a 403', async function () {
  209. pollRequest = MockApiClient.addMockResponse({
  210. url: `/api/0/organizations/org-slug/issues/?cursor=${PREVIOUS_PAGE_CURSOR}:0:1`,
  211. body: [],
  212. statusCode: 403,
  213. });
  214. await renderComponent();
  215. // Enable real time control
  216. await userEvent.click(
  217. await screen.findByRole('button', {name: 'Enable real-time updates'}),
  218. {delay: null}
  219. );
  220. // Each poll request gets delayed by additional 3s, up to max of 60s
  221. jest.advanceTimersByTime(3001);
  222. expect(pollRequest).toHaveBeenCalledTimes(1);
  223. jest.advanceTimersByTime(9001);
  224. expect(pollRequest).toHaveBeenCalledTimes(1);
  225. });
  226. it('stops polling for new issues when endpoint returns a 404', async function () {
  227. pollRequest = MockApiClient.addMockResponse({
  228. url: `/api/0/organizations/org-slug/issues/?cursor=${PREVIOUS_PAGE_CURSOR}:0:1`,
  229. body: [],
  230. statusCode: 404,
  231. });
  232. await renderComponent();
  233. // Enable real time control
  234. await userEvent.click(
  235. await screen.findByRole('button', {name: 'Enable real-time updates'}),
  236. {delay: null}
  237. );
  238. // Each poll request gets delayed by additional 3s, up to max of 60s
  239. jest.advanceTimersByTime(3001);
  240. expect(pollRequest).toHaveBeenCalledTimes(1);
  241. jest.advanceTimersByTime(9001);
  242. expect(pollRequest).toHaveBeenCalledTimes(1);
  243. });
  244. });