overview.spec.tsx 43 KB

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