Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-12-07 03:07:51 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2019-12-07 03:07:51 +0300
commit4e375367b78bb44bd00957522cd9fc3e6d403fef (patch)
tree059b1ce541e4128bf03683407d7b5bbbc2094ed5 /spec/frontend
parent99ddca0d88f1e4e49d61b1aa9d41b5785528d1dc (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/ide/components/branches/item_spec.js56
-rw-r--r--spec/frontend/ide/components/branches/search_list_spec.js2
-rw-r--r--spec/frontend/ide/components/merge_requests/list_spec.js2
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js89
-rw-r--r--spec/frontend/ide/components/pipelines/list_spec.js4
-rw-r--r--spec/frontend/ide/components/preview/navigator_spec.js167
-rw-r--r--spec/frontend/ide/helpers.js52
-rw-r--r--spec/frontend/ide/ide_router_spec.js44
-rw-r--r--spec/frontend/ide/stores/getters_spec.js313
-rw-r--r--spec/frontend/ide/stores/modules/branches/actions_spec.js163
-rw-r--r--spec/frontend/ide/stores/modules/branches/mutations_spec.js51
-rw-r--r--spec/frontend/ide/stores/modules/commit/getters_spec.js295
-rw-r--r--spec/frontend/ide/stores/modules/file_templates/actions_spec.js346
-rw-r--r--spec/frontend/ide/stores/modules/merge_requests/actions_spec.js209
-rw-r--r--spec/frontend/ide/stores/modules/merge_requests/mutations_spec.js56
-rw-r--r--spec/frontend/ide/stores/modules/pane/actions_spec.js66
-rw-r--r--spec/frontend/ide/stores/modules/pipelines/actions_spec.js441
-rw-r--r--spec/frontend/ide/stores/modules/pipelines/mutations_spec.js213
-rw-r--r--spec/frontend/ide/stores/mutations/file_spec.js444
-rw-r--r--spec/frontend/ide/stores/mutations/tree_spec.js118
-rw-r--r--spec/frontend/ide/stores/mutations_spec.js747
-rw-r--r--spec/frontend/ide/stores/utils_spec.js696
-rw-r--r--spec/frontend/ide/utils_spec.js3
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;