overview.spec.jsx 44 KB

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