overview.spec.tsx 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677
  1. import {browserHistory} from 'react-router';
  2. import merge from 'lodash/merge';
  3. import {Group} from 'sentry-fixture/group';
  4. import {GroupStats} from 'sentry-fixture/groupStats';
  5. import {Member} from 'sentry-fixture/member';
  6. import {Organization} from 'sentry-fixture/organization';
  7. import {Project} from 'sentry-fixture/project';
  8. import {Search} from 'sentry-fixture/search';
  9. import {Tags} from 'sentry-fixture/tags';
  10. import {initializeOrg} from 'sentry-test/initializeOrg';
  11. import {
  12. act,
  13. render,
  14. screen,
  15. userEvent,
  16. waitFor,
  17. waitForElementToBeRemoved,
  18. } from 'sentry-test/reactTestingLibrary';
  19. import {textWithMarkupMatcher} from 'sentry-test/utils';
  20. import GroupStore from 'sentry/stores/groupStore';
  21. import ProjectsStore from 'sentry/stores/projectsStore';
  22. import TagStore from 'sentry/stores/tagStore';
  23. import {SavedSearchVisibility} from 'sentry/types';
  24. import localStorageWrapper from 'sentry/utils/localStorage';
  25. import * as parseLinkHeader from 'sentry/utils/parseLinkHeader';
  26. import IssueListWithStores, {IssueListOverview} from 'sentry/views/issueList/overview';
  27. // Mock <IssueListActions>
  28. jest.mock('sentry/views/issueList/actions', () => jest.fn(() => null));
  29. const DEFAULT_LINKS_HEADER =
  30. '<http://127.0.0.1:8000/api/0/organizations/org-slug/issues/?cursor=1443575731:0:1>; rel="previous"; results="false"; cursor="1443575731:0:1", ' +
  31. '<http://127.0.0.1:8000/api/0/organizations/org-slug/issues/?cursor=1443575000:0:0>; rel="next"; results="true"; cursor="1443575000:0:0"';
  32. const project = Project({
  33. id: '3559',
  34. name: 'Foo Project',
  35. slug: 'project-slug',
  36. firstEvent: new Date().toISOString(),
  37. });
  38. const {organization, router, routerContext} = initializeOrg({
  39. organization: {
  40. id: '1337',
  41. slug: 'org-slug',
  42. features: ['global-views'],
  43. access: [],
  44. },
  45. router: {
  46. location: {query: {}, search: ''},
  47. params: {},
  48. },
  49. project,
  50. });
  51. const routerProps = {
  52. params: router.params,
  53. location: router.location,
  54. };
  55. describe('IssueList', function () {
  56. let props;
  57. const tags = Tags();
  58. const group = Group({project});
  59. const groupStats = GroupStats();
  60. const savedSearch = Search({
  61. id: '789',
  62. query: 'is:unresolved TypeError',
  63. sort: 'date',
  64. name: 'Unresolved TypeErrors',
  65. });
  66. let fetchTagsRequest: jest.Mock;
  67. let fetchMembersRequest: jest.Mock;
  68. const api = new MockApiClient();
  69. const parseLinkHeaderSpy = jest.spyOn(parseLinkHeader, 'default');
  70. beforeEach(function () {
  71. // The tests fail because we have a "component update was not wrapped in act" error.
  72. // It should be safe to ignore this error, but we should remove the mock once we move to react testing library
  73. // eslint-disable-next-line no-console
  74. jest.spyOn(console, 'error').mockImplementation(jest.fn());
  75. MockApiClient.addMockResponse({
  76. url: '/organizations/org-slug/issues/',
  77. body: [group],
  78. headers: {
  79. Link: DEFAULT_LINKS_HEADER,
  80. },
  81. });
  82. MockApiClient.addMockResponse({
  83. url: '/organizations/org-slug/issues-stats/',
  84. body: [groupStats],
  85. });
  86. MockApiClient.addMockResponse({
  87. url: '/organizations/org-slug/searches/',
  88. body: [savedSearch],
  89. });
  90. MockApiClient.addMockResponse({
  91. url: '/organizations/org-slug/recent-searches/',
  92. body: [],
  93. });
  94. MockApiClient.addMockResponse({
  95. url: '/organizations/org-slug/recent-searches/',
  96. method: 'POST',
  97. body: [],
  98. });
  99. MockApiClient.addMockResponse({
  100. url: '/organizations/org-slug/issues-count/',
  101. method: 'GET',
  102. body: [{}],
  103. });
  104. MockApiClient.addMockResponse({
  105. url: '/organizations/org-slug/processingissues/',
  106. method: 'GET',
  107. body: [
  108. {
  109. project: 'test-project',
  110. numIssues: 1,
  111. hasIssues: true,
  112. lastSeen: '2019-01-16T15:39:11.081Z',
  113. },
  114. ],
  115. });
  116. fetchTagsRequest = MockApiClient.addMockResponse({
  117. url: '/organizations/org-slug/tags/',
  118. method: 'GET',
  119. body: tags,
  120. });
  121. fetchMembersRequest = MockApiClient.addMockResponse({
  122. url: '/organizations/org-slug/users/',
  123. method: 'GET',
  124. body: [Member({projects: [project.slug]})],
  125. });
  126. MockApiClient.addMockResponse({
  127. url: '/organizations/org-slug/sent-first-event/',
  128. body: {sentFirstEvent: true},
  129. });
  130. MockApiClient.addMockResponse({
  131. url: '/organizations/org-slug/projects/',
  132. body: [project],
  133. });
  134. TagStore.init?.();
  135. props = {
  136. api,
  137. savedSearchLoading: false,
  138. savedSearches: [savedSearch],
  139. useOrgSavedSearches: true,
  140. selection: {
  141. projects: [parseInt(organization.projects[0].id, 10)],
  142. environments: [],
  143. datetime: {period: '14d'},
  144. },
  145. location: {query: {query: 'is:unresolved'}, search: 'query=is:unresolved'},
  146. params: {},
  147. organization,
  148. tags: tags.reduce((acc, tag) => {
  149. acc[tag.key] = tag;
  150. return acc;
  151. }),
  152. };
  153. });
  154. afterEach(function () {
  155. jest.clearAllMocks();
  156. MockApiClient.clearMockResponses();
  157. localStorageWrapper.clear();
  158. });
  159. describe('withStores and feature flags', function () {
  160. let savedSearchesRequest: jest.Mock;
  161. let recentSearchesRequest: jest.Mock;
  162. let issuesRequest: jest.Mock;
  163. beforeEach(function () {
  164. recentSearchesRequest = MockApiClient.addMockResponse({
  165. url: '/organizations/org-slug/recent-searches/',
  166. method: 'GET',
  167. body: [],
  168. });
  169. savedSearchesRequest = MockApiClient.addMockResponse({
  170. url: '/organizations/org-slug/searches/',
  171. body: [savedSearch],
  172. });
  173. issuesRequest = MockApiClient.addMockResponse({
  174. url: '/organizations/org-slug/issues/',
  175. body: [group],
  176. headers: {
  177. Link: DEFAULT_LINKS_HEADER,
  178. },
  179. });
  180. });
  181. it('loads group rows with default query (no pinned queries, async and no query in URL)', async function () {
  182. render(<IssueListWithStores {...routerProps} />, {
  183. context: routerContext,
  184. });
  185. // Loading saved searches
  186. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  187. expect(savedSearchesRequest).toHaveBeenCalledTimes(1);
  188. await userEvent.click(await screen.findByDisplayValue('is:unresolved'));
  189. // auxillary requests being made
  190. expect(recentSearchesRequest).toHaveBeenCalledTimes(1);
  191. expect(fetchTagsRequest).toHaveBeenCalledTimes(1);
  192. expect(fetchMembersRequest).toHaveBeenCalledTimes(1);
  193. // primary /issues/ request
  194. expect(issuesRequest).toHaveBeenCalledWith(
  195. expect.anything(),
  196. expect.objectContaining({
  197. // Should be called with default query
  198. data: expect.stringContaining('is%3Aunresolved'),
  199. })
  200. );
  201. expect(screen.getByDisplayValue('is:unresolved')).toBeInTheDocument();
  202. expect(screen.getByRole('button', {name: /custom search/i})).toBeInTheDocument();
  203. });
  204. it('loads with query in URL and pinned queries', async function () {
  205. savedSearchesRequest = MockApiClient.addMockResponse({
  206. url: '/organizations/org-slug/searches/',
  207. body: [
  208. savedSearch,
  209. Search({
  210. id: '123',
  211. name: 'My Pinned Search',
  212. isPinned: true,
  213. query: 'is:resolved',
  214. }),
  215. ],
  216. });
  217. const location = TestStubs.location({query: {query: 'level:foo'}});
  218. render(<IssueListWithStores {...merge({}, routerProps, {location})} />, {
  219. context: routerContext,
  220. router: {location},
  221. });
  222. await waitFor(() => {
  223. // Main /issues/ request
  224. expect(issuesRequest).toHaveBeenCalledWith(
  225. expect.anything(),
  226. expect.objectContaining({
  227. // Should be called with default query
  228. data: expect.stringContaining('level%3Afoo'),
  229. })
  230. );
  231. });
  232. expect(screen.getByDisplayValue('level:foo')).toBeInTheDocument();
  233. // Tab shows "custom search"
  234. expect(screen.getByRole('button', {name: 'Custom Search'})).toBeInTheDocument();
  235. });
  236. it('loads with a pinned custom query', async function () {
  237. savedSearchesRequest = MockApiClient.addMockResponse({
  238. url: '/organizations/org-slug/searches/',
  239. body: [
  240. savedSearch,
  241. Search({
  242. id: '123',
  243. name: 'My Pinned Search',
  244. isPinned: true,
  245. isGlobal: false,
  246. query: 'is:resolved',
  247. }),
  248. ],
  249. });
  250. render(<IssueListWithStores {...routerProps} />, {
  251. context: routerContext,
  252. });
  253. await waitFor(() => {
  254. expect(issuesRequest).toHaveBeenCalledWith(
  255. expect.anything(),
  256. expect.objectContaining({
  257. // Should be called with default query
  258. data: expect.stringContaining('is%3Aresolved'),
  259. })
  260. );
  261. });
  262. expect(screen.getByDisplayValue('is:resolved')).toBeInTheDocument();
  263. // Organization saved search selector should have default saved search selected
  264. expect(screen.getByRole('button', {name: 'My Default Search'})).toBeInTheDocument();
  265. });
  266. it('shows archived tab', async function () {
  267. render(
  268. <IssueListWithStores
  269. {...routerProps}
  270. organization={{...organization, features: ['escalating-issues']}}
  271. />,
  272. {
  273. context: routerContext,
  274. }
  275. );
  276. await waitFor(() => {
  277. expect(issuesRequest).toHaveBeenCalled();
  278. });
  279. expect(screen.getByDisplayValue('is:unresolved')).toBeInTheDocument();
  280. // TODO(workflow): remove this test when we remove the feature flag
  281. expect(screen.getByRole('tab', {name: 'Archived'})).toBeInTheDocument();
  282. });
  283. it('loads with a saved query', async function () {
  284. savedSearchesRequest = MockApiClient.addMockResponse({
  285. url: '/organizations/org-slug/searches/',
  286. body: [
  287. Search({
  288. id: '123',
  289. name: 'Assigned to Me',
  290. isPinned: false,
  291. isGlobal: true,
  292. query: 'assigned:me',
  293. sort: 'priority',
  294. type: 0,
  295. }),
  296. ],
  297. });
  298. const localRouter = {params: {searchId: '123'}};
  299. render(<IssueListWithStores {...merge({}, routerProps, localRouter)} />, {
  300. context: routerContext,
  301. router: localRouter,
  302. });
  303. await waitFor(() => {
  304. expect(issuesRequest).toHaveBeenCalledWith(
  305. expect.anything(),
  306. expect.objectContaining({
  307. // Should be called with default query
  308. data:
  309. expect.stringContaining('assigned%3Ame') &&
  310. expect.stringContaining('sort=priority'),
  311. })
  312. );
  313. });
  314. expect(screen.getByDisplayValue('assigned:me')).toBeInTheDocument();
  315. // Organization saved search selector should have default saved search selected
  316. expect(screen.getByRole('button', {name: 'Assigned to Me'})).toBeInTheDocument();
  317. });
  318. it('loads with a query in URL', async function () {
  319. savedSearchesRequest = MockApiClient.addMockResponse({
  320. url: '/organizations/org-slug/searches/',
  321. body: [
  322. Search({
  323. id: '123',
  324. name: 'Assigned to Me',
  325. isPinned: false,
  326. isGlobal: true,
  327. query: 'assigned:me',
  328. type: 0,
  329. }),
  330. ],
  331. });
  332. const localRouter = {location: {query: {query: 'level:error'}}};
  333. render(<IssueListWithStores {...merge({}, routerProps, localRouter)} />, {
  334. context: routerContext,
  335. });
  336. await waitFor(() => {
  337. expect(issuesRequest).toHaveBeenCalledWith(
  338. expect.anything(),
  339. expect.objectContaining({
  340. // Should be called with default query
  341. data: expect.stringContaining('level%3Aerror'),
  342. })
  343. );
  344. });
  345. expect(screen.getByDisplayValue('level:error')).toBeInTheDocument();
  346. // Organization saved search selector should have default saved search selected
  347. expect(screen.getByRole('button', {name: 'Custom Search'})).toBeInTheDocument();
  348. });
  349. it('loads with an empty query in URL', async function () {
  350. savedSearchesRequest = MockApiClient.addMockResponse({
  351. url: '/organizations/org-slug/searches/',
  352. body: [
  353. Search({
  354. id: '123',
  355. name: 'My Pinned Search',
  356. isPinned: true,
  357. isGlobal: false,
  358. query: 'is:resolved',
  359. }),
  360. ],
  361. });
  362. render(
  363. <IssueListWithStores
  364. {...merge({}, routerProps, {location: {query: {query: undefined}}})}
  365. />,
  366. {context: routerContext}
  367. );
  368. await waitFor(() => {
  369. expect(issuesRequest).toHaveBeenCalledWith(
  370. expect.anything(),
  371. expect.objectContaining({
  372. // Should be called with empty query
  373. data: expect.stringContaining(''),
  374. })
  375. );
  376. });
  377. expect(screen.getByDisplayValue('is:resolved')).toBeInTheDocument();
  378. // Organization saved search selector should have default saved search selected
  379. expect(screen.getByRole('button', {name: 'My Default Search'})).toBeInTheDocument();
  380. });
  381. it('caches the search results', async function () {
  382. act(() => ProjectsStore.loadInitialData([project]));
  383. issuesRequest = MockApiClient.addMockResponse({
  384. url: '/organizations/org-slug/issues/',
  385. body: [...new Array(25)].map((_, i) => Group({id: `${i}`})),
  386. headers: {
  387. Link: DEFAULT_LINKS_HEADER,
  388. 'X-Hits': '500',
  389. 'X-Max-Hits': '1000',
  390. },
  391. });
  392. const defaultProps = {
  393. ...props,
  394. ...routerProps,
  395. useOrgSavedSearches: true,
  396. selection: {
  397. projects: [],
  398. environments: [],
  399. datetime: {period: '14d'},
  400. },
  401. organization: Organization({
  402. features: ['issue-stream-performance', 'issue-stream-performance-cache'],
  403. projects: [],
  404. }),
  405. };
  406. const {unmount} = render(<IssueListWithStores {...defaultProps} />, {
  407. context: routerContext,
  408. organization: defaultProps.organization,
  409. });
  410. expect(
  411. await screen.findByText(textWithMarkupMatcher('1-25 of 500'))
  412. ).toBeInTheDocument();
  413. expect(issuesRequest).toHaveBeenCalledTimes(1);
  414. unmount();
  415. // Mount component again, getting from cache
  416. render(<IssueListWithStores {...defaultProps} />, {
  417. context: routerContext,
  418. organization: defaultProps.organization,
  419. });
  420. expect(
  421. await screen.findByText(textWithMarkupMatcher('1-25 of 500'))
  422. ).toBeInTheDocument();
  423. expect(issuesRequest).toHaveBeenCalledTimes(1);
  424. });
  425. it('1 search', async function () {
  426. const localSavedSearch = {...savedSearch, projectId: null};
  427. savedSearchesRequest = MockApiClient.addMockResponse({
  428. url: '/organizations/org-slug/searches/',
  429. body: [localSavedSearch],
  430. });
  431. render(<IssueListWithStores {...routerProps} />, {
  432. context: routerContext,
  433. });
  434. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  435. await userEvent.click(screen.getByRole('button', {name: /custom search/i}));
  436. await userEvent.click(screen.getByRole('button', {name: localSavedSearch.name}));
  437. expect(browserHistory.push).toHaveBeenLastCalledWith(
  438. expect.objectContaining({
  439. pathname: '/organizations/org-slug/issues/searches/789/',
  440. })
  441. );
  442. });
  443. it('clears a saved search when a custom one is entered', async function () {
  444. savedSearchesRequest = MockApiClient.addMockResponse({
  445. url: '/organizations/org-slug/searches/',
  446. body: [
  447. savedSearch,
  448. Search({
  449. id: '123',
  450. name: 'Pinned search',
  451. isPinned: true,
  452. isGlobal: false,
  453. query: 'is:resolved',
  454. }),
  455. ],
  456. });
  457. render(<IssueListWithStores {...routerProps} />, {
  458. context: routerContext,
  459. });
  460. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  461. const queryInput = screen.getByDisplayValue('is:resolved');
  462. await userEvent.clear(queryInput);
  463. await userEvent.type(queryInput, 'dogs{enter}');
  464. expect(browserHistory.push).toHaveBeenLastCalledWith(
  465. expect.objectContaining({
  466. pathname: '/organizations/org-slug/issues/',
  467. query: {
  468. environment: [],
  469. project: [],
  470. referrer: 'issue-list',
  471. sort: '',
  472. query: 'dogs',
  473. statsPeriod: '14d',
  474. },
  475. })
  476. );
  477. });
  478. it('pins a custom query', async function () {
  479. const pinnedSearch = {
  480. id: '666',
  481. name: 'My Pinned Search',
  482. query: 'assigned:me level:fatal',
  483. sort: 'date',
  484. isPinned: true,
  485. visibility: SavedSearchVisibility.ORGANIZATION,
  486. };
  487. savedSearchesRequest = MockApiClient.addMockResponse({
  488. url: '/organizations/org-slug/searches/',
  489. body: [savedSearch],
  490. });
  491. const {rerender} = render(<IssueListWithStores {...routerProps} />, {
  492. context: routerContext,
  493. });
  494. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  495. const queryInput = screen.getByDisplayValue('is:unresolved');
  496. await userEvent.clear(queryInput);
  497. await userEvent.type(queryInput, 'assigned:me level:fatal{enter}');
  498. expect((browserHistory.push as jest.Mock).mock.calls[0][0]).toEqual(
  499. expect.objectContaining({
  500. query: expect.objectContaining({
  501. query: 'assigned:me level:fatal',
  502. }),
  503. })
  504. );
  505. await tick();
  506. const routerWithQuery = {location: {query: {query: 'assigned:me level:fatal'}}};
  507. rerender(<IssueListWithStores {...merge({}, routerProps, routerWithQuery)} />);
  508. expect(screen.getByRole('button', {name: 'Custom Search'})).toBeInTheDocument();
  509. MockApiClient.clearMockResponses();
  510. const createPin = MockApiClient.addMockResponse({
  511. url: '/organizations/org-slug/pinned-searches/',
  512. method: 'PUT',
  513. body: pinnedSearch,
  514. });
  515. MockApiClient.addMockResponse({
  516. url: '/organizations/org-slug/searches/',
  517. body: [savedSearch, pinnedSearch],
  518. });
  519. await userEvent.click(screen.getByLabelText(/Set as Default/i));
  520. await waitFor(() => {
  521. expect(createPin).toHaveBeenCalled();
  522. expect(browserHistory.replace).toHaveBeenLastCalledWith(
  523. expect.objectContaining({
  524. pathname: '/organizations/org-slug/issues/searches/666/',
  525. query: {
  526. referrer: 'search-bar',
  527. },
  528. search: '',
  529. })
  530. );
  531. });
  532. });
  533. it('unpins a custom query', async function () {
  534. const pinnedSearch = Search({
  535. id: '666',
  536. name: 'My Pinned Search',
  537. query: 'assigned:me level:fatal',
  538. sort: 'date',
  539. isPinned: true,
  540. visibility: SavedSearchVisibility.ORGANIZATION,
  541. });
  542. savedSearchesRequest = MockApiClient.addMockResponse({
  543. url: '/organizations/org-slug/searches/',
  544. body: [pinnedSearch],
  545. });
  546. const deletePin = MockApiClient.addMockResponse({
  547. url: '/organizations/org-slug/pinned-searches/',
  548. method: 'DELETE',
  549. });
  550. const routerWithSavedSearch = {
  551. params: {searchId: pinnedSearch.id},
  552. };
  553. render(<IssueListWithStores {...merge({}, routerProps, routerWithSavedSearch)} />, {
  554. context: routerContext,
  555. router: routerWithSavedSearch,
  556. });
  557. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  558. expect(screen.getByRole('button', {name: 'My Default Search'})).toBeInTheDocument();
  559. await userEvent.click(screen.getByLabelText(/Remove Default/i));
  560. await waitFor(() => {
  561. expect(deletePin).toHaveBeenCalled();
  562. });
  563. await waitFor(() => {
  564. expect(browserHistory.replace).toHaveBeenLastCalledWith(
  565. expect.objectContaining({
  566. pathname: '/organizations/org-slug/issues/',
  567. })
  568. );
  569. });
  570. });
  571. it('pins a saved query', async function () {
  572. const assignedToMe = Search({
  573. id: '234',
  574. name: 'Assigned to Me',
  575. isPinned: false,
  576. isGlobal: true,
  577. query: 'assigned:me',
  578. sort: 'date',
  579. type: 0,
  580. });
  581. savedSearchesRequest = MockApiClient.addMockResponse({
  582. url: '/organizations/org-slug/searches/',
  583. body: [savedSearch, assignedToMe],
  584. });
  585. const createPin = MockApiClient.addMockResponse({
  586. url: '/organizations/org-slug/pinned-searches/',
  587. method: 'PUT',
  588. body: {
  589. ...savedSearch,
  590. isPinned: true,
  591. },
  592. });
  593. const routerWithSavedSearch = {params: {searchId: '789'}};
  594. render(<IssueListWithStores {...merge({}, routerProps, routerWithSavedSearch)} />, {
  595. context: routerContext,
  596. router: routerWithSavedSearch,
  597. });
  598. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  599. expect(screen.getByRole('button', {name: savedSearch.name})).toBeInTheDocument();
  600. await userEvent.click(screen.getByLabelText(/set as default/i));
  601. await waitFor(() => {
  602. expect(createPin).toHaveBeenCalled();
  603. expect(browserHistory.replace).toHaveBeenLastCalledWith(
  604. expect.objectContaining({
  605. pathname: '/organizations/org-slug/issues/searches/789/',
  606. })
  607. );
  608. });
  609. });
  610. it('pinning search should keep project selected', async function () {
  611. savedSearchesRequest = MockApiClient.addMockResponse({
  612. url: '/organizations/org-slug/searches/',
  613. body: [savedSearch],
  614. });
  615. const {routerContext: newRouterContext, router: newRouter} = initializeOrg({
  616. router: {
  617. location: {
  618. query: {
  619. project: ['123'],
  620. environment: ['prod'],
  621. query: 'assigned:me level:fatal',
  622. },
  623. },
  624. },
  625. });
  626. render(
  627. <IssueListWithStores
  628. {...newRouter}
  629. selection={{
  630. projects: [123],
  631. environments: ['prod'],
  632. datetime: {
  633. end: null,
  634. period: null,
  635. start: null,
  636. utc: null,
  637. },
  638. }}
  639. />,
  640. {context: newRouterContext}
  641. );
  642. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  643. const createPin = MockApiClient.addMockResponse({
  644. url: '/organizations/org-slug/pinned-searches/',
  645. method: 'PUT',
  646. body: {
  647. ...savedSearch,
  648. id: '666',
  649. name: 'My Pinned Search',
  650. query: 'assigned:me level:fatal',
  651. sort: 'date',
  652. isPinned: true,
  653. },
  654. });
  655. await userEvent.click(screen.getByLabelText(/set as default/i));
  656. await waitFor(() => {
  657. expect(createPin).toHaveBeenCalled();
  658. expect(browserHistory.replace).toHaveBeenLastCalledWith(
  659. expect.objectContaining({
  660. pathname: '/organizations/org-slug/issues/searches/666/',
  661. query: expect.objectContaining({
  662. project: ['123'],
  663. environment: ['prod'],
  664. query: 'assigned:me level:fatal',
  665. referrer: 'search-bar',
  666. }),
  667. })
  668. );
  669. });
  670. });
  671. it('unpinning search should keep project selected', async function () {
  672. const localSavedSearch = {
  673. ...savedSearch,
  674. id: '666',
  675. isPinned: true,
  676. query: 'assigned:me level:fatal',
  677. };
  678. savedSearchesRequest = MockApiClient.addMockResponse({
  679. url: '/organizations/org-slug/searches/',
  680. body: [localSavedSearch],
  681. });
  682. const deletePin = MockApiClient.addMockResponse({
  683. url: '/organizations/org-slug/pinned-searches/',
  684. method: 'DELETE',
  685. });
  686. const {routerContext: newRouterContext, router: newRouter} = initializeOrg(
  687. merge({}, router, {
  688. router: {
  689. location: {
  690. query: {
  691. project: ['123'],
  692. environment: ['prod'],
  693. query: 'assigned:me level:fatal',
  694. },
  695. },
  696. params: {searchId: '666'},
  697. },
  698. })
  699. );
  700. render(
  701. <IssueListWithStores
  702. {...newRouter}
  703. selection={{
  704. projects: [123],
  705. environments: ['prod'],
  706. datetime: {
  707. end: null,
  708. period: null,
  709. start: null,
  710. utc: null,
  711. },
  712. }}
  713. savedSearch={localSavedSearch}
  714. />,
  715. {context: newRouterContext}
  716. );
  717. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  718. await userEvent.click(screen.getByLabelText(/Remove Default/i));
  719. await waitFor(() => {
  720. expect(deletePin).toHaveBeenCalled();
  721. expect(browserHistory.replace).toHaveBeenLastCalledWith(
  722. expect.objectContaining({
  723. pathname: '/organizations/org-slug/issues/',
  724. query: expect.objectContaining({
  725. project: ['123'],
  726. environment: ['prod'],
  727. query: 'assigned:me level:fatal',
  728. referrer: 'search-bar',
  729. }),
  730. })
  731. );
  732. });
  733. });
  734. it('does not allow pagination to "previous" while on first page and resets cursors when navigating back to initial page', async function () {
  735. const {rerender} = render(<IssueListWithStores {...routerProps} />, {
  736. context: routerContext,
  737. });
  738. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  739. expect(screen.getByRole('button', {name: 'Previous'})).toBeDisabled();
  740. issuesRequest = MockApiClient.addMockResponse({
  741. url: '/organizations/org-slug/issues/',
  742. body: [group],
  743. headers: {
  744. 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"',
  745. },
  746. });
  747. await userEvent.click(screen.getByRole('button', {name: 'Next'}));
  748. let pushArgs = {
  749. pathname: '/organizations/org-slug/issues/',
  750. query: {
  751. cursor: '1443575000:0:0',
  752. page: 1,
  753. environment: [],
  754. project: [],
  755. query: 'is:unresolved',
  756. statsPeriod: '14d',
  757. referrer: 'issue-list',
  758. },
  759. };
  760. await waitFor(() => {
  761. expect(browserHistory.push).toHaveBeenLastCalledWith(pushArgs);
  762. });
  763. rerender(<IssueListWithStores {...merge({}, routerProps, {location: pushArgs})} />);
  764. expect(screen.getByRole('button', {name: 'Previous'})).toBeEnabled();
  765. // Click next again
  766. await userEvent.click(screen.getByRole('button', {name: 'Next'}));
  767. pushArgs = {
  768. pathname: '/organizations/org-slug/issues/',
  769. query: {
  770. cursor: '1443574000:0:0',
  771. page: 2,
  772. environment: [],
  773. project: [],
  774. query: 'is:unresolved',
  775. statsPeriod: '14d',
  776. referrer: 'issue-list',
  777. },
  778. };
  779. await waitFor(() => {
  780. expect(browserHistory.push).toHaveBeenLastCalledWith(pushArgs);
  781. });
  782. rerender(<IssueListWithStores {...merge({}, routerProps, {location: pushArgs})} />);
  783. // Click previous
  784. await userEvent.click(screen.getByRole('button', {name: 'Previous'}));
  785. pushArgs = {
  786. pathname: '/organizations/org-slug/issues/',
  787. query: {
  788. cursor: '1443575000:0:1',
  789. page: 1,
  790. environment: [],
  791. project: [],
  792. query: 'is:unresolved',
  793. statsPeriod: '14d',
  794. referrer: 'issue-list',
  795. },
  796. };
  797. await waitFor(() => {
  798. expect(browserHistory.push).toHaveBeenLastCalledWith(pushArgs);
  799. });
  800. rerender(<IssueListWithStores {...merge({}, routerProps, {location: pushArgs})} />);
  801. // Click previous back to initial page
  802. await userEvent.click(screen.getByRole('button', {name: 'Previous'}));
  803. await waitFor(() => {
  804. // cursor is undefined because "prev" cursor is === initial "next" cursor
  805. expect(browserHistory.push).toHaveBeenLastCalledWith({
  806. pathname: '/organizations/org-slug/issues/',
  807. query: {
  808. cursor: undefined,
  809. environment: [],
  810. page: undefined,
  811. project: [],
  812. query: 'is:unresolved',
  813. statsPeriod: '14d',
  814. referrer: 'issue-list',
  815. },
  816. });
  817. });
  818. });
  819. });
  820. describe('transitionTo', function () {
  821. it('pushes to history when query is updated', async function () {
  822. MockApiClient.addMockResponse({
  823. url: '/organizations/org-slug/issues/',
  824. body: [],
  825. headers: {
  826. Link: DEFAULT_LINKS_HEADER,
  827. },
  828. });
  829. render(<IssueListOverview {...props} />, {
  830. context: routerContext,
  831. });
  832. const queryInput = screen.getByDisplayValue('is:unresolved');
  833. await userEvent.clear(queryInput);
  834. await userEvent.type(queryInput, 'is:ignored{enter}');
  835. expect(browserHistory.push).toHaveBeenCalledWith({
  836. pathname: '/organizations/org-slug/issues/',
  837. query: {
  838. environment: [],
  839. project: [parseInt(project.id, 10)],
  840. query: 'is:ignored',
  841. statsPeriod: '14d',
  842. referrer: 'issue-list',
  843. },
  844. });
  845. });
  846. });
  847. it('fetches tags and members', async function () {
  848. render(<IssueListOverview {...routerProps} {...props} />, {context: routerContext});
  849. await waitFor(() => {
  850. expect(fetchTagsRequest).toHaveBeenCalled();
  851. expect(fetchMembersRequest).toHaveBeenCalled();
  852. });
  853. });
  854. describe('componentDidUpdate fetching groups', function () {
  855. let fetchDataMock;
  856. beforeEach(function () {
  857. fetchDataMock = MockApiClient.addMockResponse({
  858. url: '/organizations/org-slug/issues/',
  859. body: [group],
  860. headers: {
  861. Link: DEFAULT_LINKS_HEADER,
  862. },
  863. });
  864. fetchDataMock.mockReset();
  865. });
  866. it('fetches data on selection change', function () {
  867. const {rerender} = render(<IssueListOverview {...routerProps} {...props} />, {
  868. context: routerContext,
  869. });
  870. rerender(
  871. <IssueListOverview
  872. {...routerProps}
  873. {...props}
  874. selection={{projects: [99], environments: [], datetime: {period: '24h'}}}
  875. />
  876. );
  877. expect(fetchDataMock).toHaveBeenCalled();
  878. });
  879. it('fetches data on savedSearch change', function () {
  880. const {rerender} = render(<IssueListOverview {...routerProps} {...props} />, {
  881. context: routerContext,
  882. });
  883. rerender(
  884. <IssueListOverview
  885. {...routerProps}
  886. {...props}
  887. savedSearch={{id: '1', query: 'is:resolved'}}
  888. />
  889. );
  890. expect(fetchDataMock).toHaveBeenCalled();
  891. });
  892. it('uses correct statsPeriod when fetching issues list and no datetime given', function () {
  893. const {rerender} = render(<IssueListOverview {...routerProps} {...props} />, {
  894. context: routerContext,
  895. });
  896. const selection = {projects: [99], environments: [], datetime: {}};
  897. rerender(<IssueListOverview {...routerProps} {...props} selection={selection} />);
  898. expect(fetchDataMock).toHaveBeenLastCalledWith(
  899. '/organizations/org-slug/issues/',
  900. expect.objectContaining({
  901. data: 'collapse=stats&collapse=unhandled&expand=owners&expand=inbox&limit=25&project=99&query=is%3Aunresolved&savedSearch=1&shortIdLookup=1&statsPeriod=14d',
  902. })
  903. );
  904. });
  905. });
  906. describe('componentDidUpdate fetching members', function () {
  907. it('fetches memberlist and tags list on project change', function () {
  908. const {rerender} = render(<IssueListOverview {...routerProps} {...props} />, {
  909. context: routerContext,
  910. });
  911. // Called during componentDidMount
  912. expect(fetchMembersRequest).toHaveBeenCalledTimes(1);
  913. expect(fetchTagsRequest).toHaveBeenCalledTimes(1);
  914. const selection = {
  915. projects: [99],
  916. environments: [],
  917. datetime: {period: '24h'},
  918. };
  919. rerender(<IssueListOverview {...routerProps} {...props} selection={selection} />);
  920. expect(fetchMembersRequest).toHaveBeenCalledTimes(2);
  921. expect(fetchTagsRequest).toHaveBeenCalledTimes(2);
  922. });
  923. });
  924. describe('render states', function () {
  925. it('displays the loading icon when saved searches are loading', function () {
  926. render(<IssueListOverview {...routerProps} {...props} savedSearchLoading />, {
  927. context: routerContext,
  928. });
  929. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  930. });
  931. it('displays an error when issues fail to load', async function () {
  932. MockApiClient.addMockResponse({
  933. url: '/organizations/org-slug/issues/',
  934. status: 500,
  935. statusCode: 500,
  936. });
  937. render(<IssueListOverview {...routerProps} {...props} />, {
  938. context: routerContext,
  939. });
  940. expect(await screen.findByTestId('loading-error')).toBeInTheDocument();
  941. });
  942. it('displays congrats robots animation with only is:unresolved query', async function () {
  943. MockApiClient.addMockResponse({
  944. url: '/organizations/org-slug/issues/',
  945. body: [],
  946. headers: {
  947. Link: DEFAULT_LINKS_HEADER,
  948. },
  949. });
  950. render(<IssueListOverview {...routerProps} {...props} />, {context: routerContext});
  951. expect(
  952. await screen.findByText(/We couldn't find any issues that matched your filters/i)
  953. ).toBeInTheDocument();
  954. });
  955. it('displays an empty resultset with a non-default query', async function () {
  956. MockApiClient.addMockResponse({
  957. url: '/organizations/org-slug/issues/',
  958. body: [],
  959. headers: {
  960. Link: DEFAULT_LINKS_HEADER,
  961. },
  962. });
  963. render(<IssueListOverview {...routerProps} {...props} />, {context: routerContext});
  964. await userEvent.type(
  965. screen.getByDisplayValue('is:unresolved'),
  966. ' level:error{enter}'
  967. );
  968. expect(
  969. await screen.findByText(/We couldn't find any issues that matched your filters/i)
  970. ).toBeInTheDocument();
  971. });
  972. });
  973. describe('Error Robot', function () {
  974. const createWrapper = async moreProps => {
  975. MockApiClient.addMockResponse({
  976. url: '/organizations/org-slug/issues/',
  977. body: [],
  978. headers: {
  979. Link: DEFAULT_LINKS_HEADER,
  980. },
  981. });
  982. const defaultProps = {
  983. ...props,
  984. useOrgSavedSearches: true,
  985. selection: {
  986. projects: [],
  987. environments: [],
  988. datetime: {period: '14d'},
  989. },
  990. ...merge({}, routerProps, {
  991. params: {},
  992. location: {query: {query: 'is:unresolved'}, search: 'query=is:unresolved'},
  993. }),
  994. organization: Organization({
  995. projects: [],
  996. }),
  997. ...moreProps,
  998. };
  999. render(<IssueListOverview {...defaultProps} />, {
  1000. context: routerContext,
  1001. });
  1002. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  1003. };
  1004. it('displays when no projects selected and all projects user is member of, async does not have first event', async function () {
  1005. const projects = [
  1006. Project({
  1007. id: '1',
  1008. slug: 'foo',
  1009. isMember: true,
  1010. firstEvent: undefined,
  1011. }),
  1012. Project({
  1013. id: '2',
  1014. slug: 'bar',
  1015. isMember: true,
  1016. firstEvent: undefined,
  1017. }),
  1018. Project({
  1019. id: '3',
  1020. slug: 'baz',
  1021. isMember: true,
  1022. firstEvent: undefined,
  1023. }),
  1024. ];
  1025. MockApiClient.addMockResponse({
  1026. url: '/organizations/org-slug/sent-first-event/',
  1027. query: {
  1028. is_member: true,
  1029. },
  1030. body: {sentFirstEvent: false},
  1031. });
  1032. MockApiClient.addMockResponse({
  1033. url: '/organizations/org-slug/projects/',
  1034. body: projects,
  1035. });
  1036. MockApiClient.addMockResponse({
  1037. url: '/projects/org-slug/foo/issues/',
  1038. body: [],
  1039. });
  1040. await createWrapper({
  1041. organization: Organization({
  1042. projects,
  1043. }),
  1044. });
  1045. expect(screen.getByTestId('awaiting-events')).toBeInTheDocument();
  1046. });
  1047. it('does not display when no projects selected and any projects have a first event', async function () {
  1048. const projects = [
  1049. Project({
  1050. id: '1',
  1051. slug: 'foo',
  1052. isMember: true,
  1053. firstEvent: '',
  1054. }),
  1055. Project({
  1056. id: '2',
  1057. slug: 'bar',
  1058. isMember: true,
  1059. firstEvent: new Date().toISOString(),
  1060. }),
  1061. Project({
  1062. id: '3',
  1063. slug: 'baz',
  1064. isMember: true,
  1065. firstEvent: new Date().toISOString(),
  1066. }),
  1067. ];
  1068. MockApiClient.addMockResponse({
  1069. url: '/organizations/org-slug/sent-first-event/',
  1070. query: {
  1071. is_member: true,
  1072. },
  1073. body: {sentFirstEvent: true},
  1074. });
  1075. MockApiClient.addMockResponse({
  1076. url: '/organizations/org-slug/projects/',
  1077. body: projects,
  1078. });
  1079. await createWrapper({
  1080. organization: Organization({
  1081. projects,
  1082. }),
  1083. });
  1084. expect(screen.queryByTestId('awaiting-events')).not.toBeInTheDocument();
  1085. });
  1086. it('displays when all selected projects do not have first event', async function () {
  1087. const projects = [
  1088. Project({
  1089. id: '1',
  1090. slug: 'foo',
  1091. isMember: true,
  1092. firstEvent: undefined,
  1093. }),
  1094. Project({
  1095. id: '2',
  1096. slug: 'bar',
  1097. isMember: true,
  1098. firstEvent: undefined,
  1099. }),
  1100. Project({
  1101. id: '3',
  1102. slug: 'baz',
  1103. isMember: true,
  1104. firstEvent: undefined,
  1105. }),
  1106. ];
  1107. MockApiClient.addMockResponse({
  1108. url: '/organizations/org-slug/sent-first-event/',
  1109. query: {
  1110. project: [1, 2],
  1111. },
  1112. body: {sentFirstEvent: false},
  1113. });
  1114. MockApiClient.addMockResponse({
  1115. url: '/organizations/org-slug/projects/',
  1116. body: projects,
  1117. });
  1118. MockApiClient.addMockResponse({
  1119. url: '/projects/org-slug/foo/issues/',
  1120. body: [],
  1121. });
  1122. await createWrapper({
  1123. selection: {
  1124. projects: [1, 2],
  1125. environments: [],
  1126. datetime: {period: '14d'},
  1127. },
  1128. organization: Organization({
  1129. projects,
  1130. }),
  1131. });
  1132. expect(await screen.findByTestId('awaiting-events')).toBeInTheDocument();
  1133. });
  1134. it('does not display when any selected projects have first event', async function () {
  1135. const projects = [
  1136. Project({
  1137. id: '1',
  1138. slug: 'foo',
  1139. isMember: true,
  1140. firstEvent: undefined,
  1141. }),
  1142. Project({
  1143. id: '2',
  1144. slug: 'bar',
  1145. isMember: true,
  1146. firstEvent: new Date().toISOString(),
  1147. }),
  1148. Project({
  1149. id: '3',
  1150. slug: 'baz',
  1151. isMember: true,
  1152. firstEvent: new Date().toISOString(),
  1153. }),
  1154. ];
  1155. MockApiClient.addMockResponse({
  1156. url: '/organizations/org-slug/sent-first-event/',
  1157. query: {
  1158. project: [1, 2],
  1159. },
  1160. body: {sentFirstEvent: true},
  1161. });
  1162. MockApiClient.addMockResponse({
  1163. url: '/organizations/org-slug/projects/',
  1164. body: projects,
  1165. });
  1166. await createWrapper({
  1167. selection: {
  1168. projects: [1, 2],
  1169. environments: [],
  1170. datetime: {period: '14d'},
  1171. },
  1172. organization: Organization({
  1173. projects,
  1174. }),
  1175. });
  1176. expect(screen.queryByTestId('awaiting-events')).not.toBeInTheDocument();
  1177. });
  1178. });
  1179. it('displays a count that represents the current page', function () {
  1180. act(() => ProjectsStore.loadInitialData([project]));
  1181. MockApiClient.addMockResponse({
  1182. url: '/organizations/org-slug/issues/',
  1183. body: [...new Array(25)].map((_, i) => Group({id: `${i}`})),
  1184. headers: {
  1185. Link: DEFAULT_LINKS_HEADER,
  1186. 'X-Hits': '500',
  1187. 'X-Max-Hits': '1000',
  1188. },
  1189. });
  1190. parseLinkHeaderSpy.mockReturnValue({
  1191. next: {
  1192. results: true,
  1193. cursor: '',
  1194. href: '',
  1195. },
  1196. previous: {
  1197. results: false,
  1198. cursor: '',
  1199. href: '',
  1200. },
  1201. });
  1202. props = {
  1203. ...props,
  1204. location: {
  1205. query: {
  1206. cursor: 'some cursor',
  1207. page: 1,
  1208. },
  1209. },
  1210. };
  1211. const {routerContext: newRouterContext} = initializeOrg();
  1212. const {rerender} = render(<IssueListOverview {...props} />, {
  1213. context: newRouterContext,
  1214. });
  1215. expect(screen.getByText(textWithMarkupMatcher('1-25 of 500'))).toBeInTheDocument();
  1216. parseLinkHeaderSpy.mockReturnValue({
  1217. next: {
  1218. results: true,
  1219. cursor: '',
  1220. href: '',
  1221. },
  1222. previous: {
  1223. results: true,
  1224. cursor: '',
  1225. href: '',
  1226. },
  1227. });
  1228. rerender(<IssueListOverview {...props} />);
  1229. expect(screen.getByText(textWithMarkupMatcher('26-50 of 500'))).toBeInTheDocument();
  1230. });
  1231. describe('project low priority queue alert', function () {
  1232. const {routerContext: newRouterContext} = initializeOrg();
  1233. beforeEach(function () {
  1234. act(() => ProjectsStore.reset());
  1235. });
  1236. it('does not render event processing alert', function () {
  1237. act(() => ProjectsStore.loadInitialData([project]));
  1238. render(<IssueListOverview {...props} />, {
  1239. context: newRouterContext,
  1240. });
  1241. expect(screen.queryByText(/event processing/i)).not.toBeInTheDocument();
  1242. });
  1243. describe('renders alert', function () {
  1244. it('for one project', function () {
  1245. act(() =>
  1246. ProjectsStore.loadInitialData([
  1247. {...project, eventProcessing: {symbolicationDegraded: true}},
  1248. ])
  1249. );
  1250. render(<IssueListOverview {...props} />, {
  1251. context: routerContext,
  1252. });
  1253. expect(
  1254. screen.getByText(/Event Processing for this project is currently degraded/i)
  1255. ).toBeInTheDocument();
  1256. });
  1257. it('for multiple projects', function () {
  1258. const projectBar = Project({
  1259. id: '3560',
  1260. name: 'Bar Project',
  1261. slug: 'project-slug-bar',
  1262. });
  1263. act(() =>
  1264. ProjectsStore.loadInitialData([
  1265. {
  1266. ...project,
  1267. slug: 'project-slug',
  1268. eventProcessing: {symbolicationDegraded: true},
  1269. },
  1270. {
  1271. ...projectBar,
  1272. slug: 'project-slug-bar',
  1273. eventProcessing: {symbolicationDegraded: true},
  1274. },
  1275. ])
  1276. );
  1277. render(
  1278. <IssueListOverview
  1279. {...props}
  1280. selection={{
  1281. ...props.selection,
  1282. projects: [Number(project.id), Number(projectBar.id)],
  1283. }}
  1284. />,
  1285. {
  1286. context: newRouterContext,
  1287. }
  1288. );
  1289. expect(
  1290. screen.getByText(
  1291. textWithMarkupMatcher(
  1292. 'Event Processing for the project-slug, project-slug-bar projects is currently degraded.'
  1293. )
  1294. )
  1295. ).toBeInTheDocument();
  1296. });
  1297. });
  1298. });
  1299. });
  1300. describe('stats', () => {
  1301. const tags = Tags();
  1302. const groupStats = GroupStats();
  1303. const savedSearch = Search({
  1304. id: '789',
  1305. query: 'is:unresolved TypeError',
  1306. sort: 'date',
  1307. name: 'Unresolved TypeErrors',
  1308. });
  1309. beforeEach(() => {
  1310. GroupStore.loadInitialData([]);
  1311. MockApiClient.clearMockResponses();
  1312. MockApiClient.addMockResponse({
  1313. url: '/organizations/org-slug/searches/',
  1314. body: [savedSearch],
  1315. });
  1316. MockApiClient.addMockResponse({
  1317. url: '/organizations/org-slug/recent-searches/',
  1318. body: [],
  1319. });
  1320. MockApiClient.addMockResponse({
  1321. url: '/organizations/org-slug/recent-searches/',
  1322. method: 'POST',
  1323. body: [],
  1324. });
  1325. MockApiClient.addMockResponse({
  1326. url: '/organizations/org-slug/issues-count/',
  1327. method: 'GET',
  1328. body: [{}],
  1329. });
  1330. MockApiClient.addMockResponse({
  1331. url: '/organizations/org-slug/processingissues/',
  1332. method: 'GET',
  1333. body: [
  1334. {
  1335. project: 'test-project',
  1336. numIssues: 1,
  1337. hasIssues: true,
  1338. lastSeen: '2019-01-16T15:39:11.081Z',
  1339. },
  1340. ],
  1341. });
  1342. MockApiClient.addMockResponse({
  1343. url: '/organizations/org-slug/tags/',
  1344. method: 'GET',
  1345. body: tags,
  1346. });
  1347. MockApiClient.addMockResponse({
  1348. url: '/organizations/org-slug/users/',
  1349. method: 'GET',
  1350. body: [Member({projects: [project.slug]})],
  1351. });
  1352. MockApiClient.addMockResponse({
  1353. url: '/organizations/org-slug/sent-first-event/',
  1354. body: {sentFirstEvent: true},
  1355. });
  1356. MockApiClient.addMockResponse({
  1357. url: '/organizations/org-slug/projects/',
  1358. body: [project],
  1359. });
  1360. });
  1361. it('fetches stats for route', async () => {
  1362. const customRouterProps = {
  1363. params: router.params,
  1364. location: router.location,
  1365. };
  1366. const groups: any[] = [];
  1367. for (let i = 0; i < 25; i++) {
  1368. groups.push(Group({project, id: `${i}`}));
  1369. }
  1370. const issuesRequest = MockApiClient.addMockResponse({
  1371. url: '/organizations/org-slug/issues/',
  1372. body: groups,
  1373. headers: {
  1374. Link: DEFAULT_LINKS_HEADER,
  1375. },
  1376. });
  1377. const statsRequest = MockApiClient.addMockResponse({
  1378. url: '/organizations/org-slug/issues-stats/',
  1379. body: [groupStats],
  1380. });
  1381. render(<IssueListWithStores {...customRouterProps} />, {
  1382. context: routerContext,
  1383. });
  1384. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  1385. expect(issuesRequest).toHaveBeenCalled();
  1386. expect(statsRequest.mock.calls[0][1].query).toEqual(
  1387. expect.objectContaining({
  1388. groups: groups.map(g => g.id),
  1389. })
  1390. );
  1391. expect(await screen.findAllByTestId('group')).toHaveLength(25);
  1392. expect(screen.queryAllByTestId('loading-placeholder')).toHaveLength(0);
  1393. });
  1394. it('refetches stats for another route', async () => {
  1395. const customRouterProps = {
  1396. params: router.params,
  1397. location: router.location,
  1398. };
  1399. const groups: any[] = [];
  1400. const statsForGroups: any[] = [];
  1401. for (let i = 0; i < 25; i++) {
  1402. groups.push(Group({project, id: `${i}`}));
  1403. statsForGroups.push(GroupStats({id: `${i}`}));
  1404. }
  1405. const forReviewGroups: any[] = [];
  1406. const forReviewGroupStats: any[] = [];
  1407. for (let i = 0; i < 25; i++) {
  1408. forReviewGroups.push(Group({project, id: `${groups.length + i}`}));
  1409. forReviewGroupStats.push(GroupStats({id: `${groups.length + i}`}));
  1410. }
  1411. const issuesRequest = MockApiClient.addMockResponse({
  1412. url: '/organizations/org-slug/issues/',
  1413. body: groups,
  1414. headers: {
  1415. Link: DEFAULT_LINKS_HEADER,
  1416. },
  1417. });
  1418. const statsRequest = MockApiClient.addMockResponse({
  1419. url: '/organizations/org-slug/issues-stats/',
  1420. body: [statsForGroups],
  1421. });
  1422. const {rerender} = render(<IssueListWithStores {...customRouterProps} />, {
  1423. context: routerContext,
  1424. });
  1425. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  1426. expect(issuesRequest).toHaveBeenCalled();
  1427. expect(statsRequest).toHaveBeenCalled();
  1428. expect(await screen.findAllByTestId('group')).toHaveLength(25);
  1429. expect(screen.queryAllByTestId('loading-placeholder')).toHaveLength(0);
  1430. const newRouteProps = {
  1431. ...customRouterProps,
  1432. location: {
  1433. ...customRouterProps.location,
  1434. query: {
  1435. query:
  1436. 'is:unresolved is:for_review assigned_or_suggested:[me, my_teams, none] ',
  1437. },
  1438. hash: '',
  1439. },
  1440. };
  1441. const forReviewGroupsRequest = MockApiClient.addMockResponse({
  1442. url: '/organizations/org-slug/issues/',
  1443. body: forReviewGroups,
  1444. headers: {
  1445. Link: DEFAULT_LINKS_HEADER,
  1446. },
  1447. });
  1448. const forReviewStatsRequest = MockApiClient.addMockResponse({
  1449. url: '/organizations/org-slug/issues-stats/',
  1450. body: [forReviewGroupStats],
  1451. });
  1452. rerender(<IssueListWithStores {...newRouteProps} />);
  1453. expect(forReviewGroupsRequest).toHaveBeenCalled();
  1454. expect(forReviewStatsRequest).toHaveBeenCalled();
  1455. expect(await screen.findAllByTestId('group')).toHaveLength(25);
  1456. expect(screen.queryAllByTestId('loading-placeholder')).toHaveLength(0);
  1457. });
  1458. });