overview.spec.tsx 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473
  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, 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. 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(organization.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. projects: [],
  394. }),
  395. };
  396. const {unmount} = render(<IssueListWithStores {...defaultProps} />, {
  397. router,
  398. organization: defaultProps.organization,
  399. });
  400. expect(
  401. await screen.findByText(textWithMarkupMatcher('1-25 of 500'))
  402. ).toBeInTheDocument();
  403. expect(issuesRequest).toHaveBeenCalledTimes(1);
  404. unmount();
  405. // Mount component again, getting from cache
  406. render(<IssueListWithStores {...defaultProps} />, {
  407. router,
  408. organization: defaultProps.organization,
  409. });
  410. expect(
  411. await screen.findByText(textWithMarkupMatcher('1-25 of 500'))
  412. ).toBeInTheDocument();
  413. expect(issuesRequest).toHaveBeenCalledTimes(1);
  414. });
  415. it('1 search', async function () {
  416. const localSavedSearch = {...savedSearch, projectId: null};
  417. savedSearchesRequest = MockApiClient.addMockResponse({
  418. url: '/organizations/org-slug/searches/',
  419. body: [localSavedSearch],
  420. });
  421. render(<IssueListWithStores {...routerProps} />, {
  422. router,
  423. });
  424. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  425. await userEvent.click(screen.getByRole('button', {name: /custom search/i}));
  426. await userEvent.click(screen.getByRole('button', {name: localSavedSearch.name}));
  427. expect(browserHistory.push).toHaveBeenLastCalledWith(
  428. expect.objectContaining({
  429. pathname: '/organizations/org-slug/issues/searches/789/',
  430. })
  431. );
  432. });
  433. it('clears a saved search when a custom one is entered', async function () {
  434. savedSearchesRequest = MockApiClient.addMockResponse({
  435. url: '/organizations/org-slug/searches/',
  436. body: [
  437. savedSearch,
  438. SearchFixture({
  439. id: '123',
  440. name: 'Pinned search',
  441. isPinned: true,
  442. isGlobal: false,
  443. query: 'is:resolved',
  444. }),
  445. ],
  446. });
  447. render(<IssueListWithStores {...routerProps} />, {
  448. router,
  449. });
  450. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  451. const queryInput = screen.getByDisplayValue('is:resolved');
  452. await userEvent.clear(queryInput);
  453. await userEvent.type(queryInput, 'dogs{enter}');
  454. expect(browserHistory.push).toHaveBeenLastCalledWith(
  455. expect.objectContaining({
  456. pathname: '/organizations/org-slug/issues/',
  457. query: {
  458. environment: [],
  459. project: [],
  460. referrer: 'issue-list',
  461. sort: '',
  462. query: 'dogs',
  463. statsPeriod: '14d',
  464. },
  465. })
  466. );
  467. });
  468. it('pins a custom query', async function () {
  469. const pinnedSearch = {
  470. id: '666',
  471. name: 'My Pinned Search',
  472. query: 'assigned:me level:fatal',
  473. sort: 'date',
  474. isPinned: true,
  475. visibility: SavedSearchVisibility.ORGANIZATION,
  476. };
  477. savedSearchesRequest = MockApiClient.addMockResponse({
  478. url: '/organizations/org-slug/searches/',
  479. body: [savedSearch],
  480. });
  481. const {rerender} = render(<IssueListWithStores {...routerProps} />, {
  482. router,
  483. });
  484. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  485. const queryInput = screen.getByDisplayValue('is:unresolved');
  486. await userEvent.clear(queryInput);
  487. await userEvent.type(queryInput, 'assigned:me level:fatal{enter}');
  488. expect((browserHistory.push as jest.Mock).mock.calls[0][0]).toEqual(
  489. expect.objectContaining({
  490. query: expect.objectContaining({
  491. query: 'assigned:me level:fatal',
  492. }),
  493. })
  494. );
  495. await tick();
  496. const routerWithQuery = {location: {query: {query: 'assigned:me level:fatal'}}};
  497. rerender(<IssueListWithStores {...merge({}, routerProps, routerWithQuery)} />);
  498. expect(screen.getByRole('button', {name: 'Custom Search'})).toBeInTheDocument();
  499. MockApiClient.clearMockResponses();
  500. const createPin = MockApiClient.addMockResponse({
  501. url: '/organizations/org-slug/pinned-searches/',
  502. method: 'PUT',
  503. body: pinnedSearch,
  504. });
  505. MockApiClient.addMockResponse({
  506. url: '/organizations/org-slug/searches/',
  507. body: [savedSearch, pinnedSearch],
  508. });
  509. await userEvent.click(screen.getByLabelText(/Set as Default/i));
  510. await waitFor(() => {
  511. expect(createPin).toHaveBeenCalled();
  512. expect(browserHistory.replace).toHaveBeenLastCalledWith(
  513. expect.objectContaining({
  514. pathname: '/organizations/org-slug/issues/searches/666/',
  515. query: {
  516. referrer: 'search-bar',
  517. },
  518. search: '',
  519. })
  520. );
  521. });
  522. });
  523. it('unpins a custom query', async function () {
  524. const pinnedSearch = SearchFixture({
  525. id: '666',
  526. name: 'My Pinned Search',
  527. query: 'assigned:me level:fatal',
  528. sort: 'date',
  529. isPinned: true,
  530. visibility: SavedSearchVisibility.ORGANIZATION,
  531. });
  532. savedSearchesRequest = MockApiClient.addMockResponse({
  533. url: '/organizations/org-slug/searches/',
  534. body: [pinnedSearch],
  535. });
  536. const deletePin = MockApiClient.addMockResponse({
  537. url: '/organizations/org-slug/pinned-searches/',
  538. method: 'DELETE',
  539. });
  540. const routerWithSavedSearch = {
  541. params: {searchId: pinnedSearch.id},
  542. };
  543. render(<IssueListWithStores {...merge({}, routerProps, routerWithSavedSearch)} />, {
  544. router: routerWithSavedSearch,
  545. });
  546. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  547. expect(screen.getByRole('button', {name: 'My Default Search'})).toBeInTheDocument();
  548. await userEvent.click(screen.getByLabelText(/Remove Default/i));
  549. await waitFor(() => {
  550. expect(deletePin).toHaveBeenCalled();
  551. });
  552. await waitFor(() => {
  553. expect(browserHistory.replace).toHaveBeenLastCalledWith(
  554. expect.objectContaining({
  555. pathname: '/organizations/org-slug/issues/',
  556. })
  557. );
  558. });
  559. });
  560. it('pins a saved query', async function () {
  561. const assignedToMe = SearchFixture({
  562. id: '234',
  563. name: 'Assigned to Me',
  564. isPinned: false,
  565. isGlobal: true,
  566. query: 'assigned:me',
  567. sort: 'date',
  568. type: 0,
  569. });
  570. savedSearchesRequest = MockApiClient.addMockResponse({
  571. url: '/organizations/org-slug/searches/',
  572. body: [savedSearch, assignedToMe],
  573. });
  574. const createPin = MockApiClient.addMockResponse({
  575. url: '/organizations/org-slug/pinned-searches/',
  576. method: 'PUT',
  577. body: {
  578. ...savedSearch,
  579. isPinned: true,
  580. },
  581. });
  582. const routerWithSavedSearch = {params: {searchId: '789'}};
  583. render(<IssueListWithStores {...merge({}, routerProps, routerWithSavedSearch)} />, {
  584. router: routerWithSavedSearch,
  585. });
  586. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  587. expect(screen.getByRole('button', {name: savedSearch.name})).toBeInTheDocument();
  588. await userEvent.click(screen.getByLabelText(/set as default/i));
  589. await waitFor(() => {
  590. expect(createPin).toHaveBeenCalled();
  591. expect(browserHistory.replace).toHaveBeenLastCalledWith(
  592. expect.objectContaining({
  593. pathname: '/organizations/org-slug/issues/searches/789/',
  594. })
  595. );
  596. });
  597. });
  598. it('pinning search should keep project selected', async function () {
  599. savedSearchesRequest = MockApiClient.addMockResponse({
  600. url: '/organizations/org-slug/searches/',
  601. body: [savedSearch],
  602. });
  603. const {router: newRouter} = initializeOrg({
  604. router: {
  605. location: {
  606. query: {
  607. project: ['123'],
  608. environment: ['prod'],
  609. query: 'assigned:me level:fatal',
  610. },
  611. },
  612. },
  613. });
  614. render(
  615. <IssueListWithStores
  616. {...newRouter}
  617. selection={{
  618. projects: [123],
  619. environments: ['prod'],
  620. datetime: {
  621. end: null,
  622. period: null,
  623. start: null,
  624. utc: null,
  625. },
  626. }}
  627. />,
  628. {router: newRouter}
  629. );
  630. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  631. const createPin = MockApiClient.addMockResponse({
  632. url: '/organizations/org-slug/pinned-searches/',
  633. method: 'PUT',
  634. body: {
  635. ...savedSearch,
  636. id: '666',
  637. name: 'My Pinned Search',
  638. query: 'assigned:me level:fatal',
  639. sort: 'date',
  640. isPinned: true,
  641. },
  642. });
  643. await userEvent.click(screen.getByLabelText(/set as default/i));
  644. await waitFor(() => {
  645. expect(createPin).toHaveBeenCalled();
  646. expect(browserHistory.replace).toHaveBeenLastCalledWith(
  647. expect.objectContaining({
  648. pathname: '/organizations/org-slug/issues/searches/666/',
  649. query: expect.objectContaining({
  650. project: ['123'],
  651. environment: ['prod'],
  652. query: 'assigned:me level:fatal',
  653. referrer: 'search-bar',
  654. }),
  655. })
  656. );
  657. });
  658. });
  659. it('unpinning search should keep project selected', async function () {
  660. const localSavedSearch = {
  661. ...savedSearch,
  662. id: '666',
  663. isPinned: true,
  664. query: 'assigned:me level:fatal',
  665. };
  666. savedSearchesRequest = MockApiClient.addMockResponse({
  667. url: '/organizations/org-slug/searches/',
  668. body: [localSavedSearch],
  669. });
  670. const deletePin = MockApiClient.addMockResponse({
  671. url: '/organizations/org-slug/pinned-searches/',
  672. method: 'DELETE',
  673. });
  674. const {router: newRouter} = initializeOrg(
  675. merge({}, router, {
  676. router: {
  677. location: {
  678. query: {
  679. project: ['123'],
  680. environment: ['prod'],
  681. query: 'assigned:me level:fatal',
  682. },
  683. },
  684. params: {searchId: '666'},
  685. },
  686. })
  687. );
  688. render(
  689. <IssueListWithStores
  690. {...newRouter}
  691. selection={{
  692. projects: [123],
  693. environments: ['prod'],
  694. datetime: {
  695. end: null,
  696. period: null,
  697. start: null,
  698. utc: null,
  699. },
  700. }}
  701. savedSearch={localSavedSearch}
  702. />,
  703. {router: newRouter}
  704. );
  705. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  706. await userEvent.click(screen.getByLabelText(/Remove Default/i));
  707. await waitFor(() => {
  708. expect(deletePin).toHaveBeenCalled();
  709. expect(browserHistory.replace).toHaveBeenLastCalledWith(
  710. expect.objectContaining({
  711. pathname: '/organizations/org-slug/issues/',
  712. query: expect.objectContaining({
  713. project: ['123'],
  714. environment: ['prod'],
  715. query: 'assigned:me level:fatal',
  716. referrer: 'search-bar',
  717. }),
  718. })
  719. );
  720. });
  721. });
  722. it('does not allow pagination to "previous" while on first page and resets cursors when navigating back to initial page', async function () {
  723. const {rerender} = render(<IssueListWithStores {...routerProps} />, {
  724. router,
  725. });
  726. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  727. expect(screen.getByRole('button', {name: 'Previous'})).toBeDisabled();
  728. issuesRequest = MockApiClient.addMockResponse({
  729. url: '/organizations/org-slug/issues/',
  730. body: [group],
  731. headers: {
  732. 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"',
  733. },
  734. });
  735. await userEvent.click(screen.getByRole('button', {name: 'Next'}));
  736. let pushArgs = {
  737. pathname: '/organizations/org-slug/issues/',
  738. query: {
  739. cursor: '1443575000:0:0',
  740. page: 1,
  741. environment: [],
  742. project: [],
  743. query: 'is:unresolved',
  744. statsPeriod: '14d',
  745. referrer: 'issue-list',
  746. },
  747. };
  748. await waitFor(() => {
  749. expect(browserHistory.push).toHaveBeenLastCalledWith(pushArgs);
  750. });
  751. rerender(<IssueListWithStores {...merge({}, routerProps, {location: pushArgs})} />);
  752. expect(screen.getByRole('button', {name: 'Previous'})).toBeEnabled();
  753. // Click next again
  754. await userEvent.click(screen.getByRole('button', {name: 'Next'}));
  755. pushArgs = {
  756. pathname: '/organizations/org-slug/issues/',
  757. query: {
  758. cursor: '1443574000:0:0',
  759. page: 2,
  760. environment: [],
  761. project: [],
  762. query: 'is:unresolved',
  763. statsPeriod: '14d',
  764. referrer: 'issue-list',
  765. },
  766. };
  767. await waitFor(() => {
  768. expect(browserHistory.push).toHaveBeenLastCalledWith(pushArgs);
  769. });
  770. rerender(<IssueListWithStores {...merge({}, routerProps, {location: pushArgs})} />);
  771. // Click previous
  772. await userEvent.click(screen.getByRole('button', {name: 'Previous'}));
  773. pushArgs = {
  774. pathname: '/organizations/org-slug/issues/',
  775. query: {
  776. cursor: '1443575000:0:1',
  777. page: 1,
  778. environment: [],
  779. project: [],
  780. query: 'is:unresolved',
  781. statsPeriod: '14d',
  782. referrer: 'issue-list',
  783. },
  784. };
  785. await waitFor(() => {
  786. expect(browserHistory.push).toHaveBeenLastCalledWith(pushArgs);
  787. });
  788. rerender(<IssueListWithStores {...merge({}, routerProps, {location: pushArgs})} />);
  789. // Click previous back to initial page
  790. await userEvent.click(screen.getByRole('button', {name: 'Previous'}));
  791. await waitFor(() => {
  792. // cursor is undefined because "prev" cursor is === initial "next" cursor
  793. expect(browserHistory.push).toHaveBeenLastCalledWith({
  794. pathname: '/organizations/org-slug/issues/',
  795. query: {
  796. cursor: undefined,
  797. environment: [],
  798. page: undefined,
  799. project: [],
  800. query: 'is:unresolved',
  801. statsPeriod: '14d',
  802. referrer: 'issue-list',
  803. },
  804. });
  805. });
  806. });
  807. });
  808. describe('transitionTo', function () {
  809. it('pushes to history when query is updated', async function () {
  810. MockApiClient.addMockResponse({
  811. url: '/organizations/org-slug/issues/',
  812. body: [],
  813. headers: {
  814. Link: DEFAULT_LINKS_HEADER,
  815. },
  816. });
  817. render(<IssueListOverview {...props} />, {
  818. router,
  819. });
  820. const queryInput = screen.getByDisplayValue('is:unresolved');
  821. await userEvent.clear(queryInput);
  822. await userEvent.type(queryInput, 'is:ignored{enter}');
  823. expect(browserHistory.push).toHaveBeenCalledWith({
  824. pathname: '/organizations/org-slug/issues/',
  825. query: {
  826. environment: [],
  827. project: [parseInt(project.id, 10)],
  828. query: 'is:ignored',
  829. statsPeriod: '14d',
  830. referrer: 'issue-list',
  831. },
  832. });
  833. });
  834. });
  835. it('fetches tags and members', async function () {
  836. render(<IssueListOverview {...routerProps} {...props} />, {router});
  837. await waitFor(() => {
  838. expect(fetchTagsRequest).toHaveBeenCalled();
  839. expect(fetchMembersRequest).toHaveBeenCalled();
  840. });
  841. });
  842. describe('componentDidUpdate fetching groups', function () {
  843. let fetchDataMock;
  844. beforeEach(function () {
  845. fetchDataMock = MockApiClient.addMockResponse({
  846. url: '/organizations/org-slug/issues/',
  847. body: [group],
  848. headers: {
  849. Link: DEFAULT_LINKS_HEADER,
  850. },
  851. });
  852. fetchDataMock.mockReset();
  853. });
  854. it('fetches data on selection change', function () {
  855. const {rerender} = render(<IssueListOverview {...routerProps} {...props} />, {
  856. router,
  857. });
  858. rerender(
  859. <IssueListOverview
  860. {...routerProps}
  861. {...props}
  862. selection={{projects: [99], environments: [], datetime: {period: '24h'}}}
  863. />
  864. );
  865. expect(fetchDataMock).toHaveBeenCalled();
  866. });
  867. it('fetches data on savedSearch change', function () {
  868. const {rerender} = render(<IssueListOverview {...routerProps} {...props} />, {
  869. router,
  870. });
  871. rerender(
  872. <IssueListOverview
  873. {...routerProps}
  874. {...props}
  875. savedSearch={{id: '1', query: 'is:resolved'}}
  876. />
  877. );
  878. expect(fetchDataMock).toHaveBeenCalled();
  879. });
  880. it('uses correct statsPeriod when fetching issues list and no datetime given', function () {
  881. const {rerender} = render(<IssueListOverview {...routerProps} {...props} />, {
  882. router,
  883. });
  884. const selection = {projects: [99], environments: [], datetime: {}};
  885. rerender(<IssueListOverview {...routerProps} {...props} selection={selection} />);
  886. expect(fetchDataMock).toHaveBeenLastCalledWith(
  887. '/organizations/org-slug/issues/',
  888. expect.objectContaining({
  889. data: 'collapse=stats&collapse=unhandled&expand=owners&expand=inbox&limit=25&project=99&query=is%3Aunresolved&savedSearch=1&shortIdLookup=1&statsPeriod=14d',
  890. })
  891. );
  892. });
  893. });
  894. describe('componentDidUpdate fetching members', function () {
  895. it('fetches memberlist and tags list on project change', function () {
  896. const {rerender} = render(<IssueListOverview {...routerProps} {...props} />, {
  897. router,
  898. });
  899. // Called during componentDidMount
  900. expect(fetchMembersRequest).toHaveBeenCalledTimes(1);
  901. expect(fetchTagsRequest).toHaveBeenCalledTimes(1);
  902. const selection = {
  903. projects: [99],
  904. environments: [],
  905. datetime: {period: '24h'},
  906. };
  907. rerender(<IssueListOverview {...routerProps} {...props} selection={selection} />);
  908. expect(fetchMembersRequest).toHaveBeenCalledTimes(2);
  909. expect(fetchTagsRequest).toHaveBeenCalledTimes(2);
  910. });
  911. });
  912. describe('render states', function () {
  913. it('displays the loading icon when saved searches are loading', function () {
  914. render(<IssueListOverview {...routerProps} {...props} savedSearchLoading />, {
  915. router,
  916. });
  917. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  918. });
  919. it('displays an error when issues fail to load', async function () {
  920. MockApiClient.addMockResponse({
  921. url: '/organizations/org-slug/issues/',
  922. status: 500,
  923. statusCode: 500,
  924. });
  925. render(<IssueListOverview {...routerProps} {...props} />, {
  926. router,
  927. });
  928. expect(await screen.findByTestId('loading-error')).toBeInTheDocument();
  929. });
  930. it('displays congrats robots animation with only is:unresolved query', async function () {
  931. MockApiClient.addMockResponse({
  932. url: '/organizations/org-slug/issues/',
  933. body: [],
  934. headers: {
  935. Link: DEFAULT_LINKS_HEADER,
  936. },
  937. });
  938. render(<IssueListOverview {...routerProps} {...props} />, {router});
  939. expect(
  940. await screen.findByText(/We couldn't find any issues that matched your filters/i)
  941. ).toBeInTheDocument();
  942. });
  943. it('displays an empty resultset with a non-default query', async function () {
  944. MockApiClient.addMockResponse({
  945. url: '/organizations/org-slug/issues/',
  946. body: [],
  947. headers: {
  948. Link: DEFAULT_LINKS_HEADER,
  949. },
  950. });
  951. render(<IssueListOverview {...routerProps} {...props} />, {router});
  952. await userEvent.type(
  953. screen.getByDisplayValue('is:unresolved'),
  954. ' level:error{enter}'
  955. );
  956. expect(
  957. await screen.findByText(/We couldn't find any issues that matched your filters/i)
  958. ).toBeInTheDocument();
  959. });
  960. });
  961. describe('Error Robot', function () {
  962. const createWrapper = async moreProps => {
  963. MockApiClient.addMockResponse({
  964. url: '/organizations/org-slug/issues/',
  965. body: [],
  966. headers: {
  967. Link: DEFAULT_LINKS_HEADER,
  968. },
  969. });
  970. const defaultProps = {
  971. ...props,
  972. useOrgSavedSearches: true,
  973. selection: {
  974. projects: [],
  975. environments: [],
  976. datetime: {period: '14d'},
  977. },
  978. ...merge({}, routerProps, {
  979. params: {},
  980. location: {query: {query: 'is:unresolved'}, search: 'query=is:unresolved'},
  981. }),
  982. organization: OrganizationFixture({
  983. projects: [],
  984. }),
  985. ...moreProps,
  986. };
  987. render(<IssueListOverview {...defaultProps} />, {router});
  988. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  989. };
  990. it('displays when no projects selected and all projects user is member of, async does not have first event', async function () {
  991. const projects = [
  992. ProjectFixture({
  993. id: '1',
  994. slug: 'foo',
  995. isMember: true,
  996. firstEvent: null,
  997. }),
  998. ProjectFixture({
  999. id: '2',
  1000. slug: 'bar',
  1001. isMember: true,
  1002. firstEvent: null,
  1003. }),
  1004. ProjectFixture({
  1005. id: '3',
  1006. slug: 'baz',
  1007. isMember: true,
  1008. firstEvent: null,
  1009. }),
  1010. ];
  1011. MockApiClient.addMockResponse({
  1012. url: '/organizations/org-slug/sent-first-event/',
  1013. query: {
  1014. is_member: true,
  1015. },
  1016. body: {sentFirstEvent: false},
  1017. });
  1018. MockApiClient.addMockResponse({
  1019. url: '/organizations/org-slug/projects/',
  1020. body: projects,
  1021. });
  1022. MockApiClient.addMockResponse({
  1023. url: '/projects/org-slug/foo/issues/',
  1024. body: [],
  1025. });
  1026. await createWrapper({
  1027. organization: OrganizationFixture({
  1028. projects,
  1029. }),
  1030. });
  1031. expect(await screen.findByTestId('awaiting-events')).toBeInTheDocument();
  1032. });
  1033. it('does not display when no projects selected and any projects have a first event', async function () {
  1034. const projects = [
  1035. ProjectFixture({
  1036. id: '1',
  1037. slug: 'foo',
  1038. isMember: true,
  1039. firstEvent: null,
  1040. }),
  1041. ProjectFixture({
  1042. id: '2',
  1043. slug: 'bar',
  1044. isMember: true,
  1045. firstEvent: new Date().toISOString(),
  1046. }),
  1047. ProjectFixture({
  1048. id: '3',
  1049. slug: 'baz',
  1050. isMember: true,
  1051. firstEvent: null,
  1052. }),
  1053. ];
  1054. MockApiClient.addMockResponse({
  1055. url: '/organizations/org-slug/sent-first-event/',
  1056. query: {
  1057. is_member: true,
  1058. },
  1059. body: {sentFirstEvent: true},
  1060. });
  1061. MockApiClient.addMockResponse({
  1062. url: '/organizations/org-slug/projects/',
  1063. body: projects,
  1064. });
  1065. await createWrapper({
  1066. organization: OrganizationFixture({
  1067. projects,
  1068. }),
  1069. });
  1070. expect(screen.queryByTestId('awaiting-events')).not.toBeInTheDocument();
  1071. });
  1072. it('displays when all selected projects do not have first event', async function () {
  1073. const projects = [
  1074. ProjectFixture({
  1075. id: '1',
  1076. slug: 'foo',
  1077. isMember: true,
  1078. firstEvent: null,
  1079. }),
  1080. ProjectFixture({
  1081. id: '2',
  1082. slug: 'bar',
  1083. isMember: true,
  1084. firstEvent: null,
  1085. }),
  1086. ProjectFixture({
  1087. id: '3',
  1088. slug: 'baz',
  1089. isMember: true,
  1090. firstEvent: null,
  1091. }),
  1092. ];
  1093. MockApiClient.addMockResponse({
  1094. url: '/organizations/org-slug/sent-first-event/',
  1095. query: {
  1096. project: [1, 2],
  1097. },
  1098. body: {sentFirstEvent: false},
  1099. });
  1100. MockApiClient.addMockResponse({
  1101. url: '/organizations/org-slug/projects/',
  1102. body: projects,
  1103. });
  1104. MockApiClient.addMockResponse({
  1105. url: '/projects/org-slug/foo/issues/',
  1106. body: [],
  1107. });
  1108. await createWrapper({
  1109. selection: {
  1110. projects: [1, 2],
  1111. environments: [],
  1112. datetime: {period: '14d'},
  1113. },
  1114. organization: OrganizationFixture({
  1115. projects,
  1116. }),
  1117. });
  1118. expect(await screen.findByTestId('awaiting-events')).toBeInTheDocument();
  1119. });
  1120. it('does not display when any selected projects have first event', async function () {
  1121. const projects = [
  1122. ProjectFixture({
  1123. id: '1',
  1124. slug: 'foo',
  1125. isMember: true,
  1126. firstEvent: null,
  1127. }),
  1128. ProjectFixture({
  1129. id: '2',
  1130. slug: 'bar',
  1131. isMember: true,
  1132. firstEvent: new Date().toISOString(),
  1133. }),
  1134. ProjectFixture({
  1135. id: '3',
  1136. slug: 'baz',
  1137. isMember: true,
  1138. firstEvent: new Date().toISOString(),
  1139. }),
  1140. ];
  1141. MockApiClient.addMockResponse({
  1142. url: '/organizations/org-slug/sent-first-event/',
  1143. query: {
  1144. project: [1, 2],
  1145. },
  1146. body: {sentFirstEvent: true},
  1147. });
  1148. MockApiClient.addMockResponse({
  1149. url: '/organizations/org-slug/projects/',
  1150. body: projects,
  1151. });
  1152. await createWrapper({
  1153. selection: {
  1154. projects: [1, 2],
  1155. environments: [],
  1156. datetime: {period: '14d'},
  1157. },
  1158. organization: OrganizationFixture({
  1159. projects,
  1160. }),
  1161. });
  1162. expect(screen.queryByTestId('awaiting-events')).not.toBeInTheDocument();
  1163. });
  1164. });
  1165. it('displays a count that represents the current page', function () {
  1166. MockApiClient.addMockResponse({
  1167. url: '/organizations/org-slug/issues/',
  1168. body: [...new Array(25)].map((_, i) => ({id: i})),
  1169. headers: {
  1170. Link: DEFAULT_LINKS_HEADER,
  1171. 'X-Hits': '500',
  1172. 'X-Max-Hits': '1000',
  1173. },
  1174. });
  1175. parseLinkHeaderSpy.mockReturnValue({
  1176. next: {
  1177. results: true,
  1178. cursor: '',
  1179. href: '',
  1180. },
  1181. previous: {
  1182. results: false,
  1183. cursor: '',
  1184. href: '',
  1185. },
  1186. });
  1187. props = {
  1188. ...props,
  1189. location: {
  1190. query: {
  1191. cursor: 'some cursor',
  1192. page: 1,
  1193. },
  1194. },
  1195. };
  1196. const {router: newRouter} = initializeOrg();
  1197. const {rerender} = render(<IssueListOverview {...props} />, {
  1198. router: newRouter,
  1199. });
  1200. expect(screen.getByText(textWithMarkupMatcher('1-25 of 500'))).toBeInTheDocument();
  1201. parseLinkHeaderSpy.mockReturnValue({
  1202. next: {
  1203. results: true,
  1204. cursor: '',
  1205. href: '',
  1206. },
  1207. previous: {
  1208. results: true,
  1209. cursor: '',
  1210. href: '',
  1211. },
  1212. });
  1213. rerender(<IssueListOverview {...props} />);
  1214. expect(screen.getByText(textWithMarkupMatcher('26-50 of 500'))).toBeInTheDocument();
  1215. });
  1216. describe('project low trends queue alert', function () {
  1217. const {router: newRouter} = initializeOrg();
  1218. beforeEach(function () {
  1219. act(() => ProjectsStore.reset());
  1220. });
  1221. it('does not render event processing alert', function () {
  1222. act(() => ProjectsStore.loadInitialData([project]));
  1223. render(<IssueListOverview {...props} />, {
  1224. router: newRouter,
  1225. });
  1226. expect(screen.queryByText(/event processing/i)).not.toBeInTheDocument();
  1227. });
  1228. describe('renders alert', function () {
  1229. it('for one project', function () {
  1230. act(() =>
  1231. ProjectsStore.loadInitialData([
  1232. {...project, eventProcessing: {symbolicationDegraded: true}},
  1233. ])
  1234. );
  1235. render(<IssueListOverview {...props} />, {router});
  1236. expect(
  1237. screen.getByText(/Event Processing for this project is currently degraded/i)
  1238. ).toBeInTheDocument();
  1239. });
  1240. it('for multiple projects', function () {
  1241. const projectBar = ProjectFixture({
  1242. id: '3560',
  1243. name: 'Bar Project',
  1244. slug: 'project-slug-bar',
  1245. });
  1246. act(() =>
  1247. ProjectsStore.loadInitialData([
  1248. {
  1249. ...project,
  1250. slug: 'project-slug',
  1251. eventProcessing: {symbolicationDegraded: true},
  1252. },
  1253. {
  1254. ...projectBar,
  1255. slug: 'project-slug-bar',
  1256. eventProcessing: {symbolicationDegraded: true},
  1257. },
  1258. ])
  1259. );
  1260. render(
  1261. <IssueListOverview
  1262. {...props}
  1263. selection={{
  1264. ...props.selection,
  1265. projects: [Number(project.id), Number(projectBar.id)],
  1266. }}
  1267. />,
  1268. {
  1269. router: newRouter,
  1270. }
  1271. );
  1272. expect(
  1273. screen.getByText(
  1274. textWithMarkupMatcher(
  1275. 'Event Processing for the project-slug, project-slug-bar projects is currently degraded.'
  1276. )
  1277. )
  1278. ).toBeInTheDocument();
  1279. });
  1280. });
  1281. });
  1282. });