diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-07 03:07:51 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-07 03:07:51 +0300 |
commit | 4e375367b78bb44bd00957522cd9fc3e6d403fef (patch) | |
tree | 059b1ce541e4128bf03683407d7b5bbbc2094ed5 /spec/frontend | |
parent | 99ddca0d88f1e4e49d61b1aa9d41b5785528d1dc (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
23 files changed, 4573 insertions, 4 deletions
diff --git a/spec/frontend/ide/components/branches/item_spec.js b/spec/frontend/ide/components/branches/item_spec.js new file mode 100644 index 00000000000..1db3750609b --- /dev/null +++ b/spec/frontend/ide/components/branches/item_spec.js @@ -0,0 +1,56 @@ +import Vue from 'vue'; +import mountCompontent from 'helpers/vue_mount_component_helper'; +import router from '~/ide/ide_router'; +import Item from '~/ide/components/branches/item.vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; +import { projectData } from '../../mock_data'; + +const TEST_BRANCH = { + name: 'master', + committedDate: '2018-01-05T05:50Z', +}; +const TEST_PROJECT_ID = projectData.name_with_namespace; + +describe('IDE branch item', () => { + const Component = Vue.extend(Item); + let vm; + + beforeEach(() => { + vm = mountCompontent(Component, { + item: { ...TEST_BRANCH }, + projectId: TEST_PROJECT_ID, + isActive: false, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders branch name and timeago', () => { + const timeText = getTimeago().format(TEST_BRANCH.committedDate); + + expect(vm.$el.textContent).toContain(TEST_BRANCH.name); + expect(vm.$el.querySelector('time')).toHaveText(timeText); + expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null); + }); + + it('renders link to branch', () => { + const expectedHref = router.resolve(`/project/${TEST_PROJECT_ID}/edit/${TEST_BRANCH.name}`) + .href; + + expect(vm.$el.textContent).toMatch('a'); + expect(vm.$el).toHaveAttr('href', expectedHref); + }); + + it('renders icon if isActive', done => { + vm.isActive = true; + + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.ic-mobile-issue-close')).not.toBe(null); + }) + .then(done) + .catch(done.fail); + }); +}); diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js index 5cfe1c25c6b..d26dfc48ff8 100644 --- a/spec/frontend/ide/components/branches/search_list_spec.js +++ b/spec/frontend/ide/components/branches/search_list_spec.js @@ -1,9 +1,9 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import List from '~/ide/components/branches/search_list.vue'; import Item from '~/ide/components/branches/item.vue'; -import { GlLoadingIcon } from '@gitlab/ui'; import { branches } from '../../mock_data'; const localVue = createLocalVue(); diff --git a/spec/frontend/ide/components/merge_requests/list_spec.js b/spec/frontend/ide/components/merge_requests/list_spec.js index 86a311acad4..76806dcba69 100644 --- a/spec/frontend/ide/components/merge_requests/list_spec.js +++ b/spec/frontend/ide/components/merge_requests/list_spec.js @@ -1,9 +1,9 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; import List from '~/ide/components/merge_requests/list.vue'; import Item from '~/ide/components/merge_requests/item.vue'; import TokenedInput from '~/ide/components/shared/tokened_input.vue'; -import { GlLoadingIcon } from '@gitlab/ui'; import { mergeRequests as mergeRequestsMock } from '../../mock_data'; const localVue = createLocalVue(); diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js new file mode 100644 index 00000000000..6f7228add4e --- /dev/null +++ b/spec/frontend/ide/components/panes/right_spec.js @@ -0,0 +1,89 @@ +import Vue from 'vue'; +import '~/behaviors/markdown/render_gfm'; +import { createStore } from '~/ide/stores'; +import RightPane from '~/ide/components/panes/right.vue'; +import { rightSidebarViews } from '~/ide/constants'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; + +describe('IDE right pane', () => { + let Component; + let vm; + + beforeAll(() => { + Component = Vue.extend(RightPane); + }); + + beforeEach(() => { + const store = createStore(); + + vm = createComponentWithStore(Component, store).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('active', () => { + it('renders merge request button as active', done => { + vm.$store.state.rightPane.isOpen = true; + vm.$store.state.rightPane.currentView = rightSidebarViews.mergeRequestInfo.name; + vm.$store.state.currentMergeRequestId = '123'; + vm.$store.state.currentProjectId = 'gitlab-ce'; + vm.$store.state.currentMergeRequestId = 1; + vm.$store.state.projects['gitlab-ce'] = { + mergeRequests: { + 1: { + iid: 1, + title: 'Testing', + title_html: '<span class="title-html">Testing</span>', + description: 'Description', + description_html: '<p class="description-html">Description HTML</p>', + }, + }, + }; + + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.ide-sidebar-link.active')).not.toBe(null); + expect( + vm.$el.querySelector('.ide-sidebar-link.active').getAttribute('data-original-title'), + ).toBe('Merge Request'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('click', () => { + beforeEach(() => { + jest.spyOn(vm, 'open').mockReturnValue(); + }); + + it('sets view to merge request', done => { + vm.$store.state.currentMergeRequestId = '123'; + + vm.$nextTick(() => { + vm.$el.querySelector('.ide-sidebar-link').click(); + + expect(vm.open).toHaveBeenCalledWith(rightSidebarViews.mergeRequestInfo); + + done(); + }); + }); + }); + + describe('live preview', () => { + it('renders live preview button', done => { + Vue.set(vm.$store.state.entries, 'package.json', { + name: 'package.json', + }); + vm.$store.state.clientsidePreviewEnabled = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('button[aria-label="Live preview"]')).not.toBeNull(); + + done(); + }); + }); + }); +}); diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js index a974139a8f9..91152dffafa 100644 --- a/spec/frontend/ide/components/pipelines/list_spec.js +++ b/spec/frontend/ide/components/pipelines/list_spec.js @@ -1,11 +1,11 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { TEST_HOST } from 'helpers/test_constants'; import List from '~/ide/components/pipelines/list.vue'; import JobsList from '~/ide/components/jobs/list.vue'; import Tab from '~/vue_shared/components/tabs/tab.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import { GlLoadingIcon } from '@gitlab/ui'; -import { TEST_HOST } from 'helpers/test_constants'; import { pipelines } from '../../../../javascripts/ide/mock_data'; const localVue = createLocalVue(); diff --git a/spec/frontend/ide/components/preview/navigator_spec.js b/spec/frontend/ide/components/preview/navigator_spec.js new file mode 100644 index 00000000000..650bb7660a4 --- /dev/null +++ b/spec/frontend/ide/components/preview/navigator_spec.js @@ -0,0 +1,167 @@ +import Vue from 'vue'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'helpers/test_constants'; +import ClientsideNavigator from '~/ide/components/preview/navigator.vue'; + +describe('IDE clientside preview navigator', () => { + let vm; + let Component; + let manager; + + beforeAll(() => { + Component = Vue.extend(ClientsideNavigator); + }); + + beforeEach(() => { + manager = { bundlerURL: TEST_HOST, iframe: { src: '' } }; + + vm = mountComponent(Component, { manager }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders readonly URL bar', () => { + expect(vm.$el.querySelector('input[readonly]').value).toBe('/'); + }); + + it('disables back button when navigationStack is empty', () => { + expect(vm.$el.querySelector('.ide-navigator-btn')).toHaveAttr('disabled'); + expect(vm.$el.querySelector('.ide-navigator-btn').classList).toContain('disabled-content'); + }); + + it('disables forward button when forwardNavigationStack is empty', () => { + vm.forwardNavigationStack = []; + + expect(vm.$el.querySelectorAll('.ide-navigator-btn')[1]).toHaveAttr('disabled'); + expect(vm.$el.querySelectorAll('.ide-navigator-btn')[1].classList).toContain( + 'disabled-content', + ); + }); + + it('calls back method when clicking back button', done => { + vm.navigationStack.push('/test'); + vm.navigationStack.push('/test2'); + jest.spyOn(vm, 'back').mockReturnValue(); + + vm.$nextTick(() => { + vm.$el.querySelector('.ide-navigator-btn').click(); + + expect(vm.back).toHaveBeenCalled(); + + done(); + }); + }); + + it('calls forward method when clicking forward button', done => { + vm.forwardNavigationStack.push('/test'); + jest.spyOn(vm, 'forward').mockReturnValue(); + + vm.$nextTick(() => { + vm.$el.querySelectorAll('.ide-navigator-btn')[1].click(); + + expect(vm.forward).toHaveBeenCalled(); + + done(); + }); + }); + + describe('onUrlChange', () => { + it('updates the path', () => { + vm.onUrlChange({ url: `${TEST_HOST}/url` }); + + expect(vm.path).toBe('/url'); + }); + + it('sets currentBrowsingIndex 0 if not already set', () => { + vm.onUrlChange({ url: `${TEST_HOST}/url` }); + + expect(vm.currentBrowsingIndex).toBe(0); + }); + + it('increases currentBrowsingIndex if path doesnt match', () => { + vm.onUrlChange({ url: `${TEST_HOST}/url` }); + + vm.onUrlChange({ url: `${TEST_HOST}/url2` }); + + expect(vm.currentBrowsingIndex).toBe(1); + }); + + it('does not increase currentBrowsingIndex if path matches', () => { + vm.onUrlChange({ url: `${TEST_HOST}/url` }); + + vm.onUrlChange({ url: `${TEST_HOST}/url` }); + + expect(vm.currentBrowsingIndex).toBe(0); + }); + + it('pushes path into navigation stack', () => { + vm.onUrlChange({ url: `${TEST_HOST}/url` }); + + expect(vm.navigationStack).toEqual(['/url']); + }); + }); + + describe('back', () => { + beforeEach(() => { + vm.path = '/test2'; + vm.currentBrowsingIndex = 1; + vm.navigationStack.push('/test'); + vm.navigationStack.push('/test2'); + + jest.spyOn(vm, 'visitPath').mockReturnValue(); + + vm.back(); + }); + + it('visits the last entry in navigationStack', () => { + expect(vm.visitPath).toHaveBeenCalledWith('/test'); + }); + + it('adds last entry to forwardNavigationStack', () => { + expect(vm.forwardNavigationStack).toEqual(['/test2']); + }); + + it('clears navigation stack if currentBrowsingIndex is 1', () => { + expect(vm.navigationStack).toEqual([]); + }); + + it('sets currentBrowsingIndex to null is currentBrowsingIndex is 1', () => { + expect(vm.currentBrowsingIndex).toBe(null); + }); + }); + + describe('forward', () => { + it('calls visitPath with first entry in forwardNavigationStack', () => { + jest.spyOn(vm, 'visitPath').mockReturnValue(); + + vm.forwardNavigationStack.push('/test'); + vm.forwardNavigationStack.push('/test2'); + + vm.forward(); + + expect(vm.visitPath).toHaveBeenCalledWith('/test'); + }); + }); + + describe('refresh', () => { + it('calls refresh with current path', () => { + jest.spyOn(vm, 'visitPath').mockReturnValue(); + + vm.path = '/test'; + + vm.refresh(); + + expect(vm.visitPath).toHaveBeenCalledWith('/test'); + }); + }); + + describe('visitPath', () => { + it('updates iframe src with passed in path', () => { + vm.visitPath('/testpath'); + + expect(manager.iframe.src).toBe(`${TEST_HOST}/testpath`); + }); + }); +}); diff --git a/spec/frontend/ide/helpers.js b/spec/frontend/ide/helpers.js new file mode 100644 index 00000000000..de839fa99ca --- /dev/null +++ b/spec/frontend/ide/helpers.js @@ -0,0 +1,52 @@ +import * as pathUtils from 'path'; +import { decorateData } from '~/ide/stores/utils'; +import state from '~/ide/stores/state'; +import commitState from '~/ide/stores/modules/commit/state'; +import mergeRequestsState from '~/ide/stores/modules/merge_requests/state'; +import pipelinesState from '~/ide/stores/modules/pipelines/state'; +import branchesState from '~/ide/stores/modules/branches/state'; +import fileTemplatesState from '~/ide/stores/modules/file_templates/state'; +import paneState from '~/ide/stores/modules/pane/state'; + +export const resetStore = store => { + const newState = { + ...state(), + commit: commitState(), + mergeRequests: mergeRequestsState(), + pipelines: pipelinesState(), + branches: branchesState(), + fileTemplates: fileTemplatesState(), + rightPane: paneState(), + }; + store.replaceState(newState); +}; + +export const file = (name = 'name', id = name, type = '', parent = null) => + decorateData({ + id, + type, + icon: 'icon', + url: 'url', + name, + path: parent ? `${parent.path}/${name}` : name, + parentPath: parent ? parent.path : '', + lastCommit: {}, + }); + +export const createEntriesFromPaths = paths => + paths + .map(path => ({ + name: pathUtils.basename(path), + dir: pathUtils.dirname(path), + ext: pathUtils.extname(path), + })) + .reduce((entries, path, idx) => { + const { name } = path; + const parent = path.dir ? entries[path.dir] : null; + const type = path.ext ? 'blob' : 'tree'; + const entry = file(name, (idx + 1).toString(), type, parent); + return { + [entry.path]: entry, + ...entries, + }; + }, {}); diff --git a/spec/frontend/ide/ide_router_spec.js b/spec/frontend/ide/ide_router_spec.js new file mode 100644 index 00000000000..1461b756d13 --- /dev/null +++ b/spec/frontend/ide/ide_router_spec.js @@ -0,0 +1,44 @@ +import router from '~/ide/ide_router'; +import store from '~/ide/stores'; + +describe('IDE router', () => { + const PROJECT_NAMESPACE = 'my-group/sub-group'; + const PROJECT_NAME = 'my-project'; + + afterEach(() => { + router.push('/'); + }); + + afterAll(() => { + // VueRouter leaves this window.history at the "base" url. We need to clean this up. + window.history.replaceState({}, '', '/'); + }); + + [ + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/master/-/src/blob/`, + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/master/-/src/blob`, + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob/-/src/blob`, + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/master/-/src/tree/`, + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/weird:branch/name-123/-/src/tree/`, + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/master/-/src/blob`, + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/master/-/src/edit`, + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/master/-/src/merge_requests/2`, + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/blob/-/src/blob`, + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit/blob/-/src/blob`, + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/2`, + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob`, + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit`, + `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}`, + ].forEach(route => { + it(`finds project path when route is "${route}"`, () => { + jest.spyOn(store, 'dispatch').mockReturnValue(new Promise(() => {})); + + router.push(route); + + expect(store.dispatch).toHaveBeenCalledWith('getProjectData', { + namespace: PROJECT_NAMESPACE, + projectId: PROJECT_NAME, + }); + }); + }); +}); diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js new file mode 100644 index 00000000000..d196f6f79d5 --- /dev/null +++ b/spec/frontend/ide/stores/getters_spec.js @@ -0,0 +1,313 @@ +import * as getters from '~/ide/stores/getters'; +import state from '~/ide/stores/state'; +import { file } from '../helpers'; + +describe('IDE store getters', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('activeFile', () => { + it('returns the current active file', () => { + localState.openFiles.push(file()); + localState.openFiles.push(file('active')); + localState.openFiles[1].active = true; + + expect(getters.activeFile(localState).name).toBe('active'); + }); + + it('returns undefined if no active files are found', () => { + localState.openFiles.push(file()); + localState.openFiles.push(file('active')); + + expect(getters.activeFile(localState)).toBeNull(); + }); + }); + + describe('modifiedFiles', () => { + it('returns a list of modified files', () => { + localState.openFiles.push(file()); + localState.changedFiles.push(file('changed')); + localState.changedFiles[0].changed = true; + + const modifiedFiles = getters.modifiedFiles(localState); + + expect(modifiedFiles.length).toBe(1); + expect(modifiedFiles[0].name).toBe('changed'); + }); + }); + + describe('currentMergeRequest', () => { + it('returns Current Merge Request', () => { + localState.currentProjectId = 'abcproject'; + localState.currentMergeRequestId = 1; + localState.projects.abcproject = { + mergeRequests: { + 1: { + mergeId: 1, + }, + }, + }; + + expect(getters.currentMergeRequest(localState).mergeId).toBe(1); + }); + + it('returns null if no active Merge Request was found', () => { + localState.currentProjectId = 'otherproject'; + + expect(getters.currentMergeRequest(localState)).toBeNull(); + }); + }); + + describe('allBlobs', () => { + beforeEach(() => { + Object.assign(localState.entries, { + index: { + type: 'blob', + name: 'index', + lastOpenedAt: 0, + }, + app: { + type: 'blob', + name: 'blob', + lastOpenedAt: 0, + }, + folder: { + type: 'folder', + name: 'folder', + lastOpenedAt: 0, + }, + }); + }); + + it('returns only blobs', () => { + expect(getters.allBlobs(localState).length).toBe(2); + }); + + it('returns list sorted by lastOpenedAt', () => { + localState.entries.app.lastOpenedAt = new Date().getTime(); + + expect(getters.allBlobs(localState)[0].name).toBe('blob'); + }); + }); + + describe('getChangesInFolder', () => { + it('returns length of changed files for a path', () => { + localState.changedFiles.push( + { + path: 'test/index', + name: 'index', + }, + { + path: 'app/123', + name: '123', + }, + ); + + expect(getters.getChangesInFolder(localState)('test')).toBe(1); + }); + + it('returns length of changed & staged files for a path', () => { + localState.changedFiles.push( + { + path: 'test/index', + name: 'index', + }, + { + path: 'testing/123', + name: '123', + }, + ); + + localState.stagedFiles.push( + { + path: 'test/123', + name: '123', + }, + { + path: 'test/index', + name: 'index', + }, + { + path: 'testing/12345', + name: '12345', + }, + ); + + expect(getters.getChangesInFolder(localState)('test')).toBe(2); + }); + + it('returns length of changed & tempFiles files for a path', () => { + localState.changedFiles.push( + { + path: 'test/index', + name: 'index', + }, + { + path: 'test/newfile', + name: 'newfile', + tempFile: true, + }, + ); + + expect(getters.getChangesInFolder(localState)('test')).toBe(2); + }); + }); + + describe('lastCommit', () => { + it('returns the last commit of the current branch on the current project', () => { + const commitTitle = 'Example commit title'; + const localGetters = { + currentProject: { + name: 'test-project', + }, + currentBranch: { + commit: { + title: commitTitle, + }, + }, + }; + localState.currentBranchId = 'example-branch'; + + expect(getters.lastCommit(localState, localGetters).title).toBe(commitTitle); + }); + }); + + describe('currentBranch', () => { + it('returns current projects branch', () => { + localState.currentProjectId = 'abcproject'; + localState.currentBranchId = 'master'; + localState.projects.abcproject = { + name: 'abcproject', + branches: { + master: { + name: 'master', + }, + }, + }; + const localGetters = { + findBranch: jest.fn(), + }; + getters.currentBranch(localState, localGetters); + + expect(localGetters.findBranch).toHaveBeenCalledWith('abcproject', 'master'); + }); + }); + + describe('findProject', () => { + it('returns the project matching the id', () => { + localState.currentProjectId = 'abcproject'; + localState.projects.abcproject = { + name: 'abcproject', + }; + + expect(getters.findProject(localState)('abcproject').name).toBe('abcproject'); + }); + }); + + describe('findBranch', () => { + let result; + + it('returns the selected branch from a project', () => { + localState.currentProjectId = 'abcproject'; + localState.currentBranchId = 'master'; + localState.projects.abcproject = { + name: 'abcproject', + branches: { + master: { + name: 'master', + }, + }, + }; + const localGetters = { + findProject: () => localState.projects.abcproject, + }; + + result = getters.findBranch(localState, localGetters)('abcproject', 'master'); + + expect(result.name).toBe('master'); + }); + }); + + describe('isOnDefaultBranch', () => { + it('returns false when no project exists', () => { + const localGetters = { + currentProject: undefined, + }; + + expect(getters.isOnDefaultBranch({}, localGetters)).toBeFalsy(); + }); + + it("returns true when project's default branch matches current branch", () => { + const localGetters = { + currentProject: { + default_branch: 'master', + }, + branchName: 'master', + }; + + expect(getters.isOnDefaultBranch({}, localGetters)).toBeTruthy(); + }); + + it("returns false when project's default branch doesn't match current branch", () => { + const localGetters = { + currentProject: { + default_branch: 'master', + }, + branchName: 'feature', + }; + + expect(getters.isOnDefaultBranch({}, localGetters)).toBeFalsy(); + }); + }); + + describe('packageJson', () => { + it('returns package.json entry', () => { + localState.entries['package.json'] = { + name: 'package.json', + }; + + expect(getters.packageJson(localState)).toEqual({ + name: 'package.json', + }); + }); + }); + + describe('canPushToBranch', () => { + it('returns false when no currentBranch exists', () => { + const localGetters = { + currentProject: undefined, + }; + + expect(getters.canPushToBranch({}, localGetters)).toBeFalsy(); + }); + + it('returns true when can_push to currentBranch', () => { + const localGetters = { + currentProject: { + default_branch: 'master', + }, + currentBranch: { + can_push: true, + }, + }; + + expect(getters.canPushToBranch({}, localGetters)).toBeTruthy(); + }); + + it('returns false when !can_push to currentBranch', () => { + const localGetters = { + currentProject: { + default_branch: 'master', + }, + currentBranch: { + can_push: false, + }, + }; + + expect(getters.canPushToBranch({}, localGetters)).toBeFalsy(); + }); + }); +}); diff --git a/spec/frontend/ide/stores/modules/branches/actions_spec.js b/spec/frontend/ide/stores/modules/branches/actions_spec.js new file mode 100644 index 00000000000..2ab4126cccf --- /dev/null +++ b/spec/frontend/ide/stores/modules/branches/actions_spec.js @@ -0,0 +1,163 @@ +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import axios from '~/lib/utils/axios_utils'; +import state from '~/ide/stores/modules/branches/state'; +import * as types from '~/ide/stores/modules/branches/mutation_types'; +import { + requestBranches, + receiveBranchesError, + receiveBranchesSuccess, + fetchBranches, + resetBranches, +} from '~/ide/stores/modules/branches/actions'; +import { branches, projectData } from '../../../mock_data'; + +describe('IDE branches actions', () => { + const TEST_SEARCH = 'foosearch'; + let mockedContext; + let mockedState; + let mock; + + beforeEach(() => { + mockedContext = { + dispatch() {}, + rootState: { currentProjectId: projectData.name_with_namespace }, + rootGetters: { currentProject: projectData }, + state: state(), + }; + + // testAction looks for rootGetters in state, + // so they need to be concatenated here. + mockedState = { + ...mockedContext.state, + ...mockedContext.rootGetters, + ...mockedContext.rootState, + }; + + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('requestBranches', () => { + it('should commit request', done => { + testAction( + requestBranches, + null, + mockedContext.state, + [{ type: types.REQUEST_BRANCHES }], + [], + done, + ); + }); + }); + + describe('receiveBranchesError', () => { + it('should commit error', done => { + testAction( + receiveBranchesError, + { search: TEST_SEARCH }, + mockedContext.state, + [{ type: types.RECEIVE_BRANCHES_ERROR }], + [ + { + type: 'setErrorMessage', + payload: { + text: 'Error loading branches.', + action: expect.any(Function), + actionText: 'Please try again', + actionPayload: { search: TEST_SEARCH }, + }, + }, + ], + done, + ); + }); + }); + + describe('receiveBranchesSuccess', () => { + it('should commit received data', done => { + testAction( + receiveBranchesSuccess, + branches, + mockedContext.state, + [{ type: types.RECEIVE_BRANCHES_SUCCESS, payload: branches }], + [], + done, + ); + }); + }); + + describe('fetchBranches', () => { + beforeEach(() => { + gon.api_version = 'v4'; + }); + + describe('success', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/).replyOnce(200, branches); + }); + + it('calls API with params', () => { + const apiSpy = jest.spyOn(axios, 'get'); + + fetchBranches(mockedContext, { search: TEST_SEARCH }); + + expect(apiSpy).toHaveBeenCalledWith(expect.anything(), { + params: expect.objectContaining({ search: TEST_SEARCH, sort: 'updated_desc' }), + }); + }); + + it('dispatches success with received data', done => { + testAction( + fetchBranches, + { search: TEST_SEARCH }, + mockedState, + [], + [ + { type: 'requestBranches' }, + { type: 'resetBranches' }, + { type: 'receiveBranchesSuccess', payload: branches }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/).replyOnce(500); + }); + + it('dispatches error', done => { + testAction( + fetchBranches, + { search: TEST_SEARCH }, + mockedState, + [], + [ + { type: 'requestBranches' }, + { type: 'resetBranches' }, + { type: 'receiveBranchesError', payload: { search: TEST_SEARCH } }, + ], + done, + ); + }); + }); + + describe('resetBranches', () => { + it('commits reset', done => { + testAction( + resetBranches, + null, + mockedContext.state, + [{ type: types.RESET_BRANCHES }], + [], + done, + ); + }); + }); + }); +}); diff --git a/spec/frontend/ide/stores/modules/branches/mutations_spec.js b/spec/frontend/ide/stores/modules/branches/mutations_spec.js new file mode 100644 index 00000000000..ed8e05bf299 --- /dev/null +++ b/spec/frontend/ide/stores/modules/branches/mutations_spec.js @@ -0,0 +1,51 @@ +import state from '~/ide/stores/modules/branches/state'; +import mutations from '~/ide/stores/modules/branches/mutations'; +import * as types from '~/ide/stores/modules/branches/mutation_types'; +import { branches } from '../../../mock_data'; + +describe('IDE branches mutations', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('REQUEST_BRANCHES', () => { + it('sets loading to true', () => { + mutations[types.REQUEST_BRANCHES](mockedState); + + expect(mockedState.isLoading).toBe(true); + }); + }); + + describe('RECEIVE_BRANCHES_ERROR', () => { + it('sets loading to false', () => { + mutations[types.RECEIVE_BRANCHES_ERROR](mockedState); + + expect(mockedState.isLoading).toBe(false); + }); + }); + + describe('RECEIVE_BRANCHES_SUCCESS', () => { + it('sets branches', () => { + const expectedBranches = branches.map(branch => ({ + name: branch.name, + committedDate: branch.commit.committed_date, + })); + + mutations[types.RECEIVE_BRANCHES_SUCCESS](mockedState, branches); + + expect(mockedState.branches).toEqual(expectedBranches); + }); + }); + + describe('RESET_BRANCHES', () => { + it('clears branches array', () => { + mockedState.branches = ['test']; + + mutations[types.RESET_BRANCHES](mockedState); + + expect(mockedState.branches).toEqual([]); + }); + }); +}); diff --git a/spec/frontend/ide/stores/modules/commit/getters_spec.js b/spec/frontend/ide/stores/modules/commit/getters_spec.js new file mode 100644 index 00000000000..07445c22917 --- /dev/null +++ b/spec/frontend/ide/stores/modules/commit/getters_spec.js @@ -0,0 +1,295 @@ +import commitState from '~/ide/stores/modules/commit/state'; +import * as getters from '~/ide/stores/modules/commit/getters'; +import consts from '~/ide/stores/modules/commit/constants'; + +describe('IDE commit module getters', () => { + let state; + + beforeEach(() => { + state = commitState(); + }); + + describe('discardDraftButtonDisabled', () => { + it('returns true when commitMessage is empty', () => { + expect(getters.discardDraftButtonDisabled(state)).toBeTruthy(); + }); + + it('returns false when commitMessage is not empty & loading is false', () => { + state.commitMessage = 'test'; + state.submitCommitLoading = false; + + expect(getters.discardDraftButtonDisabled(state)).toBeFalsy(); + }); + + it('returns true when commitMessage is not empty & loading is true', () => { + state.commitMessage = 'test'; + state.submitCommitLoading = true; + + expect(getters.discardDraftButtonDisabled(state)).toBeTruthy(); + }); + }); + + describe('placeholderBranchName', () => { + it('includes username, currentBranchId, patch & random number', () => { + gon.current_username = 'username'; + + const branch = getters.placeholderBranchName(state, null, { + currentBranchId: 'testing', + }); + + expect(branch).toMatch(/username-testing-patch-\d{5}$/); + }); + }); + + describe('branchName', () => { + const rootState = { + currentBranchId: 'master', + }; + const localGetters = { + placeholderBranchName: 'placeholder-branch-name', + }; + + beforeEach(() => { + Object.assign(state, { + newBranchName: 'state-newBranchName', + }); + }); + + it('defaults to currentBranchId when not committing to a new branch', () => { + localGetters.isCreatingNewBranch = false; + + expect(getters.branchName(state, localGetters, rootState)).toBe('master'); + }); + + describe('commit to a new branch', () => { + beforeEach(() => { + localGetters.isCreatingNewBranch = true; + }); + + it('uses newBranchName when not empty', () => { + const newBranchName = 'nonempty-branch-name'; + Object.assign(state, { + newBranchName, + }); + + expect(getters.branchName(state, localGetters, rootState)).toBe(newBranchName); + }); + + it('uses placeholderBranchName when state newBranchName is empty', () => { + Object.assign(state, { + newBranchName: '', + }); + + expect(getters.branchName(state, localGetters, rootState)).toBe('placeholder-branch-name'); + }); + }); + }); + + describe('preBuiltCommitMessage', () => { + let rootState = {}; + + beforeEach(() => { + rootState.changedFiles = []; + rootState.stagedFiles = []; + }); + + afterEach(() => { + rootState = {}; + }); + + it('returns commitMessage when set', () => { + state.commitMessage = 'test commit message'; + + expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe('test commit message'); + }); + + ['changedFiles', 'stagedFiles'].forEach(key => { + it('returns commitMessage with updated file', () => { + rootState[key].push({ + path: 'test-file', + }); + + expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe('Update test-file'); + }); + + it('returns commitMessage with updated files', () => { + rootState[key].push( + { + path: 'test-file', + }, + { + path: 'index.js', + }, + ); + + expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe( + 'Update test-file, index.js files', + ); + }); + + it('returns commitMessage with deleted files', () => { + rootState[key].push( + { + path: 'test-file', + deleted: true, + }, + { + path: 'index.js', + }, + ); + + expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe( + 'Update index.js\nDeleted test-file', + ); + }); + }); + }); + + describe('isCreatingNewBranch', () => { + it('returns false if NOT creating a new branch', () => { + state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH; + + expect(getters.isCreatingNewBranch(state)).toBeFalsy(); + }); + + it('returns true if creating a new branch', () => { + state.commitAction = consts.COMMIT_TO_NEW_BRANCH; + + expect(getters.isCreatingNewBranch(state)).toBeTruthy(); + }); + }); + + describe('shouldHideNewMrOption', () => { + let localGetters = {}; + let rootGetters = {}; + + beforeEach(() => { + localGetters = { + isCreatingNewBranch: null, + }; + rootGetters = { + isOnDefaultBranch: null, + hasMergeRequest: null, + canPushToBranch: null, + }; + }); + + describe('NO existing MR for the branch', () => { + beforeEach(() => { + rootGetters.hasMergeRequest = false; + }); + + it('should never hide "New MR" option', () => { + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + }); + }); + + describe('existing MR for the branch', () => { + beforeEach(() => { + rootGetters.hasMergeRequest = true; + }); + + it('should NOT hide "New MR" option if user can NOT push to the current branch', () => { + rootGetters.canPushToBranch = false; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + }); + + it('should hide "New MR" option if user can push to the current branch', () => { + rootGetters.canPushToBranch = true; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeTruthy(); + }); + }); + + describe('user can NOT push the branch', () => { + beforeEach(() => { + rootGetters.canPushToBranch = false; + }); + + it('should never hide "New MR" option', () => { + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + }); + }); + + describe('user can push to the branch', () => { + beforeEach(() => { + rootGetters.canPushToBranch = true; + }); + + it('should NOT hide "New MR" option if there is NO existing MR for the current branch', () => { + rootGetters.hasMergeRequest = false; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + }); + + it('should hide "New MR" option if there is existing MR for the current branch', () => { + rootGetters.hasMergeRequest = true; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeTruthy(); + }); + }); + + describe('default branch', () => { + beforeEach(() => { + rootGetters.isOnDefaultBranch = true; + }); + + describe('committing to the same branch', () => { + beforeEach(() => { + localGetters.isCreatingNewBranch = false; + rootGetters.canPushToBranch = true; + }); + + it('should hide "New MR" when there is an existing MR', () => { + rootGetters.hasMergeRequest = true; + + expect( + getters.shouldHideNewMrOption(state, localGetters, null, rootGetters), + ).toBeTruthy(); + }); + + it('should hide "New MR" when there is no existing MR', () => { + rootGetters.hasMergeRequest = false; + + expect( + getters.shouldHideNewMrOption(state, localGetters, null, rootGetters), + ).toBeTruthy(); + }); + }); + + describe('creating a new branch', () => { + beforeEach(() => { + localGetters.isCreatingNewBranch = true; + }); + + it('should NOT hide "New MR" option no matter existence of an MR or write access', () => { + rootGetters.hasMergeRequest = false; + rootGetters.canPushToBranch = true; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + + rootGetters.hasMergeRequest = true; + rootGetters.canPushToBranch = true; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + + rootGetters.hasMergeRequest = false; + rootGetters.canPushToBranch = false; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + }); + }); + }); + + it('should never hide "New MR" option when creating a new branch', () => { + localGetters.isCreatingNewBranch = true; + + rootGetters.isOnDefaultBranch = false; + rootGetters.hasMergeRequest = true; + rootGetters.canPushToBranch = true; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + }); + }); +}); diff --git a/spec/frontend/ide/stores/modules/file_templates/actions_spec.js b/spec/frontend/ide/stores/modules/file_templates/actions_spec.js new file mode 100644 index 00000000000..6c1fa163a91 --- /dev/null +++ b/spec/frontend/ide/stores/modules/file_templates/actions_spec.js @@ -0,0 +1,346 @@ +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import axios from '~/lib/utils/axios_utils'; +import createState from '~/ide/stores/modules/file_templates/state'; +import * as actions from '~/ide/stores/modules/file_templates/actions'; +import * as types from '~/ide/stores/modules/file_templates/mutation_types'; + +describe('IDE file templates actions', () => { + let state; + let mock; + + beforeEach(() => { + state = createState(); + + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('requestTemplateTypes', () => { + it('commits REQUEST_TEMPLATE_TYPES', done => { + testAction( + actions.requestTemplateTypes, + null, + state, + [{ type: types.REQUEST_TEMPLATE_TYPES }], + [], + done, + ); + }); + }); + + describe('receiveTemplateTypesError', () => { + it('commits RECEIVE_TEMPLATE_TYPES_ERROR and dispatches setErrorMessage', done => { + testAction( + actions.receiveTemplateTypesError, + null, + state, + [{ type: types.RECEIVE_TEMPLATE_TYPES_ERROR }], + [ + { + type: 'setErrorMessage', + payload: { + action: expect.any(Function), + actionText: 'Please try again', + text: 'Error loading template types.', + }, + }, + ], + done, + ); + }); + }); + + describe('receiveTemplateTypesSuccess', () => { + it('commits RECEIVE_TEMPLATE_TYPES_SUCCESS', done => { + testAction( + actions.receiveTemplateTypesSuccess, + 'test', + state, + [{ type: types.RECEIVE_TEMPLATE_TYPES_SUCCESS, payload: 'test' }], + [], + done, + ); + }); + }); + + describe('fetchTemplateTypes', () => { + describe('success', () => { + const pages = [[{ name: 'MIT' }], [{ name: 'Apache' }], [{ name: 'CC' }]]; + + beforeEach(() => { + mock.onGet(/api\/(.*)\/templates\/licenses/).reply(({ params }) => { + const pageNum = params.page; + const page = pages[pageNum - 1]; + const hasNextPage = pageNum < pages.length; + + return [200, page, hasNextPage ? { 'X-NEXT-PAGE': pageNum + 1 } : {}]; + }); + }); + + it('rejects if selectedTemplateType is empty', done => { + const dispatch = jest.fn().mockName('dispatch'); + + actions + .fetchTemplateTypes({ dispatch, state }) + .then(done.fail) + .catch(() => { + expect(dispatch).not.toHaveBeenCalled(); + + done(); + }); + }); + + it('dispatches actions', done => { + state.selectedTemplateType = { key: 'licenses' }; + + testAction( + actions.fetchTemplateTypes, + null, + state, + [], + [ + { type: 'requestTemplateTypes' }, + { type: 'receiveTemplateTypesSuccess', payload: pages[0] }, + { type: 'receiveTemplateTypesSuccess', payload: pages[0].concat(pages[1]) }, + { + type: 'receiveTemplateTypesSuccess', + payload: pages[0].concat(pages[1]).concat(pages[2]), + }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/api\/(.*)\/templates\/licenses/).replyOnce(500); + }); + + it('dispatches actions', done => { + state.selectedTemplateType = { key: 'licenses' }; + + testAction( + actions.fetchTemplateTypes, + null, + state, + [], + [{ type: 'requestTemplateTypes' }, { type: 'receiveTemplateTypesError' }], + done, + ); + }); + }); + }); + + describe('setSelectedTemplateType', () => { + it('commits SET_SELECTED_TEMPLATE_TYPE', () => { + const commit = jest.fn().mockName('commit'); + const options = { + commit, + dispatch() {}, + rootGetters: { activeFile: { name: 'test', prevPath: '' } }, + }; + + actions.setSelectedTemplateType(options, { name: 'test' }); + + expect(commit).toHaveBeenCalledWith(types.SET_SELECTED_TEMPLATE_TYPE, { name: 'test' }); + }); + + it('dispatches discardFileChanges if prevPath matches templates name', () => { + const dispatch = jest.fn().mockName('dispatch'); + const options = { + commit() {}, + + dispatch, + rootGetters: { activeFile: { name: 'test', path: 'test', prevPath: 'test' } }, + }; + + actions.setSelectedTemplateType(options, { name: 'test' }); + + expect(dispatch).toHaveBeenCalledWith('discardFileChanges', 'test', { root: true }); + }); + + it('dispatches renameEntry if file name doesnt match', () => { + const dispatch = jest.fn().mockName('dispatch'); + const options = { + commit() {}, + + dispatch, + rootGetters: { activeFile: { name: 'oldtest', path: 'oldtest', prevPath: '' } }, + }; + + actions.setSelectedTemplateType(options, { name: 'test' }); + + expect(dispatch).toHaveBeenCalledWith( + 'renameEntry', + { path: 'oldtest', name: 'test' }, + { root: true }, + ); + }); + }); + + describe('receiveTemplateError', () => { + it('dispatches setErrorMessage', done => { + testAction( + actions.receiveTemplateError, + 'test', + state, + [], + [ + { + type: 'setErrorMessage', + payload: { + action: expect.any(Function), + actionText: 'Please try again', + text: 'Error loading template.', + actionPayload: 'test', + }, + }, + ], + done, + ); + }); + }); + + describe('fetchTemplate', () => { + describe('success', () => { + beforeEach(() => { + mock + .onGet(/api\/(.*)\/templates\/licenses\/mit/) + .replyOnce(200, { content: 'MIT content' }); + mock + .onGet(/api\/(.*)\/templates\/licenses\/testing/) + .replyOnce(200, { content: 'testing content' }); + }); + + it('dispatches setFileTemplate if template already has content', done => { + const template = { content: 'already has content' }; + + testAction( + actions.fetchTemplate, + template, + state, + [], + [{ type: 'setFileTemplate', payload: template }], + done, + ); + }); + + it('dispatches success', done => { + const template = { key: 'mit' }; + + state.selectedTemplateType = { key: 'licenses' }; + + testAction( + actions.fetchTemplate, + template, + state, + [], + [{ type: 'setFileTemplate', payload: { content: 'MIT content' } }], + done, + ); + }); + + it('dispatches success and uses name key for API call', done => { + const template = { name: 'testing' }; + + state.selectedTemplateType = { key: 'licenses' }; + + testAction( + actions.fetchTemplate, + template, + state, + [], + [{ type: 'setFileTemplate', payload: { content: 'testing content' } }], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/api\/(.*)\/templates\/licenses\/mit/).replyOnce(500); + }); + + it('dispatches error', done => { + const template = { name: 'testing' }; + + state.selectedTemplateType = { key: 'licenses' }; + + testAction( + actions.fetchTemplate, + template, + state, + [], + [{ type: 'receiveTemplateError', payload: template }], + done, + ); + }); + }); + }); + + describe('setFileTemplate', () => { + it('dispatches changeFileContent', () => { + const dispatch = jest.fn().mockName('dispatch'); + const commit = jest.fn().mockName('commit'); + const rootGetters = { activeFile: { path: 'test' } }; + + actions.setFileTemplate({ dispatch, commit, rootGetters }, { content: 'content' }); + + expect(dispatch).toHaveBeenCalledWith( + 'changeFileContent', + { path: 'test', content: 'content' }, + { root: true }, + ); + }); + + it('commits SET_UPDATE_SUCCESS', () => { + const dispatch = jest.fn().mockName('dispatch'); + const commit = jest.fn().mockName('commit'); + const rootGetters = { activeFile: { path: 'test' } }; + + actions.setFileTemplate({ dispatch, commit, rootGetters }, { content: 'content' }); + + expect(commit).toHaveBeenCalledWith('SET_UPDATE_SUCCESS', true); + }); + }); + + describe('undoFileTemplate', () => { + it('dispatches changeFileContent', () => { + const dispatch = jest.fn().mockName('dispatch'); + const commit = jest.fn().mockName('commit'); + const rootGetters = { activeFile: { path: 'test', raw: 'raw content' } }; + + actions.undoFileTemplate({ dispatch, commit, rootGetters }); + + expect(dispatch).toHaveBeenCalledWith( + 'changeFileContent', + { path: 'test', content: 'raw content' }, + { root: true }, + ); + }); + + it('commits SET_UPDATE_SUCCESS', () => { + const dispatch = jest.fn().mockName('dispatch'); + const commit = jest.fn().mockName('commit'); + const rootGetters = { activeFile: { path: 'test', raw: 'raw content' } }; + + actions.undoFileTemplate({ dispatch, commit, rootGetters }); + + expect(commit).toHaveBeenCalledWith('SET_UPDATE_SUCCESS', false); + }); + + it('dispatches discardFileChanges if file has prevPath', () => { + const dispatch = jest.fn().mockName('dispatch'); + const rootGetters = { activeFile: { path: 'test', prevPath: 'newtest', raw: 'raw content' } }; + + actions.undoFileTemplate({ dispatch, commit() {}, rootGetters }); + + expect(dispatch).toHaveBeenCalledWith('discardFileChanges', 'test', { root: true }); + }); + }); +}); diff --git a/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js b/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js new file mode 100644 index 00000000000..ec472ab418f --- /dev/null +++ b/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js @@ -0,0 +1,209 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import state from '~/ide/stores/modules/merge_requests/state'; +import * as types from '~/ide/stores/modules/merge_requests/mutation_types'; +import { + requestMergeRequests, + receiveMergeRequestsError, + receiveMergeRequestsSuccess, + fetchMergeRequests, + resetMergeRequests, +} from '~/ide/stores/modules/merge_requests/actions'; +import { mergeRequests } from '../../../mock_data'; +import testAction from '../../../../helpers/vuex_action_helper'; + +describe('IDE merge requests actions', () => { + let mockedState; + let mockedRootState; + let mock; + + beforeEach(() => { + mockedState = state(); + mockedRootState = { currentProjectId: 7 }; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('requestMergeRequests', () => { + it('should commit request', done => { + testAction( + requestMergeRequests, + null, + mockedState, + [{ type: types.REQUEST_MERGE_REQUESTS }], + [], + done, + ); + }); + }); + + describe('receiveMergeRequestsError', () => { + it('should commit error', done => { + testAction( + receiveMergeRequestsError, + { type: 'created', search: '' }, + mockedState, + [{ type: types.RECEIVE_MERGE_REQUESTS_ERROR }], + [ + { + type: 'setErrorMessage', + payload: { + text: 'Error loading merge requests.', + action: expect.any(Function), + actionText: 'Please try again', + actionPayload: { type: 'created', search: '' }, + }, + }, + ], + done, + ); + }); + }); + + describe('receiveMergeRequestsSuccess', () => { + it('should commit received data', done => { + testAction( + receiveMergeRequestsSuccess, + mergeRequests, + mockedState, + [{ type: types.RECEIVE_MERGE_REQUESTS_SUCCESS, payload: mergeRequests }], + [], + done, + ); + }); + }); + + describe('fetchMergeRequests', () => { + beforeEach(() => { + gon.api_version = 'v4'; + }); + + describe('success', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/merge_requests\/?/).replyOnce(200, mergeRequests); + }); + + it('calls API with params', () => { + const apiSpy = jest.spyOn(axios, 'get'); + fetchMergeRequests( + { + dispatch() {}, + + state: mockedState, + rootState: mockedRootState, + }, + { type: 'created' }, + ); + expect(apiSpy).toHaveBeenCalledWith(expect.anything(), { + params: { scope: 'created-by-me', state: 'opened', search: '' }, + }); + }); + + it('calls API with search', () => { + const apiSpy = jest.spyOn(axios, 'get'); + fetchMergeRequests( + { + dispatch() {}, + + state: mockedState, + rootState: mockedRootState, + }, + { type: 'created', search: 'testing search' }, + ); + expect(apiSpy).toHaveBeenCalledWith(expect.anything(), { + params: { scope: 'created-by-me', state: 'opened', search: 'testing search' }, + }); + }); + + it('dispatches success with received data', done => { + testAction( + fetchMergeRequests, + { type: 'created' }, + mockedState, + [], + [ + { type: 'requestMergeRequests' }, + { type: 'resetMergeRequests' }, + { type: 'receiveMergeRequestsSuccess', payload: mergeRequests }, + ], + done, + ); + }); + }); + + describe('success without type', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/.+\/merge_requests\/?$/).replyOnce(200, mergeRequests); + }); + + it('calls API with project', () => { + const apiSpy = jest.spyOn(axios, 'get'); + fetchMergeRequests( + { + dispatch() {}, + + state: mockedState, + rootState: mockedRootState, + }, + { type: null, search: 'testing search' }, + ); + expect(apiSpy).toHaveBeenCalledWith( + expect.stringMatching(`projects/${mockedRootState.currentProjectId}/merge_requests`), + { params: { state: 'opened', search: 'testing search' } }, + ); + }); + + it('dispatches success with received data', done => { + testAction( + fetchMergeRequests, + { type: null }, + { ...mockedState, ...mockedRootState }, + [], + [ + { type: 'requestMergeRequests' }, + { type: 'resetMergeRequests' }, + { type: 'receiveMergeRequestsSuccess', payload: mergeRequests }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(500); + }); + + it('dispatches error', done => { + testAction( + fetchMergeRequests, + { type: 'created', search: '' }, + mockedState, + [], + [ + { type: 'requestMergeRequests' }, + { type: 'resetMergeRequests' }, + { type: 'receiveMergeRequestsError', payload: { type: 'created', search: '' } }, + ], + done, + ); + }); + }); + }); + + describe('resetMergeRequests', () => { + it('commits reset', done => { + testAction( + resetMergeRequests, + null, + mockedState, + [{ type: types.RESET_MERGE_REQUESTS }], + [], + done, + ); + }); + }); +}); diff --git a/spec/frontend/ide/stores/modules/merge_requests/mutations_spec.js b/spec/frontend/ide/stores/modules/merge_requests/mutations_spec.js new file mode 100644 index 00000000000..d33bda3652d --- /dev/null +++ b/spec/frontend/ide/stores/modules/merge_requests/mutations_spec.js @@ -0,0 +1,56 @@ +import { TEST_HOST } from 'helpers/test_constants'; +import state from '~/ide/stores/modules/merge_requests/state'; +import mutations from '~/ide/stores/modules/merge_requests/mutations'; +import * as types from '~/ide/stores/modules/merge_requests/mutation_types'; +import { mergeRequests } from '../../../mock_data'; + +describe('IDE merge requests mutations', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('REQUEST_MERGE_REQUESTS', () => { + it('sets loading to true', () => { + mutations[types.REQUEST_MERGE_REQUESTS](mockedState); + + expect(mockedState.isLoading).toBe(true); + }); + }); + + describe('RECEIVE_MERGE_REQUESTS_ERROR', () => { + it('sets loading to false', () => { + mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](mockedState); + + expect(mockedState.isLoading).toBe(false); + }); + }); + + describe('RECEIVE_MERGE_REQUESTS_SUCCESS', () => { + it('sets merge requests', () => { + gon.gitlab_url = TEST_HOST; + mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](mockedState, mergeRequests); + + expect(mockedState.mergeRequests).toEqual([ + { + id: 1, + iid: 1, + title: 'Test merge request', + projectId: 1, + projectPathWithNamespace: 'namespace/project-path', + }, + ]); + }); + }); + + describe('RESET_MERGE_REQUESTS', () => { + it('clears merge request array', () => { + mockedState.mergeRequests = ['test']; + + mutations[types.RESET_MERGE_REQUESTS](mockedState); + + expect(mockedState.mergeRequests).toEqual([]); + }); + }); +}); diff --git a/spec/frontend/ide/stores/modules/pane/actions_spec.js b/spec/frontend/ide/stores/modules/pane/actions_spec.js new file mode 100644 index 00000000000..8c0aeaff5b3 --- /dev/null +++ b/spec/frontend/ide/stores/modules/pane/actions_spec.js @@ -0,0 +1,66 @@ +import testAction from 'helpers/vuex_action_helper'; +import * as actions from '~/ide/stores/modules/pane/actions'; +import * as types from '~/ide/stores/modules/pane/mutation_types'; + +describe('IDE pane module actions', () => { + const TEST_VIEW = { name: 'test' }; + const TEST_VIEW_KEEP_ALIVE = { name: 'test-keep-alive', keepAlive: true }; + + describe('toggleOpen', () => { + it('dispatches open if closed', done => { + testAction( + actions.toggleOpen, + TEST_VIEW, + { isOpen: false }, + [], + [{ type: 'open', payload: TEST_VIEW }], + done, + ); + }); + + it('dispatches close if opened', done => { + testAction(actions.toggleOpen, TEST_VIEW, { isOpen: true }, [], [{ type: 'close' }], done); + }); + }); + + describe('open', () => { + it('commits SET_OPEN', done => { + testAction(actions.open, null, {}, [{ type: types.SET_OPEN, payload: true }], [], done); + }); + + it('commits SET_CURRENT_VIEW if view is given', done => { + testAction( + actions.open, + TEST_VIEW, + {}, + [ + { type: types.SET_OPEN, payload: true }, + { type: types.SET_CURRENT_VIEW, payload: TEST_VIEW.name }, + ], + [], + done, + ); + }); + + it('commits KEEP_ALIVE_VIEW if keepAlive is true', done => { + testAction( + actions.open, + TEST_VIEW_KEEP_ALIVE, + {}, + [ + { type: types.SET_OPEN, payload: true }, + { type: types.SET_CURRENT_VIEW, payload: TEST_VIEW_KEEP_ALIVE.name }, + { type: types.KEEP_ALIVE_VIEW, payload: TEST_VIEW_KEEP_ALIVE.name }, + ], + [], + done, + ); + }); + }); + + describe('close', () => { + it('commits SET_OPEN', done => { + testAction(actions.close, null, {}, [{ type: types.SET_OPEN, payload: false }], [], done); + }); + }); +}); diff --git a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js new file mode 100644 index 00000000000..a58c7b8f819 --- /dev/null +++ b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js @@ -0,0 +1,441 @@ +import Visibility from 'visibilityjs'; +import MockAdapter from 'axios-mock-adapter'; +import { TEST_HOST } from 'helpers/test_constants'; +import axios from '~/lib/utils/axios_utils'; +import { + requestLatestPipeline, + receiveLatestPipelineError, + receiveLatestPipelineSuccess, + fetchLatestPipeline, + stopPipelinePolling, + clearEtagPoll, + requestJobs, + receiveJobsError, + receiveJobsSuccess, + fetchJobs, + toggleStageCollapsed, + setDetailJob, + requestJobTrace, + receiveJobTraceError, + receiveJobTraceSuccess, + fetchJobTrace, + resetLatestPipeline, +} from '~/ide/stores/modules/pipelines/actions'; +import state from '~/ide/stores/modules/pipelines/state'; +import * as types from '~/ide/stores/modules/pipelines/mutation_types'; +import { rightSidebarViews } from '~/ide/constants'; +import testAction from '../../../../helpers/vuex_action_helper'; +import { pipelines, jobs } from '../../../mock_data'; + +describe('IDE pipelines actions', () => { + let mockedState; + let mock; + + beforeEach(() => { + mockedState = state(); + mock = new MockAdapter(axios); + + gon.api_version = 'v4'; + mockedState.currentProjectId = 'test/project'; + }); + + afterEach(() => { + mock.restore(); + }); + + describe('requestLatestPipeline', () => { + it('commits request', done => { + testAction( + requestLatestPipeline, + null, + mockedState, + [{ type: types.REQUEST_LATEST_PIPELINE }], + [], + done, + ); + }); + }); + + describe('receiveLatestPipelineError', () => { + it('commits error', done => { + testAction( + receiveLatestPipelineError, + { status: 404 }, + mockedState, + [{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }], + [{ type: 'stopPipelinePolling' }], + done, + ); + }); + + it('dispatches setErrorMessage is not 404', done => { + testAction( + receiveLatestPipelineError, + { status: 500 }, + mockedState, + [{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }], + [ + { + type: 'setErrorMessage', + payload: { + text: 'An error occurred whilst fetching the latest pipeline.', + action: expect.any(Function), + actionText: 'Please try again', + actionPayload: null, + }, + }, + { type: 'stopPipelinePolling' }, + ], + done, + ); + }); + }); + + describe('receiveLatestPipelineSuccess', () => { + const rootGetters = { lastCommit: { id: '123' } }; + let commit; + + beforeEach(() => { + commit = jest.fn().mockName('commit'); + }); + + it('commits pipeline', () => { + receiveLatestPipelineSuccess({ rootGetters, commit }, { pipelines }); + expect(commit).toHaveBeenCalledWith(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipelines[0]); + }); + + it('commits false when there are no pipelines', () => { + receiveLatestPipelineSuccess({ rootGetters, commit }, { pipelines: [] }); + expect(commit).toHaveBeenCalledWith(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, false); + }); + }); + + describe('fetchLatestPipeline', () => { + beforeEach(() => {}); + + afterEach(() => { + stopPipelinePolling(); + clearEtagPoll(); + }); + + describe('success', () => { + beforeEach(() => { + mock + .onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines') + .reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' }); + }); + + it('dispatches request', done => { + jest.spyOn(axios, 'get'); + jest.spyOn(Visibility, 'hidden').mockReturnValue(false); + + const dispatch = jest.fn().mockName('dispatch'); + const rootGetters = { + lastCommit: { id: 'abc123def456ghi789jkl' }, + currentProject: { path_with_namespace: 'abc/def' }, + }; + + fetchLatestPipeline({ dispatch, rootGetters }); + + expect(dispatch).toHaveBeenCalledWith('requestLatestPipeline'); + + jest.advanceTimersByTime(1000); + + new Promise(resolve => requestAnimationFrame(resolve)) + .then(() => { + expect(axios.get).toHaveBeenCalled(); + expect(axios.get).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenCalledWith( + 'receiveLatestPipelineSuccess', + expect.anything(), + ); + + jest.advanceTimersByTime(10000); + }) + .then(() => new Promise(resolve => requestAnimationFrame(resolve))) + .then(() => { + expect(axios.get).toHaveBeenCalled(); + expect(axios.get).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenCalledWith( + 'receiveLatestPipelineSuccess', + expect.anything(), + ); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines').reply(500); + }); + + it('dispatches error', done => { + const dispatch = jest.fn().mockName('dispatch'); + const rootGetters = { + lastCommit: { id: 'abc123def456ghi789jkl' }, + currentProject: { path_with_namespace: 'abc/def' }, + }; + + fetchLatestPipeline({ dispatch, rootGetters }); + + jest.advanceTimersByTime(1500); + + new Promise(resolve => requestAnimationFrame(resolve)) + .then(() => { + expect(dispatch).toHaveBeenCalledWith('receiveLatestPipelineError', expect.anything()); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('requestJobs', () => { + it('commits request', done => { + testAction(requestJobs, 1, mockedState, [{ type: types.REQUEST_JOBS, payload: 1 }], [], done); + }); + }); + + describe('receiveJobsError', () => { + it('commits error', done => { + testAction( + receiveJobsError, + { id: 1 }, + mockedState, + [{ type: types.RECEIVE_JOBS_ERROR, payload: 1 }], + [ + { + type: 'setErrorMessage', + payload: { + text: 'An error occurred whilst loading the pipelines jobs.', + action: expect.anything(), + actionText: 'Please try again', + actionPayload: { id: 1 }, + }, + }, + ], + done, + ); + }); + }); + + describe('receiveJobsSuccess', () => { + it('commits data', done => { + testAction( + receiveJobsSuccess, + { id: 1, data: jobs }, + mockedState, + [{ type: types.RECEIVE_JOBS_SUCCESS, payload: { id: 1, data: jobs } }], + [], + done, + ); + }); + }); + + describe('fetchJobs', () => { + const stage = { id: 1, dropdownPath: `${TEST_HOST}/jobs` }; + + describe('success', () => { + beforeEach(() => { + mock.onGet(stage.dropdownPath).replyOnce(200, jobs); + }); + + it('dispatches request', done => { + testAction( + fetchJobs, + stage, + mockedState, + [], + [ + { type: 'requestJobs', payload: stage.id }, + { type: 'receiveJobsSuccess', payload: { id: stage.id, data: jobs } }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(stage.dropdownPath).replyOnce(500); + }); + + it('dispatches error', done => { + testAction( + fetchJobs, + stage, + mockedState, + [], + [ + { type: 'requestJobs', payload: stage.id }, + { type: 'receiveJobsError', payload: stage }, + ], + done, + ); + }); + }); + }); + + describe('toggleStageCollapsed', () => { + it('commits collapse', done => { + testAction( + toggleStageCollapsed, + 1, + mockedState, + [{ type: types.TOGGLE_STAGE_COLLAPSE, payload: 1 }], + [], + done, + ); + }); + }); + + describe('setDetailJob', () => { + it('commits job', done => { + testAction( + setDetailJob, + 'job', + mockedState, + [{ type: types.SET_DETAIL_JOB, payload: 'job' }], + [{ type: 'rightPane/open', payload: rightSidebarViews.jobsDetail }], + done, + ); + }); + + it('dispatches rightPane/open as pipeline when job is null', done => { + testAction( + setDetailJob, + null, + mockedState, + [{ type: types.SET_DETAIL_JOB, payload: null }], + [{ type: 'rightPane/open', payload: rightSidebarViews.pipelines }], + done, + ); + }); + + it('dispatches rightPane/open as job', done => { + testAction( + setDetailJob, + 'job', + mockedState, + [{ type: types.SET_DETAIL_JOB, payload: 'job' }], + [{ type: 'rightPane/open', payload: rightSidebarViews.jobsDetail }], + done, + ); + }); + }); + + describe('requestJobTrace', () => { + it('commits request', done => { + testAction(requestJobTrace, null, mockedState, [{ type: types.REQUEST_JOB_TRACE }], [], done); + }); + }); + + describe('receiveJobTraceError', () => { + it('commits error', done => { + testAction( + receiveJobTraceError, + null, + mockedState, + [{ type: types.RECEIVE_JOB_TRACE_ERROR }], + [ + { + type: 'setErrorMessage', + payload: { + text: 'An error occurred whilst fetching the job trace.', + action: expect.any(Function), + actionText: 'Please try again', + actionPayload: null, + }, + }, + ], + done, + ); + }); + }); + + describe('receiveJobTraceSuccess', () => { + it('commits data', done => { + testAction( + receiveJobTraceSuccess, + 'data', + mockedState, + [{ type: types.RECEIVE_JOB_TRACE_SUCCESS, payload: 'data' }], + [], + done, + ); + }); + }); + + describe('fetchJobTrace', () => { + beforeEach(() => { + mockedState.detailJob = { path: `${TEST_HOST}/project/builds` }; + }); + + describe('success', () => { + beforeEach(() => { + jest.spyOn(axios, 'get'); + mock.onGet(`${TEST_HOST}/project/builds/trace`).replyOnce(200, { html: 'html' }); + }); + + it('dispatches request', done => { + testAction( + fetchJobTrace, + null, + mockedState, + [], + [ + { type: 'requestJobTrace' }, + { type: 'receiveJobTraceSuccess', payload: { html: 'html' } }, + ], + done, + ); + }); + + it('sends get request to correct URL', () => { + fetchJobTrace({ + state: mockedState, + + dispatch() {}, + }); + expect(axios.get).toHaveBeenCalledWith(`${TEST_HOST}/project/builds/trace`, { + params: { format: 'json' }, + }); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/project/builds/trace`).replyOnce(500); + }); + + it('dispatches error', done => { + testAction( + fetchJobTrace, + null, + mockedState, + [], + [{ type: 'requestJobTrace' }, { type: 'receiveJobTraceError' }], + done, + ); + }); + }); + }); + + describe('resetLatestPipeline', () => { + it('commits reset mutations', done => { + testAction( + resetLatestPipeline, + null, + mockedState, + [ + { type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: null }, + { type: types.SET_DETAIL_JOB, payload: null }, + ], + [], + done, + ); + }); + }); +}); diff --git a/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js b/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js new file mode 100644 index 00000000000..3b7f92cfa74 --- /dev/null +++ b/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js @@ -0,0 +1,213 @@ +import mutations from '~/ide/stores/modules/pipelines/mutations'; +import state from '~/ide/stores/modules/pipelines/state'; +import * as types from '~/ide/stores/modules/pipelines/mutation_types'; +import { fullPipelinesResponse, stages, jobs } from '../../../mock_data'; + +describe('IDE pipelines mutations', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('REQUEST_LATEST_PIPELINE', () => { + it('sets loading to true', () => { + mutations[types.REQUEST_LATEST_PIPELINE](mockedState); + + expect(mockedState.isLoadingPipeline).toBe(true); + }); + }); + + describe('RECEIVE_LASTEST_PIPELINE_ERROR', () => { + it('sets loading to false', () => { + mutations[types.RECEIVE_LASTEST_PIPELINE_ERROR](mockedState); + + expect(mockedState.isLoadingPipeline).toBe(false); + }); + }); + + describe('RECEIVE_LASTEST_PIPELINE_SUCCESS', () => { + const itSetsPipelineLoadingStates = () => { + it('sets has loaded to true', () => { + expect(mockedState.hasLoadedPipeline).toBe(true); + }); + + it('sets loading to false on success', () => { + expect(mockedState.isLoadingPipeline).toBe(false); + }); + }; + + describe('with pipeline', () => { + beforeEach(() => { + mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS]( + mockedState, + fullPipelinesResponse.data.pipelines[0], + ); + }); + + itSetsPipelineLoadingStates(); + + it('sets latestPipeline', () => { + expect(mockedState.latestPipeline).toEqual({ + id: '51', + path: 'test', + commit: { id: '123' }, + details: { status: expect.any(Object) }, + yamlError: undefined, + }); + }); + + it('sets stages', () => { + expect(mockedState.stages.length).toBe(2); + expect(mockedState.stages).toEqual([ + { + id: 0, + dropdownPath: stages[0].dropdown_path, + name: stages[0].name, + status: stages[0].status, + isCollapsed: false, + isLoading: false, + jobs: [], + }, + { + id: 1, + dropdownPath: stages[1].dropdown_path, + name: stages[1].name, + status: stages[1].status, + isCollapsed: false, + isLoading: false, + jobs: [], + }, + ]); + }); + }); + + describe('with null', () => { + beforeEach(() => { + mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null); + }); + + itSetsPipelineLoadingStates(); + + it('does not set latest pipeline if pipeline is null', () => { + expect(mockedState.latestPipeline).toEqual(null); + }); + }); + }); + + describe('REQUEST_JOBS', () => { + beforeEach(() => { + mockedState.stages = stages.map((stage, i) => ({ ...stage, id: i })); + }); + + it('sets isLoading on stage', () => { + mutations[types.REQUEST_JOBS](mockedState, mockedState.stages[0].id); + + expect(mockedState.stages[0].isLoading).toBe(true); + }); + }); + + describe('RECEIVE_JOBS_ERROR', () => { + beforeEach(() => { + mockedState.stages = stages.map((stage, i) => ({ ...stage, id: i })); + }); + + it('sets isLoading on stage after error', () => { + mutations[types.RECEIVE_JOBS_ERROR](mockedState, mockedState.stages[0].id); + + expect(mockedState.stages[0].isLoading).toBe(false); + }); + }); + + describe('RECEIVE_JOBS_SUCCESS', () => { + let data; + + beforeEach(() => { + mockedState.stages = stages.map((stage, i) => ({ ...stage, id: i })); + + data = { latest_statuses: [...jobs] }; + }); + + it('updates loading', () => { + mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, { id: mockedState.stages[0].id, data }); + expect(mockedState.stages[0].isLoading).toBe(false); + }); + + it('sets jobs on stage', () => { + mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, { id: mockedState.stages[0].id, data }); + expect(mockedState.stages[0].jobs.length).toBe(jobs.length); + expect(mockedState.stages[0].jobs).toEqual( + jobs.map(job => ({ + id: job.id, + name: job.name, + status: job.status, + path: job.build_path, + rawPath: `${job.build_path}/raw`, + started: job.started, + isLoading: false, + output: '', + })), + ); + }); + }); + + describe('TOGGLE_STAGE_COLLAPSE', () => { + beforeEach(() => { + mockedState.stages = stages.map((stage, i) => ({ ...stage, id: i, isCollapsed: false })); + }); + + it('toggles collapsed state', () => { + mutations[types.TOGGLE_STAGE_COLLAPSE](mockedState, mockedState.stages[0].id); + + expect(mockedState.stages[0].isCollapsed).toBe(true); + + mutations[types.TOGGLE_STAGE_COLLAPSE](mockedState, mockedState.stages[0].id); + + expect(mockedState.stages[0].isCollapsed).toBe(false); + }); + }); + + describe('SET_DETAIL_JOB', () => { + it('sets detail job', () => { + mutations[types.SET_DETAIL_JOB](mockedState, jobs[0]); + + expect(mockedState.detailJob).toEqual(jobs[0]); + }); + }); + + describe('REQUEST_JOB_TRACE', () => { + beforeEach(() => { + mockedState.detailJob = { ...jobs[0] }; + }); + + it('sets loading on detail job', () => { + mutations[types.REQUEST_JOB_TRACE](mockedState); + + expect(mockedState.detailJob.isLoading).toBe(true); + }); + }); + + describe('RECEIVE_JOB_TRACE_ERROR', () => { + beforeEach(() => { + mockedState.detailJob = { ...jobs[0], isLoading: true }; + }); + + it('sets loading to false on detail job', () => { + mutations[types.RECEIVE_JOB_TRACE_ERROR](mockedState); + + expect(mockedState.detailJob.isLoading).toBe(false); + }); + }); + + describe('RECEIVE_JOB_TRACE_SUCCESS', () => { + beforeEach(() => { + mockedState.detailJob = { ...jobs[0], isLoading: true }; + }); + + it('sets output on detail job', () => { + mutations[types.RECEIVE_JOB_TRACE_SUCCESS](mockedState, { html: 'html' }); + expect(mockedState.detailJob.output).toBe('html'); + expect(mockedState.detailJob.isLoading).toBe(false); + }); + }); +}); diff --git a/spec/frontend/ide/stores/mutations/file_spec.js b/spec/frontend/ide/stores/mutations/file_spec.js new file mode 100644 index 00000000000..91506c1b46c --- /dev/null +++ b/spec/frontend/ide/stores/mutations/file_spec.js @@ -0,0 +1,444 @@ +import mutations from '~/ide/stores/mutations/file'; +import state from '~/ide/stores/state'; +import { FILE_VIEW_MODE_PREVIEW } from '~/ide/constants'; +import { file } from '../../helpers'; + +describe('IDE store file mutations', () => { + let localState; + let localFile; + + beforeEach(() => { + localState = state(); + localFile = { ...file(), type: 'blob' }; + + localState.entries[localFile.path] = localFile; + }); + + describe('SET_FILE_ACTIVE', () => { + it('sets the file active', () => { + mutations.SET_FILE_ACTIVE(localState, { + path: localFile.path, + active: true, + }); + + expect(localFile.active).toBeTruthy(); + }); + + it('sets pending tab as not active', () => { + localState.openFiles.push({ ...localFile, pending: true, active: true }); + + mutations.SET_FILE_ACTIVE(localState, { + path: localFile.path, + active: true, + }); + + expect(localState.openFiles[0].active).toBe(false); + }); + }); + + describe('TOGGLE_FILE_OPEN', () => { + beforeEach(() => { + mutations.TOGGLE_FILE_OPEN(localState, localFile.path); + }); + + it('adds into opened files', () => { + expect(localFile.opened).toBeTruthy(); + expect(localState.openFiles.length).toBe(1); + }); + + it('removes from opened files', () => { + mutations.TOGGLE_FILE_OPEN(localState, localFile.path); + + expect(localFile.opened).toBeFalsy(); + expect(localState.openFiles.length).toBe(0); + }); + }); + + describe('SET_FILE_DATA', () => { + it('sets extra file data', () => { + mutations.SET_FILE_DATA(localState, { + data: { + blame_path: 'blame', + commits_path: 'commits', + permalink: 'permalink', + raw_path: 'raw', + binary: true, + render_error: 'render_error', + }, + file: localFile, + }); + + expect(localFile.blamePath).toBe('blame'); + expect(localFile.commitsPath).toBe('commits'); + expect(localFile.permalink).toBe('permalink'); + expect(localFile.rawPath).toBe('raw'); + expect(localFile.binary).toBeTruthy(); + expect(localFile.renderError).toBe('render_error'); + expect(localFile.raw).toBeNull(); + expect(localFile.baseRaw).toBeNull(); + }); + + it('sets extra file data to all arrays concerned', () => { + localState.stagedFiles = [localFile]; + localState.changedFiles = [localFile]; + localState.openFiles = [localFile]; + + const rawPath = 'foo/bar/blah.md'; + + mutations.SET_FILE_DATA(localState, { + data: { + raw_path: rawPath, + }, + file: localFile, + }); + + expect(localState.stagedFiles[0].rawPath).toEqual(rawPath); + expect(localState.changedFiles[0].rawPath).toEqual(rawPath); + expect(localState.openFiles[0].rawPath).toEqual(rawPath); + expect(localFile.rawPath).toEqual(rawPath); + }); + + it('does not mutate certain props on the file', () => { + const path = 'New Path'; + const name = 'New Name'; + localFile.path = path; + localFile.name = name; + + localState.stagedFiles = [localFile]; + localState.changedFiles = [localFile]; + localState.openFiles = [localFile]; + + mutations.SET_FILE_DATA(localState, { + data: { + path: 'Old Path', + name: 'Old Name', + raw: 'Old Raw', + base_raw: 'Old Base Raw', + }, + file: localFile, + }); + + [ + localState.stagedFiles[0], + localState.changedFiles[0], + localState.openFiles[0], + localFile, + ].forEach(f => { + expect(f).toEqual( + expect.objectContaining({ + path, + name, + raw: null, + baseRaw: null, + }), + ); + }); + }); + }); + + describe('SET_FILE_RAW_DATA', () => { + it('sets raw data', () => { + mutations.SET_FILE_RAW_DATA(localState, { + file: localFile, + raw: 'testing', + }); + + expect(localFile.raw).toBe('testing'); + }); + + it('adds raw data to open pending file', () => { + localState.openFiles.push({ ...localFile, pending: true }); + + mutations.SET_FILE_RAW_DATA(localState, { + file: localFile, + raw: 'testing', + }); + + expect(localState.openFiles[0].raw).toBe('testing'); + }); + + it('does not add raw data to open pending tempFile file', () => { + localState.openFiles.push({ ...localFile, pending: true, tempFile: true }); + + mutations.SET_FILE_RAW_DATA(localState, { + file: localFile, + raw: 'testing', + }); + + expect(localState.openFiles[0].raw).not.toBe('testing'); + }); + }); + + describe('SET_FILE_BASE_RAW_DATA', () => { + it('sets raw data from base branch', () => { + mutations.SET_FILE_BASE_RAW_DATA(localState, { + file: localFile, + baseRaw: 'testing', + }); + + expect(localFile.baseRaw).toBe('testing'); + }); + }); + + describe('UPDATE_FILE_CONTENT', () => { + beforeEach(() => { + localFile.raw = 'test'; + }); + + it('sets content', () => { + mutations.UPDATE_FILE_CONTENT(localState, { + path: localFile.path, + content: 'test', + }); + + expect(localFile.content).toBe('test'); + }); + + it('sets changed if content does not match raw', () => { + mutations.UPDATE_FILE_CONTENT(localState, { + path: localFile.path, + content: 'testing', + }); + + expect(localFile.content).toBe('testing'); + expect(localFile.changed).toBeTruthy(); + }); + + it('sets changed if file is a temp file', () => { + localFile.tempFile = true; + + mutations.UPDATE_FILE_CONTENT(localState, { + path: localFile.path, + content: '', + }); + + expect(localFile.changed).toBeTruthy(); + }); + }); + + describe('SET_FILE_MERGE_REQUEST_CHANGE', () => { + it('sets file mr change', () => { + mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { + file: localFile, + mrChange: { + diff: 'ABC', + }, + }); + + expect(localFile.mrChange.diff).toBe('ABC'); + }); + + it('has diffMode replaced by default', () => { + mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { + file: localFile, + mrChange: { + diff: 'ABC', + }, + }); + + expect(localFile.mrChange.diffMode).toBe('replaced'); + }); + + it('has diffMode new', () => { + mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { + file: localFile, + mrChange: { + diff: 'ABC', + new_file: true, + }, + }); + + expect(localFile.mrChange.diffMode).toBe('new'); + }); + + it('has diffMode deleted', () => { + mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { + file: localFile, + mrChange: { + diff: 'ABC', + deleted_file: true, + }, + }); + + expect(localFile.mrChange.diffMode).toBe('deleted'); + }); + + it('has diffMode renamed', () => { + mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { + file: localFile, + mrChange: { + diff: 'ABC', + renamed_file: true, + }, + }); + + expect(localFile.mrChange.diffMode).toBe('renamed'); + }); + }); + + describe('DISCARD_FILE_CHANGES', () => { + beforeEach(() => { + localFile.content = 'test'; + localFile.changed = true; + localState.currentProjectId = 'gitlab-ce'; + localState.currentBranchId = 'master'; + localState.trees['gitlab-ce/master'] = { + tree: [], + }; + }); + + it('resets content and changed', () => { + mutations.DISCARD_FILE_CHANGES(localState, localFile.path); + + expect(localFile.content).toBe(''); + expect(localFile.changed).toBeFalsy(); + }); + + it('adds to root tree if deleted', () => { + localFile.deleted = true; + + mutations.DISCARD_FILE_CHANGES(localState, localFile.path); + + expect(localState.trees['gitlab-ce/master'].tree).toEqual([{ ...localFile, deleted: false }]); + }); + + it('adds to parent tree if deleted', () => { + localFile.deleted = true; + localFile.parentPath = 'parentPath'; + localState.entries.parentPath = { + tree: [], + }; + + mutations.DISCARD_FILE_CHANGES(localState, localFile.path); + + expect(localState.entries.parentPath.tree).toEqual([{ ...localFile, deleted: false }]); + }); + }); + + describe('ADD_FILE_TO_CHANGED', () => { + it('adds file into changed files array', () => { + mutations.ADD_FILE_TO_CHANGED(localState, localFile.path); + + expect(localState.changedFiles.length).toBe(1); + }); + }); + + describe('REMOVE_FILE_FROM_CHANGED', () => { + it('removes files from changed files array', () => { + localState.changedFiles.push(localFile); + + mutations.REMOVE_FILE_FROM_CHANGED(localState, localFile.path); + + expect(localState.changedFiles.length).toBe(0); + }); + }); + + describe('STAGE_CHANGE', () => { + beforeEach(() => { + mutations.STAGE_CHANGE(localState, localFile.path); + }); + + it('adds file into stagedFiles array', () => { + expect(localState.stagedFiles.length).toBe(1); + expect(localState.stagedFiles[0]).toEqual(localFile); + }); + + it('updates stagedFile if it is already staged', () => { + localFile.raw = 'testing 123'; + + mutations.STAGE_CHANGE(localState, localFile.path); + + expect(localState.stagedFiles.length).toBe(1); + expect(localState.stagedFiles[0].raw).toEqual('testing 123'); + }); + }); + + describe('UNSTAGE_CHANGE', () => { + let f; + + beforeEach(() => { + f = { ...file(), type: 'blob', staged: true }; + + localState.stagedFiles.push(f); + localState.changedFiles.push(f); + localState.entries[f.path] = f; + }); + + it('removes from stagedFiles array', () => { + mutations.UNSTAGE_CHANGE(localState, f.path); + + expect(localState.stagedFiles.length).toBe(0); + expect(localState.changedFiles.length).toBe(1); + }); + }); + + describe('TOGGLE_FILE_CHANGED', () => { + it('updates file changed status', () => { + mutations.TOGGLE_FILE_CHANGED(localState, { + file: localFile, + changed: true, + }); + + expect(localFile.changed).toBeTruthy(); + }); + }); + + describe('SET_FILE_VIEWMODE', () => { + it('updates file view mode', () => { + mutations.SET_FILE_VIEWMODE(localState, { + file: localFile, + viewMode: FILE_VIEW_MODE_PREVIEW, + }); + + expect(localFile.viewMode).toBe(FILE_VIEW_MODE_PREVIEW); + }); + }); + + describe('ADD_PENDING_TAB', () => { + beforeEach(() => { + const f = { ...file('openFile'), path: 'openFile', active: true, opened: true }; + + localState.entries[f.path] = f; + localState.openFiles.push(f); + }); + + it('adds file into openFiles as pending', () => { + mutations.ADD_PENDING_TAB(localState, { + file: localFile, + }); + + expect(localState.openFiles.length).toBe(1); + expect(localState.openFiles[0].pending).toBe(true); + expect(localState.openFiles[0].key).toBe(`pending-${localFile.key}`); + }); + + it('only allows 1 open pending file', () => { + const newFile = file('test'); + localState.entries[newFile.path] = newFile; + + mutations.ADD_PENDING_TAB(localState, { + file: localFile, + }); + + expect(localState.openFiles.length).toBe(1); + + mutations.ADD_PENDING_TAB(localState, { + file: file('test'), + }); + + expect(localState.openFiles.length).toBe(1); + expect(localState.openFiles[0].name).toBe('test'); + }); + }); + + describe('REMOVE_PENDING_TAB', () => { + it('removes pending tab from openFiles', () => { + localFile.key = 'testing'; + localState.openFiles.push(localFile); + + mutations.REMOVE_PENDING_TAB(localState, localFile); + + expect(localState.openFiles.length).toBe(0); + }); + }); +}); diff --git a/spec/frontend/ide/stores/mutations/tree_spec.js b/spec/frontend/ide/stores/mutations/tree_spec.js new file mode 100644 index 00000000000..a4b98aa9d5a --- /dev/null +++ b/spec/frontend/ide/stores/mutations/tree_spec.js @@ -0,0 +1,118 @@ +import mutations from '~/ide/stores/mutations/tree'; +import state from '~/ide/stores/state'; +import { file } from '../../helpers'; + +describe('Multi-file store tree mutations', () => { + let localState; + let localTree; + + beforeEach(() => { + localState = state(); + localTree = file(); + + localState.entries[localTree.path] = localTree; + }); + + describe('TOGGLE_TREE_OPEN', () => { + it('toggles tree open', () => { + mutations.TOGGLE_TREE_OPEN(localState, localTree.path); + + expect(localTree.opened).toBeTruthy(); + + mutations.TOGGLE_TREE_OPEN(localState, localTree.path); + + expect(localTree.opened).toBeFalsy(); + }); + }); + + describe('SET_DIRECTORY_DATA', () => { + let data; + + beforeEach(() => { + data = [file('tree'), file('foo'), file('blob')]; + }); + + it('adds directory data', () => { + localState.trees['project/master'] = { + tree: [], + }; + + mutations.SET_DIRECTORY_DATA(localState, { + data, + treePath: 'project/master', + }); + + const tree = localState.trees['project/master']; + + expect(tree.tree.length).toBe(3); + expect(tree.tree[0].name).toBe('tree'); + expect(tree.tree[1].name).toBe('foo'); + expect(tree.tree[2].name).toBe('blob'); + }); + + it('keeps loading state', () => { + mutations.CREATE_TREE(localState, { + treePath: 'project/master', + }); + mutations.SET_DIRECTORY_DATA(localState, { + data, + treePath: 'project/master', + }); + + expect(localState.trees['project/master'].loading).toBe(true); + }); + + it('does not override tree already in state, but merges the two with correct order', () => { + const openedFile = file('new'); + + localState.trees['project/master'] = { + loading: true, + tree: [openedFile], + }; + + mutations.SET_DIRECTORY_DATA(localState, { + data, + treePath: 'project/master', + }); + + const { tree } = localState.trees['project/master']; + + expect(tree.length).toBe(4); + expect(tree[0].name).toBe('blob'); + expect(tree[1].name).toBe('foo'); + expect(tree[2].name).toBe('new'); + expect(tree[3].name).toBe('tree'); + }); + + it('returns tree unchanged if the opened file is already in the tree', () => { + const openedFile = file('foo'); + localState.trees['project/master'] = { + loading: true, + tree: [openedFile], + }; + + mutations.SET_DIRECTORY_DATA(localState, { + data, + treePath: 'project/master', + }); + + const { tree } = localState.trees['project/master']; + + expect(tree.length).toBe(3); + + expect(tree[0].name).toBe('tree'); + expect(tree[1].name).toBe('foo'); + expect(tree[2].name).toBe('blob'); + }); + }); + + describe('REMOVE_ALL_CHANGES_FILES', () => { + it('removes all files from changedFiles state', () => { + localState.changedFiles.push(file('REMOVE_ALL_CHANGES_FILES')); + + mutations.REMOVE_ALL_CHANGES_FILES(localState); + + expect(localState.changedFiles.length).toBe(0); + }); + }); +}); diff --git a/spec/frontend/ide/stores/mutations_spec.js b/spec/frontend/ide/stores/mutations_spec.js new file mode 100644 index 00000000000..eb89c92914a --- /dev/null +++ b/spec/frontend/ide/stores/mutations_spec.js @@ -0,0 +1,747 @@ +import { TEST_HOST } from 'helpers/test_constants'; +import mutations from '~/ide/stores/mutations'; +import state from '~/ide/stores/state'; +import { file } from '../helpers'; + +describe('Multi-file store mutations', () => { + let localState; + let entry; + + beforeEach(() => { + localState = state(); + entry = file(); + + localState.entries[entry.path] = entry; + }); + + describe('SET_INITIAL_DATA', () => { + it('sets all initial data', () => { + mutations.SET_INITIAL_DATA(localState, { + test: 'test', + }); + + expect(localState.test).toBe('test'); + }); + }); + + describe('TOGGLE_LOADING', () => { + it('toggles loading of entry', () => { + mutations.TOGGLE_LOADING(localState, { + entry, + }); + + expect(entry.loading).toBeTruthy(); + + mutations.TOGGLE_LOADING(localState, { + entry, + }); + + expect(entry.loading).toBeFalsy(); + }); + + it('toggles loading of entry and sets specific value', () => { + mutations.TOGGLE_LOADING(localState, { + entry, + }); + + expect(entry.loading).toBeTruthy(); + + mutations.TOGGLE_LOADING(localState, { + entry, + forceValue: true, + }); + + expect(entry.loading).toBeTruthy(); + }); + }); + + describe('SET_LEFT_PANEL_COLLAPSED', () => { + it('sets left panel collapsed', () => { + mutations.SET_LEFT_PANEL_COLLAPSED(localState, true); + + expect(localState.leftPanelCollapsed).toBeTruthy(); + + mutations.SET_LEFT_PANEL_COLLAPSED(localState, false); + + expect(localState.leftPanelCollapsed).toBeFalsy(); + }); + }); + + describe('SET_RIGHT_PANEL_COLLAPSED', () => { + it('sets right panel collapsed', () => { + mutations.SET_RIGHT_PANEL_COLLAPSED(localState, true); + + expect(localState.rightPanelCollapsed).toBeTruthy(); + + mutations.SET_RIGHT_PANEL_COLLAPSED(localState, false); + + expect(localState.rightPanelCollapsed).toBeFalsy(); + }); + }); + + describe('CLEAR_STAGED_CHANGES', () => { + it('clears stagedFiles array', () => { + localState.stagedFiles.push('a'); + + mutations.CLEAR_STAGED_CHANGES(localState); + + expect(localState.stagedFiles.length).toBe(0); + }); + }); + + describe('UPDATE_VIEWER', () => { + it('sets viewer state', () => { + mutations.UPDATE_VIEWER(localState, 'diff'); + + expect(localState.viewer).toBe('diff'); + }); + }); + + describe('UPDATE_ACTIVITY_BAR_VIEW', () => { + it('updates currentActivityBar', () => { + mutations.UPDATE_ACTIVITY_BAR_VIEW(localState, 'test'); + + expect(localState.currentActivityView).toBe('test'); + }); + }); + + describe('SET_EMPTY_STATE_SVGS', () => { + it('updates empty state SVGs', () => { + mutations.SET_EMPTY_STATE_SVGS(localState, { + emptyStateSvgPath: 'emptyState', + noChangesStateSvgPath: 'noChanges', + committedStateSvgPath: 'commited', + }); + + expect(localState.emptyStateSvgPath).toBe('emptyState'); + expect(localState.noChangesStateSvgPath).toBe('noChanges'); + expect(localState.committedStateSvgPath).toBe('commited'); + }); + }); + + describe('CREATE_TMP_ENTRY', () => { + beforeEach(() => { + localState.currentProjectId = 'gitlab-ce'; + localState.currentBranchId = 'master'; + localState.trees['gitlab-ce/master'] = { + tree: [], + }; + }); + + it('creates temp entry in the tree', () => { + const tmpFile = file('test'); + mutations.CREATE_TMP_ENTRY(localState, { + data: { + entries: { + test: { ...tmpFile, tempFile: true, changed: true }, + }, + treeList: [tmpFile], + }, + projectId: 'gitlab-ce', + branchId: 'master', + }); + + expect(localState.trees['gitlab-ce/master'].tree.length).toEqual(1); + expect(localState.entries.test.tempFile).toEqual(true); + }); + + it('marks entry as replacing previous entry if the old one has been deleted', () => { + const tmpFile = file('test'); + localState.entries.test = { ...tmpFile, deleted: true }; + mutations.CREATE_TMP_ENTRY(localState, { + data: { + entries: { + test: { ...tmpFile, tempFile: true, changed: true }, + }, + treeList: [tmpFile], + }, + projectId: 'gitlab-ce', + branchId: 'master', + }); + + expect(localState.trees['gitlab-ce/master'].tree.length).toEqual(1); + expect(localState.entries.test.replaces).toEqual(true); + }); + }); + + describe('UPDATE_TEMP_FLAG', () => { + beforeEach(() => { + localState.entries.test = { ...file(), tempFile: true, changed: true }; + }); + + it('updates tempFile flag', () => { + mutations.UPDATE_TEMP_FLAG(localState, { + path: 'test', + tempFile: false, + }); + + expect(localState.entries.test.tempFile).toBe(false); + }); + + it('updates changed flag', () => { + mutations.UPDATE_TEMP_FLAG(localState, { + path: 'test', + tempFile: false, + }); + + expect(localState.entries.test.changed).toBe(false); + }); + }); + + describe('TOGGLE_FILE_FINDER', () => { + it('updates fileFindVisible', () => { + mutations.TOGGLE_FILE_FINDER(localState, true); + + expect(localState.fileFindVisible).toBe(true); + }); + }); + + describe('BURST_UNUSED_SEAL', () => { + it('updates unusedSeal', () => { + expect(localState.unusedSeal).toBe(true); + + mutations.BURST_UNUSED_SEAL(localState); + + expect(localState.unusedSeal).toBe(false); + }); + }); + + describe('SET_ERROR_MESSAGE', () => { + it('updates error message', () => { + mutations.SET_ERROR_MESSAGE(localState, 'error'); + + expect(localState.errorMessage).toBe('error'); + }); + }); + + describe('DELETE_ENTRY', () => { + beforeEach(() => { + localState.currentProjectId = 'gitlab-ce'; + localState.currentBranchId = 'master'; + localState.trees['gitlab-ce/master'] = { + tree: [], + }; + }); + + it('sets deleted flag', () => { + localState.entries.filePath = { + deleted: false, + }; + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.entries.filePath.deleted).toBe(true); + }); + + it('removes from root tree', () => { + localState.entries.filePath = { + path: 'filePath', + deleted: false, + }; + localState.trees['gitlab-ce/master'].tree.push(localState.entries.filePath); + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.trees['gitlab-ce/master'].tree).toEqual([]); + }); + + it('removes from parent tree', () => { + localState.entries.filePath = { + path: 'filePath', + deleted: false, + parentPath: 'parentPath', + }; + localState.entries.parentPath = { + tree: [localState.entries.filePath], + }; + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.entries.parentPath.tree).toEqual([]); + }); + + it('adds to changedFiles', () => { + localState.entries.filePath = { + deleted: false, + type: 'blob', + }; + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.changedFiles).toEqual([localState.entries.filePath]); + }); + + it('does not add tempFile into changedFiles', () => { + localState.entries.filePath = { + deleted: false, + type: 'blob', + tempFile: true, + }; + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.changedFiles).toEqual([]); + }); + + it('removes tempFile from changedFiles when deleted', () => { + localState.entries.filePath = { + path: 'filePath', + deleted: false, + type: 'blob', + tempFile: true, + }; + + localState.changedFiles.push({ ...localState.entries.filePath }); + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.changedFiles).toEqual([]); + }); + }); + + describe('UPDATE_FILE_AFTER_COMMIT', () => { + it('updates URLs if prevPath is set', () => { + const f = { + ...file('test'), + prevPath: 'testing-123', + rawPath: `${TEST_HOST}/testing-123`, + permalink: `${TEST_HOST}/testing-123`, + commitsPath: `${TEST_HOST}/testing-123`, + blamePath: `${TEST_HOST}/testing-123`, + replaces: true, + }; + localState.entries.test = f; + localState.changedFiles.push(f); + + mutations.UPDATE_FILE_AFTER_COMMIT(localState, { + file: f, + lastCommit: { + commit: {}, + }, + }); + + expect(f).toEqual( + expect.objectContaining({ + rawPath: `${TEST_HOST}/test`, + permalink: `${TEST_HOST}/test`, + commitsPath: `${TEST_HOST}/test`, + blamePath: `${TEST_HOST}/test`, + replaces: false, + prevId: undefined, + prevPath: undefined, + prevName: undefined, + prevUrl: undefined, + prevKey: undefined, + }), + ); + }); + }); + + describe('OPEN_NEW_ENTRY_MODAL', () => { + it('sets entryModal', () => { + localState.entries.testPath = file(); + + mutations.OPEN_NEW_ENTRY_MODAL(localState, { + type: 'test', + path: 'testPath', + }); + + expect(localState.entryModal).toEqual({ + type: 'test', + path: 'testPath', + entry: localState.entries.testPath, + }); + }); + }); + + describe('RENAME_ENTRY', () => { + beforeEach(() => { + localState.trees = { + 'gitlab-ce/master': { + tree: [], + }, + }; + localState.currentProjectId = 'gitlab-ce'; + localState.currentBranchId = 'master'; + localState.entries = { + oldPath: file('oldPath', 'oldPath', 'blob'), + }; + }); + + it('updates existing entry without creating a new one', () => { + mutations.RENAME_ENTRY(localState, { + path: 'oldPath', + name: 'newPath', + parentPath: '', + }); + + expect(localState.entries).toEqual({ + newPath: expect.objectContaining({ + path: 'newPath', + prevPath: 'oldPath', + }), + }); + }); + + it('correctly handles consecutive renames for the same entry', () => { + mutations.RENAME_ENTRY(localState, { + path: 'oldPath', + name: 'newPath', + parentPath: '', + }); + + mutations.RENAME_ENTRY(localState, { + path: 'newPath', + name: 'newestPath', + parentPath: '', + }); + + expect(localState.entries).toEqual({ + newestPath: expect.objectContaining({ + path: 'newestPath', + prevPath: 'oldPath', + }), + }); + }); + + it('correctly handles the same entry within a consecutively renamed folder', () => { + const oldPath = file('root-folder/oldPath', 'root-folder/oldPath', 'blob'); + localState.entries = { + 'root-folder': { ...file('root-folder', 'root-folder', 'tree'), tree: [oldPath] }, + 'root-folder/oldPath': oldPath, + }; + Object.assign(localState.entries['root-folder/oldPath'], { + parentPath: 'root-folder', + url: 'root-folder/oldPath-blob-root-folder/oldPath', + }); + + mutations.RENAME_ENTRY(localState, { + path: 'root-folder/oldPath', + name: 'renamed-folder/oldPath', + entryPath: null, + parentPath: '', + }); + + mutations.RENAME_ENTRY(localState, { + path: 'renamed-folder/oldPath', + name: 'simply-renamed/oldPath', + entryPath: null, + parentPath: '', + }); + + expect(localState.entries).toEqual({ + 'root-folder': expect.objectContaining({ + path: 'root-folder', + }), + 'simply-renamed/oldPath': expect.objectContaining({ + path: 'simply-renamed/oldPath', + prevPath: 'root-folder/oldPath', + }), + }); + }); + + it('renames entry, preserving old parameters', () => { + Object.assign(localState.entries.oldPath, { + url: `project/-/oldPath`, + }); + const oldPathData = localState.entries.oldPath; + + mutations.RENAME_ENTRY(localState, { + path: 'oldPath', + name: 'newPath', + parentPath: '', + }); + + expect(localState.entries.newPath).toEqual({ + ...oldPathData, + id: 'newPath', + path: 'newPath', + name: 'newPath', + url: `project/-/newPath`, + key: expect.stringMatching('newPath'), + prevId: 'oldPath', + prevName: 'oldPath', + prevPath: 'oldPath', + prevUrl: `project/-/oldPath`, + prevKey: oldPathData.key, + prevParentPath: oldPathData.parentPath, + }); + }); + + it('does not store previous attributes on temp files', () => { + Object.assign(localState.entries.oldPath, { + tempFile: true, + }); + mutations.RENAME_ENTRY(localState, { + path: 'oldPath', + name: 'newPath', + entryPath: null, + parentPath: '', + }); + + expect(localState.entries.newPath).not.toEqual( + expect.objectContaining({ + prevId: expect.anything(), + prevName: expect.anything(), + prevPath: expect.anything(), + prevUrl: expect.anything(), + prevKey: expect.anything(), + prevParentPath: expect.anything(), + }), + ); + }); + + it('properly handles files with spaces in name', () => { + const path = 'my fancy path'; + const newPath = 'new path'; + const oldEntry = { ...file(path, path, 'blob'), url: `project/-/${encodeURI(path)}` }; + + localState.entries[path] = oldEntry; + + mutations.RENAME_ENTRY(localState, { + path, + name: newPath, + entryPath: null, + parentPath: '', + }); + + expect(localState.entries[newPath]).toEqual({ + ...oldEntry, + id: newPath, + path: newPath, + name: newPath, + url: `project/-/new%20path`, + key: expect.stringMatching(newPath), + prevId: path, + prevName: path, + prevPath: path, + prevUrl: `project/-/my%20fancy%20path`, + prevKey: oldEntry.key, + prevParentPath: oldEntry.parentPath, + }); + }); + + it('adds to parent tree', () => { + const parentEntry = { + ...file('parentPath', 'parentPath', 'tree'), + tree: [localState.entries.oldPath], + }; + localState.entries.parentPath = parentEntry; + + mutations.RENAME_ENTRY(localState, { + path: 'oldPath', + name: 'newPath', + entryPath: null, + parentPath: 'parentPath', + }); + + expect(parentEntry.tree.length).toBe(1); + expect(parentEntry.tree[0].name).toBe('newPath'); + }); + + it('sorts tree after renaming an entry', () => { + const alpha = file('alpha', 'alpha', 'blob'); + const beta = file('beta', 'beta', 'blob'); + const gamma = file('gamma', 'gamma', 'blob'); + localState.entries = { + alpha, + beta, + gamma, + }; + + localState.trees['gitlab-ce/master'].tree = [alpha, beta, gamma]; + + mutations.RENAME_ENTRY(localState, { + path: 'alpha', + name: 'theta', + entryPath: null, + parentPath: '', + }); + + expect(localState.trees['gitlab-ce/master'].tree).toEqual([ + expect.objectContaining({ + name: 'beta', + }), + expect.objectContaining({ + name: 'gamma', + }), + expect.objectContaining({ + path: 'theta', + name: 'theta', + }), + ]); + }); + + it('updates openFiles with the renamed one if the original one is open', () => { + Object.assign(localState.entries.oldPath, { + opened: true, + type: 'blob', + }); + Object.assign(localState, { + openFiles: [localState.entries.oldPath], + }); + + mutations.RENAME_ENTRY(localState, { + path: 'oldPath', + name: 'newPath', + }); + + expect(localState.openFiles.length).toBe(1); + expect(localState.openFiles[0].path).toBe('newPath'); + }); + + it('does not add renamed entry to changedFiles', () => { + mutations.RENAME_ENTRY(localState, { + path: 'oldPath', + name: 'newPath', + }); + + expect(localState.changedFiles.length).toBe(0); + }); + + it('updates existing changedFiles entry with the renamed one', () => { + const origFile = { ...file('oldPath', 'oldPath', 'blob'), content: 'Foo' }; + + Object.assign(localState, { + changedFiles: [origFile], + }); + Object.assign(localState.entries, { + oldPath: origFile, + }); + + mutations.RENAME_ENTRY(localState, { + path: 'oldPath', + name: 'newPath', + }); + + expect(localState.changedFiles).toEqual([ + expect.objectContaining({ + path: 'newPath', + content: 'Foo', + }), + ]); + }); + + it('correctly saves original values if an entry is renamed multiple times', () => { + const original = { ...localState.entries.oldPath }; + const paramsToCheck = ['prevId', 'prevPath', 'prevName', 'prevUrl']; + const expectedObj = paramsToCheck.reduce( + (o, param) => ({ ...o, [param]: original[param.replace('prev', '').toLowerCase()] }), + {}, + ); + + mutations.RENAME_ENTRY(localState, { + path: 'oldPath', + name: 'newPath', + }); + + expect(localState.entries.newPath).toEqual(expect.objectContaining(expectedObj)); + + mutations.RENAME_ENTRY(localState, { + path: 'newPath', + name: 'newer', + }); + + expect(localState.entries.newer).toEqual(expect.objectContaining(expectedObj)); + }); + + describe('renaming back to original', () => { + beforeEach(() => { + const renamedEntry = { + ...file('renamed', 'renamed', 'blob'), + prevId: 'lorem/orig', + prevPath: 'lorem/orig', + prevName: 'orig', + prevUrl: 'project/-/loren/orig', + prevKey: 'lorem/orig', + prevParentPath: 'lorem', + }; + + localState.entries = { + renamed: renamedEntry, + }; + + mutations.RENAME_ENTRY(localState, { + path: 'renamed', + name: 'orig', + parentPath: 'lorem', + }); + }); + + it('renames entry and clears prev properties', () => { + expect(localState.entries).toEqual({ + 'lorem/orig': expect.objectContaining({ + id: 'lorem/orig', + path: 'lorem/orig', + name: 'orig', + prevId: undefined, + prevPath: undefined, + prevName: undefined, + prevUrl: undefined, + prevKey: undefined, + prevParentPath: undefined, + }), + }); + }); + }); + + describe('key updates', () => { + beforeEach(() => { + const rootFolder = file('rootFolder', 'rootFolder', 'tree'); + localState.entries = { + rootFolder, + oldPath: file('oldPath', 'oldPath', 'blob'), + 'oldPath.txt': file('oldPath.txt', 'oldPath.txt', 'blob'), + 'rootFolder/oldPath.md': file('oldPath.md', 'oldPath.md', 'blob', rootFolder), + }; + }); + + it('sets properly constucted key while preserving the original one', () => { + const key = 'oldPath.txt-blob-oldPath.txt'; + localState.entries['oldPath.txt'].key = key; + mutations.RENAME_ENTRY(localState, { + path: 'oldPath.txt', + name: 'newPath.md', + }); + + expect(localState.entries['newPath.md'].key).toBe('newPath.md-blob-newPath.md'); + expect(localState.entries['newPath.md'].prevKey).toBe(key); + }); + + it('correctly updates key for an entry without an extension', () => { + localState.entries.oldPath.key = 'oldPath-blob-oldPath'; + mutations.RENAME_ENTRY(localState, { + path: 'oldPath', + name: 'newPath.md', + }); + + expect(localState.entries['newPath.md'].key).toBe('newPath.md-blob-newPath.md'); + }); + + it('correctly updates key when new name does not have an extension', () => { + localState.entries['oldPath.txt'].key = 'oldPath.txt-blob-oldPath.txt'; + mutations.RENAME_ENTRY(localState, { + path: 'oldPath.txt', + name: 'newPath', + }); + + expect(localState.entries.newPath.key).toBe('newPath-blob-newPath'); + }); + + it('correctly updates key when renaming an entry in a folder', () => { + localState.entries['rootFolder/oldPath.md'].key = + 'rootFolder/oldPath.md-blob-rootFolder/oldPath.md'; + mutations.RENAME_ENTRY(localState, { + path: 'rootFolder/oldPath.md', + name: 'newPath.md', + entryPath: null, + parentPath: 'rootFolder', + }); + + expect(localState.entries['rootFolder/newPath.md'].key).toBe( + 'rootFolder/newPath.md-blob-rootFolder/newPath.md', + ); + }); + }); + }); +}); diff --git a/spec/frontend/ide/stores/utils_spec.js b/spec/frontend/ide/stores/utils_spec.js new file mode 100644 index 00000000000..1f54397db55 --- /dev/null +++ b/spec/frontend/ide/stores/utils_spec.js @@ -0,0 +1,696 @@ +import * as utils from '~/ide/stores/utils'; +import { commitActionTypes } from '~/ide/constants'; +import { file } from '../helpers'; + +describe('Multi-file store utils', () => { + describe('setPageTitle', () => { + it('sets the document page title', () => { + utils.setPageTitle('test'); + + expect(document.title).toBe('test'); + }); + }); + + describe('setPageTitleForFile', () => { + it('sets the document page title for the file passed', () => { + const f = { + path: 'README.md', + }; + + const state = { + currentBranchId: 'master', + currentProjectId: 'test/test', + }; + + utils.setPageTitleForFile(state, f); + + expect(document.title).toBe('README.md · master · test/test · GitLab'); + }); + }); + + describe('findIndexOfFile', () => { + let localState; + + beforeEach(() => { + localState = [ + { + path: '1', + }, + { + path: '2', + }, + ]; + }); + + it('finds in the index of an entry by path', () => { + const index = utils.findIndexOfFile(localState, { + path: '2', + }); + + expect(index).toBe(1); + }); + }); + + describe('findEntry', () => { + let localState; + + beforeEach(() => { + localState = { + tree: [ + { + type: 'tree', + name: 'test', + }, + { + type: 'blob', + name: 'file', + }, + ], + }; + }); + + it('returns an entry found by name', () => { + const foundEntry = utils.findEntry(localState.tree, 'tree', 'test'); + + expect(foundEntry.type).toBe('tree'); + expect(foundEntry.name).toBe('test'); + }); + + it('returns undefined when no entry found', () => { + const foundEntry = utils.findEntry(localState.tree, 'blob', 'test'); + + expect(foundEntry).toBeUndefined(); + }); + }); + + describe('createCommitPayload', () => { + it('returns API payload', () => { + const state = { + commitMessage: 'commit message', + }; + const rootState = { + stagedFiles: [ + { + ...file('staged'), + path: 'staged', + content: 'updated file content', + lastCommitSha: '123456789', + }, + { + ...file('newFile'), + path: 'added', + tempFile: true, + content: 'new file content', + base64: true, + lastCommitSha: '123456789', + }, + { ...file('deletedFile'), path: 'deletedFile', deleted: true }, + { ...file('renamedFile'), path: 'renamedFile', prevPath: 'prevPath' }, + { ...file('replacingFile'), path: 'replacingFile', replaces: true }, + ], + currentBranchId: 'master', + }; + const payload = utils.createCommitPayload({ + branch: 'master', + newBranch: false, + state, + rootState, + getters: {}, + }); + + expect(payload).toEqual({ + branch: 'master', + commit_message: 'commit message', + actions: [ + { + action: commitActionTypes.update, + file_path: 'staged', + content: 'updated file content', + encoding: 'text', + last_commit_id: '123456789', + previous_path: undefined, + }, + { + action: commitActionTypes.create, + file_path: 'added', + content: 'new file content', + encoding: 'base64', + last_commit_id: '123456789', + previous_path: undefined, + }, + { + action: commitActionTypes.delete, + file_path: 'deletedFile', + content: undefined, + encoding: 'text', + last_commit_id: undefined, + previous_path: undefined, + }, + { + action: commitActionTypes.move, + file_path: 'renamedFile', + content: null, + encoding: 'text', + last_commit_id: undefined, + previous_path: 'prevPath', + }, + { + action: commitActionTypes.update, + file_path: 'replacingFile', + content: undefined, + encoding: 'text', + last_commit_id: undefined, + previous_path: undefined, + }, + ], + start_sha: undefined, + }); + }); + + it('uses prebuilt commit message when commit message is empty', () => { + const rootState = { + stagedFiles: [ + { + ...file('staged'), + path: 'staged', + content: 'updated file content', + lastCommitSha: '123456789', + }, + { + ...file('newFile'), + path: 'added', + tempFile: true, + content: 'new file content', + base64: true, + lastCommitSha: '123456789', + }, + ], + currentBranchId: 'master', + }; + const payload = utils.createCommitPayload({ + branch: 'master', + newBranch: false, + state: {}, + rootState, + getters: { + preBuiltCommitMessage: 'prebuilt test commit message', + }, + }); + + expect(payload).toEqual({ + branch: 'master', + commit_message: 'prebuilt test commit message', + actions: [ + { + action: commitActionTypes.update, + file_path: 'staged', + content: 'updated file content', + encoding: 'text', + last_commit_id: '123456789', + previous_path: undefined, + }, + { + action: commitActionTypes.create, + file_path: 'added', + content: 'new file content', + encoding: 'base64', + last_commit_id: '123456789', + previous_path: undefined, + }, + ], + start_sha: undefined, + }); + }); + }); + + describe('commitActionForFile', () => { + it('returns deleted for deleted file', () => { + expect( + utils.commitActionForFile({ + deleted: true, + }), + ).toBe(commitActionTypes.delete); + }); + + it('returns create for tempFile', () => { + expect( + utils.commitActionForFile({ + tempFile: true, + }), + ).toBe(commitActionTypes.create); + }); + + it('returns move for moved file', () => { + expect( + utils.commitActionForFile({ + prevPath: 'test', + }), + ).toBe(commitActionTypes.move); + }); + + it('returns update by default', () => { + expect(utils.commitActionForFile({})).toBe(commitActionTypes.update); + }); + }); + + describe('getCommitFiles', () => { + it('filters out folders from the list', () => { + const files = [ + { + path: 'a', + type: 'blob', + deleted: true, + }, + { + path: 'c', + type: 'tree', + deleted: true, + }, + { + path: 'c/d', + type: 'blob', + deleted: true, + }, + ]; + + const flattendFiles = utils.getCommitFiles(files); + + expect(flattendFiles).toEqual([ + { + path: 'a', + type: 'blob', + deleted: true, + }, + { + path: 'c/d', + type: 'blob', + deleted: true, + }, + ]); + }); + }); + + describe('mergeTrees', () => { + let fromTree; + let toTree; + + beforeEach(() => { + fromTree = [file('foo')]; + toTree = [file('bar')]; + }); + + it('merges simple trees with sorting the result', () => { + toTree = [file('beta'), file('alpha'), file('gamma')]; + const res = utils.mergeTrees(fromTree, toTree); + + expect(res.length).toEqual(4); + expect(res[0].name).toEqual('alpha'); + expect(res[1].name).toEqual('beta'); + expect(res[2].name).toEqual('foo'); + expect(res[3].name).toEqual('gamma'); + expect(res[2]).toBe(fromTree[0]); + }); + + it('handles edge cases', () => { + expect(utils.mergeTrees({}, []).length).toEqual(0); + + let res = utils.mergeTrees({}, toTree); + + expect(res.length).toEqual(1); + expect(res[0].name).toEqual('bar'); + + res = utils.mergeTrees(fromTree, []); + + expect(res.length).toEqual(1); + expect(res[0].name).toEqual('foo'); + expect(res[0]).toBe(fromTree[0]); + }); + + it('merges simple trees without producing duplicates', () => { + toTree.push(file('foo')); + + const res = utils.mergeTrees(fromTree, toTree); + + expect(res.length).toEqual(2); + expect(res[0].name).toEqual('bar'); + expect(res[1].name).toEqual('foo'); + expect(res[1]).not.toBe(fromTree[0]); + }); + + it('merges nested tree into the main one without duplicates', () => { + fromTree[0].tree.push({ + ...file('alpha'), + path: 'foo/alpha', + tree: [{ ...file('beta.md'), path: 'foo/alpha/beta.md' }], + }); + + toTree.push({ + ...file('foo'), + tree: [ + { + ...file('alpha'), + path: 'foo/alpha', + tree: [{ ...file('gamma.md'), path: 'foo/alpha/gamma.md' }], + }, + ], + }); + + const res = utils.mergeTrees(fromTree, toTree); + + expect(res.length).toEqual(2); + expect(res[1].name).toEqual('foo'); + + const finalBranch = res[1].tree[0].tree; + + expect(finalBranch.length).toEqual(2); + expect(finalBranch[0].name).toEqual('beta.md'); + expect(finalBranch[1].name).toEqual('gamma.md'); + }); + + it('marks correct folders as opened as the parsing goes on', () => { + fromTree[0].tree.push({ + ...file('alpha'), + path: 'foo/alpha', + tree: [{ ...file('beta.md'), path: 'foo/alpha/beta.md' }], + }); + + toTree.push({ + ...file('foo'), + tree: [ + { + ...file('alpha'), + path: 'foo/alpha', + tree: [{ ...file('gamma.md'), path: 'foo/alpha/gamma.md' }], + }, + ], + }); + + const res = utils.mergeTrees(fromTree, toTree); + + expect(res[1].name).toEqual('foo'); + expect(res[1].opened).toEqual(true); + + expect(res[1].tree[0].name).toEqual('alpha'); + expect(res[1].tree[0].opened).toEqual(true); + }); + }); + + describe('escapeFileUrl', () => { + it('encodes URL excluding the slashes', () => { + expect(utils.escapeFileUrl('/foo-bar/file.md')).toBe('/foo-bar/file.md'); + expect(utils.escapeFileUrl('foo bar/file.md')).toBe('foo%20bar/file.md'); + expect(utils.escapeFileUrl('foo/bar/file.md')).toBe('foo/bar/file.md'); + }); + }); + + describe('swapInStateArray', () => { + let localState; + + beforeEach(() => { + localState = []; + }); + + it('swaps existing entry with a new one', () => { + const file1 = { ...file('old'), key: 'foo' }; + const file2 = file('new'); + const arr = [file1]; + + Object.assign(localState, { + dummyArray: arr, + entries: { + new: file2, + }, + }); + + utils.swapInStateArray(localState, 'dummyArray', 'foo', 'new'); + + expect(localState.dummyArray.length).toBe(1); + expect(localState.dummyArray[0]).toBe(file2); + }); + + it('does not add an item if it does not exist yet in array', () => { + const file1 = file('file'); + Object.assign(localState, { + dummyArray: [], + entries: { + file: file1, + }, + }); + + utils.swapInStateArray(localState, 'dummyArray', 'foo', 'file'); + + expect(localState.dummyArray.length).toBe(0); + }); + }); + + describe('swapInParentTreeWithSorting', () => { + let localState; + let branchInfo; + const currentProjectId = '123-foo'; + const currentBranchId = 'master'; + + beforeEach(() => { + localState = { + currentBranchId, + currentProjectId, + trees: { + [`${currentProjectId}/${currentBranchId}`]: { + tree: [], + }, + }, + entries: { + oldPath: file('oldPath', 'oldPath', 'blob'), + newPath: file('newPath', 'newPath', 'blob'), + parentPath: file('parentPath', 'parentPath', 'tree'), + }, + }; + branchInfo = localState.trees[`${currentProjectId}/${currentBranchId}`]; + }); + + it('does not change tree if newPath is not supplied', () => { + branchInfo.tree = [localState.entries.oldPath]; + + utils.swapInParentTreeWithSorting(localState, 'oldPath', undefined, undefined); + + expect(branchInfo.tree).toEqual([localState.entries.oldPath]); + }); + + describe('oldPath to replace is not defined: simple addition to tree', () => { + it('adds to tree on the state if there is no parent for the entry', () => { + expect(branchInfo.tree.length).toBe(0); + + utils.swapInParentTreeWithSorting(localState, undefined, 'oldPath', undefined); + + expect(branchInfo.tree.length).toBe(1); + expect(branchInfo.tree[0].name).toBe('oldPath'); + + utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', undefined); + + expect(branchInfo.tree.length).toBe(2); + expect(branchInfo.tree).toEqual([ + expect.objectContaining({ + name: 'newPath', + }), + expect.objectContaining({ + name: 'oldPath', + }), + ]); + }); + + it('adds to parent tree if it is supplied', () => { + utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', 'parentPath'); + + expect(localState.entries.parentPath.tree.length).toBe(1); + expect(localState.entries.parentPath.tree).toEqual([ + expect.objectContaining({ + name: 'newPath', + }), + ]); + + localState.entries.parentPath.tree = [localState.entries.oldPath]; + + utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', 'parentPath'); + + expect(localState.entries.parentPath.tree.length).toBe(2); + expect(localState.entries.parentPath.tree).toEqual([ + expect.objectContaining({ + name: 'newPath', + }), + expect.objectContaining({ + name: 'oldPath', + }), + ]); + }); + }); + + describe('swapping of the items', () => { + it('swaps entries if both paths are supplied', () => { + branchInfo.tree = [localState.entries.oldPath]; + + utils.swapInParentTreeWithSorting(localState, localState.entries.oldPath.key, 'newPath'); + + expect(branchInfo.tree).toEqual([ + expect.objectContaining({ + name: 'newPath', + }), + ]); + + utils.swapInParentTreeWithSorting(localState, localState.entries.newPath.key, 'oldPath'); + + expect(branchInfo.tree).toEqual([ + expect.objectContaining({ + name: 'oldPath', + }), + ]); + }); + + it('sorts tree after swapping the entries', () => { + const alpha = file('alpha', 'alpha', 'blob'); + const beta = file('beta', 'beta', 'blob'); + const gamma = file('gamma', 'gamma', 'blob'); + const theta = file('theta', 'theta', 'blob'); + localState.entries = { + alpha, + beta, + gamma, + theta, + }; + + branchInfo.tree = [alpha, beta, gamma]; + + utils.swapInParentTreeWithSorting(localState, alpha.key, 'theta'); + + expect(branchInfo.tree).toEqual([ + expect.objectContaining({ + name: 'beta', + }), + expect.objectContaining({ + name: 'gamma', + }), + expect.objectContaining({ + name: 'theta', + }), + ]); + + utils.swapInParentTreeWithSorting(localState, gamma.key, 'alpha'); + + expect(branchInfo.tree).toEqual([ + expect.objectContaining({ + name: 'alpha', + }), + expect.objectContaining({ + name: 'beta', + }), + expect.objectContaining({ + name: 'theta', + }), + ]); + + utils.swapInParentTreeWithSorting(localState, beta.key, 'gamma'); + + expect(branchInfo.tree).toEqual([ + expect.objectContaining({ + name: 'alpha', + }), + expect.objectContaining({ + name: 'gamma', + }), + expect.objectContaining({ + name: 'theta', + }), + ]); + }); + }); + }); + + describe('cleanTrailingSlash', () => { + [ + { + input: '', + output: '', + }, + { + input: 'abc', + output: 'abc', + }, + { + input: 'abc/', + output: 'abc', + }, + { + input: 'abc/def', + output: 'abc/def', + }, + { + input: 'abc/def/', + output: 'abc/def', + }, + ].forEach(({ input, output }) => { + it(`cleans trailing slash from string "${input}"`, () => { + expect(utils.cleanTrailingSlash(input)).toEqual(output); + }); + }); + }); + + describe('pathsAreEqual', () => { + [ + { + args: ['abc', 'abc'], + output: true, + }, + { + args: ['abc', 'def'], + output: false, + }, + { + args: ['abc/', 'abc'], + output: true, + }, + { + args: ['abc/abc', 'abc'], + output: false, + }, + { + args: ['/', ''], + output: true, + }, + { + args: ['', '/'], + output: true, + }, + { + args: [false, '/'], + output: true, + }, + ].forEach(({ args, output }) => { + it(`cleans and tests equality (${JSON.stringify(args)})`, () => { + expect(utils.pathsAreEqual(...args)).toEqual(output); + }); + }); + }); + + describe('addFinalNewlineIfNeeded', () => { + it('adds a newline if it doesnt already exist', () => { + [ + { + input: 'some text', + output: 'some text\n', + }, + { + input: 'some text\n', + output: 'some text\n', + }, + { + input: 'some text\n\n', + output: 'some text\n\n', + }, + { + input: 'some\n text', + output: 'some\n text\n', + }, + ].forEach(({ input, output }) => { + expect(utils.addFinalNewlineIfNeeded(input)).toEqual(output); + }); + }); + }); +}); diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js index 2b7dffdcd88..9d7926a4d06 100644 --- a/spec/frontend/ide/utils_spec.js +++ b/spec/frontend/ide/utils_spec.js @@ -26,15 +26,18 @@ describe('WebIDE utils', () => { entry.deleted = true; expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.deleted); }); + it('renders "addition" icon for temp entries', () => { entry.tempFile = true; expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.addition); }); + it('renders "modified" icon for newly-renamed entries', () => { entry.prevPath = 'foo/bar'; entry.tempFile = false; expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.modified); }); + it('renders "modified" icon even for temp entries if they are newly-renamed', () => { entry.prevPath = 'foo/bar'; entry.tempFile = true; |