overview.spec.tsx 43 KB

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