events.spec.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. import {withRouter, browserHistory} from 'react-router';
  2. import React from 'react';
  3. import Events, {parseRowFromLinks} from 'app/views/events/events';
  4. import {chart, doZoom} from 'app-test/helpers/charts';
  5. import {initializeOrg} from 'app-test/helpers/initializeOrg';
  6. import {getUtcToLocalDateObject} from 'app/utils/dates';
  7. import {mockRouterPush} from 'app-test/helpers/mockRouterPush';
  8. import {mount} from 'enzyme';
  9. import EventsContainer from 'app/views/events';
  10. import ProjectsStore from 'app/stores/projectsStore';
  11. jest.mock('app/utils/withLatestContext');
  12. const generatePageLinks = (current, windowSize) => {
  13. const previous = current - windowSize;
  14. const hasPrevious = previous >= 0;
  15. const next = current + windowSize;
  16. return (
  17. `<https://sentry.io/api/0/organizations/sentry/events/?statsPeriod=14d&cursor=0:${previous}:1>; rel="previous"; results="${hasPrevious}"; cursor="0:${previous}:1", ` +
  18. `<https://sentry.io/api/0/organizations/sentry/events/?statsPeriod=14d&cursor=0:${next}:0>; rel="next"; results="true"; cursor="0:${next}:0"`
  19. );
  20. };
  21. const pageOneLinks = generatePageLinks(0, 100);
  22. const pageTwoLinks = generatePageLinks(100, 100);
  23. const EventsWithRouter = withRouter(Events);
  24. describe('EventsErrors', function() {
  25. const {organization, router, routerContext} = initializeOrg({
  26. projects: [{isMember: true}, {isMember: true, slug: 'new-project', id: 3}],
  27. organization: {
  28. features: ['events'],
  29. },
  30. router: {
  31. location: {
  32. pathname: '/organizations/org-slug/events/',
  33. query: {},
  34. },
  35. },
  36. });
  37. let eventsMock;
  38. let eventsStatsMock;
  39. let eventsMetaMock;
  40. beforeAll(function() {
  41. MockApiClient.addMockResponse({
  42. url: `/organizations/${organization.slug}/recent-searches/`,
  43. body: [],
  44. });
  45. MockApiClient.addMockResponse({
  46. url: `/organizations/${organization.slug}/environments/`,
  47. body: TestStubs.Environments(),
  48. });
  49. });
  50. beforeEach(function() {
  51. // Search bar makes this request when mounted
  52. MockApiClient.addMockResponse({
  53. url: '/organizations/org-slug/tags/',
  54. body: [{count: 1, tag: 'transaction'}, {count: 2, tag: 'mechanism'}],
  55. });
  56. eventsMock = MockApiClient.addMockResponse({
  57. url: '/organizations/org-slug/events/',
  58. body: (_url, opts) => [TestStubs.OrganizationEvent(opts.query)],
  59. headers: {Link: pageOneLinks},
  60. });
  61. eventsStatsMock = MockApiClient.addMockResponse({
  62. url: '/organizations/org-slug/events-stats/',
  63. body: (_url, opts) => {
  64. return TestStubs.EventsStats(opts.query);
  65. },
  66. });
  67. eventsMetaMock = MockApiClient.addMockResponse({
  68. url: '/organizations/org-slug/events-meta/',
  69. body: {count: 5},
  70. });
  71. });
  72. it('renders with errors', async function() {
  73. MockApiClient.addMockResponse({
  74. url: `/organizations/${organization.slug}/events/`,
  75. statusCode: 500,
  76. body: {details: 'Error'},
  77. });
  78. MockApiClient.addMockResponse({
  79. url: `/organizations/${organization.slug}/events-stats/`,
  80. statusCode: 500,
  81. body: {details: 'Error'},
  82. });
  83. const wrapper = mount(
  84. <Events organization={organization} location={{query: {}}} />,
  85. routerContext
  86. );
  87. await tick();
  88. wrapper.update();
  89. expect(wrapper.find('EventsChart')).toHaveLength(1);
  90. expect(wrapper.find('EventsTable')).toHaveLength(1);
  91. expect(wrapper.find('RouteError')).toHaveLength(1);
  92. });
  93. it('renders events table', async function() {
  94. const wrapper = mount(
  95. <Events organization={organization} location={{query: {}}} />,
  96. routerContext
  97. );
  98. await tick();
  99. wrapper.update();
  100. expect(eventsStatsMock).toHaveBeenCalled();
  101. expect(eventsMetaMock).not.toHaveBeenCalled();
  102. expect(wrapper.find('LoadingIndicator')).toHaveLength(0);
  103. expect(wrapper.find('IdBadge')).toHaveLength(2);
  104. });
  105. it('renders TotalEventCount with internal flag', async function() {
  106. const newOrg = TestStubs.Organization({
  107. ...organization,
  108. features: [...organization.features, 'internal-catchall'],
  109. });
  110. const wrapper = mount(<Events organization={newOrg} location={{query: {}}} />, {
  111. ...routerContext,
  112. context: {...routerContext.context, organization: newOrg},
  113. });
  114. await tick();
  115. wrapper.update();
  116. expect(eventsMetaMock).toHaveBeenCalled();
  117. expect(wrapper.find('Feature').text()).toEqual(' of 5 (estimated)');
  118. });
  119. // This tests the component's `shouldComponentUpdate`
  120. // Use `search` to compare instead of `query` because that's what we check in `AsyncComponent`
  121. it('location.query changes updates events table', async function() {
  122. const wrapper = mount(
  123. <EventsWithRouter
  124. organization={organization}
  125. location={{
  126. search: '?statsPeriod=14d',
  127. query: {
  128. statsPeriod: '14d',
  129. },
  130. }}
  131. />,
  132. routerContext
  133. );
  134. expect(eventsMock).toHaveBeenCalledWith(
  135. expect.any(String),
  136. expect.objectContaining({
  137. query: {
  138. statsPeriod: '14d',
  139. },
  140. })
  141. );
  142. eventsMock.mockClear();
  143. const location = {
  144. query: {
  145. start: '2017-10-01T04:00:00',
  146. end: '2017-10-02T03:59:59',
  147. },
  148. search: '?start=2017-10-01T04:00:00&end=2017-10-02T03:59:59',
  149. };
  150. wrapper.setContext({
  151. router: {
  152. ...router,
  153. location,
  154. },
  155. });
  156. wrapper.update();
  157. expect(eventsMock).toHaveBeenLastCalledWith(
  158. expect.any(String),
  159. expect.objectContaining({
  160. query: {
  161. start: '2017-10-01T04:00:00',
  162. end: '2017-10-02T03:59:59',
  163. },
  164. })
  165. );
  166. });
  167. describe('Events Integration', function() {
  168. let chartRender;
  169. let tableRender;
  170. let wrapper;
  171. let newParams;
  172. beforeEach(function() {
  173. const newLocation = {
  174. ...router.location,
  175. query: {
  176. ...router.location.query,
  177. },
  178. };
  179. const newRouter = {
  180. ...router,
  181. location: newLocation,
  182. };
  183. const newRouterContext = {
  184. ...routerContext,
  185. context: {
  186. ...routerContext.context,
  187. router: newRouter,
  188. location: newLocation,
  189. },
  190. };
  191. wrapper = mount(
  192. <EventsContainer
  193. router={newRouter}
  194. organization={organization}
  195. location={newRouter.location}
  196. >
  197. <EventsWithRouter location={newRouter.location} organization={organization} />
  198. </EventsContainer>,
  199. newRouterContext
  200. );
  201. mockRouterPush(wrapper, router);
  202. // XXX: Note this spy happens AFTER initial render!
  203. tableRender = jest.spyOn(wrapper.find('EventsTable').instance(), 'render');
  204. });
  205. afterAll(function() {
  206. if (chartRender) {
  207. chartRender.mockRestore();
  208. }
  209. tableRender.mockRestore();
  210. });
  211. it('zooms using chart', async function() {
  212. expect(tableRender).toHaveBeenCalledTimes(0);
  213. await tick();
  214. wrapper.update();
  215. chartRender = jest.spyOn(wrapper.find('LineChart').instance(), 'render');
  216. doZoom(wrapper.find('EventsChart').first(), chart);
  217. await tick();
  218. wrapper.update();
  219. // After zooming, line chart should re-render once, but table does
  220. expect(chartRender).toHaveBeenCalledTimes(1);
  221. expect(tableRender).toHaveBeenCalledTimes(3);
  222. newParams = {
  223. start: '2018-11-29T00:00:00',
  224. end: '2018-12-02T00:00:00',
  225. };
  226. expect(routerContext.context.router.push).toHaveBeenLastCalledWith(
  227. expect.objectContaining({
  228. query: newParams,
  229. })
  230. );
  231. wrapper.update();
  232. expect(wrapper.find('TimeRangeSelector').prop('start')).toEqual(
  233. getUtcToLocalDateObject('2018-11-29T00:00:00')
  234. );
  235. expect(wrapper.find('TimeRangeSelector').prop('end')).toEqual(
  236. getUtcToLocalDateObject('2018-12-02T00:00:00')
  237. );
  238. });
  239. });
  240. });
  241. describe('EventsContainer', function() {
  242. let wrapper;
  243. let eventsMock;
  244. let eventsStatsMock;
  245. let eventsMetaMock;
  246. const {organization, router, routerContext} = initializeOrg({
  247. projects: [
  248. {isMember: true, isBookmarked: true},
  249. {isMember: true, slug: 'new-project', id: 3},
  250. ],
  251. organization: {
  252. features: ['events', 'internal-catchall'],
  253. },
  254. router: {
  255. location: {
  256. pathname: '/organizations/org-slug/events/',
  257. query: {},
  258. },
  259. },
  260. });
  261. beforeAll(function() {
  262. MockApiClient.addMockResponse({
  263. url: `/organizations/${organization.slug}/environments/`,
  264. body: TestStubs.Environments(),
  265. });
  266. });
  267. beforeEach(function() {
  268. // Search bar makes this request when mounted
  269. MockApiClient.addMockResponse({
  270. url: '/organizations/org-slug/tags/',
  271. body: [{count: 1, tag: 'transaction'}, {count: 2, tag: 'mechanism'}],
  272. });
  273. eventsMock = MockApiClient.addMockResponse({
  274. url: '/organizations/org-slug/events/',
  275. body: (_url, opts) => [TestStubs.OrganizationEvent(opts.query)],
  276. headers: {Link: pageOneLinks},
  277. });
  278. eventsStatsMock = MockApiClient.addMockResponse({
  279. url: '/organizations/org-slug/events-stats/',
  280. body: (_url, opts) => {
  281. return TestStubs.EventsStats(opts.query);
  282. },
  283. });
  284. eventsMetaMock = MockApiClient.addMockResponse({
  285. url: '/organizations/org-slug/events-meta/',
  286. body: {count: 5},
  287. });
  288. wrapper = mount(
  289. <EventsContainer
  290. router={router}
  291. organization={organization}
  292. location={router.location}
  293. >
  294. <EventsWithRouter location={router.location} organization={organization} />
  295. </EventsContainer>,
  296. routerContext
  297. );
  298. mockRouterPush(wrapper, router);
  299. });
  300. it('performs the correct queries when there is a search query', async function() {
  301. wrapper.find('SmartSearchBar input').simulate('change', {target: {value: 'http'}});
  302. wrapper.find('SmartSearchBar input').simulate('submit');
  303. expect(router.push).toHaveBeenLastCalledWith({
  304. pathname: '/organizations/org-slug/events/',
  305. query: {query: 'http', statsPeriod: '14d'},
  306. });
  307. await tick();
  308. await tick();
  309. wrapper.update();
  310. expect(eventsMock).toHaveBeenLastCalledWith(
  311. '/organizations/org-slug/events/',
  312. expect.objectContaining({
  313. query: {query: 'http', statsPeriod: '14d'},
  314. })
  315. );
  316. // 28d because of previous period
  317. expect(eventsStatsMock).toHaveBeenLastCalledWith(
  318. '/organizations/org-slug/events-stats/',
  319. expect.objectContaining({
  320. query: expect.objectContaining({query: 'http', statsPeriod: '28d'}),
  321. })
  322. );
  323. expect(eventsMetaMock).toHaveBeenLastCalledWith(
  324. '/organizations/org-slug/events-meta/',
  325. expect.objectContaining({
  326. query: {query: 'http', statsPeriod: '14d'},
  327. })
  328. );
  329. });
  330. it('updates when changing projects', async function() {
  331. ProjectsStore.loadInitialData(organization.projects);
  332. expect(wrapper.find('MultipleProjectSelector').prop('value')).toEqual([]);
  333. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  334. wrapper
  335. .find('MultipleProjectSelector AutoCompleteItem')
  336. .at(0)
  337. .simulate('click');
  338. await tick();
  339. wrapper.update();
  340. expect(eventsStatsMock).toHaveBeenLastCalledWith(
  341. '/organizations/org-slug/events-stats/',
  342. expect.objectContaining({
  343. query: expect.objectContaining({project: [2], statsPeriod: '28d'}),
  344. })
  345. );
  346. expect(eventsMock).toHaveBeenLastCalledWith(
  347. '/organizations/org-slug/events/',
  348. expect.objectContaining({
  349. // This is not an array because of `mockRouterPush`
  350. query: {project: '2', statsPeriod: '14d'},
  351. })
  352. );
  353. });
  354. it('handles direct event hit', async function() {
  355. const eventId = 'a'.repeat(32);
  356. browserHistory.replace = jest.fn();
  357. eventsMock = MockApiClient.addMockResponse({
  358. url: '/organizations/org-slug/events/',
  359. body: (_url, opts) => [
  360. TestStubs.OrganizationEvent({...opts.query, eventID: eventId}),
  361. ],
  362. headers: {'X-Sentry-Direct-Hit': '1'},
  363. });
  364. wrapper = mount(
  365. <Events organization={organization} location={{query: eventId}} />,
  366. routerContext
  367. );
  368. expect(eventsMock).toHaveBeenCalled();
  369. expect(browserHistory.replace).toHaveBeenCalledWith(
  370. `/organizations/org-slug/projects/project-slug/events/${eventId}/`
  371. );
  372. });
  373. });
  374. describe('parseRowFromLinks', function() {
  375. it('calculates rows for first page', function() {
  376. expect(parseRowFromLinks(pageOneLinks, 10)).toBe('1-10');
  377. expect(parseRowFromLinks(pageOneLinks, 100)).toBe('1-100');
  378. });
  379. it('calculates rows for the second page', function() {
  380. expect(parseRowFromLinks(pageTwoLinks, 10)).toBe('101-110');
  381. expect(parseRowFromLinks(pageTwoLinks, 100)).toBe('101-200');
  382. });
  383. it('calculates rows for variable number of pages and window sizes', function() {
  384. let currentWindowSize = 1;
  385. while (currentWindowSize <= 100) {
  386. let currentPage = 1; // 1-based index
  387. while (currentPage <= 100) {
  388. const zeroBasedCurrentPage = currentPage - 1; // 0-based index
  389. const expectedStart = zeroBasedCurrentPage * currentWindowSize + 1;
  390. const expectedEnd = currentWindowSize + expectedStart - 1;
  391. const expectedString = `${expectedStart}-${expectedEnd}`;
  392. const pageLinks = generatePageLinks(expectedStart - 1, currentWindowSize);
  393. expect(parseRowFromLinks(pageLinks, currentWindowSize)).toBe(expectedString);
  394. currentPage += 1;
  395. }
  396. currentWindowSize += 1;
  397. }
  398. });
  399. });