stream.spec.jsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. import React from 'react';
  2. import {shallow} from 'enzyme';
  3. import Cookies from 'js-cookie';
  4. import _ from 'lodash';
  5. import {Client} from 'app/api';
  6. import CursorPoller from 'app/utils/cursorPoller';
  7. import LoadingError from 'app/components/loadingError';
  8. import ErrorRobot from 'app/components/errorRobot';
  9. import Stream from 'app/views/stream/stream';
  10. import EnvironmentStore from 'app/stores/environmentStore';
  11. import {setActiveEnvironment} from 'app/actionCreators/environments';
  12. import {browserHistory} from 'react-router';
  13. import TagStore from 'app/stores/tagStore';
  14. jest.mock('app/stores/groupStore');
  15. const DEFAULT_LINKS_HEADER =
  16. '<http://127.0.0.1:8000/api/0/projects/org-slug/project-slug/issues/?cursor=1443575731:0:1>; rel="previous"; results="false"; cursor="1443575731:0:1", ' +
  17. '<http://127.0.0.1:8000/api/0/projects/org-slug/project-slug/issues/?cursor=1443575731:0:0>; rel="next"; results="true"; cursor="1443575731:0:0';
  18. describe('Stream', function() {
  19. let sandbox;
  20. let context;
  21. let wrapper;
  22. let props;
  23. let organization;
  24. let team;
  25. let project;
  26. let savedSearch;
  27. let groupListRequest;
  28. beforeEach(function() {
  29. sandbox = sinon.sandbox.create();
  30. organization = TestStubs.Organization({
  31. id: '1337',
  32. slug: 'org-slug',
  33. });
  34. team = TestStubs.Team({
  35. id: '2448',
  36. });
  37. project = TestStubs.ProjectDetails({
  38. id: 3559,
  39. name: 'Foo Project',
  40. slug: 'project-slug',
  41. firstEvent: true,
  42. });
  43. savedSearch = {id: '789', query: 'is:unresolved', name: 'test'};
  44. groupListRequest = MockApiClient.addMockResponse({
  45. url: '/projects/org-slug/project-slug/issues/',
  46. body: [TestStubs.Group()],
  47. headers: {
  48. Link: DEFAULT_LINKS_HEADER,
  49. },
  50. });
  51. MockApiClient.addMockResponse({
  52. url: '/projects/org-slug/project-slug/searches/',
  53. body: [savedSearch],
  54. });
  55. MockApiClient.addMockResponse({
  56. url: '/organizations/org-slug/processingissues/',
  57. method: 'GET',
  58. });
  59. sandbox.stub(browserHistory, 'push');
  60. context = {
  61. project,
  62. organization,
  63. team,
  64. };
  65. TagStore.init();
  66. props = {
  67. setProjectNavSection: function() {},
  68. location: {query: {query: 'is:unresolved'}, search: 'query=is:unresolved'},
  69. params: {orgId: organization.slug, projectId: project.slug},
  70. tags: TagStore.getAllTags(),
  71. tagsLoading: false,
  72. };
  73. });
  74. afterEach(function() {
  75. sandbox.restore();
  76. MockApiClient.clearMockResponses();
  77. });
  78. describe('fetchData()', function() {
  79. describe('complete handler', function() {
  80. beforeEach(function() {
  81. wrapper = shallow(<Stream {...props} />, {
  82. context,
  83. });
  84. sandbox.stub(CursorPoller.prototype, 'setEndpoint');
  85. });
  86. it('should reset the poller endpoint and sets cursor URL', function() {
  87. const stream = wrapper.instance();
  88. stream.state.pageLinks = DEFAULT_LINKS_HEADER;
  89. stream.state.realtimeActive = true;
  90. stream.fetchData();
  91. expect(
  92. CursorPoller.prototype.setEndpoint.calledWith(
  93. 'http://127.0.0.1:8000/api/0/projects/org-slug/project-slug/issues/?cursor=1443575731:0:1'
  94. )
  95. ).toBe(true);
  96. });
  97. it('should not enable the poller if realtimeActive is false', function() {
  98. const stream = wrapper.instance();
  99. stream.state.pageLinks = DEFAULT_LINKS_HEADER;
  100. stream.state.realtimeActive = false;
  101. stream.fetchData();
  102. expect(CursorPoller.prototype.setEndpoint.notCalled).toBeTruthy();
  103. });
  104. it("should not enable the poller if the 'previous' link has results", function() {
  105. const pageLinks =
  106. '<http://127.0.0.1:8000/api/0/projects/org-slug/project-slug/issues/?cursor=1443575731:0:1>; rel="previous"; results="true"; cursor="1443575731:0:1", ' +
  107. '<http://127.0.0.1:8000/api/0/projects/org-slug/project-slug/issues/?cursor=1443575731:0:0>; rel="next"; results="true"; cursor="1443575731:0:0';
  108. MockApiClient.addMockResponse({
  109. url: '/projects/org-slug/project-slug/issues/',
  110. body: [TestStubs.Group()],
  111. headers: {
  112. Link: pageLinks,
  113. },
  114. });
  115. wrapper = shallow(<Stream {...props} />, {
  116. context,
  117. });
  118. const stream = wrapper.instance();
  119. stream.setState({
  120. pageLinks,
  121. realtimeActive: true,
  122. });
  123. stream.fetchData();
  124. expect(CursorPoller.prototype.setEndpoint.notCalled).toBeTruthy();
  125. });
  126. }); // complete handler
  127. it('calls fetchData once on mount for a saved search', async function() {
  128. props.location = {query: {}};
  129. props.params.searchId = '1';
  130. wrapper = shallow(<Stream {...props} />, {
  131. context,
  132. });
  133. await wrapper.update();
  134. expect(groupListRequest).toHaveBeenCalledTimes(1);
  135. });
  136. it('calls fetchData once on mount if there is a query', async function() {
  137. wrapper = shallow(<Stream {...props} />, {
  138. context,
  139. });
  140. await wrapper.update();
  141. expect(groupListRequest).toHaveBeenCalledTimes(1);
  142. });
  143. it('should cancel any previous, unfinished fetches', function() {
  144. const requestCancel = sandbox.stub();
  145. let requestOptions;
  146. sandbox.stub(Client.prototype, 'request', function(url, options) {
  147. requestOptions = options;
  148. return {
  149. cancel: requestCancel,
  150. };
  151. });
  152. // NOTE: fetchData called once after render automatically
  153. const stream = wrapper.instance();
  154. // 2nd fetch should call cancel
  155. stream.fetchData();
  156. stream.fetchData();
  157. expect(requestCancel.calledOnce).toBeTruthy();
  158. expect(stream.lastRequest).toBeTruthy();
  159. // when request "completes", lastRequest is cleared
  160. requestOptions.complete({
  161. getResponseHeader: () => DEFAULT_LINKS_HEADER,
  162. });
  163. expect(stream.lastRequest).toBeNull();
  164. });
  165. it('sends environment attribute', function() {
  166. const requestCancel = sandbox.stub();
  167. let requestOptions;
  168. sandbox.stub(Client.prototype, 'request', function(url, options) {
  169. requestOptions = options;
  170. return {
  171. cancel: requestCancel,
  172. };
  173. });
  174. const stream = wrapper.instance();
  175. stream.state.activeEnvironment = {name: 'prod'};
  176. stream.state.query = 'is:unresolved environment:prod';
  177. stream.fetchData();
  178. expect(requestOptions.data.query).toContain('environment:prod');
  179. expect(requestOptions.data.environment).toBe('prod');
  180. });
  181. });
  182. describe('fetchSavedSearches()', function() {
  183. it('handles valid search id', async function() {
  184. const streamProps = {
  185. setProjectNavSection: function() {},
  186. params: {orgId: 'org-slug', projectId: 'project-slug', searchId: '789'},
  187. location: {query: {}, search: ''},
  188. };
  189. wrapper = shallow(<Stream {...streamProps} />, {
  190. context,
  191. });
  192. await wrapper.update();
  193. expect(wrapper.instance().state.searchId).toBe('789');
  194. expect(wrapper.instance().state.query).toBe('is:unresolved');
  195. });
  196. it('handles invalid search id', async function() {
  197. const streamProps = {
  198. setProjectNavSection: function() {},
  199. params: {orgId: 'org-slug', projectId: 'project-slug', searchId: 'invalid'},
  200. location: {query: {}, search: ''},
  201. };
  202. wrapper = shallow(<Stream {...streamProps} />, {
  203. context,
  204. });
  205. await wrapper.update();
  206. expect(wrapper.instance().state.searchId).toBeNull();
  207. expect(wrapper.instance().state.query).toBe('');
  208. });
  209. it('handles default saved search (no search id or query)', async function() {
  210. const streamProps = {
  211. ...props,
  212. location: {query: {}, search: ''},
  213. };
  214. MockApiClient.addMockResponse({
  215. url: '/projects/org-slug/project-slug/searches/',
  216. body: [
  217. {...savedSearch, isDefault: false},
  218. {
  219. id: 'default',
  220. query: 'is:unresolved assigned:me',
  221. name: 'default',
  222. isDefault: true,
  223. },
  224. ],
  225. });
  226. wrapper = shallow(<Stream {...streamProps} />, {
  227. context,
  228. });
  229. await wrapper.update();
  230. expect(wrapper.instance().state.searchId).toBe('default');
  231. expect(wrapper.instance().state.query).toBe('is:unresolved assigned:me');
  232. });
  233. });
  234. describe('render()', function() {
  235. beforeEach(function() {
  236. wrapper = shallow(<Stream {...props} />, {
  237. context,
  238. });
  239. });
  240. it('displays a loading indicator when component is loading', function() {
  241. wrapper.setState({loading: true});
  242. expect(wrapper.find('.loading')).toBeTruthy();
  243. });
  244. it('displays a loading indicator when data is loading', function() {
  245. wrapper.setState({dataLoading: true});
  246. expect(wrapper.find('.loading')).toBeTruthy();
  247. });
  248. it('displays an error when component has errored', function() {
  249. wrapper.setState({
  250. error: 'Something bad happened',
  251. loading: false,
  252. dataLoading: false,
  253. });
  254. expect(wrapper.find(LoadingError).length).toBeTruthy();
  255. });
  256. it('displays the group list', function() {
  257. wrapper.setState({
  258. error: false,
  259. groupIds: ['1'],
  260. loading: false,
  261. dataLoading: false,
  262. });
  263. expect(wrapper).toMatchSnapshot();
  264. expect(wrapper.find('.ref-group-list').length).toBeTruthy();
  265. });
  266. it('displays empty with no ids', function() {
  267. wrapper.setState({
  268. error: false,
  269. groupIds: [],
  270. loading: false,
  271. dataLoading: false,
  272. });
  273. expect(wrapper.find('EmptyStateWarning').length).toBeTruthy();
  274. });
  275. describe('no first event sent', function() {
  276. it('shows "awaiting events" message when no events have been sent', function() {
  277. context.project.firstEvent = false;
  278. wrapper.setState({
  279. error: false,
  280. groupIds: [],
  281. loading: false,
  282. dataLoading: false,
  283. });
  284. expect(wrapper.find(ErrorRobot)).toHaveLength(1);
  285. });
  286. it('does not show "awaiting events" when an event is recieved', function() {
  287. context.project.firstEvent = false;
  288. wrapper.setState({
  289. error: false,
  290. groupIds: ['1'],
  291. loading: false,
  292. dataLoading: false,
  293. });
  294. expect(wrapper.find('.ref-group-list').length).toBeTruthy();
  295. });
  296. });
  297. it('does not have real time event updates when events exist', function() {
  298. wrapper = shallow(<Stream {...wrapper.instance().props} />, {
  299. context: {
  300. ...context,
  301. project: {
  302. ...context.project,
  303. firstEvent: true,
  304. },
  305. },
  306. });
  307. expect(wrapper.state('realtimeActive')).toBe(false);
  308. });
  309. it('does not have real time event updates enabled when cookie is present (even if there are no events)', function() {
  310. Cookies.set('realtimeActive', 'false');
  311. wrapper = shallow(<Stream {...wrapper.instance().props} />, {
  312. context: {
  313. ...context,
  314. project: {
  315. ...context.project,
  316. firstEvent: false,
  317. },
  318. },
  319. });
  320. wrapper.setState({
  321. error: false,
  322. groupIds: [],
  323. loading: false,
  324. dataLoading: false,
  325. });
  326. Cookies.remove('realtimeActive');
  327. expect(wrapper.state('realtimeActive')).toBe(false);
  328. });
  329. it('has real time event updates enabled when there are no events', function() {
  330. wrapper = shallow(<Stream {...wrapper.instance().props} />, {
  331. context: {
  332. ...context,
  333. project: {
  334. ...context.project,
  335. firstEvent: false,
  336. },
  337. },
  338. });
  339. wrapper.setState({
  340. error: false,
  341. groupIds: [],
  342. loading: false,
  343. dataLoading: false,
  344. });
  345. expect(wrapper.state('realtimeActive')).toBe(true);
  346. });
  347. });
  348. describe('toggles environment', function() {
  349. beforeEach(function() {
  350. wrapper = shallow(<Stream {...props} />, {
  351. context,
  352. });
  353. });
  354. it('select all environments', function() {
  355. EnvironmentStore.loadInitialData(TestStubs.Environments());
  356. setActiveEnvironment(null);
  357. wrapper.setState({
  358. error: false,
  359. groupIds: ['1'],
  360. loading: false,
  361. dataLoading: false,
  362. });
  363. expect(wrapper).toMatchSnapshot();
  364. });
  365. });
  366. describe('componentWillMount()', function() {
  367. afterEach(function() {
  368. Cookies.remove('realtimeActive');
  369. });
  370. it('reads the realtimeActive state from a cookie', function() {
  371. Cookies.set('realtimeActive', 'false');
  372. const stream = wrapper.instance();
  373. expect(stream.getInitialState()).toHaveProperty('realtimeActive', false);
  374. });
  375. it('reads the true realtimeActive state from a cookie', function() {
  376. Cookies.set('realtimeActive', 'true');
  377. const stream = wrapper.instance();
  378. expect(stream.getInitialState()).toHaveProperty('realtimeActive', true);
  379. });
  380. });
  381. describe('onRealtimeChange', function() {
  382. it('sets the realtimeActive state', function() {
  383. const stream = wrapper.instance();
  384. stream.state.realtimeActive = false;
  385. stream.onRealtimeChange(true);
  386. expect(stream.state.realtimeActive).toEqual(true);
  387. expect(Cookies.get('realtimeActive')).toEqual('true');
  388. stream.onRealtimeChange(false);
  389. expect(stream.state.realtimeActive).toEqual(false);
  390. expect(Cookies.get('realtimeActive')).toEqual('false');
  391. });
  392. });
  393. describe('getInitialState', function() {
  394. it('handles query', function() {
  395. const expected = {
  396. groupIds: [],
  397. selectAllActive: false,
  398. multiSelected: false,
  399. anySelected: false,
  400. statsPeriod: '24h',
  401. realtimeActive: false,
  402. pageLinks: '',
  403. loading: false,
  404. dataLoading: true,
  405. error: false,
  406. searchId: null,
  407. query: 'is:unresolved',
  408. sort: 'date',
  409. };
  410. const actual = wrapper.instance().getInitialState();
  411. expect(_.pick(actual, _.keys(expected))).toEqual(expected);
  412. });
  413. it('handles no searchId or query', async function() {
  414. const streamProps = {
  415. ...props,
  416. location: {query: {sort: 'freq'}, search: 'sort=freq'},
  417. };
  418. const expected = {
  419. groupIds: [],
  420. selectAllActive: false,
  421. multiSelected: false,
  422. anySelected: false,
  423. statsPeriod: '24h',
  424. realtimeActive: false,
  425. loading: false,
  426. dataLoading: false,
  427. error: false,
  428. query: '',
  429. sort: 'freq',
  430. searchId: null,
  431. };
  432. wrapper = shallow(<Stream {...streamProps} />, {
  433. context,
  434. });
  435. await wrapper.update();
  436. const stream = wrapper.instance();
  437. const actual = stream.state;
  438. expect(_.pick(actual, _.keys(expected))).toEqual(expected);
  439. });
  440. it('handles valid searchId in routing params', async function() {
  441. const streamProps = {
  442. ...props,
  443. location: {query: {sort: 'freq'}, search: 'sort=freq'},
  444. params: {orgId: 'org-slug', projectId: 'project-slug', searchId: '789'},
  445. };
  446. const expected = {
  447. groupIds: [],
  448. selectAllActive: false,
  449. multiSelected: false,
  450. anySelected: false,
  451. statsPeriod: '24h',
  452. realtimeActive: false,
  453. loading: false,
  454. dataLoading: false,
  455. error: false,
  456. query: 'is:unresolved',
  457. sort: 'freq',
  458. searchId: '789',
  459. };
  460. wrapper = shallow(<Stream {...streamProps} />, {
  461. context,
  462. });
  463. wrapper.setState({
  464. savedSearchList: [{id: '789', query: 'is:unresolved', name: 'test'}],
  465. });
  466. await wrapper.update();
  467. const actual = wrapper.instance().state;
  468. expect(_.pick(actual, _.keys(expected))).toEqual(expected);
  469. });
  470. it('handles invalid searchId in routing params', async function() {
  471. const streamProps = {
  472. ...props,
  473. location: {query: {sort: 'freq'}, search: 'sort=freq'},
  474. params: {orgId: 'org-slug', projectId: 'project-slug', searchId: '799'},
  475. };
  476. const expected = {
  477. groupIds: [],
  478. selectAllActive: false,
  479. multiSelected: false,
  480. anySelected: false,
  481. statsPeriod: '24h',
  482. realtimeActive: false,
  483. loading: false,
  484. dataLoading: false,
  485. error: false,
  486. query: '',
  487. sort: 'freq',
  488. searchId: null,
  489. };
  490. wrapper = shallow(<Stream {...streamProps} />, {
  491. context,
  492. });
  493. await wrapper.update();
  494. const stream = wrapper.instance();
  495. const actual = stream.state;
  496. expect(_.pick(actual, _.keys(expected))).toEqual(expected);
  497. });
  498. });
  499. describe('getQueryState', function() {
  500. it('handles changed search id', async function() {
  501. const nextProps = {
  502. ...props,
  503. location: {
  504. pathname: '/org-slug/project-slug/searches/789/',
  505. },
  506. params: {orgId: 'org-slug', projectId: 'project-slug', searchId: '789'},
  507. };
  508. wrapper = shallow(<Stream {...props} />, {
  509. context,
  510. });
  511. await wrapper.update();
  512. const stream = wrapper.instance();
  513. const nextState = stream.getQueryState(nextProps);
  514. expect(nextState).toEqual(
  515. expect.objectContaining({searchId: '789', query: 'is:unresolved'})
  516. );
  517. });
  518. it('handles changed querystring', function() {
  519. const nextProps = {
  520. ...props,
  521. location: {
  522. query: {
  523. query: 'is:unresolved assigned:me',
  524. },
  525. },
  526. };
  527. const stream = shallow(<Stream {...props} />, {
  528. context,
  529. }).instance();
  530. const nextState = stream.getQueryState(nextProps);
  531. expect(nextState).toEqual(
  532. expect.objectContaining({searchId: null, query: 'is:unresolved assigned:me'})
  533. );
  534. });
  535. });
  536. });