overview.spec.jsx 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886
  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 defaults', function () {
  1004. wrapper.setProps({
  1005. location: {
  1006. query: {
  1007. sort: 'date',
  1008. groupStatsPeriod: '24h',
  1009. },
  1010. },
  1011. });
  1012. const value = wrapper.instance().getEndpointParams();
  1013. expect(value.groupStatsPeriod).toBeUndefined();
  1014. expect(value.sort).toBeUndefined();
  1015. });
  1016. it('uses saved search data', function () {
  1017. const value = wrapper.instance().getEndpointParams();
  1018. expect(value.query).toEqual('is:unresolved');
  1019. expect(value.project).toEqual([parseInt(savedSearch.projectId, 10)]);
  1020. });
  1021. });
  1022. describe('componentDidMount', function () {
  1023. beforeEach(function () {
  1024. wrapper = shallow(<IssueListOverview {...props} />);
  1025. });
  1026. it('fetches tags and sets state', async function () {
  1027. const instance = wrapper.instance();
  1028. await instance.componentDidMount();
  1029. expect(fetchTagsRequest).toHaveBeenCalled();
  1030. expect(instance.state.tagsLoading).toBeFalsy();
  1031. });
  1032. it('fetches members and sets state', async function () {
  1033. const instance = wrapper.instance();
  1034. await instance.componentDidMount();
  1035. wrapper.update();
  1036. expect(fetchMembersRequest).toHaveBeenCalled();
  1037. const members = instance.state.memberList;
  1038. // Spot check the resulting structure as we munge it a bit.
  1039. expect(members).toBeTruthy();
  1040. expect(members[project.slug]).toBeTruthy();
  1041. expect(members[project.slug][0].email).toBeTruthy();
  1042. });
  1043. it('fetches groups when there is no searchid', async function () {
  1044. await wrapper.instance().componentDidMount();
  1045. });
  1046. });
  1047. describe('componentDidUpdate fetching groups', function () {
  1048. let fetchDataMock;
  1049. beforeEach(function () {
  1050. fetchDataMock = MockApiClient.addMockResponse({
  1051. url: '/organizations/org-slug/issues/',
  1052. body: [group],
  1053. headers: {
  1054. Link: DEFAULT_LINKS_HEADER,
  1055. },
  1056. });
  1057. fetchDataMock.mockReset();
  1058. wrapper = shallow(<IssueListOverview {...props} />);
  1059. });
  1060. it('fetches data on selection change', function () {
  1061. const selection = {projects: [99], environments: [], datetime: {period: '24h'}};
  1062. wrapper.setProps({selection, foo: 'bar'});
  1063. expect(fetchDataMock).toHaveBeenCalled();
  1064. });
  1065. it('fetches data on savedSearch change', function () {
  1066. savedSearch = {id: '1', query: 'is:resolved'};
  1067. wrapper.setProps({savedSearch});
  1068. wrapper.update();
  1069. expect(fetchDataMock).toHaveBeenCalled();
  1070. });
  1071. it('fetches data on location change', async function () {
  1072. const queryAttrs = ['query', 'sort', 'statsPeriod', 'cursor', 'groupStatsPeriod'];
  1073. const location = cloneDeep(props.location);
  1074. for (const [i, attr] of queryAttrs.entries()) {
  1075. // reclone each iteration so that only one property changes.
  1076. const newLocation = cloneDeep(location);
  1077. newLocation.query[attr] = 'newValue';
  1078. wrapper.setProps({location: newLocation});
  1079. await tick();
  1080. wrapper.update();
  1081. // Each property change after the first will actually cause two new
  1082. // fetchData calls, one from the property change and another from a
  1083. // change in this.state.issuesLoading going from false to true.
  1084. expect(fetchDataMock).toHaveBeenCalledTimes(2 * i + 1);
  1085. }
  1086. });
  1087. it('uses correct statsPeriod when fetching issues list and no datetime given', function () {
  1088. const selection = {projects: [99], environments: [], datetime: {}};
  1089. wrapper.setProps({selection, foo: 'bar'});
  1090. expect(fetchDataMock).toHaveBeenLastCalledWith(
  1091. '/organizations/org-slug/issues/',
  1092. expect.objectContaining({
  1093. data: 'collapse=stats&expand=owners&expand=inbox&limit=25&project=99&query=is%3Aunresolved&shortIdLookup=1&statsPeriod=14d',
  1094. })
  1095. );
  1096. });
  1097. });
  1098. describe('componentDidUpdate fetching members', function () {
  1099. beforeEach(function () {
  1100. wrapper = shallow(<IssueListOverview {...props} />);
  1101. wrapper.instance().fetchData = jest.fn();
  1102. });
  1103. it('fetches memberlist on project change', function () {
  1104. // Called during componentDidMount
  1105. expect(fetchMembersRequest).toHaveBeenCalledTimes(1);
  1106. const selection = {
  1107. projects: [99],
  1108. environments: [],
  1109. datetime: {period: '24h'},
  1110. };
  1111. wrapper.setProps({selection});
  1112. wrapper.update();
  1113. expect(fetchMembersRequest).toHaveBeenCalledTimes(2);
  1114. });
  1115. });
  1116. describe('componentDidUpdate fetching tags', function () {
  1117. beforeEach(function () {
  1118. wrapper = shallow(<IssueListOverview {...props} />);
  1119. wrapper.instance().fetchData = jest.fn();
  1120. });
  1121. it('fetches tags on project change', function () {
  1122. // Called during componentDidMount
  1123. expect(fetchTagsRequest).toHaveBeenCalledTimes(1);
  1124. const selection = {
  1125. projects: [99],
  1126. environments: [],
  1127. datetime: {period: '24h'},
  1128. };
  1129. wrapper.setProps({selection});
  1130. wrapper.update();
  1131. expect(fetchTagsRequest).toHaveBeenCalledTimes(2);
  1132. });
  1133. });
  1134. describe('processingIssues', function () {
  1135. beforeEach(function () {
  1136. wrapper = mountWithThemeAndOrg(<IssueListOverview {...props} />);
  1137. });
  1138. it('fetches and displays processing issues', function () {
  1139. const instance = wrapper.instance();
  1140. instance.componentDidMount();
  1141. wrapper.update();
  1142. GroupStore.add([group]);
  1143. wrapper.setState({
  1144. groupIds: ['1'],
  1145. loading: false,
  1146. });
  1147. const issues = wrapper.find('ProcessingIssueList');
  1148. expect(issues).toHaveLength(1);
  1149. });
  1150. });
  1151. describe('render states', function () {
  1152. it('displays the loading icon', function () {
  1153. wrapper = mountWithThemeAndOrg(<IssueListOverview {...props} />);
  1154. wrapper.setState({savedSearchLoading: true});
  1155. expect(wrapper.find('LoadingIndicator')).toHaveLength(1);
  1156. });
  1157. it('displays an error', function () {
  1158. wrapper = mountWithThemeAndOrg(<IssueListOverview {...props} />);
  1159. wrapper.setState({
  1160. error: 'Things broke',
  1161. savedSearchLoading: false,
  1162. issuesLoading: false,
  1163. });
  1164. const error = wrapper.find('LoadingError');
  1165. expect(error).toHaveLength(1);
  1166. expect(error.props().message).toEqual('Things broke');
  1167. });
  1168. it('displays congrats robots animation with only is:unresolved query', async function () {
  1169. wrapper = mountWithThemeAndOrg(<IssueListOverview {...props} />);
  1170. wrapper.setState({
  1171. savedSearchLoading: false,
  1172. issuesLoading: false,
  1173. error: false,
  1174. groupIds: [],
  1175. });
  1176. await tick();
  1177. wrapper.update();
  1178. expect(wrapper.find('NoUnresolvedIssues').exists()).toBe(true);
  1179. });
  1180. it('displays an empty resultset with is:unresolved and level:error query', async function () {
  1181. const errorsOnlyQuery = {
  1182. ...props,
  1183. location: {
  1184. query: {query: 'is:unresolved level:error'},
  1185. },
  1186. };
  1187. wrapper = mountWithThemeAndOrg(<IssueListOverview {...errorsOnlyQuery} />);
  1188. wrapper.setState({
  1189. savedSearchLoading: false,
  1190. issuesLoading: false,
  1191. error: false,
  1192. groupIds: [],
  1193. fetchingSentFirstEvent: false,
  1194. sentFirstEvent: true,
  1195. });
  1196. await tick();
  1197. wrapper.update();
  1198. expect(wrapper.find('EmptyStateWarning').exists()).toBe(true);
  1199. });
  1200. it('displays an empty resultset with has:browser query', async function () {
  1201. const hasBrowserQuery = {
  1202. ...props,
  1203. location: {
  1204. query: {query: 'has:browser'},
  1205. },
  1206. };
  1207. wrapper = mountWithThemeAndOrg(<IssueListOverview {...hasBrowserQuery} />);
  1208. wrapper.setState({
  1209. savedSearchLoading: false,
  1210. issuesLoading: false,
  1211. error: false,
  1212. groupIds: [],
  1213. fetchingSentFirstEvent: false,
  1214. sentFirstEvent: true,
  1215. });
  1216. await tick();
  1217. wrapper.update();
  1218. expect(wrapper.find('EmptyStateWarning').exists()).toBe(true);
  1219. });
  1220. });
  1221. describe('Error Robot', function () {
  1222. const createWrapper = moreProps => {
  1223. const defaultProps = {
  1224. ...props,
  1225. savedSearchLoading: false,
  1226. useOrgSavedSearches: true,
  1227. selection: {
  1228. projects: [],
  1229. environments: [],
  1230. datetime: {period: '14d'},
  1231. },
  1232. location: {query: {query: 'is:unresolved'}, search: 'query=is:unresolved'},
  1233. params: {orgId: organization.slug},
  1234. organization: TestStubs.Organization({
  1235. projects: [],
  1236. }),
  1237. ...moreProps,
  1238. };
  1239. const localWrapper = mountWithThemeAndOrg(<IssueListOverview {...defaultProps} />);
  1240. localWrapper.setState({
  1241. error: false,
  1242. issuesLoading: false,
  1243. groupIds: [],
  1244. });
  1245. return localWrapper;
  1246. };
  1247. it('displays when no projects selected and all projects user is member of, does not have first event', async function () {
  1248. const projects = [
  1249. TestStubs.Project({
  1250. id: '1',
  1251. slug: 'foo',
  1252. isMember: true,
  1253. firstEvent: false,
  1254. }),
  1255. TestStubs.Project({
  1256. id: '2',
  1257. slug: 'bar',
  1258. isMember: true,
  1259. firstEvent: false,
  1260. }),
  1261. TestStubs.Project({
  1262. id: '3',
  1263. slug: 'baz',
  1264. isMember: true,
  1265. firstEvent: false,
  1266. }),
  1267. ];
  1268. MockApiClient.addMockResponse({
  1269. url: '/organizations/org-slug/sent-first-event/',
  1270. query: {
  1271. is_member: true,
  1272. },
  1273. body: {sentFirstEvent: false},
  1274. });
  1275. MockApiClient.addMockResponse({
  1276. url: '/organizations/org-slug/projects/',
  1277. body: projects,
  1278. });
  1279. MockApiClient.addMockResponse({
  1280. url: '/projects/org-slug/foo/issues/',
  1281. body: [],
  1282. });
  1283. wrapper = createWrapper({
  1284. organization: TestStubs.Organization({
  1285. projects,
  1286. }),
  1287. });
  1288. await tick();
  1289. wrapper.update();
  1290. expect(wrapper.find('ErrorRobot')).toHaveLength(1);
  1291. });
  1292. it('does not display when no projects selected and any projects have a first event', async function () {
  1293. const projects = [
  1294. TestStubs.Project({
  1295. id: '1',
  1296. slug: 'foo',
  1297. isMember: true,
  1298. firstEvent: false,
  1299. }),
  1300. TestStubs.Project({
  1301. id: '2',
  1302. slug: 'bar',
  1303. isMember: true,
  1304. firstEvent: true,
  1305. }),
  1306. TestStubs.Project({
  1307. id: '3',
  1308. slug: 'baz',
  1309. isMember: true,
  1310. firstEvent: false,
  1311. }),
  1312. ];
  1313. MockApiClient.addMockResponse({
  1314. url: '/organizations/org-slug/sent-first-event/',
  1315. query: {
  1316. is_member: true,
  1317. },
  1318. body: {sentFirstEvent: true},
  1319. });
  1320. MockApiClient.addMockResponse({
  1321. url: '/organizations/org-slug/projects/',
  1322. body: projects,
  1323. });
  1324. wrapper = createWrapper({
  1325. organization: TestStubs.Organization({
  1326. projects,
  1327. }),
  1328. });
  1329. await tick();
  1330. wrapper.update();
  1331. expect(wrapper.find('ErrorRobot')).toHaveLength(0);
  1332. });
  1333. it('displays when all selected projects do not have first event', async function () {
  1334. const projects = [
  1335. TestStubs.Project({
  1336. id: '1',
  1337. slug: 'foo',
  1338. isMember: true,
  1339. firstEvent: false,
  1340. }),
  1341. TestStubs.Project({
  1342. id: '2',
  1343. slug: 'bar',
  1344. isMember: true,
  1345. firstEvent: false,
  1346. }),
  1347. TestStubs.Project({
  1348. id: '3',
  1349. slug: 'baz',
  1350. isMember: true,
  1351. firstEvent: false,
  1352. }),
  1353. ];
  1354. MockApiClient.addMockResponse({
  1355. url: '/organizations/org-slug/sent-first-event/',
  1356. query: {
  1357. project: [1, 2],
  1358. },
  1359. body: {sentFirstEvent: false},
  1360. });
  1361. MockApiClient.addMockResponse({
  1362. url: '/organizations/org-slug/projects/',
  1363. body: projects,
  1364. });
  1365. MockApiClient.addMockResponse({
  1366. url: '/projects/org-slug/foo/issues/',
  1367. body: [],
  1368. });
  1369. wrapper = createWrapper({
  1370. selection: {
  1371. projects: [1, 2],
  1372. environments: [],
  1373. datetime: {period: '14d'},
  1374. },
  1375. organization: TestStubs.Organization({
  1376. projects,
  1377. }),
  1378. });
  1379. await tick();
  1380. wrapper.update();
  1381. expect(wrapper.find('ErrorRobot')).toHaveLength(1);
  1382. });
  1383. it('does not display when any selected projects have first event', function () {
  1384. const projects = [
  1385. TestStubs.Project({
  1386. id: '1',
  1387. slug: 'foo',
  1388. isMember: true,
  1389. firstEvent: false,
  1390. }),
  1391. TestStubs.Project({
  1392. id: '2',
  1393. slug: 'bar',
  1394. isMember: true,
  1395. firstEvent: true,
  1396. }),
  1397. TestStubs.Project({
  1398. id: '3',
  1399. slug: 'baz',
  1400. isMember: true,
  1401. firstEvent: true,
  1402. }),
  1403. ];
  1404. MockApiClient.addMockResponse({
  1405. url: '/organizations/org-slug/sent-first-event/',
  1406. query: {
  1407. project: [1, 2],
  1408. },
  1409. body: {sentFirstEvent: true},
  1410. });
  1411. MockApiClient.addMockResponse({
  1412. url: '/organizations/org-slug/projects/',
  1413. body: projects,
  1414. });
  1415. wrapper = createWrapper({
  1416. selection: {
  1417. projects: [1, 2],
  1418. environments: [],
  1419. datetime: {period: '14d'},
  1420. },
  1421. organization: TestStubs.Organization({
  1422. projects,
  1423. }),
  1424. });
  1425. expect(wrapper.find('ErrorRobot')).toHaveLength(0);
  1426. });
  1427. });
  1428. it('displays a count that represents the current page', function () {
  1429. parseLinkHeaderSpy.mockReturnValue({
  1430. next: {
  1431. results: true,
  1432. },
  1433. previous: {
  1434. results: false,
  1435. },
  1436. });
  1437. props = {
  1438. ...props,
  1439. location: {
  1440. query: {
  1441. cursor: 'some cursor',
  1442. page: 0,
  1443. },
  1444. },
  1445. };
  1446. const {routerContext} = initializeOrg();
  1447. wrapper = mountWithThemeAndOrg(<IssueListOverview {...props} />, routerContext);
  1448. wrapper.setState({
  1449. groupIds: range(0, 25).map(String),
  1450. queryCount: 500,
  1451. queryMaxCount: 1000,
  1452. pageLinks: DEFAULT_LINKS_HEADER,
  1453. });
  1454. const paginationCaption = wrapper.find('PaginationCaption');
  1455. expect(paginationCaption.text()).toBe('Showing 25 of 500 issues');
  1456. parseLinkHeaderSpy.mockReturnValue({
  1457. next: {
  1458. results: true,
  1459. },
  1460. previous: {
  1461. results: true,
  1462. },
  1463. });
  1464. wrapper.setProps({
  1465. location: {
  1466. query: {
  1467. cursor: 'some cursor',
  1468. page: 1,
  1469. },
  1470. },
  1471. });
  1472. expect(paginationCaption.text()).toBe('Showing 50 of 500 issues');
  1473. expect(wrapper.find('IssueListHeader').exists()).toBeTruthy();
  1474. });
  1475. it('displays a count that makes sense based on the current page', function () {
  1476. parseLinkHeaderSpy.mockReturnValue({
  1477. next: {
  1478. // Is at last page according to the cursor
  1479. results: false,
  1480. },
  1481. previous: {
  1482. results: true,
  1483. },
  1484. });
  1485. props = {
  1486. ...props,
  1487. location: {
  1488. query: {
  1489. cursor: 'some cursor',
  1490. page: 3,
  1491. },
  1492. },
  1493. };
  1494. const {routerContext} = initializeOrg();
  1495. wrapper = mountWithThemeAndOrg(<IssueListOverview {...props} />, routerContext);
  1496. wrapper.setState({
  1497. groupIds: range(0, 25).map(String),
  1498. queryCount: 500,
  1499. queryMaxCount: 1000,
  1500. pageLinks: DEFAULT_LINKS_HEADER,
  1501. });
  1502. const paginationCaption = wrapper.find('PaginationCaption');
  1503. expect(paginationCaption.text()).toBe('Showing 500 of 500 issues');
  1504. parseLinkHeaderSpy.mockReturnValue({
  1505. next: {
  1506. results: true,
  1507. },
  1508. previous: {
  1509. // Is at first page according to cursor
  1510. results: false,
  1511. },
  1512. });
  1513. wrapper.setProps({
  1514. location: {
  1515. query: {
  1516. cursor: 'some cursor',
  1517. page: 2,
  1518. },
  1519. },
  1520. });
  1521. expect(paginationCaption.text()).toBe('Showing 25 of 500 issues');
  1522. expect(wrapper.find('IssueListHeader').exists()).toBeTruthy();
  1523. });
  1524. it('displays a count based on items removed', function () {
  1525. parseLinkHeaderSpy.mockReturnValue({
  1526. next: {
  1527. results: true,
  1528. },
  1529. previous: {
  1530. results: true,
  1531. },
  1532. });
  1533. props = {
  1534. ...props,
  1535. location: {
  1536. query: {
  1537. cursor: 'some cursor',
  1538. page: 1,
  1539. },
  1540. },
  1541. };
  1542. const {routerContext} = initializeOrg();
  1543. wrapper = mountWithThemeAndOrg(<IssueListOverview {...props} />, routerContext);
  1544. wrapper.setState({
  1545. groupIds: range(0, 25).map(String),
  1546. queryCount: 75,
  1547. itemsRemoved: 1,
  1548. queryMaxCount: 1000,
  1549. pageLinks: DEFAULT_LINKS_HEADER,
  1550. });
  1551. const paginationCaption = wrapper.find('PaginationCaption');
  1552. // 2nd page subtracts the one removed
  1553. expect(paginationCaption.text()).toBe('Showing 49 of 74 issues');
  1554. });
  1555. describe('with relative change feature', function () {
  1556. it('defaults to larger graph selection', function () {
  1557. organization.features = ['issue-list-trend-sort'];
  1558. props.location = {
  1559. query: {query: 'is:unresolved', sort: 'trend'},
  1560. search: 'query=is:unresolved',
  1561. };
  1562. wrapper = mountWithThemeAndOrg(<IssueListOverview {...props} />);
  1563. expect(wrapper.instance().getGroupStatsPeriod()).toBe('auto');
  1564. });
  1565. });
  1566. describe('project low priority queue alert', function () {
  1567. const {routerContext} = initializeOrg();
  1568. beforeEach(function () {
  1569. act(() => ProjectsStore.reset());
  1570. });
  1571. it('does not render alert', function () {
  1572. act(() => ProjectsStore.loadInitialData([project]));
  1573. wrapper = mountWithThemeAndOrg(<IssueListOverview {...props} />, routerContext);
  1574. const eventProcessingAlert = wrapper.find('StyledGlobalEventProcessingAlert');
  1575. expect(eventProcessingAlert.exists()).toBe(true);
  1576. expect(eventProcessingAlert.isEmptyRender()).toBe(true);
  1577. });
  1578. describe('renders alert', function () {
  1579. it('for one project', function () {
  1580. act(() =>
  1581. ProjectsStore.loadInitialData([
  1582. {...project, eventProcessing: {symbolicationDegraded: true}},
  1583. ])
  1584. );
  1585. wrapper = mountWithThemeAndOrg(<IssueListOverview {...props} />, routerContext);
  1586. const eventProcessingAlert = wrapper.find('StyledGlobalEventProcessingAlert');
  1587. expect(eventProcessingAlert.exists()).toBe(true);
  1588. expect(eventProcessingAlert.isEmptyRender()).toBe(false);
  1589. expect(eventProcessingAlert.text()).toBe(
  1590. '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.'
  1591. );
  1592. });
  1593. it('for multiple projects', function () {
  1594. const projectBar = TestStubs.ProjectDetails({
  1595. id: '3560',
  1596. name: 'Bar Project',
  1597. slug: 'project-slug-bar',
  1598. });
  1599. act(() =>
  1600. ProjectsStore.loadInitialData([
  1601. {
  1602. ...project,
  1603. slug: 'project-slug',
  1604. eventProcessing: {symbolicationDegraded: true},
  1605. },
  1606. {
  1607. ...projectBar,
  1608. slug: 'project-slug-bar',
  1609. eventProcessing: {symbolicationDegraded: true},
  1610. },
  1611. ])
  1612. );
  1613. wrapper = mountWithThemeAndOrg(
  1614. <IssueListOverview
  1615. {...props}
  1616. selection={{
  1617. ...props.selection,
  1618. projects: [Number(project.id), Number(projectBar.id)],
  1619. }}
  1620. />,
  1621. routerContext
  1622. );
  1623. const eventProcessingAlert = wrapper.find('StyledGlobalEventProcessingAlert');
  1624. expect(eventProcessingAlert.exists()).toBe(true);
  1625. expect(eventProcessingAlert.isEmptyRender()).toBe(false);
  1626. expect(eventProcessingAlert.text()).toBe(
  1627. `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.`
  1628. );
  1629. });
  1630. });
  1631. });
  1632. });