PageRenderTime 43ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/ee/spec/frontend/vue_shared/dashboards/store/actions_spec.js

https://gitlab.com/realsatomic/gitlab
JavaScript | 552 lines | 490 code | 62 blank | 0 comment | 8 complexity | e518261f29b7445e32c9e89ed19d01e7 MD5 | raw file
  1. import MockAdapter from 'axios-mock-adapter';
  2. import * as actions from 'ee/vue_shared/dashboards/store/actions';
  3. import createStore from 'ee/vue_shared/dashboards/store/index';
  4. import * as types from 'ee/vue_shared/dashboards/store/mutation_types';
  5. import { mockHeaders, mockText, mockProjectData } from 'ee_jest/vue_shared/dashboards/mock_data';
  6. import testAction from 'helpers/vuex_action_helper';
  7. import createFlash from '~/flash';
  8. import axios from '~/lib/utils/axios_utils';
  9. import clearState from '../helpers';
  10. jest.mock('~/flash');
  11. describe('actions', () => {
  12. const mockAddEndpoint = 'mock-add_endpoint';
  13. const mockListEndpoint = 'mock-list_endpoint';
  14. const mockResponse = { data: 'mock-data' };
  15. const mockProjects = mockProjectData(1);
  16. const [mockOneProject] = mockProjects;
  17. let store;
  18. let mockAxios;
  19. beforeEach(() => {
  20. store = createStore();
  21. mockAxios = new MockAdapter(axios);
  22. });
  23. afterEach(() => {
  24. clearState(store);
  25. mockAxios.restore();
  26. actions.clearProjectsEtagPoll();
  27. });
  28. describe('addProjectsToDashboard', () => {
  29. it('posts selected project ids to project add endpoint', () => {
  30. store.state.projectEndpoints.add = mockAddEndpoint;
  31. store.state.selectedProjects = mockProjects;
  32. mockAxios.onPost(mockAddEndpoint).replyOnce(200, mockResponse);
  33. return testAction(
  34. actions.addProjectsToDashboard,
  35. null,
  36. store.state,
  37. [],
  38. [
  39. {
  40. type: 'receiveAddProjectsToDashboardSuccess',
  41. payload: mockResponse,
  42. },
  43. ],
  44. );
  45. });
  46. it('calls addProjectsToDashboard error handler on error', () => {
  47. mockAxios.onPost(mockAddEndpoint).replyOnce(500);
  48. return testAction(
  49. actions.addProjectsToDashboard,
  50. null,
  51. store.state,
  52. [],
  53. [{ type: 'receiveAddProjectsToDashboardError' }],
  54. );
  55. });
  56. });
  57. describe('toggleSelectedProject', () => {
  58. it(`adds a project to selectedProjects if it doesn't already exist in the list`, () => {
  59. return testAction(
  60. actions.toggleSelectedProject,
  61. mockOneProject,
  62. store.state,
  63. [
  64. {
  65. type: types.ADD_SELECTED_PROJECT,
  66. payload: mockOneProject,
  67. },
  68. ],
  69. [],
  70. );
  71. });
  72. it(`removes a project from selectedProjects if it already exist in the list`, () => {
  73. store.state.selectedProjects = mockProjects;
  74. return testAction(
  75. actions.toggleSelectedProject,
  76. mockOneProject,
  77. store.state,
  78. [
  79. {
  80. type: types.REMOVE_SELECTED_PROJECT,
  81. payload: mockOneProject,
  82. },
  83. ],
  84. [],
  85. );
  86. });
  87. });
  88. describe('receiveAddProjectsToDashboardSuccess', () => {
  89. it('fetches projects when new projects are added to the dashboard', () => {
  90. return testAction(
  91. actions.receiveAddProjectsToDashboardSuccess,
  92. {
  93. added: [1],
  94. invalid: [],
  95. duplicate: [],
  96. },
  97. store.state,
  98. [],
  99. [
  100. {
  101. type: 'forceProjectsRequest',
  102. },
  103. ],
  104. );
  105. });
  106. const errorMessage =
  107. 'This dashboard is available for public projects, and private projects in groups with a Premium plan.';
  108. const selectProjects = (count) => {
  109. for (let i = 0; i < count; i += 1) {
  110. store.dispatch('toggleSelectedProject', {
  111. id: i,
  112. name: 'mock-name',
  113. });
  114. }
  115. };
  116. const addInvalidProjects = (invalid) =>
  117. store.dispatch('receiveAddProjectsToDashboardSuccess', {
  118. added: [],
  119. invalid,
  120. duplicate: [],
  121. });
  122. it('displays an error when user tries to add one invalid project to dashboard', () => {
  123. selectProjects(1);
  124. addInvalidProjects([0]);
  125. expect(createFlash).toHaveBeenCalledWith({
  126. message: `Unable to add mock-name. ${errorMessage}`,
  127. });
  128. });
  129. it('displays an error when user tries to add two invalid projects to dashboard', () => {
  130. selectProjects(2);
  131. addInvalidProjects([0, 1]);
  132. expect(createFlash).toHaveBeenCalledWith({
  133. message: `Unable to add mock-name and mock-name. ${errorMessage}`,
  134. });
  135. });
  136. it('displays an error when user tries to add more than two invalid projects to dashboard', () => {
  137. selectProjects(3);
  138. addInvalidProjects([0, 1, 2]);
  139. expect(createFlash).toHaveBeenCalledWith({
  140. message: `Unable to add mock-name, mock-name, and mock-name. ${errorMessage}`,
  141. });
  142. });
  143. });
  144. describe('receiveAddProjectsToDashboardError', () => {
  145. it('shows error message', () => {
  146. store.dispatch('receiveAddProjectsToDashboardError');
  147. expect(createFlash).toHaveBeenCalledWith({
  148. message: mockText.ADD_PROJECTS_ERROR,
  149. });
  150. });
  151. });
  152. describe('clearSearchResults', () => {
  153. it('clears all project search results', () => {
  154. store.state.projectSearchResults = mockProjects;
  155. return testAction(
  156. actions.clearSearchResults,
  157. null,
  158. store.state,
  159. [
  160. {
  161. type: types.CLEAR_SEARCH_RESULTS,
  162. },
  163. ],
  164. [],
  165. );
  166. });
  167. });
  168. describe('fetchProjects', () => {
  169. const testListEndpoint = ({ page }) => {
  170. store.state.projectEndpoints.list = mockListEndpoint;
  171. mockAxios
  172. .onGet(mockListEndpoint, {
  173. params: {
  174. page,
  175. },
  176. })
  177. .replyOnce(200, { projects: mockProjects }, mockHeaders);
  178. return testAction(
  179. actions.fetchProjects,
  180. page,
  181. store.state,
  182. [
  183. {
  184. type: 'RECEIVE_PROJECTS_SUCCESS',
  185. payload: {
  186. headers: mockHeaders,
  187. projects: mockProjects,
  188. },
  189. },
  190. ],
  191. [{ type: 'requestProjects' }],
  192. );
  193. };
  194. it('calls project list endpoint', () => testListEndpoint({ page: null }));
  195. it('calls paginated project list endpoint', () => testListEndpoint({ page: 2 }));
  196. it('handles response errors', () => {
  197. store.state.projectEndpoints.list = mockListEndpoint;
  198. mockAxios.onGet(mockListEndpoint).replyOnce(500);
  199. return testAction(
  200. actions.fetchProjects,
  201. null,
  202. store.state,
  203. [],
  204. [{ type: 'requestProjects' }, { type: 'receiveProjectsError' }],
  205. );
  206. });
  207. });
  208. describe('requestProjects', () => {
  209. it('toggles project loading state', () => {
  210. return testAction(
  211. actions.requestProjects,
  212. null,
  213. store.state,
  214. [{ type: types.REQUEST_PROJECTS }],
  215. [],
  216. );
  217. });
  218. });
  219. describe('receiveProjectsError', () => {
  220. it('clears projects and alerts user of error', () => {
  221. store.state.projects = mockProjects;
  222. testAction(
  223. actions.receiveProjectsError,
  224. null,
  225. store.state,
  226. [
  227. {
  228. type: types.RECEIVE_PROJECTS_ERROR,
  229. },
  230. ],
  231. [],
  232. );
  233. expect(createFlash).toHaveBeenCalledWith({
  234. message: mockText.RECEIVE_PROJECTS_ERROR,
  235. });
  236. });
  237. });
  238. describe('removeProject', () => {
  239. const mockRemovePath = 'mock-removePath';
  240. it('calls project removal path and fetches projects on success', () => {
  241. mockAxios.onDelete(mockRemovePath).replyOnce(200);
  242. return testAction(
  243. actions.removeProject,
  244. mockRemovePath,
  245. null,
  246. [],
  247. [{ type: 'receiveRemoveProjectSuccess' }],
  248. );
  249. });
  250. it('passes off handling of project removal errors', () => {
  251. mockAxios.onDelete(mockRemovePath).replyOnce(500);
  252. return testAction(
  253. actions.removeProject,
  254. mockRemovePath,
  255. null,
  256. [],
  257. [{ type: 'receiveRemoveProjectError' }],
  258. );
  259. });
  260. });
  261. describe('receiveRemoveProjectSuccess', () => {
  262. it('fetches dashboard projects', () => {
  263. return testAction(
  264. actions.receiveRemoveProjectSuccess,
  265. null,
  266. null,
  267. [],
  268. [{ type: 'forceProjectsRequest' }],
  269. );
  270. });
  271. });
  272. describe('receiveRemoveProjectError', () => {
  273. it('displays project removal error', () => {
  274. return testAction(actions.receiveRemoveProjectError, null, null, [], []).then(() => {
  275. expect(createFlash).toHaveBeenCalledWith({
  276. message: mockText.REMOVE_PROJECT_ERROR,
  277. });
  278. });
  279. });
  280. });
  281. describe('fetchSearchResults', () => {
  282. it('dispatches minimumQueryMessage if the search query is falsy', () => {
  283. const searchQueries = [null, undefined, false, NaN];
  284. return Promise.all(
  285. searchQueries.map((searchQuery) => {
  286. store.state.searchQuery = searchQuery;
  287. return testAction(
  288. actions.fetchSearchResults,
  289. null,
  290. store.state,
  291. [],
  292. [
  293. {
  294. type: 'requestSearchResults',
  295. },
  296. {
  297. type: 'minimumQueryMessage',
  298. },
  299. ],
  300. );
  301. }),
  302. );
  303. });
  304. it('dispatches minimumQueryMessage if the search query was empty', () => {
  305. store.state.searchQuery = '';
  306. return testAction(
  307. actions.fetchSearchResults,
  308. null,
  309. store.state,
  310. [],
  311. [
  312. {
  313. type: 'requestSearchResults',
  314. },
  315. {
  316. type: 'minimumQueryMessage',
  317. },
  318. ],
  319. );
  320. });
  321. it(`dispatches minimumQueryMessage if the search query wasn't long enough`, () => {
  322. store.state.searchQuery = 'a';
  323. return testAction(
  324. actions.fetchSearchResults,
  325. null,
  326. store.state,
  327. [],
  328. [
  329. {
  330. type: 'requestSearchResults',
  331. },
  332. {
  333. type: 'minimumQueryMessage',
  334. },
  335. ],
  336. );
  337. });
  338. it(`dispatches the correct actions when the query is valid`, () => {
  339. mockAxios.onAny().reply(200, mockProjects, mockHeaders);
  340. store.state.searchQuery = 'mock-query';
  341. return testAction(
  342. actions.fetchSearchResults,
  343. null,
  344. store.state,
  345. [],
  346. [
  347. {
  348. type: 'requestSearchResults',
  349. },
  350. {
  351. type: 'receiveSearchResultsSuccess',
  352. payload: { data: mockProjects, headers: mockHeaders },
  353. },
  354. ],
  355. );
  356. });
  357. });
  358. describe('fetchNextPage', () => {
  359. it(`fetches the next page`, () => {
  360. mockAxios.onAny().reply(200, mockProjects, mockHeaders);
  361. store.state.pageInfo = mockHeaders.pageInfo;
  362. return testAction(
  363. actions.fetchNextPage,
  364. null,
  365. store.state,
  366. [],
  367. [
  368. {
  369. type: 'receiveNextPageSuccess',
  370. payload: { data: mockProjects, headers: mockHeaders },
  371. },
  372. ],
  373. );
  374. });
  375. it(`stops fetching if current page is the last page`, () => {
  376. mockAxios.onAny().reply(200, mockProjects, mockHeaders);
  377. store.state.pageInfo.totalPages = 3;
  378. store.state.pageInfo.currentPage = 3;
  379. return testAction(actions.fetchNextPage, mockHeaders, store.state, [], []);
  380. });
  381. });
  382. describe('requestSearchResults', () => {
  383. it(`commits the REQUEST_SEARCH_RESULTS mutation`, () => {
  384. return testAction(
  385. actions.requestSearchResults,
  386. null,
  387. store.state,
  388. [
  389. {
  390. type: types.REQUEST_SEARCH_RESULTS,
  391. },
  392. ],
  393. [],
  394. );
  395. });
  396. });
  397. describe('receiveNextPageSuccess', () => {
  398. it(`commits the RECEIVE_NEXT_PAGE_SUCCESS mutation`, () => {
  399. return testAction(
  400. actions.receiveNextPageSuccess,
  401. mockHeaders,
  402. store.state,
  403. [
  404. {
  405. type: types.RECEIVE_NEXT_PAGE_SUCCESS,
  406. payload: mockHeaders,
  407. },
  408. ],
  409. [],
  410. );
  411. });
  412. });
  413. describe('receiveSearchResultsSuccess', () => {
  414. it('commits the RECEIVE_SEARCH_RESULTS_SUCCESS mutation', () => {
  415. return testAction(
  416. actions.receiveSearchResultsSuccess,
  417. mockProjects,
  418. store.state,
  419. [
  420. {
  421. type: types.RECEIVE_SEARCH_RESULTS_SUCCESS,
  422. payload: mockProjects,
  423. },
  424. ],
  425. [],
  426. );
  427. });
  428. });
  429. describe('receiveSearchResultsError', () => {
  430. it('commits the RECEIVE_SEARCH_RESULTS_ERROR mutation', () => {
  431. return testAction(
  432. actions.receiveSearchResultsError,
  433. ['error'],
  434. store.state,
  435. [
  436. {
  437. type: types.RECEIVE_SEARCH_RESULTS_ERROR,
  438. },
  439. ],
  440. [],
  441. );
  442. });
  443. });
  444. describe('setProjectEndpoints', () => {
  445. it('commits project list and add endpoints', () => {
  446. return testAction(
  447. actions.setProjectEndpoints,
  448. {
  449. add: mockAddEndpoint,
  450. list: mockListEndpoint,
  451. },
  452. store.state,
  453. [
  454. {
  455. type: types.SET_PROJECT_ENDPOINT_LIST,
  456. payload: mockListEndpoint,
  457. },
  458. {
  459. type: types.SET_PROJECT_ENDPOINT_ADD,
  460. payload: mockAddEndpoint,
  461. },
  462. ],
  463. [],
  464. );
  465. });
  466. });
  467. describe('paginateDashboard', () => {
  468. it('fetches a new page of projects', () => {
  469. const newPage = 2;
  470. return testAction(
  471. actions.paginateDashboard,
  472. newPage,
  473. store.state,
  474. [],
  475. [
  476. {
  477. type: 'stopProjectsPolling',
  478. },
  479. {
  480. type: 'clearProjectsEtagPoll',
  481. },
  482. {
  483. type: 'fetchProjects',
  484. payload: newPage,
  485. },
  486. ],
  487. );
  488. });
  489. });
  490. });