projects.spec.jsx 17 KB

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