projects.spec.jsx 17 KB

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