projects.spec.jsx 13 KB

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