projects.spec.jsx 17 KB

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