projects.spec.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. import {ProjectFixture} from 'sentry-fixture/project';
  2. import {TeamFixture} from 'sentry-fixture/team';
  3. import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  4. import ProjectsStore from 'sentry/stores/projectsStore';
  5. import Projects from 'sentry/utils/projects';
  6. describe('utils.projects', function () {
  7. const renderer = jest.fn(() => null);
  8. const createWrapper = (props = {}) =>
  9. render(<Projects orgId="org-slug" children={renderer} {...props} />); // eslint-disable-line
  10. beforeEach(function () {
  11. renderer.mockClear();
  12. MockApiClient.clearMockResponses();
  13. ProjectsStore.reset();
  14. act(() =>
  15. ProjectsStore.loadInitialData([
  16. ProjectFixture({id: '1', slug: 'foo'}),
  17. ProjectFixture({id: '2', slug: 'bar'}),
  18. ])
  19. );
  20. });
  21. describe('with predefined list of slugs', function () {
  22. it('gets projects that are in the ProjectsStore', function () {
  23. createWrapper({slugs: ['foo', 'bar']});
  24. // This is initial state
  25. expect(renderer).toHaveBeenCalledWith(
  26. expect.objectContaining({
  27. fetching: false,
  28. isIncomplete: null,
  29. hasMore: null,
  30. projects: [
  31. expect.objectContaining({
  32. id: '1',
  33. slug: 'foo',
  34. }),
  35. expect.objectContaining({
  36. id: '2',
  37. slug: 'bar',
  38. }),
  39. ],
  40. })
  41. );
  42. });
  43. it('fetches projects from API if not found in store', async function () {
  44. const request = MockApiClient.addMockResponse({
  45. url: '/organizations/org-slug/projects/',
  46. body: [
  47. ProjectFixture({
  48. id: '100',
  49. slug: 'a',
  50. }),
  51. ProjectFixture({
  52. id: '101',
  53. slug: 'b',
  54. }),
  55. ],
  56. });
  57. createWrapper({slugs: ['foo', 'a', 'b']});
  58. // This is initial state
  59. expect(renderer).toHaveBeenCalledWith(
  60. expect.objectContaining({
  61. fetching: true,
  62. isIncomplete: null,
  63. hasMore: null,
  64. projects: [
  65. {slug: 'a'},
  66. {slug: 'b'},
  67. expect.objectContaining({
  68. id: '1',
  69. slug: 'foo',
  70. }),
  71. ],
  72. })
  73. );
  74. await waitFor(() =>
  75. expect(request).toHaveBeenCalledWith(
  76. expect.anything(),
  77. expect.objectContaining({
  78. query: {
  79. query: 'slug:a slug:b',
  80. collapse: ['latestDeploys', 'unusedFeatures'],
  81. },
  82. })
  83. )
  84. );
  85. await waitFor(() =>
  86. expect(renderer).toHaveBeenCalledWith(
  87. expect.objectContaining({
  88. fetching: false,
  89. isIncomplete: false,
  90. hasMore: null,
  91. projects: [
  92. expect.objectContaining({
  93. id: '100',
  94. slug: 'a',
  95. }),
  96. expect.objectContaining({
  97. id: '101',
  98. slug: 'b',
  99. }),
  100. expect.objectContaining({
  101. id: '1',
  102. slug: 'foo',
  103. }),
  104. ],
  105. })
  106. )
  107. );
  108. });
  109. it('only has partial results from API', async function () {
  110. const request = MockApiClient.addMockResponse({
  111. url: '/organizations/org-slug/projects/',
  112. body: [
  113. ProjectFixture({
  114. id: '100',
  115. slug: 'a',
  116. }),
  117. ],
  118. });
  119. createWrapper({slugs: ['foo', 'a', 'b']});
  120. // This is initial state
  121. expect(renderer).toHaveBeenCalledWith(
  122. expect.objectContaining({
  123. fetching: true,
  124. isIncomplete: null,
  125. hasMore: null,
  126. projects: [
  127. {slug: 'a'},
  128. {slug: 'b'},
  129. expect.objectContaining({
  130. id: '1',
  131. slug: 'foo',
  132. }),
  133. ],
  134. })
  135. );
  136. await waitFor(() =>
  137. expect(request).toHaveBeenCalledWith(
  138. expect.anything(),
  139. expect.objectContaining({
  140. query: {
  141. query: 'slug:a slug:b',
  142. collapse: ['latestDeploys', 'unusedFeatures'],
  143. },
  144. })
  145. )
  146. );
  147. await waitFor(() =>
  148. expect(renderer).toHaveBeenCalledWith(
  149. expect.objectContaining({
  150. fetching: false,
  151. isIncomplete: true,
  152. hasMore: null,
  153. projects: [
  154. expect.objectContaining({
  155. id: '100',
  156. slug: 'a',
  157. }),
  158. {
  159. slug: 'b',
  160. },
  161. expect.objectContaining({
  162. id: '1',
  163. slug: 'foo',
  164. }),
  165. ],
  166. })
  167. )
  168. );
  169. });
  170. it('responds to updated projects from the project store', async function () {
  171. createWrapper({slugs: ['foo', 'bar']});
  172. await waitFor(() =>
  173. expect(renderer).toHaveBeenCalledWith(
  174. expect.objectContaining({
  175. fetching: false,
  176. isIncomplete: null,
  177. hasMore: null,
  178. projects: [
  179. expect.objectContaining({
  180. id: '1',
  181. slug: 'foo',
  182. }),
  183. expect.objectContaining({
  184. id: '2',
  185. slug: 'bar',
  186. }),
  187. ],
  188. })
  189. )
  190. );
  191. const newTeam = TeamFixture();
  192. act(() => ProjectsStore.onAddTeam(newTeam, 'foo'));
  193. await waitFor(() =>
  194. expect(renderer).toHaveBeenCalledWith(
  195. expect.objectContaining({
  196. fetching: false,
  197. isIncomplete: null,
  198. hasMore: null,
  199. projects: [
  200. expect.objectContaining({
  201. id: '1',
  202. slug: 'foo',
  203. teams: [newTeam],
  204. }),
  205. expect.objectContaining({
  206. id: '2',
  207. slug: 'bar',
  208. }),
  209. ],
  210. })
  211. )
  212. );
  213. });
  214. });
  215. describe('with predefined list of project ids', function () {
  216. it('gets project ids that are in the ProjectsStore', function () {
  217. createWrapper({projectIds: [1, 2]});
  218. // This is initial state
  219. expect(renderer).toHaveBeenCalledWith(
  220. expect.objectContaining({
  221. fetching: false,
  222. isIncomplete: null,
  223. hasMore: null,
  224. projects: [
  225. expect.objectContaining({
  226. id: '1',
  227. slug: 'foo',
  228. }),
  229. expect.objectContaining({
  230. id: '2',
  231. slug: 'bar',
  232. }),
  233. ],
  234. })
  235. );
  236. });
  237. it('fetches projects from API if ids not found in store', async function () {
  238. const request = MockApiClient.addMockResponse({
  239. url: '/organizations/org-slug/projects/',
  240. body: [
  241. ProjectFixture({
  242. id: '1',
  243. slug: 'foo',
  244. }),
  245. ProjectFixture({
  246. id: '100',
  247. slug: 'a',
  248. }),
  249. ProjectFixture({
  250. id: '101',
  251. slug: 'b',
  252. }),
  253. ],
  254. });
  255. createWrapper({projectIds: [1, 100, 101]});
  256. // This is initial state
  257. expect(renderer).toHaveBeenCalledWith(
  258. expect.objectContaining({
  259. fetching: true,
  260. isIncomplete: null,
  261. hasMore: null,
  262. projects: [],
  263. })
  264. );
  265. await waitFor(() =>
  266. expect(request).toHaveBeenCalledWith(
  267. expect.anything(),
  268. expect.objectContaining({
  269. query: {
  270. collapse: ['latestDeploys', 'unusedFeatures'],
  271. },
  272. })
  273. )
  274. );
  275. await waitFor(() =>
  276. expect(renderer).toHaveBeenCalledWith(
  277. expect.objectContaining({
  278. fetching: false,
  279. isIncomplete: null,
  280. hasMore: false,
  281. projects: [
  282. expect.objectContaining({
  283. id: '1',
  284. slug: 'foo',
  285. }),
  286. expect.objectContaining({
  287. id: '100',
  288. slug: 'a',
  289. }),
  290. expect.objectContaining({
  291. id: '101',
  292. slug: 'b',
  293. }),
  294. ],
  295. })
  296. )
  297. );
  298. });
  299. });
  300. describe('with no pre-defined projects', function () {
  301. let request;
  302. beforeEach(function () {
  303. request = MockApiClient.addMockResponse({
  304. url: '/organizations/org-slug/projects/',
  305. body: [
  306. ProjectFixture({
  307. id: '100',
  308. slug: 'a',
  309. }),
  310. ProjectFixture({
  311. id: '101',
  312. slug: 'b',
  313. }),
  314. ],
  315. headers: {
  316. Link:
  317. '<http://127.0.0.1:8000/api/0/organizations/org-slug/projects/?cursor=1443575731:0:1>; rel="previous"; results="true"; cursor="1443575731:0:1", ' +
  318. '<http://127.0.0.1:8000/api/0/organizations/org-slug/projects/?cursor=1443575731:0:0>; rel="next"; results="true"; cursor="1443575731:0:0',
  319. },
  320. });
  321. act(() => ProjectsStore.loadInitialData([]));
  322. });
  323. it('fetches projects from API', async function () {
  324. createWrapper();
  325. // This is initial state
  326. expect(renderer).toHaveBeenCalledWith(
  327. expect.objectContaining({
  328. fetching: true,
  329. isIncomplete: null,
  330. hasMore: null,
  331. projects: [],
  332. })
  333. );
  334. await waitFor(() =>
  335. expect(request).toHaveBeenCalledWith(
  336. expect.anything(),
  337. expect.objectContaining({
  338. query: {
  339. collapse: ['latestDeploys', 'unusedFeatures'],
  340. },
  341. })
  342. )
  343. );
  344. await waitFor(() =>
  345. expect(renderer).toHaveBeenCalledWith(
  346. expect.objectContaining({
  347. fetching: false,
  348. isIncomplete: null,
  349. hasMore: true,
  350. projects: [
  351. expect.objectContaining({
  352. id: '100',
  353. slug: 'a',
  354. }),
  355. expect.objectContaining({
  356. id: '101',
  357. slug: 'b',
  358. }),
  359. ],
  360. })
  361. )
  362. );
  363. });
  364. it('queries API for more projects and replaces results', async function () {
  365. const myRenderer = jest.fn(({onSearch}) => (
  366. <input onChange={({target}) => onSearch(target.value)} />
  367. ));
  368. createWrapper({children: myRenderer});
  369. // This is initial state
  370. expect(myRenderer).toHaveBeenCalledWith(
  371. expect.objectContaining({
  372. fetching: true,
  373. isIncomplete: null,
  374. hasMore: null,
  375. projects: [],
  376. })
  377. );
  378. request.mockClear();
  379. request = MockApiClient.addMockResponse({
  380. url: '/organizations/org-slug/projects/',
  381. body: [
  382. ProjectFixture({
  383. id: '102',
  384. slug: 'test1',
  385. }),
  386. ProjectFixture({
  387. id: '103',
  388. slug: 'test2',
  389. }),
  390. ],
  391. });
  392. await userEvent.type(screen.getByRole('textbox'), 'test');
  393. expect(request).toHaveBeenCalledWith(
  394. expect.anything(),
  395. expect.objectContaining({
  396. query: {
  397. query: 'test',
  398. collapse: ['latestDeploys', 'unusedFeatures'],
  399. },
  400. })
  401. );
  402. await waitFor(() =>
  403. expect(myRenderer).toHaveBeenLastCalledWith(
  404. expect.objectContaining({
  405. fetching: false,
  406. isIncomplete: null,
  407. hasMore: false,
  408. projects: [
  409. expect.objectContaining({
  410. id: '102',
  411. slug: 'test1',
  412. }),
  413. expect.objectContaining({
  414. id: '103',
  415. slug: 'test2',
  416. }),
  417. ],
  418. })
  419. )
  420. );
  421. });
  422. it('queries API for more projects and appends results', async function () {
  423. const myRenderer = jest.fn(({onSearch}) => (
  424. <input onChange={({target}) => onSearch(target.value, {append: true})} />
  425. ));
  426. createWrapper({children: myRenderer});
  427. request.mockClear();
  428. request = MockApiClient.addMockResponse({
  429. url: '/organizations/org-slug/projects/',
  430. body: [
  431. ProjectFixture({
  432. id: '102',
  433. slug: 'test1',
  434. }),
  435. ProjectFixture({
  436. id: '103',
  437. slug: 'test2',
  438. }),
  439. ],
  440. });
  441. await userEvent.type(screen.getByRole('textbox'), 'test');
  442. expect(request).toHaveBeenCalledWith(
  443. expect.anything(),
  444. expect.objectContaining({
  445. query: {
  446. query: 'test',
  447. collapse: ['latestDeploys', 'unusedFeatures'],
  448. },
  449. })
  450. );
  451. await waitFor(() =>
  452. expect(myRenderer).toHaveBeenLastCalledWith(
  453. expect.objectContaining({
  454. fetching: false,
  455. isIncomplete: null,
  456. hasMore: false,
  457. projects: [
  458. expect.objectContaining({
  459. id: '100',
  460. slug: 'a',
  461. }),
  462. expect.objectContaining({
  463. id: '101',
  464. slug: 'b',
  465. }),
  466. expect.objectContaining({
  467. id: '102',
  468. slug: 'test1',
  469. }),
  470. expect.objectContaining({
  471. id: '103',
  472. slug: 'test2',
  473. }),
  474. ],
  475. })
  476. )
  477. );
  478. // Should not have duplicates
  479. await userEvent.type(screen.getByRole('textbox'), 'test');
  480. await waitFor(() =>
  481. expect(myRenderer).toHaveBeenLastCalledWith(
  482. expect.objectContaining({
  483. projects: [
  484. expect.objectContaining({
  485. id: '100',
  486. slug: 'a',
  487. }),
  488. expect.objectContaining({
  489. id: '101',
  490. slug: 'b',
  491. }),
  492. expect.objectContaining({
  493. id: '102',
  494. slug: 'test1',
  495. }),
  496. expect.objectContaining({
  497. id: '103',
  498. slug: 'test2',
  499. }),
  500. ],
  501. })
  502. )
  503. );
  504. });
  505. });
  506. describe('with all projects prop', function () {
  507. let mockProjects;
  508. let request;
  509. beforeEach(function () {
  510. mockProjects = [
  511. ProjectFixture({
  512. id: '100',
  513. slug: 'a',
  514. }),
  515. ProjectFixture({
  516. id: '101',
  517. slug: 'b',
  518. }),
  519. ProjectFixture({
  520. id: '102',
  521. slug: 'c',
  522. }),
  523. ];
  524. request = MockApiClient.addMockResponse({
  525. url: '/organizations/org-slug/projects/',
  526. body: mockProjects,
  527. });
  528. ProjectsStore.reset();
  529. });
  530. it('can query for a list of all projects and save it to the store', async function () {
  531. const loadInitialData = jest.spyOn(ProjectsStore, 'loadInitialData');
  532. createWrapper({allProjects: true});
  533. // This is initial state
  534. expect(renderer).toHaveBeenCalledWith(
  535. expect.objectContaining({
  536. fetching: true,
  537. isIncomplete: null,
  538. hasMore: null,
  539. projects: [],
  540. })
  541. );
  542. // wait for request to resolve
  543. await waitFor(() =>
  544. expect(request).toHaveBeenCalledWith(
  545. expect.anything(),
  546. expect.objectContaining({
  547. query: {all_projects: 1, collapse: ['latestDeploys', 'unusedFeatures']},
  548. })
  549. )
  550. );
  551. await waitFor(() => {
  552. expect(renderer).toHaveBeenCalledWith(
  553. expect.objectContaining({
  554. fetching: false,
  555. isIncomplete: null,
  556. hasMore: false,
  557. projects: mockProjects,
  558. })
  559. );
  560. });
  561. // expect the store action to be called
  562. expect(loadInitialData).toHaveBeenCalledWith(mockProjects);
  563. loadInitialData.mockRestore();
  564. });
  565. it('does not refetch projects that are already loaded in the store', async function () {
  566. act(() => ProjectsStore.loadInitialData(mockProjects));
  567. const loadInitialData = jest.spyOn(ProjectsStore, 'loadInitialData');
  568. createWrapper({allProjects: true});
  569. await waitFor(() =>
  570. expect(renderer).toHaveBeenCalledWith(
  571. expect.objectContaining({
  572. fetching: false,
  573. isIncomplete: null,
  574. hasMore: false,
  575. projects: mockProjects,
  576. })
  577. )
  578. );
  579. expect(request).not.toHaveBeenCalled();
  580. expect(loadInitialData).not.toHaveBeenCalled();
  581. loadInitialData.mockRestore();
  582. });
  583. it('responds to updated projects from the project store', async function () {
  584. act(() => ProjectsStore.loadInitialData(mockProjects));
  585. createWrapper({allProjects: true});
  586. await waitFor(() =>
  587. expect(renderer).toHaveBeenCalledWith(
  588. expect.objectContaining({
  589. fetching: false,
  590. isIncomplete: null,
  591. hasMore: false,
  592. projects: mockProjects,
  593. })
  594. )
  595. );
  596. const newTeam = TeamFixture();
  597. act(() => ProjectsStore.onAddTeam(newTeam, 'a'));
  598. // Expect new team information to be available
  599. await waitFor(() =>
  600. expect(renderer).toHaveBeenCalledWith(
  601. expect.objectContaining({
  602. fetching: false,
  603. isIncomplete: null,
  604. hasMore: false,
  605. projects: [
  606. expect.objectContaining({
  607. id: '100',
  608. slug: 'a',
  609. teams: [newTeam],
  610. }),
  611. expect.objectContaining({
  612. id: '101',
  613. slug: 'b',
  614. }),
  615. expect.objectContaining({
  616. id: '102',
  617. slug: 'c',
  618. }),
  619. ],
  620. })
  621. )
  622. );
  623. });
  624. });
  625. });