overview.spec.jsx 53 KB

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