overview.spec.tsx 43 KB

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