overview.spec.jsx 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830
  1. import {browserHistory} from 'react-router';
  2. import cloneDeep from 'lodash/cloneDeep';
  3. import range from 'lodash/range';
  4. import {mountWithTheme, shallow} from 'sentry-test/enzyme';
  5. import {initializeOrg} from 'sentry-test/initializeOrg';
  6. import ErrorRobot from 'app/components/errorRobot';
  7. import StreamGroup from 'app/components/stream/group';
  8. import GroupStore from 'app/stores/groupStore';
  9. import TagStore from 'app/stores/tagStore';
  10. import * as parseLinkHeader from 'app/utils/parseLinkHeader';
  11. import IssueListWithStores, {IssueListOverview} from 'app/views/issueList/overview';
  12. // Mock <IssueListSidebar> and <IssueListActions>
  13. jest.mock('app/views/issueList/sidebar', () => jest.fn(() => null));
  14. jest.mock('app/views/issueList/actions', () => jest.fn(() => null));
  15. jest.mock('app/components/stream/group', () => jest.fn(() => null));
  16. jest.mock('app/views/issueList/noGroupsHandler/congratsRobots', () =>
  17. jest.fn(() => null)
  18. );
  19. const DEFAULT_LINKS_HEADER =
  20. '<http://127.0.0.1:8000/api/0/organizations/org-slug/issues/?cursor=1443575731:0:1>; rel="previous"; results="false"; cursor="1443575731:0:1", ' +
  21. '<http://127.0.0.1:8000/api/0/organizations/org-slug/issues/?cursor=1443575000:0:0>; rel="next"; results="true"; cursor="1443575000:0:0"';
  22. describe('IssueList', function () {
  23. let wrapper;
  24. let props;
  25. let organization;
  26. let project;
  27. let group;
  28. let groupStats;
  29. let savedSearch;
  30. let fetchTagsRequest;
  31. let fetchMembersRequest;
  32. const api = new MockApiClient();
  33. beforeEach(function () {
  34. MockApiClient.clearMockResponses();
  35. project = TestStubs.ProjectDetails({
  36. id: '3559',
  37. name: 'Foo Project',
  38. slug: 'project-slug',
  39. firstEvent: true,
  40. });
  41. organization = TestStubs.Organization({
  42. id: '1337',
  43. slug: 'org-slug',
  44. access: ['releases'],
  45. features: [],
  46. projects: [project],
  47. });
  48. savedSearch = TestStubs.Search({
  49. id: '789',
  50. query: 'is:unresolved',
  51. sort: 'date',
  52. name: 'Unresolved Issues',
  53. projectId: project.id,
  54. });
  55. group = TestStubs.Group({project});
  56. MockApiClient.addMockResponse({
  57. url: '/organizations/org-slug/issues/',
  58. body: [group],
  59. headers: {
  60. Link: DEFAULT_LINKS_HEADER,
  61. },
  62. });
  63. groupStats = TestStubs.GroupStats();
  64. MockApiClient.addMockResponse({
  65. url: '/organizations/org-slug/issues-stats/',
  66. body: [groupStats],
  67. });
  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/recent-searches/',
  78. method: 'POST',
  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. const tags = TestStubs.Tags();
  94. fetchTagsRequest = MockApiClient.addMockResponse({
  95. url: '/organizations/org-slug/tags/',
  96. method: 'GET',
  97. body: tags,
  98. });
  99. fetchMembersRequest = MockApiClient.addMockResponse({
  100. url: '/organizations/org-slug/users/',
  101. method: 'GET',
  102. body: [TestStubs.Member({projects: [project.slug]})],
  103. });
  104. MockApiClient.addMockResponse({
  105. url: '/organizations/org-slug/sent-first-event/',
  106. body: {sentFirstEvent: true},
  107. });
  108. MockApiClient.addMockResponse({
  109. url: '/organizations/org-slug/projects/',
  110. body: [project],
  111. });
  112. TagStore.init();
  113. props = {
  114. api,
  115. savedSearchLoading: false,
  116. savedSearches: [savedSearch],
  117. useOrgSavedSearches: true,
  118. selection: {
  119. projects: [parseInt(organization.projects[0].id, 10)],
  120. environments: [],
  121. datetime: {period: '14d'},
  122. },
  123. location: {query: {query: 'is:unresolved'}, search: 'query=is:unresolved'},
  124. params: {orgId: organization.slug},
  125. organization,
  126. tags: tags.reduce((acc, tag) => {
  127. acc[tag.key] = tag;
  128. return acc;
  129. }),
  130. };
  131. });
  132. afterEach(function () {
  133. jest.clearAllMocks();
  134. MockApiClient.clearMockResponses();
  135. if (wrapper) {
  136. wrapper.unmount();
  137. }
  138. wrapper = null;
  139. });
  140. describe('withStores and feature flags', function () {
  141. const {router, routerContext} = initializeOrg({
  142. organization: {
  143. features: ['global-views'],
  144. slug: 'org-slug',
  145. },
  146. router: {
  147. location: {query: {}, search: ''},
  148. params: {orgId: 'org-slug'},
  149. },
  150. });
  151. const defaultProps = {};
  152. let savedSearchesRequest;
  153. let recentSearchesRequest;
  154. let issuesRequest;
  155. /* helpers */
  156. const getSavedSearchTitle = w =>
  157. w.find('SavedSearchSelector DropdownMenu ButtonTitle').text();
  158. const getSearchBarValue = w =>
  159. w.find('SmartSearchBarContainer textarea').prop('value').trim();
  160. const createWrapper = ({params, location, ...p} = {}) => {
  161. const newRouter = {
  162. ...router,
  163. params: {
  164. ...router.params,
  165. ...params,
  166. },
  167. location: {
  168. ...router.location,
  169. ...location,
  170. },
  171. };
  172. wrapper = mountWithTheme(
  173. <IssueListWithStores {...newRouter} {...defaultProps} {...p} />,
  174. routerContext
  175. );
  176. };
  177. beforeEach(function () {
  178. StreamGroup.mockClear();
  179. recentSearchesRequest = MockApiClient.addMockResponse({
  180. url: '/organizations/org-slug/recent-searches/',
  181. method: 'GET',
  182. body: [],
  183. });
  184. savedSearchesRequest = MockApiClient.addMockResponse({
  185. url: '/organizations/org-slug/searches/',
  186. body: [savedSearch],
  187. });
  188. issuesRequest = MockApiClient.addMockResponse({
  189. url: '/organizations/org-slug/issues/',
  190. body: [group],
  191. headers: {
  192. Link: DEFAULT_LINKS_HEADER,
  193. },
  194. });
  195. });
  196. it('loads group rows with default query (no pinned queries, and no query in URL)', async function () {
  197. createWrapper();
  198. // Loading saved searches
  199. expect(savedSearchesRequest).toHaveBeenCalledTimes(1);
  200. // Update stores with saved searches
  201. await tick();
  202. await tick();
  203. wrapper.update();
  204. // auxillary requests being made
  205. expect(recentSearchesRequest).toHaveBeenCalledTimes(1);
  206. expect(fetchTagsRequest).toHaveBeenCalledTimes(1);
  207. expect(fetchMembersRequest).toHaveBeenCalledTimes(1);
  208. // primary /issues/ request
  209. expect(issuesRequest).toHaveBeenCalledWith(
  210. expect.anything(),
  211. expect.objectContaining({
  212. // Should be called with default query
  213. data: expect.stringContaining('is%3Aunresolved'),
  214. })
  215. );
  216. expect(getSearchBarValue(wrapper)).toBe('is:unresolved');
  217. // Organization saved search selector should have default saved search selected
  218. expect(getSavedSearchTitle(wrapper)).toBe('Unresolved Issues');
  219. // This is mocked
  220. expect(StreamGroup).toHaveBeenCalled();
  221. });
  222. it('loads with query in URL and pinned queries', async function () {
  223. savedSearchesRequest = MockApiClient.addMockResponse({
  224. url: '/organizations/org-slug/searches/',
  225. body: [
  226. savedSearch,
  227. TestStubs.Search({
  228. id: '123',
  229. name: 'My Pinned Search',
  230. isPinned: true,
  231. query: 'is:resolved',
  232. }),
  233. ],
  234. });
  235. createWrapper({
  236. location: {
  237. query: {
  238. query: 'level:foo',
  239. },
  240. },
  241. });
  242. // Update stores with saved searches
  243. await tick();
  244. await tick();
  245. wrapper.update();
  246. // Main /issues/ request
  247. expect(issuesRequest).toHaveBeenCalledWith(
  248. expect.anything(),
  249. expect.objectContaining({
  250. // Should be called with default query
  251. data: expect.stringContaining('level%3Afoo'),
  252. })
  253. );
  254. expect(getSearchBarValue(wrapper)).toBe('level:foo');
  255. // Custom search
  256. expect(getSavedSearchTitle(wrapper)).toBe('Custom Search');
  257. });
  258. it('loads with a pinned saved query', async function () {
  259. savedSearchesRequest = MockApiClient.addMockResponse({
  260. url: '/organizations/org-slug/searches/',
  261. body: [
  262. savedSearch,
  263. TestStubs.Search({
  264. id: '123',
  265. name: 'Org Custom',
  266. isPinned: true,
  267. isGlobal: false,
  268. isOrgCustom: true,
  269. query: 'is:resolved',
  270. }),
  271. ],
  272. });
  273. createWrapper();
  274. await tick();
  275. await tick();
  276. wrapper.update();
  277. expect(issuesRequest).toHaveBeenCalledWith(
  278. expect.anything(),
  279. expect.objectContaining({
  280. // Should be called with default query
  281. data: expect.stringContaining('is%3Aresolved'),
  282. })
  283. );
  284. expect(getSearchBarValue(wrapper)).toBe('is:resolved');
  285. // Organization saved search selector should have default saved search selected
  286. expect(getSavedSearchTitle(wrapper)).toBe('Org Custom');
  287. });
  288. it('loads with a pinned custom query', async function () {
  289. savedSearchesRequest = MockApiClient.addMockResponse({
  290. url: '/organizations/org-slug/searches/',
  291. body: [
  292. savedSearch,
  293. TestStubs.Search({
  294. id: '123',
  295. name: 'My Pinned Search',
  296. isPinned: true,
  297. isGlobal: false,
  298. isOrgCustom: false,
  299. query: 'is:resolved',
  300. }),
  301. ],
  302. });
  303. createWrapper();
  304. await tick();
  305. await tick();
  306. wrapper.update();
  307. expect(issuesRequest).toHaveBeenCalledWith(
  308. expect.anything(),
  309. expect.objectContaining({
  310. // Should be called with default query
  311. data: expect.stringContaining('is%3Aresolved'),
  312. })
  313. );
  314. expect(getSearchBarValue(wrapper)).toBe('is:resolved');
  315. // Organization saved search selector should have default saved search selected
  316. expect(getSavedSearchTitle(wrapper)).toBe('My Pinned Search');
  317. });
  318. it('loads with a saved query', async function () {
  319. savedSearchesRequest = MockApiClient.addMockResponse({
  320. url: '/organizations/org-slug/searches/',
  321. body: [
  322. TestStubs.Search({
  323. id: '123',
  324. name: 'Assigned to Me',
  325. isPinned: false,
  326. isGlobal: true,
  327. query: 'assigned:me',
  328. sort: 'priority',
  329. projectId: null,
  330. type: 0,
  331. }),
  332. ],
  333. });
  334. createWrapper({params: {searchId: '123'}});
  335. await tick();
  336. await tick();
  337. wrapper.update();
  338. expect(issuesRequest).toHaveBeenCalledWith(
  339. expect.anything(),
  340. expect.objectContaining({
  341. // Should be called with default query
  342. data:
  343. expect.stringContaining('assigned%3Ame') &&
  344. expect.stringContaining('sort=priority'),
  345. })
  346. );
  347. expect(getSearchBarValue(wrapper)).toBe('assigned:me');
  348. // Organization saved search selector should have default saved search selected
  349. expect(getSavedSearchTitle(wrapper)).toBe('Assigned to Me');
  350. });
  351. it('loads with a query in URL', async function () {
  352. savedSearchesRequest = MockApiClient.addMockResponse({
  353. url: '/organizations/org-slug/searches/',
  354. body: [
  355. TestStubs.Search({
  356. id: '123',
  357. name: 'Assigned to Me',
  358. isPinned: false,
  359. isGlobal: true,
  360. query: 'assigned:me',
  361. projectId: null,
  362. type: 0,
  363. }),
  364. ],
  365. });
  366. createWrapper({location: {query: {query: 'level:error'}}});
  367. await tick();
  368. await tick();
  369. wrapper.update();
  370. expect(issuesRequest).toHaveBeenCalledWith(
  371. expect.anything(),
  372. expect.objectContaining({
  373. // Should be called with default query
  374. data: expect.stringContaining('level%3Aerror'),
  375. })
  376. );
  377. expect(getSearchBarValue(wrapper)).toBe('level:error');
  378. // Organization saved search selector should have default saved search selected
  379. expect(getSavedSearchTitle(wrapper)).toBe('Custom Search');
  380. });
  381. it('loads with an empty query in URL', async function () {
  382. savedSearchesRequest = MockApiClient.addMockResponse({
  383. url: '/organizations/org-slug/searches/',
  384. body: [
  385. TestStubs.Search({
  386. id: '123',
  387. name: 'My Pinned Search',
  388. isPinned: true,
  389. isGlobal: false,
  390. isOrgCustom: false,
  391. query: 'is:resolved',
  392. }),
  393. ],
  394. });
  395. createWrapper({location: {query: {query: undefined}}});
  396. await tick();
  397. await tick();
  398. wrapper.update();
  399. expect(issuesRequest).toHaveBeenCalledWith(
  400. expect.anything(),
  401. expect.objectContaining({
  402. // Should be called with empty query
  403. data: expect.stringContaining(''),
  404. })
  405. );
  406. expect(getSearchBarValue(wrapper)).toBe('is:resolved');
  407. // Organization saved search selector should have default saved search selected
  408. expect(getSavedSearchTitle(wrapper)).toBe('My Pinned Search');
  409. });
  410. it('selects a saved search and changes sort', async function () {
  411. const localSavedSearch = {...savedSearch, projectId: null};
  412. savedSearchesRequest = MockApiClient.addMockResponse({
  413. url: '/organizations/org-slug/searches/',
  414. body: [localSavedSearch],
  415. });
  416. createWrapper();
  417. await tick();
  418. await tick();
  419. wrapper.update();
  420. wrapper.find('SavedSearchSelector DropdownButton').simulate('click');
  421. wrapper.find('SavedSearchSelector MenuItem a').first().simulate('click');
  422. expect(browserHistory.push).toHaveBeenLastCalledWith(
  423. expect.objectContaining({
  424. pathname: '/organizations/org-slug/issues/searches/789/',
  425. })
  426. );
  427. // Need to update component
  428. wrapper.setProps({
  429. savedSearch: localSavedSearch,
  430. location: {
  431. ...router.location,
  432. pathname: '/organizations/org-slug/issues/searches/789/',
  433. query: {
  434. sort: 'freq',
  435. environment: [],
  436. project: [],
  437. },
  438. },
  439. });
  440. await tick();
  441. wrapper.update();
  442. wrapper.find('IssueListSortOptions DropdownButton').simulate('click');
  443. wrapper.find('DropdownItem').at(3).find('MenuItem span').at(1).simulate('click');
  444. expect(browserHistory.push).toHaveBeenLastCalledWith(
  445. expect.objectContaining({
  446. pathname: '/organizations/org-slug/issues/searches/789/',
  447. query: {
  448. environment: [],
  449. project: [],
  450. sort: 'freq',
  451. statsPeriod: '14d',
  452. },
  453. })
  454. );
  455. });
  456. it('clears a saved search when a custom one is entered', async function () {
  457. savedSearchesRequest = MockApiClient.addMockResponse({
  458. url: '/organizations/org-slug/searches/',
  459. body: [
  460. savedSearch,
  461. TestStubs.Search({
  462. id: '123',
  463. name: 'Pinned search',
  464. isPinned: true,
  465. isGlobal: false,
  466. isOrgCustom: true,
  467. query: 'is:resolved',
  468. }),
  469. ],
  470. });
  471. createWrapper();
  472. await tick();
  473. await tick();
  474. await wrapper.update();
  475. // Update the search textarea
  476. wrapper
  477. .find('IssueListFilters SmartSearchBar textarea')
  478. .simulate('change', {target: {value: 'dogs'}});
  479. // Submit the form
  480. wrapper.find('IssueListFilters SmartSearchBar form').simulate('submit');
  481. await wrapper.update();
  482. expect(browserHistory.push).toHaveBeenLastCalledWith(
  483. expect.objectContaining({
  484. pathname: '/organizations/org-slug/issues/',
  485. query: {
  486. environment: [],
  487. project: [],
  488. query: 'dogs',
  489. statsPeriod: '14d',
  490. },
  491. })
  492. );
  493. });
  494. it('pins and unpins a custom query', async function () {
  495. savedSearchesRequest = MockApiClient.addMockResponse({
  496. url: '/organizations/org-slug/searches/',
  497. body: [savedSearch],
  498. });
  499. createWrapper();
  500. await tick();
  501. await tick();
  502. wrapper.update();
  503. const createPin = MockApiClient.addMockResponse({
  504. url: '/organizations/org-slug/pinned-searches/',
  505. method: 'PUT',
  506. body: {
  507. ...savedSearch,
  508. id: '666',
  509. name: 'My Pinned Search',
  510. query: 'assigned:me level:fatal',
  511. sort: 'date',
  512. isPinned: true,
  513. },
  514. });
  515. const deletePin = MockApiClient.addMockResponse({
  516. url: '/organizations/org-slug/pinned-searches/',
  517. method: 'DELETE',
  518. });
  519. wrapper
  520. .find('SmartSearchBar textarea')
  521. .simulate('change', {target: {value: 'assigned:me level:fatal'}});
  522. wrapper.find('SmartSearchBar form').simulate('submit');
  523. expect(browserHistory.push.mock.calls[0][0]).toEqual(
  524. expect.objectContaining({
  525. query: expect.objectContaining({
  526. query: 'assigned:me level:fatal',
  527. }),
  528. })
  529. );
  530. await tick();
  531. wrapper.setProps({
  532. location: {
  533. ...router.location,
  534. query: {
  535. query: 'assigned:me level:fatal',
  536. },
  537. },
  538. });
  539. expect(wrapper.find('SavedSearchSelector ButtonTitle').text()).toBe(
  540. 'Custom Search'
  541. );
  542. wrapper.find('Button[aria-label="Pin this search"] button').simulate('click');
  543. expect(createPin).toHaveBeenCalled();
  544. await tick();
  545. wrapper.update();
  546. expect(browserHistory.push).toHaveBeenLastCalledWith(
  547. expect.objectContaining({
  548. pathname: '/organizations/org-slug/issues/searches/666/',
  549. query: {},
  550. search: '',
  551. })
  552. );
  553. wrapper.setProps({
  554. params: {
  555. ...router.params,
  556. searchId: '666',
  557. },
  558. });
  559. await tick();
  560. wrapper.update();
  561. expect(wrapper.find('SavedSearchSelector ButtonTitle').text()).toBe(
  562. 'My Pinned Search'
  563. );
  564. wrapper.find('Button[aria-label="Unpin this search"] button').simulate('click');
  565. expect(deletePin).toHaveBeenCalled();
  566. await tick();
  567. wrapper.update();
  568. expect(browserHistory.push).toHaveBeenLastCalledWith(
  569. expect.objectContaining({
  570. pathname: '/organizations/org-slug/issues/',
  571. query: {
  572. query: 'assigned:me level:fatal',
  573. sort: 'date',
  574. },
  575. })
  576. );
  577. });
  578. it('pins and unpins a saved query', async function () {
  579. const assignedToMe = TestStubs.Search({
  580. id: '234',
  581. name: 'Assigned to Me',
  582. isPinned: false,
  583. isGlobal: true,
  584. query: 'assigned:me',
  585. sort: 'date',
  586. projectId: null,
  587. type: 0,
  588. });
  589. savedSearchesRequest = MockApiClient.addMockResponse({
  590. url: '/organizations/org-slug/searches/',
  591. body: [savedSearch, assignedToMe],
  592. });
  593. createWrapper();
  594. await tick();
  595. await tick();
  596. wrapper.update();
  597. let createPin = MockApiClient.addMockResponse({
  598. url: '/organizations/org-slug/pinned-searches/',
  599. method: 'PUT',
  600. body: {
  601. ...savedSearch,
  602. isPinned: true,
  603. },
  604. });
  605. wrapper.find('SavedSearchSelector DropdownButton').simulate('click');
  606. wrapper.find('SavedSearchSelector MenuItem a').first().simulate('click');
  607. await tick();
  608. expect(browserHistory.push).toHaveBeenLastCalledWith(
  609. expect.objectContaining({
  610. pathname: '/organizations/org-slug/issues/searches/789/',
  611. query: {
  612. environment: [],
  613. project: ['3559'],
  614. statsPeriod: '14d',
  615. sort: 'date',
  616. },
  617. })
  618. );
  619. wrapper.setProps({
  620. params: {
  621. ...router.params,
  622. searchId: '789',
  623. },
  624. });
  625. expect(wrapper.find('SavedSearchSelector ButtonTitle').text()).toBe(
  626. 'Unresolved Issues'
  627. );
  628. wrapper.find('Button[aria-label="Pin this search"] button').simulate('click');
  629. expect(createPin).toHaveBeenCalled();
  630. await tick();
  631. wrapper.update();
  632. expect(browserHistory.push).toHaveBeenLastCalledWith(
  633. expect.objectContaining({
  634. pathname: '/organizations/org-slug/issues/searches/789/',
  635. })
  636. );
  637. wrapper.setProps({
  638. params: {
  639. ...router.params,
  640. searchId: '789',
  641. },
  642. });
  643. await tick();
  644. wrapper.update();
  645. expect(wrapper.find('SavedSearchSelector ButtonTitle').text()).toBe(
  646. 'Unresolved Issues'
  647. );
  648. // Select other saved search
  649. wrapper.find('SavedSearchSelector DropdownButton').simulate('click');
  650. wrapper.find('SavedSearchSelector MenuItem a').at(1).simulate('click');
  651. expect(browserHistory.push).toHaveBeenLastCalledWith(
  652. expect.objectContaining({
  653. pathname: '/organizations/org-slug/issues/searches/234/',
  654. query: {
  655. project: [],
  656. environment: [],
  657. statsPeriod: '14d',
  658. sort: 'date',
  659. },
  660. })
  661. );
  662. wrapper.setProps({
  663. params: {
  664. ...router.params,
  665. searchId: '234',
  666. },
  667. });
  668. expect(wrapper.find('SavedSearchSelector ButtonTitle').text()).toBe(
  669. 'Assigned to Me'
  670. );
  671. createPin = MockApiClient.addMockResponse({
  672. url: '/organizations/org-slug/pinned-searches/',
  673. method: 'PUT',
  674. body: {
  675. ...assignedToMe,
  676. isPinned: true,
  677. },
  678. });
  679. wrapper.find('Button[aria-label="Pin this search"] button').simulate('click');
  680. expect(createPin).toHaveBeenCalled();
  681. await tick();
  682. wrapper.update();
  683. expect(browserHistory.push).toHaveBeenLastCalledWith(
  684. expect.objectContaining({
  685. pathname: '/organizations/org-slug/issues/searches/234/',
  686. })
  687. );
  688. wrapper.setProps({
  689. params: {
  690. ...router.params,
  691. searchId: '234',
  692. },
  693. });
  694. await tick();
  695. wrapper.update();
  696. expect(wrapper.find('SavedSearchSelector ButtonTitle').text()).toBe(
  697. 'Assigned to Me'
  698. );
  699. });
  700. it('pinning and unpinning searches should keep project selected', async function () {
  701. savedSearchesRequest = MockApiClient.addMockResponse({
  702. url: '/organizations/org-slug/searches/',
  703. body: [savedSearch],
  704. });
  705. createWrapper({
  706. selection: {
  707. projects: [123],
  708. environments: ['prod'],
  709. datetime: {},
  710. },
  711. location: {query: {project: ['123'], environment: ['prod']}},
  712. });
  713. await tick();
  714. await tick();
  715. wrapper.update();
  716. const deletePin = MockApiClient.addMockResponse({
  717. url: '/organizations/org-slug/pinned-searches/',
  718. method: 'DELETE',
  719. });
  720. const createPin = MockApiClient.addMockResponse({
  721. url: '/organizations/org-slug/pinned-searches/',
  722. method: 'PUT',
  723. body: {
  724. ...savedSearch,
  725. id: '666',
  726. name: 'My Pinned Search',
  727. query: 'assigned:me level:fatal',
  728. sort: 'date',
  729. isPinned: true,
  730. },
  731. });
  732. wrapper
  733. .find('SmartSearchBar textarea')
  734. .simulate('change', {target: {value: 'assigned:me level:fatal'}});
  735. wrapper.find('SmartSearchBar form').simulate('submit');
  736. await tick();
  737. expect(browserHistory.push).toHaveBeenLastCalledWith(
  738. expect.objectContaining({
  739. query: expect.objectContaining({
  740. project: [123],
  741. environment: ['prod'],
  742. query: 'assigned:me level:fatal',
  743. }),
  744. })
  745. );
  746. const newRouter = {
  747. ...router,
  748. location: {
  749. ...router.location,
  750. query: {
  751. ...router.location.query,
  752. project: [123],
  753. environment: ['prod'],
  754. query: 'assigned:me level:fatal',
  755. },
  756. },
  757. };
  758. wrapper.setProps({...newRouter, router: newRouter});
  759. wrapper.setContext({router: newRouter});
  760. wrapper.update();
  761. wrapper.find('Button[aria-label="Pin this search"] button').simulate('click');
  762. expect(createPin).toHaveBeenCalled();
  763. await tick();
  764. wrapper.update();
  765. expect(browserHistory.push).toHaveBeenLastCalledWith(
  766. expect.objectContaining({
  767. pathname: '/organizations/org-slug/issues/searches/666/',
  768. query: expect.objectContaining({
  769. project: [123],
  770. environment: ['prod'],
  771. query: 'assigned:me level:fatal',
  772. }),
  773. })
  774. );
  775. wrapper.setProps({
  776. params: {
  777. ...router.params,
  778. searchId: '666',
  779. },
  780. });
  781. await tick();
  782. wrapper.update();
  783. wrapper.find('Button[aria-label="Unpin this search"] button').simulate('click');
  784. expect(deletePin).toHaveBeenCalled();
  785. await tick();
  786. wrapper.update();
  787. expect(browserHistory.push).toHaveBeenLastCalledWith(
  788. expect.objectContaining({
  789. pathname: '/organizations/org-slug/issues/',
  790. query: expect.objectContaining({
  791. project: [123],
  792. environment: ['prod'],
  793. query: 'assigned:me level:fatal',
  794. }),
  795. })
  796. );
  797. });
  798. it.todo('saves a new query');
  799. it.todo('loads pinned search when invalid saved search id is accessed');
  800. it('does not allow pagination to "previous" while on first page and resets cursors when navigating back to initial page', async function () {
  801. let pushArgs;
  802. createWrapper();
  803. await tick();
  804. await tick();
  805. wrapper.update();
  806. expect(wrapper.find('Pagination Button').first().prop('disabled')).toBe(true);
  807. issuesRequest = MockApiClient.addMockResponse({
  808. url: '/organizations/org-slug/issues/',
  809. body: [group],
  810. headers: {
  811. Link:
  812. '<http://127.0.0.1:8000/api/0/organizations/org-slug/issues/?cursor=1443575000:0:0>; rel="previous"; results="true"; cursor="1443575000:0:1", <http://127.0.0.1:8000/api/0/organizations/org-slug/issues/?cursor=1443574000:0:0>; rel="next"; results="true"; cursor="1443574000:0:0"',
  813. },
  814. });
  815. // Click next
  816. wrapper.find('Pagination Button').last().simulate('click');
  817. await tick();
  818. pushArgs = {
  819. pathname: '/organizations/org-slug/issues/',
  820. query: {
  821. cursor: '1443575000:0:0',
  822. page: 1,
  823. environment: [],
  824. project: [],
  825. query: 'is:unresolved',
  826. statsPeriod: '14d',
  827. },
  828. };
  829. expect(browserHistory.push).toHaveBeenLastCalledWith(pushArgs);
  830. wrapper.setProps({location: pushArgs});
  831. wrapper.setContext({location: pushArgs});
  832. expect(wrapper.find('Pagination Button').first().prop('disabled')).toBe(false);
  833. // Click next again
  834. wrapper.find('Pagination Button').last().simulate('click');
  835. await tick();
  836. pushArgs = {
  837. pathname: '/organizations/org-slug/issues/',
  838. query: {
  839. cursor: '1443574000:0:0',
  840. page: 2,
  841. environment: [],
  842. project: [],
  843. query: 'is:unresolved',
  844. statsPeriod: '14d',
  845. },
  846. };
  847. expect(browserHistory.push).toHaveBeenLastCalledWith(pushArgs);
  848. wrapper.setProps({location: pushArgs});
  849. wrapper.setContext({location: pushArgs});
  850. // Click previous
  851. wrapper.find('Pagination Button').first().simulate('click');
  852. await tick();
  853. pushArgs = {
  854. pathname: '/organizations/org-slug/issues/',
  855. query: {
  856. cursor: '1443575000:0:1',
  857. page: 1,
  858. environment: [],
  859. project: [],
  860. query: 'is:unresolved',
  861. statsPeriod: '14d',
  862. },
  863. };
  864. expect(browserHistory.push).toHaveBeenLastCalledWith(pushArgs);
  865. wrapper.setProps({location: pushArgs});
  866. wrapper.setContext({location: pushArgs});
  867. // Click previous back to initial page
  868. wrapper.find('Pagination Button').first().simulate('click');
  869. await tick();
  870. // cursor is undefined because "prev" cursor is === initial "next" cursor
  871. expect(browserHistory.push).toHaveBeenLastCalledWith({
  872. pathname: '/organizations/org-slug/issues/',
  873. query: {
  874. cursor: undefined,
  875. environment: [],
  876. page: undefined,
  877. project: [],
  878. query: 'is:unresolved',
  879. statsPeriod: '14d',
  880. },
  881. });
  882. });
  883. });
  884. describe('transitionTo', function () {
  885. let instance;
  886. beforeEach(function () {
  887. wrapper = shallow(<IssueListOverview {...props} />);
  888. instance = wrapper.instance();
  889. });
  890. it('transitions to query updates', function () {
  891. instance.transitionTo({query: 'is:ignored'});
  892. expect(browserHistory.push).toHaveBeenCalledWith({
  893. pathname: '/organizations/org-slug/issues/',
  894. query: {
  895. environment: [],
  896. project: [parseInt(project.id, 10)],
  897. query: 'is:ignored',
  898. statsPeriod: '14d',
  899. },
  900. });
  901. });
  902. it('transitions to cursor with project-less saved search', function () {
  903. savedSearch = {
  904. id: 123,
  905. projectId: null,
  906. query: 'foo:bar',
  907. };
  908. instance.transitionTo({cursor: '1554756114000:0:0'}, savedSearch);
  909. // should keep the current project selection as we're going to the next page.
  910. expect(browserHistory.push).toHaveBeenCalledWith({
  911. pathname: '/organizations/org-slug/issues/searches/123/',
  912. query: {
  913. environment: [],
  914. project: [parseInt(project.id, 10)],
  915. cursor: '1554756114000:0:0',
  916. statsPeriod: '14d',
  917. },
  918. });
  919. });
  920. it('transitions to cursor with project saved search', function () {
  921. savedSearch = {
  922. id: 123,
  923. projectId: 999,
  924. query: 'foo:bar',
  925. };
  926. instance.transitionTo({cursor: '1554756114000:0:0'}, savedSearch);
  927. // should keep the current project selection as we're going to the next page.
  928. expect(browserHistory.push).toHaveBeenCalledWith({
  929. pathname: '/organizations/org-slug/issues/searches/123/',
  930. query: {
  931. environment: [],
  932. project: [parseInt(project.id, 10)],
  933. cursor: '1554756114000:0:0',
  934. statsPeriod: '14d',
  935. },
  936. });
  937. });
  938. it('transitions to saved search that has a projectId', function () {
  939. savedSearch = {
  940. id: 123,
  941. projectId: 99,
  942. query: 'foo:bar',
  943. };
  944. instance.transitionTo(undefined, savedSearch);
  945. expect(browserHistory.push).toHaveBeenCalledWith({
  946. pathname: '/organizations/org-slug/issues/searches/123/',
  947. query: {
  948. environment: [],
  949. project: [savedSearch.projectId],
  950. statsPeriod: '14d',
  951. },
  952. });
  953. });
  954. it('transitions to saved search with a sort', function () {
  955. savedSearch = {
  956. id: 123,
  957. project: null,
  958. query: 'foo:bar',
  959. sort: 'freq',
  960. };
  961. instance.transitionTo(undefined, savedSearch);
  962. expect(browserHistory.push).toHaveBeenCalledWith({
  963. pathname: '/organizations/org-slug/issues/searches/123/',
  964. query: {
  965. environment: [],
  966. project: [parseInt(project.id, 10)],
  967. statsPeriod: '14d',
  968. sort: savedSearch.sort,
  969. },
  970. });
  971. });
  972. it('goes to all projects when using a basic saved search and global-views feature', function () {
  973. organization.features = ['global-views'];
  974. savedSearch = {
  975. id: 1,
  976. project: null,
  977. query: 'is:unresolved',
  978. };
  979. instance.transitionTo(undefined, savedSearch);
  980. expect(browserHistory.push).toHaveBeenCalledWith({
  981. pathname: '/organizations/org-slug/issues/searches/1/',
  982. query: {
  983. project: [parseInt(project.id, 10)],
  984. environment: [],
  985. statsPeriod: '14d',
  986. },
  987. });
  988. });
  989. it('retains project selection when using a basic saved search and no global-views feature', function () {
  990. organization.features = [];
  991. savedSearch = {
  992. id: 1,
  993. projectId: null,
  994. query: 'is:unresolved',
  995. };
  996. instance.transitionTo(undefined, savedSearch);
  997. expect(browserHistory.push).toHaveBeenCalledWith({
  998. pathname: '/organizations/org-slug/issues/searches/1/',
  999. query: {
  1000. environment: [],
  1001. project: props.selection.projects,
  1002. statsPeriod: '14d',
  1003. },
  1004. });
  1005. });
  1006. });
  1007. describe('getEndpointParams', function () {
  1008. beforeEach(function () {
  1009. wrapper = shallow(<IssueListOverview {...props} />);
  1010. });
  1011. it('omits null values', function () {
  1012. wrapper.setProps({
  1013. selection: {
  1014. projects: null,
  1015. environments: null,
  1016. datetime: {period: '14d'},
  1017. },
  1018. });
  1019. const value = wrapper.instance().getEndpointParams();
  1020. expect(value.project).toBeUndefined();
  1021. expect(value.projects).toBeUndefined();
  1022. expect(value.environment).toBeUndefined();
  1023. expect(value.environments).toBeUndefined();
  1024. expect(value.statsPeriod).toEqual('14d');
  1025. });
  1026. it('omits defaults', function () {
  1027. wrapper.setProps({
  1028. location: {
  1029. query: {
  1030. sort: 'date',
  1031. groupStatsPeriod: '24h',
  1032. },
  1033. },
  1034. });
  1035. const value = wrapper.instance().getEndpointParams();
  1036. expect(value.groupStatsPeriod).toBeUndefined();
  1037. expect(value.sort).toBeUndefined();
  1038. });
  1039. it('uses saved search data', function () {
  1040. const value = wrapper.instance().getEndpointParams();
  1041. expect(value.query).toEqual(savedSearch.query);
  1042. expect(value.project).toEqual([parseInt(savedSearch.projectId, 10)]);
  1043. });
  1044. });
  1045. describe('componentDidMount', function () {
  1046. beforeEach(function () {
  1047. wrapper = shallow(<IssueListOverview {...props} />);
  1048. });
  1049. it('fetches tags and sets state', async function () {
  1050. const instance = wrapper.instance();
  1051. await instance.componentDidMount();
  1052. expect(fetchTagsRequest).toHaveBeenCalled();
  1053. expect(instance.state.tagsLoading).toBeFalsy();
  1054. });
  1055. it('fetches members and sets state', async function () {
  1056. const instance = wrapper.instance();
  1057. await instance.componentDidMount();
  1058. await wrapper.update();
  1059. expect(fetchMembersRequest).toHaveBeenCalled();
  1060. const members = instance.state.memberList;
  1061. // Spot check the resulting structure as we munge it a bit.
  1062. expect(members).toBeTruthy();
  1063. expect(members[project.slug]).toBeTruthy();
  1064. expect(members[project.slug][0].email).toBeTruthy();
  1065. });
  1066. it('fetches groups when there is no searchid', async function () {
  1067. await wrapper.instance().componentDidMount();
  1068. });
  1069. });
  1070. describe('componentDidUpdate fetching groups', function () {
  1071. let fetchDataMock;
  1072. beforeEach(function () {
  1073. fetchDataMock = MockApiClient.addMockResponse({
  1074. url: '/organizations/org-slug/issues/',
  1075. body: [group],
  1076. headers: {
  1077. Link: DEFAULT_LINKS_HEADER,
  1078. },
  1079. });
  1080. fetchDataMock.mockReset();
  1081. wrapper = shallow(<IssueListOverview {...props} />);
  1082. });
  1083. it('fetches data on selection change', function () {
  1084. const selection = {projects: [99], environments: [], datetime: {period: '24h'}};
  1085. wrapper.setProps({selection, foo: 'bar'});
  1086. expect(fetchDataMock).toHaveBeenCalled();
  1087. });
  1088. it('fetches data on savedSearch change', function () {
  1089. savedSearch = {id: '1', query: 'is:resolved'};
  1090. wrapper.setProps({savedSearch});
  1091. wrapper.update();
  1092. expect(fetchDataMock).toHaveBeenCalled();
  1093. });
  1094. it('fetches data on location change', async function () {
  1095. const queryAttrs = ['query', 'sort', 'statsPeriod', 'cursor', 'groupStatsPeriod'];
  1096. const location = cloneDeep(props.location);
  1097. for (const [i, attr] of queryAttrs.entries()) {
  1098. // reclone each iteration so that only one property changes.
  1099. const newLocation = cloneDeep(location);
  1100. newLocation.query[attr] = 'newValue';
  1101. wrapper.setProps({location: newLocation});
  1102. await tick();
  1103. wrapper.update();
  1104. // Each property change after the first will actually cause two new
  1105. // fetchData calls, one from the property change and another from a
  1106. // change in this.state.issuesLoading going from false to true.
  1107. expect(fetchDataMock).toHaveBeenCalledTimes(2 * i + 1);
  1108. }
  1109. });
  1110. it('uses correct statsPeriod when fetching issues list and no datetime given', async function () {
  1111. const selection = {projects: [99], environments: [], datetime: {}};
  1112. wrapper.setProps({selection, foo: 'bar'});
  1113. expect(fetchDataMock).toHaveBeenLastCalledWith(
  1114. '/organizations/org-slug/issues/',
  1115. expect.objectContaining({
  1116. data:
  1117. 'collapse=stats&expand=owners&limit=25&project=99&query=is%3Aunresolved&shortIdLookup=1&statsPeriod=14d',
  1118. })
  1119. );
  1120. });
  1121. });
  1122. describe('componentDidUpdate fetching members', function () {
  1123. beforeEach(function () {
  1124. wrapper = shallow(<IssueListOverview {...props} />);
  1125. wrapper.instance().fetchData = jest.fn();
  1126. });
  1127. it('fetches memberlist on project change', function () {
  1128. // Called during componentDidMount
  1129. expect(fetchMembersRequest).toHaveBeenCalledTimes(1);
  1130. const selection = {
  1131. projects: [99],
  1132. environments: [],
  1133. datetime: {period: '24h'},
  1134. };
  1135. wrapper.setProps({selection});
  1136. wrapper.update();
  1137. expect(fetchMembersRequest).toHaveBeenCalledTimes(2);
  1138. });
  1139. });
  1140. describe('componentDidUpdate fetching tags', function () {
  1141. beforeEach(function () {
  1142. wrapper = shallow(<IssueListOverview {...props} />);
  1143. wrapper.instance().fetchData = jest.fn();
  1144. });
  1145. it('fetches tags on project change', function () {
  1146. // Called during componentDidMount
  1147. expect(fetchTagsRequest).toHaveBeenCalledTimes(1);
  1148. const selection = {
  1149. projects: [99],
  1150. environments: [],
  1151. datetime: {period: '24h'},
  1152. };
  1153. wrapper.setProps({selection});
  1154. wrapper.update();
  1155. expect(fetchTagsRequest).toHaveBeenCalledTimes(2);
  1156. });
  1157. });
  1158. describe('processingIssues', function () {
  1159. beforeEach(function () {
  1160. wrapper = mountWithTheme(<IssueListOverview {...props} />);
  1161. });
  1162. it('fetches and displays processing issues', async function () {
  1163. const instance = wrapper.instance();
  1164. instance.componentDidMount();
  1165. await wrapper.update();
  1166. GroupStore.add([group]);
  1167. wrapper.setState({
  1168. groupIds: ['1'],
  1169. loading: false,
  1170. });
  1171. const issues = wrapper.find('ProcessingIssueList');
  1172. expect(issues).toHaveLength(1);
  1173. });
  1174. });
  1175. describe('render states', function () {
  1176. beforeEach(function () {
  1177. wrapper = mountWithTheme(<IssueListOverview {...props} />);
  1178. });
  1179. it('displays the loading icon', function () {
  1180. wrapper.setState({savedSearchLoading: true});
  1181. expect(wrapper.find('LoadingIndicator')).toHaveLength(1);
  1182. });
  1183. it('displays an error', function () {
  1184. wrapper.setState({
  1185. error: 'Things broke',
  1186. savedSearchLoading: false,
  1187. issuesLoading: false,
  1188. });
  1189. const error = wrapper.find('LoadingError');
  1190. expect(error).toHaveLength(1);
  1191. expect(error.props().message).toEqual('Things broke');
  1192. });
  1193. it('displays congrats robots animation with only is:unresolved query', async function () {
  1194. wrapper.setState({
  1195. savedSearchLoading: false,
  1196. issuesLoading: false,
  1197. error: false,
  1198. groupIds: [],
  1199. });
  1200. await tick();
  1201. wrapper.update();
  1202. expect(wrapper.find('NoUnresolvedIssues').exists()).toBe(true);
  1203. });
  1204. it('displays an empty resultset with is:unresolved and level:error query', async function () {
  1205. const errorsOnlyQuery = {
  1206. ...props,
  1207. location: {
  1208. query: {query: 'is:unresolved level:error'},
  1209. },
  1210. };
  1211. wrapper = mountWithTheme(<IssueListOverview {...errorsOnlyQuery} />);
  1212. wrapper.setState({
  1213. savedSearchLoading: false,
  1214. issuesLoading: false,
  1215. error: false,
  1216. groupIds: [],
  1217. fetchingSentFirstEvent: false,
  1218. sentFirstEvent: true,
  1219. });
  1220. await tick();
  1221. wrapper.update();
  1222. expect(wrapper.find('EmptyStateWarning').exists()).toBe(true);
  1223. });
  1224. it('displays an empty resultset with has:browser query', async function () {
  1225. const hasBrowserQuery = {
  1226. ...props,
  1227. location: {
  1228. query: {query: 'has:browser'},
  1229. },
  1230. };
  1231. wrapper = mountWithTheme(<IssueListOverview {...hasBrowserQuery} />);
  1232. wrapper.setState({
  1233. savedSearchLoading: false,
  1234. issuesLoading: false,
  1235. error: false,
  1236. groupIds: [],
  1237. fetchingSentFirstEvent: false,
  1238. sentFirstEvent: true,
  1239. });
  1240. await tick();
  1241. wrapper.update();
  1242. expect(wrapper.find('EmptyStateWarning').exists()).toBe(true);
  1243. });
  1244. });
  1245. describe('Error Robot', function () {
  1246. const createWrapper = moreProps => {
  1247. const defaultProps = {
  1248. ...props,
  1249. savedSearchLoading: false,
  1250. useOrgSavedSearches: true,
  1251. selection: {
  1252. projects: [],
  1253. environments: [],
  1254. datetime: {period: '14d'},
  1255. },
  1256. location: {query: {query: 'is:unresolved'}, search: 'query=is:unresolved'},
  1257. params: {orgId: organization.slug},
  1258. organization: TestStubs.Organization({
  1259. projects: [],
  1260. }),
  1261. ...moreProps,
  1262. };
  1263. const localWrapper = mountWithTheme(<IssueListOverview {...defaultProps} />);
  1264. localWrapper.setState({
  1265. error: false,
  1266. issuesLoading: false,
  1267. groupIds: [],
  1268. });
  1269. return localWrapper;
  1270. };
  1271. it('displays when no projects selected and all projects user is member of, does not have first event', async function () {
  1272. const projects = [
  1273. TestStubs.Project({
  1274. id: '1',
  1275. slug: 'foo',
  1276. isMember: true,
  1277. firstEvent: false,
  1278. }),
  1279. TestStubs.Project({
  1280. id: '2',
  1281. slug: 'bar',
  1282. isMember: true,
  1283. firstEvent: false,
  1284. }),
  1285. TestStubs.Project({
  1286. id: '3',
  1287. slug: 'baz',
  1288. isMember: true,
  1289. firstEvent: false,
  1290. }),
  1291. ];
  1292. MockApiClient.addMockResponse({
  1293. url: '/organizations/org-slug/sent-first-event/',
  1294. query: {
  1295. is_member: true,
  1296. },
  1297. body: {sentFirstEvent: false},
  1298. });
  1299. MockApiClient.addMockResponse({
  1300. url: '/organizations/org-slug/projects/',
  1301. body: projects,
  1302. });
  1303. MockApiClient.addMockResponse({
  1304. url: '/projects/org-slug/foo/issues/',
  1305. body: [],
  1306. });
  1307. wrapper = createWrapper({
  1308. organization: TestStubs.Organization({
  1309. projects,
  1310. }),
  1311. });
  1312. await tick();
  1313. wrapper.update();
  1314. expect(wrapper.find(ErrorRobot)).toHaveLength(1);
  1315. });
  1316. it('does not display when no projects selected and any projects have a first event', async function () {
  1317. const projects = [
  1318. TestStubs.Project({
  1319. id: '1',
  1320. slug: 'foo',
  1321. isMember: true,
  1322. firstEvent: false,
  1323. }),
  1324. TestStubs.Project({
  1325. id: '2',
  1326. slug: 'bar',
  1327. isMember: true,
  1328. firstEvent: true,
  1329. }),
  1330. TestStubs.Project({
  1331. id: '3',
  1332. slug: 'baz',
  1333. isMember: true,
  1334. firstEvent: false,
  1335. }),
  1336. ];
  1337. MockApiClient.addMockResponse({
  1338. url: '/organizations/org-slug/sent-first-event/',
  1339. query: {
  1340. is_member: true,
  1341. },
  1342. body: {sentFirstEvent: true},
  1343. });
  1344. MockApiClient.addMockResponse({
  1345. url: '/organizations/org-slug/projects/',
  1346. body: projects,
  1347. });
  1348. wrapper = createWrapper({
  1349. organization: TestStubs.Organization({
  1350. projects,
  1351. }),
  1352. });
  1353. await tick();
  1354. wrapper.update();
  1355. expect(wrapper.find(ErrorRobot)).toHaveLength(0);
  1356. });
  1357. it('displays when all selected projects do not have first event', async function () {
  1358. const projects = [
  1359. TestStubs.Project({
  1360. id: '1',
  1361. slug: 'foo',
  1362. isMember: true,
  1363. firstEvent: false,
  1364. }),
  1365. TestStubs.Project({
  1366. id: '2',
  1367. slug: 'bar',
  1368. isMember: true,
  1369. firstEvent: false,
  1370. }),
  1371. TestStubs.Project({
  1372. id: '3',
  1373. slug: 'baz',
  1374. isMember: true,
  1375. firstEvent: false,
  1376. }),
  1377. ];
  1378. MockApiClient.addMockResponse({
  1379. url: '/organizations/org-slug/sent-first-event/',
  1380. query: {
  1381. project: [1, 2],
  1382. },
  1383. body: {sentFirstEvent: false},
  1384. });
  1385. MockApiClient.addMockResponse({
  1386. url: '/organizations/org-slug/projects/',
  1387. body: projects,
  1388. });
  1389. MockApiClient.addMockResponse({
  1390. url: '/projects/org-slug/foo/issues/',
  1391. body: [],
  1392. });
  1393. wrapper = createWrapper({
  1394. selection: {
  1395. projects: [1, 2],
  1396. environments: [],
  1397. datetime: {period: '14d'},
  1398. },
  1399. organization: TestStubs.Organization({
  1400. projects,
  1401. }),
  1402. });
  1403. await tick();
  1404. wrapper.update();
  1405. expect(wrapper.find(ErrorRobot)).toHaveLength(1);
  1406. });
  1407. it('does not display when any selected projects have first event', function () {
  1408. const projects = [
  1409. TestStubs.Project({
  1410. id: '1',
  1411. slug: 'foo',
  1412. isMember: true,
  1413. firstEvent: false,
  1414. }),
  1415. TestStubs.Project({
  1416. id: '2',
  1417. slug: 'bar',
  1418. isMember: true,
  1419. firstEvent: true,
  1420. }),
  1421. TestStubs.Project({
  1422. id: '3',
  1423. slug: 'baz',
  1424. isMember: true,
  1425. firstEvent: true,
  1426. }),
  1427. ];
  1428. MockApiClient.addMockResponse({
  1429. url: '/organizations/org-slug/sent-first-event/',
  1430. query: {
  1431. project: [1, 2],
  1432. },
  1433. body: {sentFirstEvent: true},
  1434. });
  1435. MockApiClient.addMockResponse({
  1436. url: '/organizations/org-slug/projects/',
  1437. body: projects,
  1438. });
  1439. wrapper = createWrapper({
  1440. selection: {
  1441. projects: [1, 2],
  1442. environments: [],
  1443. datetime: {period: '14d'},
  1444. },
  1445. organization: TestStubs.Organization({
  1446. projects,
  1447. }),
  1448. });
  1449. expect(wrapper.find(ErrorRobot)).toHaveLength(0);
  1450. });
  1451. });
  1452. describe('with inbox feature', function () {
  1453. const parseLinkHeaderSpy = jest.spyOn(parseLinkHeader, 'default');
  1454. it('renders inbox layout', function () {
  1455. organization.features = ['inbox'];
  1456. organization.experiments = {InboxExperiment: 1};
  1457. wrapper = mountWithTheme(<IssueListOverview {...props} />);
  1458. expect(wrapper.find('IssueListHeader').exists()).toBeTruthy();
  1459. });
  1460. it('displays a count that represents the current page', function () {
  1461. organization.features = ['inbox'];
  1462. parseLinkHeaderSpy.mockReturnValue({
  1463. next: {
  1464. results: true,
  1465. },
  1466. previous: {
  1467. results: false,
  1468. },
  1469. });
  1470. props = {
  1471. ...props,
  1472. location: {
  1473. query: {
  1474. cursor: 'some cursor',
  1475. page: 0,
  1476. },
  1477. },
  1478. };
  1479. wrapper = mountWithTheme(<IssueListOverview {...props} />);
  1480. wrapper.setState({
  1481. groupIds: range(0, 25).map(String),
  1482. queryCount: 500,
  1483. queryMaxCount: 1000,
  1484. });
  1485. const paginationWrapper = wrapper.find('PaginationWrapper');
  1486. expect(paginationWrapper.text()).toBe('Showing 25 of 500 issues');
  1487. parseLinkHeaderSpy.mockReturnValue({
  1488. next: {
  1489. results: true,
  1490. },
  1491. previous: {
  1492. results: true,
  1493. },
  1494. });
  1495. wrapper.setProps({
  1496. location: {
  1497. query: {
  1498. cursor: 'some cursor',
  1499. page: 1,
  1500. },
  1501. },
  1502. });
  1503. expect(paginationWrapper.text()).toBe('Showing 50 of 500 issues');
  1504. expect(wrapper.find('IssueListHeader').exists()).toBeTruthy();
  1505. });
  1506. it('displays a count that makes sense based on the current page', function () {
  1507. organization.features = ['inbox'];
  1508. parseLinkHeaderSpy.mockReturnValue({
  1509. next: {
  1510. // Is at last page according to the cursor
  1511. results: false,
  1512. },
  1513. previous: {
  1514. results: true,
  1515. },
  1516. });
  1517. props = {
  1518. ...props,
  1519. location: {
  1520. query: {
  1521. cursor: 'some cursor',
  1522. page: 3,
  1523. },
  1524. },
  1525. };
  1526. wrapper = mountWithTheme(<IssueListOverview {...props} />);
  1527. wrapper.setState({
  1528. groupIds: range(0, 25).map(String),
  1529. queryCount: 500,
  1530. queryMaxCount: 1000,
  1531. });
  1532. const paginationWrapper = wrapper.find('PaginationWrapper');
  1533. expect(paginationWrapper.text()).toBe('Showing 500 of 500 issues');
  1534. parseLinkHeaderSpy.mockReturnValue({
  1535. next: {
  1536. results: true,
  1537. },
  1538. previous: {
  1539. // Is at first page according to cursor
  1540. results: false,
  1541. },
  1542. });
  1543. wrapper.setProps({
  1544. location: {
  1545. query: {
  1546. cursor: 'some cursor',
  1547. page: 2,
  1548. },
  1549. },
  1550. });
  1551. expect(paginationWrapper.text()).toBe('Showing 25 of 500 issues');
  1552. expect(wrapper.find('IssueListHeader').exists()).toBeTruthy();
  1553. });
  1554. it('displays a count based on items removed', function () {
  1555. organization.features = ['inbox'];
  1556. parseLinkHeaderSpy.mockReturnValue({
  1557. next: {
  1558. results: true,
  1559. },
  1560. previous: {
  1561. results: true,
  1562. },
  1563. });
  1564. props = {
  1565. ...props,
  1566. location: {
  1567. query: {
  1568. cursor: 'some cursor',
  1569. page: 1,
  1570. },
  1571. },
  1572. };
  1573. wrapper = mountWithTheme(<IssueListOverview {...props} />);
  1574. wrapper.setState({
  1575. groupIds: range(0, 25).map(String),
  1576. queryCount: 75,
  1577. itemsRemoved: 1,
  1578. queryMaxCount: 1000,
  1579. });
  1580. const paginationWrapper = wrapper.find('PaginationWrapper');
  1581. // 2nd page subtracts the one removed
  1582. expect(paginationWrapper.text()).toBe('Showing 49 of 74 issues');
  1583. });
  1584. });
  1585. describe('with relative change feature', function () {
  1586. it('defaults to larger graph selection', function () {
  1587. organization.features = ['issue-list-trend-sort'];
  1588. props.location = {
  1589. query: {query: 'is:unresolved', sort: 'trend'},
  1590. search: 'query=is:unresolved',
  1591. };
  1592. wrapper = mountWithTheme(<IssueListOverview {...props} />);
  1593. expect(wrapper.instance().getGroupStatsPeriod()).toBe('auto');
  1594. });
  1595. });
  1596. });