overview.spec.tsx 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489
  1. import {browserHistory} from 'react-router';
  2. import merge from 'lodash/merge';
  3. import {GroupStats} from 'sentry-fixture/groupStats';
  4. import LocationFixture from 'sentry-fixture/locationFixture';
  5. import {Organization} from 'sentry-fixture/organization';
  6. import {Project as ProjectFixture} from 'sentry-fixture/project';
  7. import {Search} from 'sentry-fixture/search';
  8. import {Tags} from 'sentry-fixture/tags';
  9. import {initializeOrg} from 'sentry-test/initializeOrg';
  10. import {
  11. act,
  12. render,
  13. screen,
  14. userEvent,
  15. waitFor,
  16. waitForElementToBeRemoved,
  17. } from 'sentry-test/reactTestingLibrary';
  18. import {textWithMarkupMatcher} from 'sentry-test/utils';
  19. import StreamGroup from 'sentry/components/stream/group';
  20. import ProjectsStore from 'sentry/stores/projectsStore';
  21. import TagStore from 'sentry/stores/tagStore';
  22. import {SavedSearchVisibility} from 'sentry/types';
  23. import localStorageWrapper from 'sentry/utils/localStorage';
  24. import * as parseLinkHeader from 'sentry/utils/parseLinkHeader';
  25. import IssueListWithStores, {IssueListOverview} from 'sentry/views/issueList/overview';
  26. // Mock <IssueListActions>
  27. jest.mock('sentry/views/issueList/actions', () => jest.fn(() => null));
  28. jest.mock('sentry/components/stream/group', () => 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 = ProjectFixture({
  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 = TestStubs.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: [TestStubs.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. jest.mocked(StreamGroup).mockClear();
  165. recentSearchesRequest = MockApiClient.addMockResponse({
  166. url: '/organizations/org-slug/recent-searches/',
  167. method: 'GET',
  168. body: [],
  169. });
  170. savedSearchesRequest = MockApiClient.addMockResponse({
  171. url: '/organizations/org-slug/searches/',
  172. body: [savedSearch],
  173. });
  174. issuesRequest = MockApiClient.addMockResponse({
  175. url: '/organizations/org-slug/issues/',
  176. body: [group],
  177. headers: {
  178. Link: DEFAULT_LINKS_HEADER,
  179. },
  180. });
  181. });
  182. it('loads group rows with default query (no pinned queries, async and no query in URL)', async function () {
  183. render(<IssueListWithStores {...routerProps} />, {
  184. context: routerContext,
  185. });
  186. // Loading saved searches
  187. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  188. expect(savedSearchesRequest).toHaveBeenCalledTimes(1);
  189. await userEvent.click(await screen.findByDisplayValue('is:unresolved'));
  190. // auxillary requests being made
  191. expect(recentSearchesRequest).toHaveBeenCalledTimes(1);
  192. expect(fetchTagsRequest).toHaveBeenCalledTimes(1);
  193. expect(fetchMembersRequest).toHaveBeenCalledTimes(1);
  194. // primary /issues/ request
  195. expect(issuesRequest).toHaveBeenCalledWith(
  196. expect.anything(),
  197. expect.objectContaining({
  198. // Should be called with default query
  199. data: expect.stringContaining('is%3Aunresolved'),
  200. })
  201. );
  202. expect(screen.getByDisplayValue('is:unresolved')).toBeInTheDocument();
  203. expect(screen.getByRole('button', {name: /custom search/i})).toBeInTheDocument();
  204. });
  205. it('loads with query in URL and pinned queries', async function () {
  206. savedSearchesRequest = MockApiClient.addMockResponse({
  207. url: '/organizations/org-slug/searches/',
  208. body: [
  209. savedSearch,
  210. Search({
  211. id: '123',
  212. name: 'My Pinned Search',
  213. isPinned: true,
  214. query: 'is:resolved',
  215. }),
  216. ],
  217. });
  218. const location = LocationFixture({query: {query: 'level:foo'}});
  219. render(<IssueListWithStores {...merge({}, routerProps, {location})} />, {
  220. context: routerContext,
  221. router: {location},
  222. });
  223. await waitFor(() => {
  224. // Main /issues/ request
  225. expect(issuesRequest).toHaveBeenCalledWith(
  226. expect.anything(),
  227. expect.objectContaining({
  228. // Should be called with default query
  229. data: expect.stringContaining('level%3Afoo'),
  230. })
  231. );
  232. });
  233. expect(screen.getByDisplayValue('level:foo')).toBeInTheDocument();
  234. // Tab shows "custom search"
  235. expect(screen.getByRole('button', {name: 'Custom Search'})).toBeInTheDocument();
  236. });
  237. it('loads with a pinned custom query', async function () {
  238. savedSearchesRequest = MockApiClient.addMockResponse({
  239. url: '/organizations/org-slug/searches/',
  240. body: [
  241. savedSearch,
  242. Search({
  243. id: '123',
  244. name: 'My Pinned Search',
  245. isPinned: true,
  246. isGlobal: false,
  247. query: 'is:resolved',
  248. }),
  249. ],
  250. });
  251. render(<IssueListWithStores {...routerProps} />, {
  252. context: routerContext,
  253. });
  254. await waitFor(() => {
  255. expect(issuesRequest).toHaveBeenCalledWith(
  256. expect.anything(),
  257. expect.objectContaining({
  258. // Should be called with default query
  259. data: expect.stringContaining('is%3Aresolved'),
  260. })
  261. );
  262. });
  263. expect(screen.getByDisplayValue('is:resolved')).toBeInTheDocument();
  264. // Organization saved search selector should have default saved search selected
  265. expect(screen.getByRole('button', {name: 'My Default Search'})).toBeInTheDocument();
  266. });
  267. it('shows archived tab', async function () {
  268. render(
  269. <IssueListWithStores
  270. {...routerProps}
  271. organization={{...organization, features: ['escalating-issues']}}
  272. />,
  273. {
  274. context: routerContext,
  275. }
  276. );
  277. await waitFor(() => {
  278. expect(issuesRequest).toHaveBeenCalled();
  279. });
  280. expect(screen.getByDisplayValue('is:unresolved')).toBeInTheDocument();
  281. // TODO(workflow): remove this test when we remove the feature flag
  282. expect(screen.getByRole('tab', {name: 'Archived'})).toBeInTheDocument();
  283. });
  284. it('loads with a saved query', async function () {
  285. savedSearchesRequest = MockApiClient.addMockResponse({
  286. url: '/organizations/org-slug/searches/',
  287. body: [
  288. Search({
  289. id: '123',
  290. name: 'Assigned to Me',
  291. isPinned: false,
  292. isGlobal: true,
  293. query: 'assigned:me',
  294. sort: 'priority',
  295. type: 0,
  296. }),
  297. ],
  298. });
  299. const localRouter = {params: {searchId: '123'}};
  300. render(<IssueListWithStores {...merge({}, routerProps, localRouter)} />, {
  301. context: routerContext,
  302. router: localRouter,
  303. });
  304. await waitFor(() => {
  305. expect(issuesRequest).toHaveBeenCalledWith(
  306. expect.anything(),
  307. expect.objectContaining({
  308. // Should be called with default query
  309. data:
  310. expect.stringContaining('assigned%3Ame') &&
  311. expect.stringContaining('sort=priority'),
  312. })
  313. );
  314. });
  315. expect(screen.getByDisplayValue('assigned:me')).toBeInTheDocument();
  316. // Organization saved search selector should have default saved search selected
  317. expect(screen.getByRole('button', {name: 'Assigned to Me'})).toBeInTheDocument();
  318. });
  319. it('loads with a query in URL', async function () {
  320. savedSearchesRequest = MockApiClient.addMockResponse({
  321. url: '/organizations/org-slug/searches/',
  322. body: [
  323. Search({
  324. id: '123',
  325. name: 'Assigned to Me',
  326. isPinned: false,
  327. isGlobal: true,
  328. query: 'assigned:me',
  329. type: 0,
  330. }),
  331. ],
  332. });
  333. const localRouter = {location: {query: {query: 'level:error'}}};
  334. render(<IssueListWithStores {...merge({}, routerProps, localRouter)} />, {
  335. context: routerContext,
  336. });
  337. await waitFor(() => {
  338. expect(issuesRequest).toHaveBeenCalledWith(
  339. expect.anything(),
  340. expect.objectContaining({
  341. // Should be called with default query
  342. data: expect.stringContaining('level%3Aerror'),
  343. })
  344. );
  345. });
  346. expect(screen.getByDisplayValue('level:error')).toBeInTheDocument();
  347. // Organization saved search selector should have default saved search selected
  348. expect(screen.getByRole('button', {name: 'Custom Search'})).toBeInTheDocument();
  349. });
  350. it('loads with an empty query in URL', async function () {
  351. savedSearchesRequest = MockApiClient.addMockResponse({
  352. url: '/organizations/org-slug/searches/',
  353. body: [
  354. Search({
  355. id: '123',
  356. name: 'My Pinned Search',
  357. isPinned: true,
  358. isGlobal: false,
  359. query: 'is:resolved',
  360. }),
  361. ],
  362. });
  363. render(
  364. <IssueListWithStores
  365. {...merge({}, routerProps, {location: {query: {query: undefined}}})}
  366. />,
  367. {context: routerContext}
  368. );
  369. await waitFor(() => {
  370. expect(issuesRequest).toHaveBeenCalledWith(
  371. expect.anything(),
  372. expect.objectContaining({
  373. // Should be called with empty query
  374. data: expect.stringContaining(''),
  375. })
  376. );
  377. });
  378. expect(screen.getByDisplayValue('is:resolved')).toBeInTheDocument();
  379. // Organization saved search selector should have default saved search selected
  380. expect(screen.getByRole('button', {name: 'My Default Search'})).toBeInTheDocument();
  381. });
  382. it('caches the search results', async function () {
  383. issuesRequest = MockApiClient.addMockResponse({
  384. url: '/organizations/org-slug/issues/',
  385. body: [...new Array(25)].map((_, i) => ({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. ProjectFixture({
  1007. id: '1',
  1008. slug: 'foo',
  1009. isMember: true,
  1010. firstEvent: null,
  1011. }),
  1012. ProjectFixture({
  1013. id: '2',
  1014. slug: 'bar',
  1015. isMember: true,
  1016. firstEvent: null,
  1017. }),
  1018. ProjectFixture({
  1019. id: '3',
  1020. slug: 'baz',
  1021. isMember: true,
  1022. firstEvent: null,
  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. ProjectFixture({
  1050. id: '1',
  1051. slug: 'foo',
  1052. isMember: true,
  1053. firstEvent: null,
  1054. }),
  1055. ProjectFixture({
  1056. id: '2',
  1057. slug: 'bar',
  1058. isMember: true,
  1059. firstEvent: new Date().toISOString(),
  1060. }),
  1061. ProjectFixture({
  1062. id: '3',
  1063. slug: 'baz',
  1064. isMember: true,
  1065. firstEvent: null,
  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. ProjectFixture({
  1089. id: '1',
  1090. slug: 'foo',
  1091. isMember: true,
  1092. firstEvent: null,
  1093. }),
  1094. ProjectFixture({
  1095. id: '2',
  1096. slug: 'bar',
  1097. isMember: true,
  1098. firstEvent: null,
  1099. }),
  1100. ProjectFixture({
  1101. id: '3',
  1102. slug: 'baz',
  1103. isMember: true,
  1104. firstEvent: null,
  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. ProjectFixture({
  1137. id: '1',
  1138. slug: 'foo',
  1139. isMember: true,
  1140. firstEvent: null,
  1141. }),
  1142. ProjectFixture({
  1143. id: '2',
  1144. slug: 'bar',
  1145. isMember: true,
  1146. firstEvent: new Date().toISOString(),
  1147. }),
  1148. ProjectFixture({
  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. MockApiClient.addMockResponse({
  1181. url: '/organizations/org-slug/issues/',
  1182. body: [...new Array(25)].map((_, i) => ({id: i})),
  1183. headers: {
  1184. Link: DEFAULT_LINKS_HEADER,
  1185. 'X-Hits': '500',
  1186. 'X-Max-Hits': '1000',
  1187. },
  1188. });
  1189. parseLinkHeaderSpy.mockReturnValue({
  1190. next: {
  1191. results: true,
  1192. cursor: '',
  1193. href: '',
  1194. },
  1195. previous: {
  1196. results: false,
  1197. cursor: '',
  1198. href: '',
  1199. },
  1200. });
  1201. props = {
  1202. ...props,
  1203. location: {
  1204. query: {
  1205. cursor: 'some cursor',
  1206. page: 1,
  1207. },
  1208. },
  1209. };
  1210. const {routerContext: newRouterContext} = initializeOrg();
  1211. const {rerender} = render(<IssueListOverview {...props} />, {
  1212. context: newRouterContext,
  1213. });
  1214. expect(screen.getByText(textWithMarkupMatcher('1-25 of 500'))).toBeInTheDocument();
  1215. parseLinkHeaderSpy.mockReturnValue({
  1216. next: {
  1217. results: true,
  1218. cursor: '',
  1219. href: '',
  1220. },
  1221. previous: {
  1222. results: true,
  1223. cursor: '',
  1224. href: '',
  1225. },
  1226. });
  1227. rerender(<IssueListOverview {...props} />);
  1228. expect(screen.getByText(textWithMarkupMatcher('26-50 of 500'))).toBeInTheDocument();
  1229. });
  1230. describe('project low priority queue alert', function () {
  1231. const {routerContext: newRouterContext} = initializeOrg();
  1232. beforeEach(function () {
  1233. act(() => ProjectsStore.reset());
  1234. });
  1235. it('does not render event processing alert', function () {
  1236. act(() => ProjectsStore.loadInitialData([project]));
  1237. render(<IssueListOverview {...props} />, {
  1238. context: newRouterContext,
  1239. });
  1240. expect(screen.queryByText(/event processing/i)).not.toBeInTheDocument();
  1241. });
  1242. describe('renders alert', function () {
  1243. it('for one project', function () {
  1244. act(() =>
  1245. ProjectsStore.loadInitialData([
  1246. {...project, eventProcessing: {symbolicationDegraded: true}},
  1247. ])
  1248. );
  1249. render(<IssueListOverview {...props} />, {
  1250. context: routerContext,
  1251. });
  1252. expect(
  1253. screen.getByText(/Event Processing for this project is currently degraded/i)
  1254. ).toBeInTheDocument();
  1255. });
  1256. it('for multiple projects', function () {
  1257. const projectBar = ProjectFixture({
  1258. id: '3560',
  1259. name: 'Bar Project',
  1260. slug: 'project-slug-bar',
  1261. });
  1262. act(() =>
  1263. ProjectsStore.loadInitialData([
  1264. {
  1265. ...project,
  1266. slug: 'project-slug',
  1267. eventProcessing: {symbolicationDegraded: true},
  1268. },
  1269. {
  1270. ...projectBar,
  1271. slug: 'project-slug-bar',
  1272. eventProcessing: {symbolicationDegraded: true},
  1273. },
  1274. ])
  1275. );
  1276. render(
  1277. <IssueListOverview
  1278. {...props}
  1279. selection={{
  1280. ...props.selection,
  1281. projects: [Number(project.id), Number(projectBar.id)],
  1282. }}
  1283. />,
  1284. {
  1285. context: newRouterContext,
  1286. }
  1287. );
  1288. expect(
  1289. screen.getByText(
  1290. textWithMarkupMatcher(
  1291. 'Event Processing for the project-slug, project-slug-bar projects is currently degraded.'
  1292. )
  1293. )
  1294. ).toBeInTheDocument();
  1295. });
  1296. });
  1297. });
  1298. });