summaryrefslogtreecommitdiff
path: root/spec/frontend/ide/stores
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 11:18:50 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 11:18:50 +0000
commit8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781 (patch)
treea77e7fe7a93de11213032ed4ab1f33a3db51b738 /spec/frontend/ide/stores
parent00b35af3db1abfe813a778f643dad221aad51fca (diff)
downloadgitlab-ce-8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781.tar.gz
Add latest changes from gitlab-org/gitlab@13-1-stable-ee
Diffstat (limited to 'spec/frontend/ide/stores')
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js40
-rw-r--r--spec/frontend/ide/stores/actions/merge_request_spec.js504
-rw-r--r--spec/frontend/ide/stores/actions/project_spec.js397
-rw-r--r--spec/frontend/ide/stores/actions/tree_spec.js218
-rw-r--r--spec/frontend/ide/stores/actions_spec.js1062
-rw-r--r--spec/frontend/ide/stores/extend_spec.js74
-rw-r--r--spec/frontend/ide/stores/getters_spec.js65
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js598
-rw-r--r--spec/frontend/ide/stores/modules/pane/getters_spec.js32
-rw-r--r--spec/frontend/ide/stores/modules/router/actions_spec.js19
-rw-r--r--spec/frontend/ide/stores/modules/router/mutations_spec.js23
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js289
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js300
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js169
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/setup_spec.js40
-rw-r--r--spec/frontend/ide/stores/modules/terminal/getters_spec.js50
-rw-r--r--spec/frontend/ide/stores/modules/terminal/messages_spec.js38
-rw-r--r--spec/frontend/ide/stores/modules/terminal/mutations_spec.js142
-rw-r--r--spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js118
-rw-r--r--spec/frontend/ide/stores/modules/terminal_sync/mutations_spec.js89
-rw-r--r--spec/frontend/ide/stores/mutations/file_spec.js37
-rw-r--r--spec/frontend/ide/stores/mutations_spec.js36
-rw-r--r--spec/frontend/ide/stores/plugins/terminal_spec.js58
-rw-r--r--spec/frontend/ide/stores/plugins/terminal_sync_spec.js72
-rw-r--r--spec/frontend/ide/stores/utils_spec.js93
25 files changed, 4339 insertions, 224 deletions
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index 43cb06f5d92..e2dc7626c67 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -5,7 +5,7 @@ import { createStore } from '~/ide/stores';
import * as actions from '~/ide/stores/actions/file';
import * as types from '~/ide/stores/mutation_types';
import service from '~/ide/services';
-import router from '~/ide/ide_router';
+import { createRouter } from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
import { file } from '../../helpers';
@@ -16,6 +16,7 @@ describe('IDE store file actions', () => {
let mock;
let originalGon;
let store;
+ let router;
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -26,6 +27,7 @@ describe('IDE store file actions', () => {
};
store = createStore();
+ router = createRouter(store);
jest.spyOn(store, 'commit');
jest.spyOn(store, 'dispatch');
@@ -44,7 +46,6 @@ describe('IDE store file actions', () => {
localFile = file('testFile');
localFile.active = true;
localFile.opened = true;
- localFile.parentTreeUrl = 'parentTreeUrl';
store.state.openFiles.push(localFile);
store.state.entries[localFile.path] = localFile;
@@ -254,13 +255,8 @@ describe('IDE store file actions', () => {
mock.onGet(`${RELATIVE_URL_ROOT}/test/test/-/7297abc/${localFile.path}`).replyOnce(
200,
{
- blame_path: 'blame_path',
- commits_path: 'commits_path',
- permalink: 'permalink',
raw_path: 'raw_path',
binary: false,
- html: '123',
- render_error: '',
},
{
'page-title': 'testing getFileData',
@@ -281,17 +277,6 @@ describe('IDE store file actions', () => {
.catch(done.fail);
});
- it('sets the file data', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(localFile.blamePath).toBe('blame_path');
-
- done();
- })
- .catch(done.fail);
- });
-
it('sets document title with the branchId', done => {
store
.dispatch('getFileData', { path: localFile.path })
@@ -348,13 +333,8 @@ describe('IDE store file actions', () => {
mock.onGet(`${RELATIVE_URL_ROOT}/test/test/-/7297abc/old-dull-file`).replyOnce(
200,
{
- blame_path: 'blame_path',
- commits_path: 'commits_path',
- permalink: 'permalink',
raw_path: 'raw_path',
binary: false,
- html: '123',
- render_error: '',
},
{
'page-title': 'testing old-dull-file',
@@ -587,20 +567,6 @@ describe('IDE store file actions', () => {
})
.catch(done.fail);
});
-
- it('bursts unused seal', done => {
- store
- .dispatch('changeFileContent', {
- path: tmpFile.path,
- content: 'content',
- })
- .then(() => {
- expect(store.state.unusedSeal).toBe(false);
-
- done();
- })
- .catch(done.fail);
- });
});
describe('with changed file', () => {
diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js
new file mode 100644
index 00000000000..cb4eebd97d9
--- /dev/null
+++ b/spec/frontend/ide/stores/actions/merge_request_spec.js
@@ -0,0 +1,504 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import store from '~/ide/stores';
+import createFlash from '~/flash';
+import {
+ getMergeRequestData,
+ getMergeRequestChanges,
+ getMergeRequestVersions,
+ openMergeRequest,
+} from '~/ide/stores/actions/merge_request';
+import service from '~/ide/services';
+import { leftSidebarViews, PERMISSION_READ_MR } from '~/ide/constants';
+import { resetStore } from '../../helpers';
+
+const TEST_PROJECT = 'abcproject';
+const TEST_PROJECT_ID = 17;
+
+jest.mock('~/flash');
+
+describe('IDE store merge request actions', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ store.state.projects[TEST_PROJECT] = {
+ id: TEST_PROJECT_ID,
+ mergeRequests: {},
+ userPermissions: {
+ [PERMISSION_READ_MR]: true,
+ },
+ };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ resetStore(store);
+ });
+
+ describe('getMergeRequestsForBranch', () => {
+ describe('success', () => {
+ const mrData = { iid: 2, source_branch: 'bar' };
+ const mockData = [mrData];
+
+ describe('base case', () => {
+ beforeEach(() => {
+ jest.spyOn(service, 'getProjectMergeRequests');
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, mockData);
+ });
+
+ it('calls getProjectMergeRequests service method', done => {
+ store
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
+ .then(() => {
+ expect(service.getProjectMergeRequests).toHaveBeenCalledWith(TEST_PROJECT, {
+ source_branch: 'bar',
+ source_project_id: TEST_PROJECT_ID,
+ order_by: 'created_at',
+ per_page: 1,
+ });
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the "Merge Request" Object', done => {
+ store
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests).toEqual({
+ '2': expect.objectContaining(mrData),
+ });
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets "Current Merge Request" object to the most recent MR', done => {
+ store
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
+ .then(() => {
+ expect(store.state.currentMergeRequestId).toEqual('2');
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does nothing if user cannot read MRs', done => {
+ store.state.projects[TEST_PROJECT].userPermissions[PERMISSION_READ_MR] = false;
+
+ store
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
+ .then(() => {
+ expect(service.getProjectMergeRequests).not.toHaveBeenCalled();
+ expect(store.state.currentMergeRequestId).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('no merge requests for branch available case', () => {
+ beforeEach(() => {
+ jest.spyOn(service, 'getProjectMergeRequests');
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, []);
+ });
+
+ it('does not fail if there are no merge requests for current branch', done => {
+ store
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'foo' })
+ .then(() => {
+ expect(store.state.projects[TEST_PROJECT].mergeRequests).toEqual({});
+ expect(store.state.currentMergeRequestId).toEqual('');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).networkError();
+ });
+
+ it('flashes message, if error', done => {
+ store
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
+ .catch(() => {
+ expect(createFlash).toHaveBeenCalled();
+ expect(createFlash.mock.calls[0][0]).toBe('Error fetching merge requests for bar');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('getMergeRequestData', () => {
+ describe('success', () => {
+ beforeEach(() => {
+ jest.spyOn(service, 'getProjectMergeRequestData');
+
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/)
+ .reply(200, { title: 'mergerequest' });
+ });
+
+ it('calls getProjectMergeRequestData service method', done => {
+ store
+ .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestData).toHaveBeenCalledWith(TEST_PROJECT, 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Object', done => {
+ store
+ .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.currentMergeRequestId).toBe(1);
+ expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].title).toBe(
+ 'mergerequest',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jest.fn();
+
+ getMergeRequestData(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ { projectId: TEST_PROJECT, mergeRequestId: 1 },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occurred while loading the merge request.',
+ action: expect.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: TEST_PROJECT,
+ mergeRequestId: 1,
+ force: false,
+ },
+ });
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getMergeRequestChanges', () => {
+ beforeEach(() => {
+ store.state.projects[TEST_PROJECT].mergeRequests['1'] = { changes: [] };
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ jest.spyOn(service, 'getProjectMergeRequestChanges');
+
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/)
+ .reply(200, { title: 'mergerequest' });
+ });
+
+ it('calls getProjectMergeRequestChanges service method', done => {
+ store
+ .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith(TEST_PROJECT, 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Changes Object', done => {
+ store
+ .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].changes.title).toBe(
+ 'mergerequest',
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jest.fn();
+
+ getMergeRequestChanges(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ { projectId: TEST_PROJECT, mergeRequestId: 1 },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occurred while loading the merge request changes.',
+ action: expect.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: TEST_PROJECT,
+ mergeRequestId: 1,
+ force: false,
+ },
+ });
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getMergeRequestVersions', () => {
+ beforeEach(() => {
+ store.state.projects[TEST_PROJECT].mergeRequests['1'] = { versions: [] };
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/)
+ .reply(200, [{ id: 789 }]);
+ jest.spyOn(service, 'getProjectMergeRequestVersions');
+ });
+
+ it('calls getProjectMergeRequestVersions service method', done => {
+ store
+ .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith(TEST_PROJECT, 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Versions Object', done => {
+ store
+ .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].versions.length).toBe(1);
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jest.fn();
+
+ getMergeRequestVersions(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ { projectId: TEST_PROJECT, mergeRequestId: 1 },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occurred while loading the merge request version data.',
+ action: expect.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: TEST_PROJECT,
+ mergeRequestId: 1,
+ force: false,
+ },
+ });
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('openMergeRequest', () => {
+ const mr = {
+ projectId: TEST_PROJECT,
+ targetProjectId: 'defproject',
+ mergeRequestId: 2,
+ };
+ let testMergeRequest;
+ let testMergeRequestChanges;
+
+ const mockGetters = { findBranch: () => ({ commit: { id: 'abcd2322' } }) };
+
+ beforeEach(() => {
+ testMergeRequest = {
+ source_branch: 'abcbranch',
+ };
+ testMergeRequestChanges = {
+ changes: [],
+ };
+ store.state.entries = {
+ foo: {
+ type: 'blob',
+ },
+ bar: {
+ type: 'blob',
+ },
+ };
+
+ store.state.currentProjectId = 'test/test';
+ store.state.currentBranchId = 'master';
+
+ store.state.projects['test/test'] = {
+ branches: {
+ master: {
+ commit: {
+ id: '7297abc',
+ },
+ },
+ abcbranch: {
+ commit: {
+ id: '29020fc',
+ },
+ },
+ },
+ };
+
+ const originalDispatch = store.dispatch;
+
+ jest.spyOn(store, 'dispatch').mockImplementation((type, payload) => {
+ switch (type) {
+ case 'getMergeRequestData':
+ return Promise.resolve(testMergeRequest);
+ case 'getMergeRequestChanges':
+ return Promise.resolve(testMergeRequestChanges);
+ case 'getFiles':
+ case 'getMergeRequestVersions':
+ case 'getBranchData':
+ case 'setFileMrChange':
+ return Promise.resolve();
+ default:
+ return originalDispatch(type, payload);
+ }
+ });
+ jest.spyOn(service, 'getFileData').mockImplementation(() =>
+ Promise.resolve({
+ headers: {},
+ }),
+ );
+ });
+
+ it('dispatches actions for merge request data', done => {
+ openMergeRequest({ state: store.state, dispatch: store.dispatch, getters: mockGetters }, mr)
+ .then(() => {
+ expect(store.dispatch.mock.calls).toEqual([
+ ['getMergeRequestData', mr],
+ ['setCurrentBranchId', testMergeRequest.source_branch],
+ [
+ 'getBranchData',
+ {
+ projectId: mr.projectId,
+ branchId: testMergeRequest.source_branch,
+ },
+ ],
+ [
+ 'getFiles',
+ {
+ projectId: mr.projectId,
+ branchId: testMergeRequest.source_branch,
+ ref: 'abcd2322',
+ },
+ ],
+ ['getMergeRequestVersions', mr],
+ ['getMergeRequestChanges', mr],
+ ]);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('updates activity bar view and gets file data, if changes are found', done => {
+ store.state.entries.foo = {
+ url: 'test',
+ type: 'blob',
+ };
+ store.state.entries.bar = {
+ url: 'test',
+ type: 'blob',
+ };
+
+ testMergeRequestChanges.changes = [
+ { new_path: 'foo', path: 'foo' },
+ { new_path: 'bar', path: 'bar' },
+ ];
+
+ openMergeRequest({ state: store.state, dispatch: store.dispatch, getters: mockGetters }, mr)
+ .then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'updateActivityBarView',
+ leftSidebarViews.review.name,
+ );
+
+ testMergeRequestChanges.changes.forEach((change, i) => {
+ expect(store.dispatch).toHaveBeenCalledWith('setFileMrChange', {
+ file: store.state.entries[change.new_path],
+ mrChange: change,
+ });
+
+ expect(store.dispatch).toHaveBeenCalledWith('getFileData', {
+ path: change.new_path,
+ makeFileActive: i === 0,
+ openFile: true,
+ });
+ });
+
+ expect(store.state.openFiles.length).toBe(testMergeRequestChanges.changes.length);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('flashes message, if error', done => {
+ store.dispatch.mockRejectedValue();
+
+ openMergeRequest(store, mr)
+ .catch(() => {
+ expect(createFlash).toHaveBeenCalledWith(expect.any(String));
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/actions/project_spec.js b/spec/frontend/ide/stores/actions/project_spec.js
new file mode 100644
index 00000000000..64024c12903
--- /dev/null
+++ b/spec/frontend/ide/stores/actions/project_spec.js
@@ -0,0 +1,397 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { createStore } from '~/ide/stores';
+import {
+ refreshLastCommitData,
+ showBranchNotFoundError,
+ createNewBranchFromDefault,
+ loadEmptyBranch,
+ openBranch,
+ loadFile,
+ loadBranch,
+} from '~/ide/stores/actions';
+import service from '~/ide/services';
+import api from '~/api';
+import testAction from 'helpers/vuex_action_helper';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+
+const TEST_PROJECT_ID = 'abc/def';
+
+describe('IDE store project actions', () => {
+ let mock;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ mock = new MockAdapter(axios);
+
+ store.state.projects[TEST_PROJECT_ID] = {
+ branches: {},
+ };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('refreshLastCommitData', () => {
+ beforeEach(() => {
+ store.state.currentProjectId = 'abc/def';
+ store.state.currentBranchId = 'master';
+ store.state.projects['abc/def'] = {
+ id: 4,
+ branches: {
+ master: {
+ commit: null,
+ },
+ },
+ };
+ jest.spyOn(service, 'getBranchData').mockResolvedValue({
+ data: {
+ commit: { id: '123' },
+ },
+ });
+ });
+
+ it('calls the service', done => {
+ store
+ .dispatch('refreshLastCommitData', {
+ projectId: store.state.currentProjectId,
+ branchId: store.state.currentBranchId,
+ })
+ .then(() => {
+ expect(service.getBranchData).toHaveBeenCalledWith('abc/def', 'master');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('commits getBranchData', done => {
+ testAction(
+ refreshLastCommitData,
+ {
+ projectId: store.state.currentProjectId,
+ branchId: store.state.currentBranchId,
+ },
+ store.state,
+ // mutations
+ [
+ {
+ type: 'SET_BRANCH_COMMIT',
+ payload: {
+ projectId: TEST_PROJECT_ID,
+ branchId: 'master',
+ commit: { id: '123' },
+ },
+ },
+ ],
+ // action
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('showBranchNotFoundError', () => {
+ it('dispatches setErrorMessage', done => {
+ testAction(
+ showBranchNotFoundError,
+ 'master',
+ null,
+ [],
+ [
+ {
+ type: 'setErrorMessage',
+ payload: {
+ text: "Branch <strong>master</strong> was not found in this project's repository.",
+ action: expect.any(Function),
+ actionText: 'Create branch',
+ actionPayload: 'master',
+ },
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('createNewBranchFromDefault', () => {
+ useMockLocationHelper();
+
+ beforeEach(() => {
+ jest.spyOn(api, 'createBranch').mockResolvedValue();
+ });
+
+ it('calls API', done => {
+ createNewBranchFromDefault(
+ {
+ state: {
+ currentProjectId: 'project-path',
+ },
+ getters: {
+ currentProject: {
+ default_branch: 'master',
+ },
+ },
+ dispatch() {},
+ },
+ 'new-branch-name',
+ )
+ .then(() => {
+ expect(api.createBranch).toHaveBeenCalledWith('project-path', {
+ ref: 'master',
+ branch: 'new-branch-name',
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('clears error message', done => {
+ const dispatchSpy = jest.fn().mockName('dispatch');
+
+ createNewBranchFromDefault(
+ {
+ state: {
+ currentProjectId: 'project-path',
+ },
+ getters: {
+ currentProject: {
+ default_branch: 'master',
+ },
+ },
+ dispatch: dispatchSpy,
+ },
+ 'new-branch-name',
+ )
+ .then(() => {
+ expect(dispatchSpy).toHaveBeenCalledWith('setErrorMessage', null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('reloads window', done => {
+ createNewBranchFromDefault(
+ {
+ state: {
+ currentProjectId: 'project-path',
+ },
+ getters: {
+ currentProject: {
+ default_branch: 'master',
+ },
+ },
+ dispatch() {},
+ },
+ 'new-branch-name',
+ )
+ .then(() => {
+ expect(window.location.reload).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('loadEmptyBranch', () => {
+ it('creates a blank tree and sets loading state to false', done => {
+ testAction(
+ loadEmptyBranch,
+ { projectId: TEST_PROJECT_ID, branchId: 'master' },
+ store.state,
+ [
+ { type: 'CREATE_TREE', payload: { treePath: `${TEST_PROJECT_ID}/master` } },
+ {
+ type: 'TOGGLE_LOADING',
+ payload: { entry: store.state.trees[`${TEST_PROJECT_ID}/master`], forceValue: false },
+ },
+ ],
+ expect.any(Object),
+ done,
+ );
+ });
+
+ it('does nothing, if tree already exists', done => {
+ const trees = { [`${TEST_PROJECT_ID}/master`]: [] };
+
+ testAction(
+ loadEmptyBranch,
+ { projectId: TEST_PROJECT_ID, branchId: 'master' },
+ { trees },
+ [],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('loadFile', () => {
+ beforeEach(() => {
+ Object.assign(store.state, {
+ entries: {
+ foo: { pending: false },
+ 'foo/bar-pending': { pending: true },
+ 'foo/bar': { pending: false },
+ },
+ });
+ jest.spyOn(store, 'dispatch').mockImplementation();
+ });
+
+ it('does nothing, if basePath is not given', () => {
+ loadFile(store, { basePath: undefined });
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+
+ it('handles tree entry action, if basePath is given and the entry is not pending', () => {
+ loadFile(store, { basePath: 'foo/bar/' });
+
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'handleTreeEntryAction',
+ store.state.entries['foo/bar'],
+ );
+ });
+
+ it('does not handle tree entry action, if entry is pending', () => {
+ loadFile(store, { basePath: 'foo/bar-pending/' });
+
+ expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', expect.anything());
+ });
+
+ it('creates a new temp file supplied via URL if the file does not exist yet', () => {
+ loadFile(store, { basePath: 'not-existent.md' });
+
+ expect(store.dispatch.mock.calls).toHaveLength(1);
+
+ expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', expect.anything());
+
+ expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
+ name: 'not-existent.md',
+ type: 'blob',
+ });
+ });
+ });
+
+ describe('loadBranch', () => {
+ const projectId = TEST_PROJECT_ID;
+ const branchId = '123-lorem';
+ const ref = 'abcd2322';
+
+ it('when empty repo, loads empty branch', done => {
+ const mockGetters = { emptyRepo: true };
+
+ testAction(
+ loadBranch,
+ { projectId, branchId },
+ { ...store.state, ...mockGetters },
+ [],
+ [{ type: 'loadEmptyBranch', payload: { projectId, branchId } }],
+ done,
+ );
+ });
+
+ it('when branch already exists, does nothing', done => {
+ store.state.projects[projectId].branches[branchId] = {};
+
+ testAction(loadBranch, { projectId, branchId }, store.state, [], [], done);
+ });
+
+ it('fetches branch data', done => {
+ const mockGetters = { findBranch: () => ({ commit: { id: ref } }) };
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+
+ loadBranch(
+ { getters: mockGetters, state: store.state, dispatch: store.dispatch },
+ { projectId, branchId },
+ )
+ .then(() => {
+ expect(store.dispatch.mock.calls).toEqual([
+ ['getBranchData', { projectId, branchId }],
+ ['getMergeRequestsForBranch', { projectId, branchId }],
+ ['getFiles', { projectId, branchId, ref }],
+ ]);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('shows an error if branch can not be fetched', done => {
+ jest.spyOn(store, 'dispatch').mockReturnValue(Promise.reject());
+
+ loadBranch(store, { projectId, branchId })
+ .then(done.fail)
+ .catch(() => {
+ expect(store.dispatch.mock.calls).toEqual([
+ ['getBranchData', { projectId, branchId }],
+ ['showBranchNotFoundError', branchId],
+ ]);
+ done();
+ });
+ });
+ });
+
+ describe('openBranch', () => {
+ const projectId = TEST_PROJECT_ID;
+ const branchId = '123-lorem';
+
+ const branch = {
+ projectId,
+ branchId,
+ };
+
+ beforeEach(() => {
+ Object.assign(store.state, {
+ entries: {
+ foo: { pending: false },
+ 'foo/bar-pending': { pending: true },
+ 'foo/bar': { pending: false },
+ },
+ });
+ });
+
+ describe('existing branch', () => {
+ beforeEach(() => {
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+ });
+
+ it('dispatches branch actions', done => {
+ openBranch(store, branch)
+ .then(() => {
+ expect(store.dispatch.mock.calls).toEqual([
+ ['setCurrentBranchId', branchId],
+ ['loadBranch', { projectId, branchId }],
+ ['loadFile', { basePath: undefined }],
+ ]);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('non-existent branch', () => {
+ beforeEach(() => {
+ jest.spyOn(store, 'dispatch').mockReturnValue(Promise.reject());
+ });
+
+ it('dispatches correct branch actions', done => {
+ openBranch(store, branch)
+ .then(val => {
+ expect(store.dispatch.mock.calls).toEqual([
+ ['setCurrentBranchId', branchId],
+ ['loadBranch', { projectId, branchId }],
+ ]);
+
+ expect(val).toEqual(
+ new Error(
+ `An error occurred while getting files for - <strong>${projectId}/${branchId}</strong>`,
+ ),
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/actions/tree_spec.js b/spec/frontend/ide/stores/actions/tree_spec.js
new file mode 100644
index 00000000000..44e2fcab436
--- /dev/null
+++ b/spec/frontend/ide/stores/actions/tree_spec.js
@@ -0,0 +1,218 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import { showTreeEntry, getFiles, setDirectoryData } from '~/ide/stores/actions/tree';
+import * as types from '~/ide/stores/mutation_types';
+import axios from '~/lib/utils/axios_utils';
+import { createStore } from '~/ide/stores';
+import service from '~/ide/services';
+import { createRouter } from '~/ide/ide_router';
+import { file, createEntriesFromPaths } from '../../helpers';
+
+describe('Multi-file store tree actions', () => {
+ let projectTree;
+ let mock;
+ let store;
+ let router;
+
+ const basicCallParameters = {
+ endpoint: 'rootEndpoint',
+ projectId: 'abcproject',
+ branch: 'master',
+ branchId: 'master',
+ ref: '12345678',
+ };
+
+ beforeEach(() => {
+ store = createStore();
+ router = createRouter(store);
+ jest.spyOn(router, 'push').mockImplementation();
+
+ mock = new MockAdapter(axios);
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ web_url: '',
+ path_with_namespace: 'foo/abcproject',
+ };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('getFiles', () => {
+ describe('success', () => {
+ beforeEach(() => {
+ jest.spyOn(service, 'getFiles');
+
+ mock
+ .onGet(/(.*)/)
+ .replyOnce(200, [
+ 'file.txt',
+ 'folder/fileinfolder.js',
+ 'folder/subfolder/fileinsubfolder.js',
+ ]);
+ });
+
+ it('calls service getFiles', () => {
+ return (
+ store
+ .dispatch('getFiles', basicCallParameters)
+ // getFiles actions calls lodash.defer
+ .then(() => jest.runOnlyPendingTimers())
+ .then(() => {
+ expect(service.getFiles).toHaveBeenCalledWith('foo/abcproject', '12345678');
+ })
+ );
+ });
+
+ it('adds data into tree', done => {
+ store
+ .dispatch('getFiles', basicCallParameters)
+ .then(() => {
+ // The populating of the tree is deferred for performance reasons.
+ // See this merge request for details: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/25700
+ jest.advanceTimersByTime(1);
+ })
+ .then(() => {
+ projectTree = store.state.trees['abcproject/master'];
+
+ expect(projectTree.tree.length).toBe(2);
+ expect(projectTree.tree[0].type).toBe('tree');
+ expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
+ expect(projectTree.tree[1].type).toBe('blob');
+ expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
+ expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('error', () => {
+ it('dispatches error action', done => {
+ const dispatch = jest.fn();
+
+ store.state.projects = {
+ 'abc/def': {
+ web_url: `${gl.TEST_HOST}/files`,
+ branches: {
+ 'master-testing': {
+ commit: {
+ id: '12345',
+ },
+ },
+ },
+ },
+ };
+ const getters = {
+ findBranch: () => store.state.projects['abc/def'].branches['master-testing'],
+ };
+
+ mock.onGet(/(.*)/).replyOnce(500);
+
+ getFiles(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ getters,
+ },
+ {
+ projectId: 'abc/def',
+ branchId: 'master-testing',
+ },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occurred while loading all the files.',
+ action: expect.any(Function),
+ actionText: 'Please try again',
+ actionPayload: { projectId: 'abc/def', branchId: 'master-testing' },
+ });
+ done();
+ });
+ });
+ });
+ });
+
+ describe('toggleTreeOpen', () => {
+ let tree;
+
+ beforeEach(() => {
+ tree = file('testing', '1', 'tree');
+ store.state.entries[tree.path] = tree;
+ });
+
+ it('toggles the tree open', done => {
+ store
+ .dispatch('toggleTreeOpen', tree.path)
+ .then(() => {
+ expect(tree.opened).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('showTreeEntry', () => {
+ beforeEach(() => {
+ const paths = [
+ 'grandparent',
+ 'ancestor',
+ 'grandparent/parent',
+ 'grandparent/aunt',
+ 'grandparent/parent/child.txt',
+ 'grandparent/aunt/cousing.txt',
+ ];
+
+ Object.assign(store.state.entries, createEntriesFromPaths(paths));
+ });
+
+ it('opens the parents', done => {
+ testAction(
+ showTreeEntry,
+ 'grandparent/parent/child.txt',
+ store.state,
+ [{ type: types.SET_TREE_OPEN, payload: 'grandparent/parent' }],
+ [{ type: 'showTreeEntry', payload: 'grandparent/parent' }],
+ done,
+ );
+ });
+ });
+
+ describe('setDirectoryData', () => {
+ it('sets tree correctly if there are no opened files yet', done => {
+ const treeFile = file({ name: 'README.md' });
+ store.state.trees['abcproject/master'] = {};
+
+ testAction(
+ setDirectoryData,
+ { projectId: 'abcproject', branchId: 'master', treeList: [treeFile] },
+ store.state,
+ [
+ {
+ type: types.SET_DIRECTORY_DATA,
+ payload: {
+ treePath: 'abcproject/master',
+ data: [treeFile],
+ },
+ },
+ {
+ type: types.TOGGLE_LOADING,
+ payload: {
+ entry: {},
+ forceValue: false,
+ },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js
new file mode 100644
index 00000000000..f77dbd80025
--- /dev/null
+++ b/spec/frontend/ide/stores/actions_spec.js
@@ -0,0 +1,1062 @@
+import MockAdapter from 'axios-mock-adapter';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { createStore } from '~/ide/stores';
+import { createRouter } from '~/ide/ide_router';
+import {
+ stageAllChanges,
+ unstageAllChanges,
+ toggleFileFinder,
+ setCurrentBranchId,
+ setEmptyStateSvgs,
+ updateActivityBarView,
+ updateTempFlagForEntry,
+ setErrorMessage,
+ deleteEntry,
+ renameEntry,
+ getBranchData,
+ createTempEntry,
+ discardAllChanges,
+} from '~/ide/stores/actions';
+import axios from '~/lib/utils/axios_utils';
+import * as types from '~/ide/stores/mutation_types';
+import { file } from '../helpers';
+import testAction from '../../helpers/vuex_action_helper';
+import eventHub from '~/ide/eventhub';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+ joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
+}));
+
+describe('Multi-file store actions', () => {
+ let store;
+ let router;
+
+ beforeEach(() => {
+ store = createStore();
+ router = createRouter(store);
+
+ jest.spyOn(store, 'commit');
+ jest.spyOn(store, 'dispatch');
+ jest.spyOn(router, 'push').mockImplementation();
+ });
+
+ describe('redirectToUrl', () => {
+ it('calls visitUrl', done => {
+ store
+ .dispatch('redirectToUrl', 'test')
+ .then(() => {
+ expect(visitUrl).toHaveBeenCalledWith('test');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('setInitialData', () => {
+ it('commits initial data', done => {
+ store
+ .dispatch('setInitialData', { canCommit: true })
+ .then(() => {
+ expect(store.state.canCommit).toBeTruthy();
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('discardAllChanges', () => {
+ const paths = ['to_discard', 'another_one_to_discard'];
+
+ beforeEach(() => {
+ paths.forEach(path => {
+ const f = file(path);
+ f.changed = true;
+
+ store.state.openFiles.push(f);
+ store.state.changedFiles.push(f);
+ store.state.entries[f.path] = f;
+ });
+ });
+
+ it('discards all changes in file', () => {
+ const expectedCalls = paths.map(path => ['restoreOriginalFile', path]);
+
+ discardAllChanges(store);
+
+ expect(store.dispatch.mock.calls).toEqual(expect.arrayContaining(expectedCalls));
+ });
+
+ it('removes all files from changedFiles state', done => {
+ store
+ .dispatch('discardAllChanges')
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(0);
+ expect(store.state.openFiles.length).toBe(2);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('createTempEntry', () => {
+ beforeEach(() => {
+ document.body.innerHTML += '<div class="flash-container"></div>';
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'mybranch';
+
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+ store.state.projects.abcproject = {
+ web_url: '',
+ };
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ describe('tree', () => {
+ it('creates temp tree', done => {
+ store
+ .dispatch('createTempEntry', {
+ branchId: store.state.currentBranchId,
+ name: 'test',
+ type: 'tree',
+ })
+ .then(() => {
+ const entry = store.state.entries.test;
+
+ expect(entry).not.toBeNull();
+ expect(entry.type).toBe('tree');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('creates new folder inside another tree', done => {
+ const tree = {
+ type: 'tree',
+ name: 'testing',
+ path: 'testing',
+ tree: [],
+ };
+
+ store.state.entries[tree.path] = tree;
+
+ store
+ .dispatch('createTempEntry', {
+ branchId: store.state.currentBranchId,
+ name: 'testing/test',
+ type: 'tree',
+ })
+ .then(() => {
+ expect(tree.tree[0].tempFile).toBeTruthy();
+ expect(tree.tree[0].name).toBe('test');
+ expect(tree.tree[0].type).toBe('tree');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not create new tree if already exists', done => {
+ const tree = {
+ type: 'tree',
+ path: 'testing',
+ tempFile: false,
+ tree: [],
+ };
+
+ store.state.entries[tree.path] = tree;
+
+ store
+ .dispatch('createTempEntry', {
+ branchId: store.state.currentBranchId,
+ name: 'testing',
+ type: 'tree',
+ })
+ .then(() => {
+ expect(store.state.entries[tree.path].tempFile).toEqual(false);
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('blob', () => {
+ it('creates temp file', done => {
+ const name = 'test';
+
+ store
+ .dispatch('createTempEntry', {
+ name,
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(() => {
+ const f = store.state.entries[name];
+
+ expect(f.tempFile).toBeTruthy();
+ expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds tmp file to open files', done => {
+ const name = 'test';
+
+ store
+ .dispatch('createTempEntry', {
+ name,
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(() => {
+ const f = store.state.entries[name];
+
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(f.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds tmp file to staged files', done => {
+ const name = 'test';
+
+ store
+ .dispatch('createTempEntry', {
+ name,
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(() => {
+ expect(store.state.stagedFiles).toEqual([expect.objectContaining({ name })]);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets tmp file as active', () => {
+ createTempEntry(store, { name: 'test', branchId: 'mybranch', type: 'blob' });
+
+ expect(store.dispatch).toHaveBeenCalledWith('setFileActive', 'test');
+ });
+
+ it('creates flash message if file already exists', done => {
+ const f = file('test', '1', 'blob');
+ store.state.trees['abcproject/mybranch'].tree = [f];
+ store.state.entries[f.path] = f;
+
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(() => {
+ expect(document.querySelector('.flash-alert')?.textContent.trim()).toEqual(
+ `The name "${f.name}" is already taken in this directory.`,
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('scrollToTab', () => {
+ it('focuses the current active element', done => {
+ document.body.innerHTML +=
+ '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>';
+ const el = document.querySelector('.repo-tab');
+ jest.spyOn(el, 'focus').mockImplementation();
+
+ store
+ .dispatch('scrollToTab')
+ .then(() => {
+ setImmediate(() => {
+ expect(el.focus).toHaveBeenCalled();
+
+ document.getElementById('tabs').remove();
+
+ done();
+ });
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('stage/unstageAllChanges', () => {
+ let file1;
+ let file2;
+
+ beforeEach(() => {
+ file1 = { ...file('test'), content: 'changed test', raw: 'test' };
+ file2 = { ...file('test2'), content: 'changed test2', raw: 'test2' };
+
+ store.state.openFiles = [file1];
+ store.state.changedFiles = [file1];
+ store.state.stagedFiles = [{ ...file2, content: 'staged test' }];
+
+ store.state.entries = {
+ [file1.path]: { ...file1 },
+ [file2.path]: { ...file2 },
+ };
+ });
+
+ describe('stageAllChanges', () => {
+ it('adds all files from changedFiles to stagedFiles', () => {
+ stageAllChanges(store);
+
+ expect(store.commit.mock.calls).toEqual(
+ expect.arrayContaining([
+ [types.SET_LAST_COMMIT_MSG, ''],
+ [types.STAGE_CHANGE, expect.objectContaining({ path: file1.path })],
+ ]),
+ );
+ });
+
+ it('opens pending tab if a change exists in that file', () => {
+ stageAllChanges(store);
+
+ expect(store.dispatch.mock.calls).toEqual([
+ [
+ 'openPendingTab',
+ { file: { ...file1, staged: true, changed: true }, keyPrefix: 'staged' },
+ ],
+ ]);
+ });
+
+ it('does not open pending tab if no change exists in that file', () => {
+ store.state.entries[file1.path].content = 'test';
+ store.state.stagedFiles = [file1];
+ store.state.changedFiles = [store.state.entries[file1.path]];
+
+ stageAllChanges(store);
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('unstageAllChanges', () => {
+ it('removes all files from stagedFiles after unstaging', () => {
+ unstageAllChanges(store);
+
+ expect(store.commit.mock.calls).toEqual(
+ expect.arrayContaining([
+ [types.UNSTAGE_CHANGE, expect.objectContaining({ path: file2.path })],
+ ]),
+ );
+ });
+
+ it('opens pending tab if a change exists in that file', () => {
+ unstageAllChanges(store);
+
+ expect(store.dispatch.mock.calls).toEqual([
+ ['openPendingTab', { file: file1, keyPrefix: 'unstaged' }],
+ ]);
+ });
+
+ it('does not open pending tab if no change exists in that file', () => {
+ store.state.entries[file1.path].content = 'test';
+ store.state.stagedFiles = [file1];
+ store.state.changedFiles = [store.state.entries[file1.path]];
+
+ unstageAllChanges(store);
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('updateViewer', () => {
+ it('updates viewer state', done => {
+ store
+ .dispatch('updateViewer', 'diff')
+ .then(() => {
+ expect(store.state.viewer).toBe('diff');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateActivityBarView', () => {
+ it('commits UPDATE_ACTIVITY_BAR_VIEW', done => {
+ testAction(
+ updateActivityBarView,
+ 'test',
+ {},
+ [{ type: 'UPDATE_ACTIVITY_BAR_VIEW', payload: 'test' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setEmptyStateSvgs', () => {
+ it('commits setEmptyStateSvgs', done => {
+ testAction(
+ setEmptyStateSvgs,
+ 'svg',
+ {},
+ [{ type: 'SET_EMPTY_STATE_SVGS', payload: 'svg' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('updateTempFlagForEntry', () => {
+ it('commits UPDATE_TEMP_FLAG', done => {
+ const f = {
+ ...file(),
+ path: 'test',
+ tempFile: true,
+ };
+ store.state.entries[f.path] = f;
+
+ testAction(
+ updateTempFlagForEntry,
+ { file: f, tempFile: false },
+ store.state,
+ [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
+ [],
+ done,
+ );
+ });
+
+ it('commits UPDATE_TEMP_FLAG and dispatches for parent', done => {
+ const parent = {
+ ...file(),
+ path: 'testing',
+ };
+ const f = {
+ ...file(),
+ path: 'test',
+ parentPath: 'testing',
+ };
+ store.state.entries[parent.path] = parent;
+ store.state.entries[f.path] = f;
+
+ testAction(
+ updateTempFlagForEntry,
+ { file: f, tempFile: false },
+ store.state,
+ [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
+ [{ type: 'updateTempFlagForEntry', payload: { file: parent, tempFile: false } }],
+ done,
+ );
+ });
+
+ it('does not dispatch for parent, if parent does not exist', done => {
+ const f = {
+ ...file(),
+ path: 'test',
+ parentPath: 'testing',
+ };
+ store.state.entries[f.path] = f;
+
+ testAction(
+ updateTempFlagForEntry,
+ { file: f, tempFile: false },
+ store.state,
+ [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setCurrentBranchId', () => {
+ it('commits setCurrentBranchId', done => {
+ testAction(
+ setCurrentBranchId,
+ 'branchId',
+ {},
+ [{ type: 'SET_CURRENT_BRANCH', payload: 'branchId' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('toggleFileFinder', () => {
+ it('commits TOGGLE_FILE_FINDER', done => {
+ testAction(
+ toggleFileFinder,
+ true,
+ null,
+ [{ type: 'TOGGLE_FILE_FINDER', payload: true }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setErrorMessage', () => {
+ it('commis error messsage', done => {
+ testAction(
+ setErrorMessage,
+ 'error',
+ null,
+ [{ type: types.SET_ERROR_MESSAGE, payload: 'error' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('deleteEntry', () => {
+ it('commits entry deletion', done => {
+ store.state.entries.path = 'testing';
+
+ testAction(
+ deleteEntry,
+ 'path',
+ store.state,
+ [{ type: types.DELETE_ENTRY, payload: 'path' }],
+ [{ type: 'stageChange', payload: 'path' }, { type: 'triggerFilesChange' }],
+ done,
+ );
+ });
+
+ it('does not delete a folder after it is emptied', done => {
+ const testFolder = {
+ type: 'tree',
+ tree: [],
+ };
+ const testEntry = {
+ path: 'testFolder/entry-to-delete',
+ parentPath: 'testFolder',
+ opened: false,
+ tree: [],
+ };
+ testFolder.tree.push(testEntry);
+ store.state.entries = {
+ testFolder,
+ 'testFolder/entry-to-delete': testEntry,
+ };
+
+ testAction(
+ deleteEntry,
+ 'testFolder/entry-to-delete',
+ store.state,
+ [{ type: types.DELETE_ENTRY, payload: 'testFolder/entry-to-delete' }],
+ [
+ { type: 'stageChange', payload: 'testFolder/entry-to-delete' },
+ { type: 'triggerFilesChange' },
+ ],
+ done,
+ );
+ });
+
+ describe('when renamed', () => {
+ let testEntry;
+
+ beforeEach(() => {
+ testEntry = {
+ path: 'test',
+ name: 'test',
+ prevPath: 'test_old',
+ prevName: 'test_old',
+ prevParentPath: '',
+ };
+
+ store.state.entries = { test: testEntry };
+ });
+
+ describe('and previous does not exist', () => {
+ it('reverts the rename before deleting', done => {
+ testAction(
+ deleteEntry,
+ testEntry.path,
+ store.state,
+ [],
+ [
+ {
+ type: 'renameEntry',
+ payload: {
+ path: testEntry.path,
+ name: testEntry.prevName,
+ parentPath: testEntry.prevParentPath,
+ },
+ },
+ {
+ type: 'deleteEntry',
+ payload: testEntry.prevPath,
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('and previous exists', () => {
+ beforeEach(() => {
+ const oldEntry = {
+ path: testEntry.prevPath,
+ name: testEntry.prevName,
+ };
+
+ store.state.entries[oldEntry.path] = oldEntry;
+ });
+
+ it('does not revert rename before deleting', done => {
+ testAction(
+ deleteEntry,
+ testEntry.path,
+ store.state,
+ [{ type: types.DELETE_ENTRY, payload: testEntry.path }],
+ [{ type: 'stageChange', payload: testEntry.path }, { type: 'triggerFilesChange' }],
+ done,
+ );
+ });
+
+ it('when previous is deleted, it reverts rename before deleting', done => {
+ store.state.entries[testEntry.prevPath].deleted = true;
+
+ testAction(
+ deleteEntry,
+ testEntry.path,
+ store.state,
+ [],
+ [
+ {
+ type: 'renameEntry',
+ payload: {
+ path: testEntry.path,
+ name: testEntry.prevName,
+ parentPath: testEntry.prevParentPath,
+ },
+ },
+ {
+ type: 'deleteEntry',
+ payload: testEntry.prevPath,
+ },
+ ],
+ done,
+ );
+ });
+ });
+ });
+ });
+
+ describe('renameEntry', () => {
+ describe('purging of file model cache', () => {
+ beforeEach(() => {
+ jest.spyOn(eventHub, '$emit').mockImplementation();
+ });
+
+ it('does not purge model cache for temporary entries that got renamed', done => {
+ Object.assign(store.state.entries, {
+ test: {
+ ...file('test'),
+ key: 'foo-key',
+ type: 'blob',
+ tempFile: true,
+ },
+ });
+
+ store
+ .dispatch('renameEntry', {
+ path: 'test',
+ name: 'new',
+ })
+ .then(() => {
+ expect(eventHub.$emit.mock.calls).not.toContain('editor.update.model.dispose.foo-bar');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('purges model cache for renamed entry', done => {
+ Object.assign(store.state.entries, {
+ test: {
+ ...file('test'),
+ key: 'foo-key',
+ type: 'blob',
+ tempFile: false,
+ },
+ });
+
+ store
+ .dispatch('renameEntry', {
+ path: 'test',
+ name: 'new',
+ })
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalled();
+ expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.dispose.foo-key`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('single entry', () => {
+ let origEntry;
+ let renamedEntry;
+
+ beforeEach(() => {
+ // Need to insert both because `testAction` doesn't actually call the mutation
+ origEntry = file('orig', 'orig', 'blob');
+ renamedEntry = {
+ ...file('renamed', 'renamed', 'blob'),
+ prevKey: origEntry.key,
+ prevName: origEntry.name,
+ prevPath: origEntry.path,
+ };
+
+ Object.assign(store.state.entries, {
+ orig: origEntry,
+ renamed: renamedEntry,
+ });
+ });
+
+ it('by default renames an entry and stages it', () => {
+ const dispatch = jest.fn();
+ const commit = jest.fn();
+
+ renameEntry(
+ { dispatch, commit, state: store.state, getters: store.getters },
+ { path: 'orig', name: 'renamed' },
+ );
+
+ expect(commit.mock.calls).toEqual([
+ [types.RENAME_ENTRY, { path: 'orig', name: 'renamed', parentPath: undefined }],
+ [types.STAGE_CHANGE, expect.objectContaining({ path: 'renamed' })],
+ ]);
+ });
+
+ it('if not changed, completely unstages and discards entry if renamed to original', done => {
+ testAction(
+ renameEntry,
+ { path: 'renamed', name: 'orig' },
+ store.state,
+ [
+ {
+ type: types.RENAME_ENTRY,
+ payload: {
+ path: 'renamed',
+ name: 'orig',
+ parentPath: undefined,
+ },
+ },
+ {
+ type: types.REMOVE_FILE_FROM_STAGED_AND_CHANGED,
+ payload: origEntry,
+ },
+ ],
+ [{ type: 'triggerFilesChange' }],
+ done,
+ );
+ });
+
+ it('if already in changed, does not add to change', done => {
+ store.state.changedFiles.push(renamedEntry);
+
+ testAction(
+ renameEntry,
+ { path: 'orig', name: 'renamed' },
+ store.state,
+ [expect.objectContaining({ type: types.RENAME_ENTRY })],
+ [{ type: 'triggerFilesChange' }],
+ done,
+ );
+ });
+
+ it('routes to the renamed file if the original file has been opened', done => {
+ Object.assign(store.state.entries.orig, {
+ opened: true,
+ url: '/foo-bar.md',
+ });
+
+ store
+ .dispatch('renameEntry', {
+ path: 'orig',
+ name: 'renamed',
+ })
+ .then(() => {
+ expect(router.push.mock.calls).toHaveLength(1);
+ expect(router.push).toHaveBeenCalledWith(`/project/foo-bar.md`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('folder', () => {
+ let folder;
+ let file1;
+ let file2;
+
+ beforeEach(() => {
+ folder = file('folder', 'folder', 'tree');
+ file1 = file('file-1', 'file-1', 'blob', folder);
+ file2 = file('file-2', 'file-2', 'blob', folder);
+
+ folder.tree = [file1, file2];
+
+ Object.assign(store.state.entries, {
+ [folder.path]: folder,
+ [file1.path]: file1,
+ [file2.path]: file2,
+ });
+ });
+
+ it('updates entries in a folder correctly, when folder is renamed', done => {
+ store
+ .dispatch('renameEntry', {
+ path: 'folder',
+ name: 'new-folder',
+ })
+ .then(() => {
+ const keys = Object.keys(store.state.entries);
+
+ expect(keys.length).toBe(3);
+ expect(keys.indexOf('new-folder')).toBe(0);
+ expect(keys.indexOf('new-folder/file-1')).toBe(1);
+ expect(keys.indexOf('new-folder/file-2')).toBe(2);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('discards renaming of an entry if the root folder is renamed back to a previous name', done => {
+ const rootFolder = file('old-folder', 'old-folder', 'tree');
+ const testEntry = file('test', 'test', 'blob', rootFolder);
+
+ Object.assign(store.state, {
+ entries: {
+ 'old-folder': {
+ ...rootFolder,
+ tree: [testEntry],
+ },
+ 'old-folder/test': testEntry,
+ },
+ });
+
+ store
+ .dispatch('renameEntry', {
+ path: 'old-folder',
+ name: 'new-folder',
+ })
+ .then(() => {
+ const { entries } = store.state;
+
+ expect(Object.keys(entries).length).toBe(2);
+ expect(entries['old-folder']).toBeUndefined();
+ expect(entries['old-folder/test']).toBeUndefined();
+
+ expect(entries['new-folder']).toBeDefined();
+ expect(entries['new-folder/test']).toEqual(
+ expect.objectContaining({
+ path: 'new-folder/test',
+ name: 'test',
+ prevPath: 'old-folder/test',
+ prevName: 'test',
+ }),
+ );
+ })
+ .then(() =>
+ store.dispatch('renameEntry', {
+ path: 'new-folder',
+ name: 'old-folder',
+ }),
+ )
+ .then(() => {
+ const { entries } = store.state;
+
+ expect(Object.keys(entries).length).toBe(2);
+ expect(entries['new-folder']).toBeUndefined();
+ expect(entries['new-folder/test']).toBeUndefined();
+
+ expect(entries['old-folder']).toBeDefined();
+ expect(entries['old-folder/test']).toEqual(
+ expect.objectContaining({
+ path: 'old-folder/test',
+ name: 'test',
+ prevPath: undefined,
+ prevName: undefined,
+ }),
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('with file in directory', () => {
+ const parentPath = 'original-dir';
+ const newParentPath = 'new-dir';
+ const fileName = 'test.md';
+ const filePath = `${parentPath}/${fileName}`;
+
+ let rootDir;
+
+ beforeEach(() => {
+ const parentEntry = file(parentPath, parentPath, 'tree');
+ const fileEntry = file(filePath, filePath, 'blob', parentEntry);
+ rootDir = {
+ tree: [],
+ };
+
+ Object.assign(store.state, {
+ entries: {
+ [parentPath]: {
+ ...parentEntry,
+ tree: [fileEntry],
+ },
+ [filePath]: fileEntry,
+ },
+ trees: {
+ '/': rootDir,
+ },
+ });
+ });
+
+ it('creates new directory', done => {
+ expect(store.state.entries[newParentPath]).toBeUndefined();
+
+ store
+ .dispatch('renameEntry', { path: filePath, name: fileName, parentPath: newParentPath })
+ .then(() => {
+ expect(store.state.entries[newParentPath]).toEqual(
+ expect.objectContaining({
+ path: newParentPath,
+ type: 'tree',
+ tree: expect.arrayContaining([
+ store.state.entries[`${newParentPath}/${fileName}`],
+ ]),
+ }),
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('when new directory exists', () => {
+ let newDir;
+
+ beforeEach(() => {
+ newDir = file(newParentPath, newParentPath, 'tree');
+
+ store.state.entries[newDir.path] = newDir;
+ rootDir.tree.push(newDir);
+ });
+
+ it('inserts in new directory', done => {
+ expect(newDir.tree).toEqual([]);
+
+ store
+ .dispatch('renameEntry', {
+ path: filePath,
+ name: fileName,
+ parentPath: newParentPath,
+ })
+ .then(() => {
+ expect(newDir.tree).toEqual([store.state.entries[`${newParentPath}/${fileName}`]]);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('when new directory is deleted, it undeletes it', done => {
+ store.dispatch('deleteEntry', newParentPath);
+
+ expect(store.state.entries[newParentPath].deleted).toBe(true);
+ expect(rootDir.tree.some(x => x.path === newParentPath)).toBe(false);
+
+ store
+ .dispatch('renameEntry', {
+ path: filePath,
+ name: fileName,
+ parentPath: newParentPath,
+ })
+ .then(() => {
+ expect(store.state.entries[newParentPath].deleted).toBe(false);
+ expect(rootDir.tree.some(x => x.path === newParentPath)).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+ });
+ });
+
+ describe('getBranchData', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('error', () => {
+ let dispatch;
+ let callParams;
+
+ beforeEach(() => {
+ callParams = [
+ {
+ commit() {},
+ state: store.state,
+ },
+ {
+ projectId: 'abc/def',
+ branchId: 'master-testing',
+ },
+ ];
+ dispatch = jest.fn();
+ document.body.innerHTML += '<div class="flash-container"></div>';
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ it('passes the error further unchanged without dispatching any action when response is 404', done => {
+ mock.onGet(/(.*)/).replyOnce(404);
+
+ getBranchData(...callParams)
+ .then(done.fail)
+ .catch(e => {
+ expect(dispatch.mock.calls).toHaveLength(0);
+ expect(e.response.status).toEqual(404);
+ expect(document.querySelector('.flash-alert')).toBeNull();
+ done();
+ });
+ });
+
+ it('does not pass the error further and flashes an alert if error is not 404', done => {
+ mock.onGet(/(.*)/).replyOnce(418);
+
+ getBranchData(...callParams)
+ .then(done.fail)
+ .catch(e => {
+ expect(dispatch.mock.calls).toHaveLength(0);
+ expect(e.response).toBeUndefined();
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/extend_spec.js b/spec/frontend/ide/stores/extend_spec.js
new file mode 100644
index 00000000000..b0f1063153e
--- /dev/null
+++ b/spec/frontend/ide/stores/extend_spec.js
@@ -0,0 +1,74 @@
+import extendStore from '~/ide/stores/extend';
+import terminalPlugin from '~/ide/stores/plugins/terminal';
+import terminalSyncPlugin from '~/ide/stores/plugins/terminal_sync';
+
+jest.mock('~/ide/stores/plugins/terminal', () => jest.fn());
+jest.mock('~/ide/stores/plugins/terminal_sync', () => jest.fn());
+
+describe('ide/stores/extend', () => {
+ let prevGon;
+ let store;
+ let el;
+
+ beforeEach(() => {
+ prevGon = global.gon;
+ store = {};
+ el = {};
+
+ [terminalPlugin, terminalSyncPlugin].forEach(x => {
+ const plugin = jest.fn();
+
+ x.mockImplementation(() => plugin);
+ });
+ });
+
+ afterEach(() => {
+ global.gon = prevGon;
+ terminalPlugin.mockClear();
+ terminalSyncPlugin.mockClear();
+ });
+
+ const withGonFeatures = features => {
+ global.gon = { ...global.gon, features };
+ };
+
+ describe('terminalPlugin', () => {
+ beforeEach(() => {
+ extendStore(store, el);
+ });
+
+ it('is created', () => {
+ expect(terminalPlugin).toHaveBeenCalledWith(el);
+ });
+
+ it('is called with store', () => {
+ expect(terminalPlugin()).toHaveBeenCalledWith(store);
+ });
+ });
+
+ describe('terminalSyncPlugin', () => {
+ describe('when buildServiceProxy feature is enabled', () => {
+ beforeEach(() => {
+ withGonFeatures({ buildServiceProxy: true });
+
+ extendStore(store, el);
+ });
+
+ it('is created', () => {
+ expect(terminalSyncPlugin).toHaveBeenCalledWith(el);
+ });
+
+ it('is called with store', () => {
+ expect(terminalSyncPlugin()).toHaveBeenCalledWith(store);
+ });
+ });
+
+ describe('when buildServiceProxy feature is disabled', () => {
+ it('is not created', () => {
+ extendStore(store, el);
+
+ expect(terminalSyncPlugin).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index 408ea2b2939..dcf05329ce0 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -417,4 +417,69 @@ describe('IDE store getters', () => {
expect(localStore.getters[getterName]).toBe(val);
});
});
+
+ describe('entryExists', () => {
+ beforeEach(() => {
+ localState.entries = {
+ foo: file('foo', 'foo', 'tree'),
+ 'foo/bar.png': file(),
+ };
+ });
+
+ it.each`
+ path | deleted | value
+ ${'foo/bar.png'} | ${false} | ${true}
+ ${'foo/bar.png'} | ${true} | ${false}
+ ${'foo'} | ${false} | ${true}
+ `(
+ 'returns $value for an existing entry path: $path (deleted: $deleted)',
+ ({ path, deleted, value }) => {
+ localState.entries[path].deleted = deleted;
+
+ expect(localStore.getters.entryExists(path)).toBe(value);
+ },
+ );
+
+ it('returns false for a non existing entry path', () => {
+ expect(localStore.getters.entryExists('bar.baz')).toBe(false);
+ });
+ });
+
+ describe('getAvailableFileName', () => {
+ it.each`
+ path | newPath
+ ${'foo'} | ${'foo_1'}
+ ${'foo__93.png'} | ${'foo__94.png'}
+ ${'foo/bar.png'} | ${'foo/bar_1.png'}
+ ${'foo/bar--34.png'} | ${'foo/bar--35.png'}
+ ${'foo/bar 2.png'} | ${'foo/bar 3.png'}
+ ${'foo/bar-621.png'} | ${'foo/bar-622.png'}
+ ${'jquery.min.js'} | ${'jquery_1.min.js'}
+ ${'my_spec_22.js.snap'} | ${'my_spec_23.js.snap'}
+ ${'subtitles5.mp4.srt'} | ${'subtitles_6.mp4.srt'}
+ ${'sample_file.mp3'} | ${'sample_file_1.mp3'}
+ ${'Screenshot 2020-05-26 at 10.53.08 PM.png'} | ${'Screenshot 2020-05-26 at 11.53.08 PM.png'}
+ `('suffixes the path with a number if the path already exists', ({ path, newPath }) => {
+ localState.entries[path] = file();
+
+ expect(localStore.getters.getAvailableFileName(path)).toBe(newPath);
+ });
+
+ it('loops through all incremented entries and keeps trying until a file path that does not exist is found', () => {
+ localState.entries = {
+ 'bar/baz_1.png': file(),
+ 'bar/baz_2.png': file(),
+ 'bar/baz_3.png': file(),
+ 'bar/baz_4.png': file(),
+ 'bar/baz_5.png': file(),
+ 'bar/baz_72.png': file(),
+ };
+
+ expect(localStore.getters.getAvailableFileName('bar/baz_1.png')).toBe('bar/baz_6.png');
+ });
+
+ it('returns the entry path as is if the path does not exist', () => {
+ expect(localStore.getters.getAvailableFileName('foo-bar1.jpg')).toBe('foo-bar1.jpg');
+ });
+ });
});
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
new file mode 100644
index 00000000000..a14879112fd
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -0,0 +1,598 @@
+import { file } from 'jest/ide/helpers';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { createStore } from '~/ide/stores';
+import service from '~/ide/services';
+import { createRouter } from '~/ide/ide_router';
+import eventHub from '~/ide/eventhub';
+import consts from '~/ide/stores/modules/commit/constants';
+import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
+import * as actions from '~/ide/stores/modules/commit/actions';
+import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants';
+import testAction from '../../../../helpers/vuex_action_helper';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrl: jest.fn(),
+}));
+
+const TEST_COMMIT_SHA = '123456789';
+
+describe('IDE commit module actions', () => {
+ let mock;
+ let store;
+ let router;
+
+ beforeEach(() => {
+ store = createStore();
+ router = createRouter(store);
+ gon.api_version = 'v1';
+ mock = new MockAdapter(axios);
+ jest.spyOn(router, 'push').mockImplementation();
+
+ mock.onGet('/api/v1/projects/abcproject/repository/branches/master').reply(200);
+ });
+
+ afterEach(() => {
+ delete gon.api_version;
+ mock.restore();
+ });
+
+ describe('updateCommitMessage', () => {
+ it('updates store with new commit message', done => {
+ store
+ .dispatch('commit/updateCommitMessage', 'testing')
+ .then(() => {
+ expect(store.state.commit.commitMessage).toBe('testing');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('discardDraft', () => {
+ it('resets commit message to blank', done => {
+ store.state.commit.commitMessage = 'testing';
+
+ store
+ .dispatch('commit/discardDraft')
+ .then(() => {
+ expect(store.state.commit.commitMessage).not.toBe('testing');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateCommitAction', () => {
+ it('updates store with new commit action', done => {
+ store
+ .dispatch('commit/updateCommitAction', '1')
+ .then(() => {
+ expect(store.state.commit.commitAction).toBe('1');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('sets shouldCreateMR to true if "Create new MR" option is visible', done => {
+ Object.assign(store.state, {
+ shouldHideNewMrOption: false,
+ });
+
+ testAction(
+ actions.updateCommitAction,
+ {},
+ store.state,
+ [
+ {
+ type: mutationTypes.UPDATE_COMMIT_ACTION,
+ payload: { commitAction: expect.anything() },
+ },
+ { type: mutationTypes.TOGGLE_SHOULD_CREATE_MR, payload: true },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('sets shouldCreateMR to false if "Create new MR" option is hidden', done => {
+ Object.assign(store.state, {
+ shouldHideNewMrOption: true,
+ });
+
+ testAction(
+ actions.updateCommitAction,
+ {},
+ store.state,
+ [
+ {
+ type: mutationTypes.UPDATE_COMMIT_ACTION,
+ payload: { commitAction: expect.anything() },
+ },
+ { type: mutationTypes.TOGGLE_SHOULD_CREATE_MR, payload: false },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('updateBranchName', () => {
+ it('updates store with new branch name', done => {
+ store
+ .dispatch('commit/updateBranchName', 'branch-name')
+ .then(() => {
+ expect(store.state.commit.newBranchName).toBe('branch-name');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('setLastCommitMessage', () => {
+ beforeEach(() => {
+ Object.assign(store.state, {
+ currentProjectId: 'abcproject',
+ projects: {
+ abcproject: {
+ web_url: 'http://testing',
+ },
+ },
+ });
+ });
+
+ it('updates commit message with short_id', done => {
+ store
+ .dispatch('commit/setLastCommitMessage', { short_id: '123' })
+ .then(() => {
+ expect(store.state.lastCommitMsg).toContain(
+ 'Your changes have been committed. Commit <a href="http://testing/-/commit/123" class="commit-sha">123</a>',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('updates commit message with stats', done => {
+ store
+ .dispatch('commit/setLastCommitMessage', {
+ short_id: '123',
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ })
+ .then(() => {
+ expect(store.state.lastCommitMsg).toBe(
+ 'Your changes have been committed. Commit <a href="http://testing/-/commit/123" class="commit-sha">123</a> with 1 additions, 2 deletions.',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateFilesAfterCommit', () => {
+ const data = {
+ id: '123',
+ message: 'testing commit message',
+ committed_date: '123',
+ committer_name: 'root',
+ };
+ const branch = 'master';
+ let f;
+
+ beforeEach(() => {
+ jest.spyOn(eventHub, '$emit').mockImplementation();
+
+ f = file('changedFile');
+ Object.assign(f, {
+ active: true,
+ changed: true,
+ content: 'file content',
+ });
+
+ Object.assign(store.state, {
+ currentProjectId: 'abcproject',
+ currentBranchId: 'master',
+ projects: {
+ abcproject: {
+ web_url: 'web_url',
+ branches: {
+ master: {
+ workingReference: '',
+ commit: {
+ short_id: TEST_COMMIT_SHA,
+ },
+ },
+ },
+ },
+ },
+ stagedFiles: [
+ f,
+ {
+ ...file('changedFile2'),
+ changed: true,
+ },
+ ],
+ });
+
+ store.state.openFiles = store.state.stagedFiles;
+ store.state.stagedFiles.forEach(stagedFile => {
+ store.state.entries[stagedFile.path] = stagedFile;
+ });
+ });
+
+ it('updates stores working reference', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(store.state.projects.abcproject.branches.master.workingReference).toBe(data.id);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('resets all files changed status', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ store.state.openFiles.forEach(entry => {
+ expect(entry.changed).toBeFalsy();
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('sets files commit data', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(f.lastCommitSha).toBe(data.id);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('updates raw content for changed file', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(f.raw).toBe(f.content);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('emits changed event for file', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.content.${f.key}`, {
+ content: f.content,
+ changed: false,
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('commitChanges', () => {
+ beforeEach(() => {
+ document.body.innerHTML += '<div class="flash-container"></div>';
+
+ const f = {
+ ...file('changed'),
+ type: 'blob',
+ active: true,
+ lastCommitSha: TEST_COMMIT_SHA,
+ content: '\n',
+ raw: '\n',
+ };
+
+ Object.assign(store.state, {
+ stagedFiles: [f],
+ changedFiles: [f],
+ openFiles: [f],
+ currentProjectId: 'abcproject',
+ currentBranchId: 'master',
+ projects: {
+ abcproject: {
+ web_url: 'webUrl',
+ branches: {
+ master: {
+ workingReference: '1',
+ commit: {
+ id: TEST_COMMIT_SHA,
+ },
+ },
+ },
+ userPermissions: {
+ [PERMISSION_CREATE_MR]: true,
+ },
+ },
+ },
+ });
+
+ store.state.commit.commitAction = '2';
+ store.state.commit.commitMessage = 'testing 123';
+
+ store.state.openFiles.forEach(localF => {
+ store.state.entries[localF.path] = localF;
+ });
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ describe('success', () => {
+ const COMMIT_RESPONSE = {
+ id: '123456',
+ short_id: '123',
+ message: 'test message',
+ committed_date: 'date',
+ parent_ids: '321',
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ };
+
+ beforeEach(() => {
+ jest.spyOn(service, 'commit').mockResolvedValue({ data: COMMIT_RESPONSE });
+ });
+
+ it('calls service', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(service.commit).toHaveBeenCalledWith('abcproject', {
+ branch: expect.anything(),
+ commit_message: 'testing 123',
+ actions: [
+ {
+ action: commitActionTypes.update,
+ file_path: expect.anything(),
+ content: '\n',
+ encoding: expect.anything(),
+ last_commit_id: undefined,
+ previous_path: undefined,
+ },
+ ],
+ start_sha: TEST_COMMIT_SHA,
+ });
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sends lastCommit ID when not creating new branch', done => {
+ store.state.commit.commitAction = '1';
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(service.commit).toHaveBeenCalledWith('abcproject', {
+ branch: expect.anything(),
+ commit_message: 'testing 123',
+ actions: [
+ {
+ action: commitActionTypes.update,
+ file_path: expect.anything(),
+ content: '\n',
+ encoding: expect.anything(),
+ last_commit_id: TEST_COMMIT_SHA,
+ previous_path: undefined,
+ },
+ ],
+ start_sha: undefined,
+ });
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets last Commit Msg', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.state.lastCommitMsg).toBe(
+ 'Your changes have been committed. Commit <a href="webUrl/-/commit/123" class="commit-sha">123</a> with 1 additions, 2 deletions.',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds commit data to files', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.state.entries[store.state.openFiles[0].path].lastCommitSha).toBe(
+ COMMIT_RESPONSE.id,
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('resets stores commit actions', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.state.commit.commitAction).not.toBe(consts.COMMIT_TO_NEW_BRANCH);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('removes all staged files', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.state.stagedFiles.length).toBe(0);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('merge request', () => {
+ it('redirects to new merge request page', done => {
+ jest.spyOn(eventHub, '$on').mockImplementation();
+
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.shouldCreateMR = true;
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(visitUrl).toHaveBeenCalledWith(
+ `webUrl/-/merge_requests/new?merge_request[source_branch]=${
+ store.getters['commit/placeholderBranchName']
+ }&merge_request[target_branch]=master&nav_source=webide`,
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not redirect to new merge request page when shouldCreateMR is not checked', done => {
+ jest.spyOn(eventHub, '$on').mockImplementation();
+
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.shouldCreateMR = false;
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(visitUrl).not.toHaveBeenCalled();
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('resets changed files before redirecting', () => {
+ jest.spyOn(eventHub, '$on').mockImplementation();
+
+ store.state.commit.commitAction = '3';
+
+ return store.dispatch('commit/commitChanges').then(() => {
+ expect(store.state.stagedFiles.length).toBe(0);
+ });
+ });
+ });
+ });
+
+ describe('failed', () => {
+ beforeEach(() => {
+ jest.spyOn(service, 'commit').mockResolvedValue({
+ data: {
+ message: 'failed message',
+ },
+ });
+ });
+
+ it('shows failed message', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ const alert = document.querySelector('.flash-container');
+
+ expect(alert.textContent.trim()).toBe('failed message');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('first commit of a branch', () => {
+ const COMMIT_RESPONSE = {
+ id: '123456',
+ short_id: '123',
+ message: 'test message',
+ committed_date: 'date',
+ parent_ids: [],
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ };
+
+ it('commits TOGGLE_EMPTY_STATE mutation on empty repo', done => {
+ jest.spyOn(service, 'commit').mockResolvedValue({ data: COMMIT_RESPONSE });
+ jest.spyOn(store, 'commit');
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.commit.mock.calls).toEqual(
+ expect.arrayContaining([
+ ['TOGGLE_EMPTY_STATE', expect.any(Object), expect.any(Object)],
+ ]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not commmit TOGGLE_EMPTY_STATE mutation on existing project', done => {
+ COMMIT_RESPONSE.parent_ids.push('1234');
+ jest.spyOn(service, 'commit').mockResolvedValue({ data: COMMIT_RESPONSE });
+ jest.spyOn(store, 'commit');
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.commit.mock.calls).not.toEqual(
+ expect.arrayContaining([
+ ['TOGGLE_EMPTY_STATE', expect.any(Object), expect.any(Object)],
+ ]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('toggleShouldCreateMR', () => {
+ it('commits both toggle and interacting with MR checkbox actions', done => {
+ testAction(
+ actions.toggleShouldCreateMR,
+ {},
+ store.state,
+ [{ type: mutationTypes.TOGGLE_SHOULD_CREATE_MR }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/modules/pane/getters_spec.js b/spec/frontend/ide/stores/modules/pane/getters_spec.js
index 8a213323de0..a321571f058 100644
--- a/spec/frontend/ide/stores/modules/pane/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/pane/getters_spec.js
@@ -7,20 +7,6 @@ describe('IDE pane module getters', () => {
[TEST_VIEW]: true,
};
- describe('isActiveView', () => {
- it('returns true if given view matches currentView', () => {
- const result = getters.isActiveView({ currentView: 'A' })('A');
-
- expect(result).toBe(true);
- });
-
- it('returns false if given view does not match currentView', () => {
- const result = getters.isActiveView({ currentView: 'A' })('B');
-
- expect(result).toBe(false);
- });
- });
-
describe('isAliveView', () => {
it('returns true if given view is in keepAliveViews', () => {
const result = getters.isAliveView({ keepAliveViews: TEST_KEEP_ALIVE_VIEWS }, {})(TEST_VIEW);
@@ -29,25 +15,25 @@ describe('IDE pane module getters', () => {
});
it('returns true if given view is active view and open', () => {
- const result = getters.isAliveView(
- { ...state(), isOpen: true },
- { isActiveView: () => true },
- )(TEST_VIEW);
+ const result = getters.isAliveView({ ...state(), isOpen: true, currentView: TEST_VIEW })(
+ TEST_VIEW,
+ );
expect(result).toBe(true);
});
it('returns false if given view is active view and closed', () => {
- const result = getters.isAliveView(state(), { isActiveView: () => true })(TEST_VIEW);
+ const result = getters.isAliveView({ ...state(), currentView: TEST_VIEW })(TEST_VIEW);
expect(result).toBe(false);
});
it('returns false if given view is not activeView', () => {
- const result = getters.isAliveView(
- { ...state(), isOpen: true },
- { isActiveView: () => false },
- )(TEST_VIEW);
+ const result = getters.isAliveView({
+ ...state(),
+ isOpen: true,
+ currentView: `${TEST_VIEW}_other`,
+ })(TEST_VIEW);
expect(result).toBe(false);
});
diff --git a/spec/frontend/ide/stores/modules/router/actions_spec.js b/spec/frontend/ide/stores/modules/router/actions_spec.js
new file mode 100644
index 00000000000..4795eae2b79
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/router/actions_spec.js
@@ -0,0 +1,19 @@
+import * as actions from '~/ide/stores/modules/router/actions';
+import * as types from '~/ide/stores/modules/router/mutation_types';
+import testAction from 'helpers/vuex_action_helper';
+
+const TEST_PATH = 'test/path/abc';
+
+describe('ide/stores/modules/router/actions', () => {
+ describe('push', () => {
+ it('commits mutation', () => {
+ return testAction(
+ actions.push,
+ TEST_PATH,
+ {},
+ [{ type: types.PUSH, payload: TEST_PATH }],
+ [],
+ );
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/modules/router/mutations_spec.js b/spec/frontend/ide/stores/modules/router/mutations_spec.js
new file mode 100644
index 00000000000..a4a83c9344d
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/router/mutations_spec.js
@@ -0,0 +1,23 @@
+import mutations from '~/ide/stores/modules/router/mutations';
+import * as types from '~/ide/stores/modules/router/mutation_types';
+import createState from '~/ide/stores/modules/router/state';
+
+const TEST_PATH = 'test/path/abc';
+
+describe('ide/stores/modules/router/mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ });
+
+ describe(types.PUSH, () => {
+ it('updates state', () => {
+ expect(state.fullPath).toBe('');
+
+ mutations[types.PUSH](state, TEST_PATH);
+
+ expect(state.fullPath).toBe(TEST_PATH);
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
new file mode 100644
index 00000000000..242b1579be7
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
@@ -0,0 +1,289 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'spec/test_constants';
+import {
+ CHECK_CONFIG,
+ CHECK_RUNNERS,
+ RETRY_RUNNERS_INTERVAL,
+} from '~/ide/stores/modules/terminal/constants';
+import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
+import * as messages from '~/ide/stores/modules/terminal/messages';
+import * as actions from '~/ide/stores/modules/terminal/actions/checks';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
+
+const TEST_PROJECT_PATH = 'lorem/root';
+const TEST_BRANCH_ID = 'master';
+const TEST_YAML_HELP_PATH = `${TEST_HOST}/test/yaml/help`;
+const TEST_RUNNERS_HELP_PATH = `${TEST_HOST}/test/runners/help`;
+
+describe('IDE store terminal check actions', () => {
+ let mock;
+ let state;
+ let rootState;
+ let rootGetters;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ state = {
+ paths: {
+ webTerminalConfigHelpPath: TEST_YAML_HELP_PATH,
+ webTerminalRunnersHelpPath: TEST_RUNNERS_HELP_PATH,
+ },
+ checks: {
+ config: { isLoading: true },
+ },
+ };
+ rootState = {
+ currentBranchId: TEST_BRANCH_ID,
+ };
+ rootGetters = {
+ currentProject: {
+ id: 7,
+ path_with_namespace: TEST_PROJECT_PATH,
+ },
+ };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('requestConfigCheck', () => {
+ it('handles request loading', () => {
+ return testAction(
+ actions.requestConfigCheck,
+ null,
+ {},
+ [{ type: mutationTypes.REQUEST_CHECK, payload: CHECK_CONFIG }],
+ [],
+ );
+ });
+ });
+
+ describe('receiveConfigCheckSuccess', () => {
+ it('handles successful response', () => {
+ return testAction(
+ actions.receiveConfigCheckSuccess,
+ null,
+ {},
+ [
+ { type: mutationTypes.SET_VISIBLE, payload: true },
+ { type: mutationTypes.RECEIVE_CHECK_SUCCESS, payload: CHECK_CONFIG },
+ ],
+ [],
+ );
+ });
+ });
+
+ describe('receiveConfigCheckError', () => {
+ it('handles error response', () => {
+ const status = httpStatus.UNPROCESSABLE_ENTITY;
+ const payload = { response: { status } };
+
+ return testAction(
+ actions.receiveConfigCheckError,
+ payload,
+ state,
+ [
+ {
+ type: mutationTypes.SET_VISIBLE,
+ payload: true,
+ },
+ {
+ type: mutationTypes.RECEIVE_CHECK_ERROR,
+ payload: {
+ type: CHECK_CONFIG,
+ message: messages.configCheckError(status, TEST_YAML_HELP_PATH),
+ },
+ },
+ ],
+ [],
+ );
+ });
+
+ [httpStatus.FORBIDDEN, httpStatus.NOT_FOUND].forEach(status => {
+ it(`hides tab, when status is ${status}`, () => {
+ const payload = { response: { status } };
+
+ return testAction(
+ actions.receiveConfigCheckError,
+ payload,
+ state,
+ [
+ {
+ type: mutationTypes.SET_VISIBLE,
+ payload: false,
+ },
+ expect.objectContaining({ type: mutationTypes.RECEIVE_CHECK_ERROR }),
+ ],
+ [],
+ );
+ });
+ });
+ });
+
+ describe('fetchConfigCheck', () => {
+ it('dispatches request and receive', () => {
+ mock.onPost(/.*\/ide_terminals\/check_config/).reply(200, {});
+
+ return testAction(
+ actions.fetchConfigCheck,
+ null,
+ {
+ ...rootGetters,
+ ...rootState,
+ },
+ [],
+ [{ type: 'requestConfigCheck' }, { type: 'receiveConfigCheckSuccess' }],
+ );
+ });
+
+ it('when error, dispatches request and receive', () => {
+ mock.onPost(/.*\/ide_terminals\/check_config/).reply(400, {});
+
+ return testAction(
+ actions.fetchConfigCheck,
+ null,
+ {
+ ...rootGetters,
+ ...rootState,
+ },
+ [],
+ [
+ { type: 'requestConfigCheck' },
+ { type: 'receiveConfigCheckError', payload: expect.any(Error) },
+ ],
+ );
+ });
+ });
+
+ describe('requestRunnersCheck', () => {
+ it('handles request loading', () => {
+ return testAction(
+ actions.requestRunnersCheck,
+ null,
+ {},
+ [{ type: mutationTypes.REQUEST_CHECK, payload: CHECK_RUNNERS }],
+ [],
+ );
+ });
+ });
+
+ describe('receiveRunnersCheckSuccess', () => {
+ it('handles successful response, with data', () => {
+ const payload = [{}];
+
+ return testAction(
+ actions.receiveRunnersCheckSuccess,
+ payload,
+ state,
+ [{ type: mutationTypes.RECEIVE_CHECK_SUCCESS, payload: CHECK_RUNNERS }],
+ [],
+ );
+ });
+
+ it('handles successful response, with empty data', () => {
+ const commitPayload = {
+ type: CHECK_RUNNERS,
+ message: messages.runnersCheckEmpty(TEST_RUNNERS_HELP_PATH),
+ };
+
+ return testAction(
+ actions.receiveRunnersCheckSuccess,
+ [],
+ state,
+ [{ type: mutationTypes.RECEIVE_CHECK_ERROR, payload: commitPayload }],
+ [{ type: 'retryRunnersCheck' }],
+ );
+ });
+ });
+
+ describe('receiveRunnersCheckError', () => {
+ it('dispatches handle with message', () => {
+ const commitPayload = {
+ type: CHECK_RUNNERS,
+ message: messages.UNEXPECTED_ERROR_RUNNERS,
+ };
+
+ return testAction(
+ actions.receiveRunnersCheckError,
+ null,
+ {},
+ [{ type: mutationTypes.RECEIVE_CHECK_ERROR, payload: commitPayload }],
+ [],
+ );
+ });
+ });
+
+ describe('retryRunnersCheck', () => {
+ it('dispatches fetch again after timeout', () => {
+ const dispatch = jest.fn().mockName('dispatch');
+
+ actions.retryRunnersCheck({ dispatch, state });
+
+ expect(dispatch).not.toHaveBeenCalled();
+
+ jest.advanceTimersByTime(RETRY_RUNNERS_INTERVAL + 1);
+
+ expect(dispatch).toHaveBeenCalledWith('fetchRunnersCheck', { background: true });
+ });
+
+ it('does not dispatch fetch if config check is error', () => {
+ const dispatch = jest.fn().mockName('dispatch');
+ state.checks.config = {
+ isLoading: false,
+ isValid: false,
+ };
+
+ actions.retryRunnersCheck({ dispatch, state });
+
+ expect(dispatch).not.toHaveBeenCalled();
+
+ jest.advanceTimersByTime(RETRY_RUNNERS_INTERVAL + 1);
+
+ expect(dispatch).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('fetchRunnersCheck', () => {
+ it('dispatches request and receive', () => {
+ mock.onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }).reply(200, []);
+
+ return testAction(
+ actions.fetchRunnersCheck,
+ {},
+ rootGetters,
+ [],
+ [{ type: 'requestRunnersCheck' }, { type: 'receiveRunnersCheckSuccess', payload: [] }],
+ );
+ });
+
+ it('does not dispatch request when background is true', () => {
+ mock.onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }).reply(200, []);
+
+ return testAction(
+ actions.fetchRunnersCheck,
+ { background: true },
+ rootGetters,
+ [],
+ [{ type: 'receiveRunnersCheckSuccess', payload: [] }],
+ );
+ });
+
+ it('dispatches request and receive, when error', () => {
+ mock.onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }).reply(500, []);
+
+ return testAction(
+ actions.fetchRunnersCheck,
+ {},
+ rootGetters,
+ [],
+ [
+ { type: 'requestRunnersCheck' },
+ { type: 'receiveRunnersCheckError', payload: expect.any(Error) },
+ ],
+ );
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
new file mode 100644
index 00000000000..4bc937b4784
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
@@ -0,0 +1,300 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
+import * as messages from '~/ide/stores/modules/terminal/messages';
+import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
+import * as actions from '~/ide/stores/modules/terminal/actions/session_controls';
+import httpStatus from '~/lib/utils/http_status';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+
+jest.mock('~/flash');
+
+const TEST_PROJECT_PATH = 'lorem/root';
+const TEST_BRANCH_ID = 'master';
+const TEST_SESSION = {
+ id: 7,
+ status: PENDING,
+ show_path: 'path/show',
+ cancel_path: 'path/cancel',
+ retry_path: 'path/retry',
+ terminal_path: 'path/terminal',
+ proxy_websocket_path: 'path/proxy',
+ services: ['test-service'],
+};
+
+describe('IDE store terminal session controls actions', () => {
+ let mock;
+ let dispatch;
+ let rootState;
+ let rootGetters;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ dispatch = jest.fn().mockName('dispatch');
+ rootState = {
+ currentBranchId: TEST_BRANCH_ID,
+ };
+ rootGetters = {
+ currentProject: {
+ id: 7,
+ path_with_namespace: TEST_PROJECT_PATH,
+ },
+ };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('requestStartSession', () => {
+ it('sets session status', () => {
+ return testAction(
+ actions.requestStartSession,
+ null,
+ {},
+ [{ type: mutationTypes.SET_SESSION_STATUS, payload: STARTING }],
+ [],
+ );
+ });
+ });
+
+ describe('receiveStartSessionSuccess', () => {
+ it('sets session and starts polling status', () => {
+ return testAction(
+ actions.receiveStartSessionSuccess,
+ TEST_SESSION,
+ {},
+ [
+ {
+ type: mutationTypes.SET_SESSION,
+ payload: {
+ id: TEST_SESSION.id,
+ status: TEST_SESSION.status,
+ showPath: TEST_SESSION.show_path,
+ cancelPath: TEST_SESSION.cancel_path,
+ retryPath: TEST_SESSION.retry_path,
+ terminalPath: TEST_SESSION.terminal_path,
+ proxyWebsocketPath: TEST_SESSION.proxy_websocket_path,
+ services: TEST_SESSION.services,
+ },
+ },
+ ],
+ [{ type: 'pollSessionStatus' }],
+ );
+ });
+ });
+
+ describe('receiveStartSessionError', () => {
+ it('flashes message', () => {
+ actions.receiveStartSessionError({ dispatch });
+
+ expect(createFlash).toHaveBeenCalledWith(messages.UNEXPECTED_ERROR_STARTING);
+ });
+
+ it('sets session status', () => {
+ return testAction(actions.receiveStartSessionError, null, {}, [], [{ type: 'killSession' }]);
+ });
+ });
+
+ describe('startSession', () => {
+ it('does nothing if session is already starting', () => {
+ const state = {
+ session: { status: STARTING },
+ };
+
+ actions.startSession({ state, dispatch });
+
+ expect(dispatch).not.toHaveBeenCalled();
+ });
+
+ it('dispatches request and receive on success', () => {
+ mock.onPost(/.*\/ide_terminals/).reply(200, TEST_SESSION);
+
+ return testAction(
+ actions.startSession,
+ null,
+ { ...rootGetters, ...rootState },
+ [],
+ [
+ { type: 'requestStartSession' },
+ { type: 'receiveStartSessionSuccess', payload: TEST_SESSION },
+ ],
+ );
+ });
+
+ it('dispatches request and receive on error', () => {
+ mock.onPost(/.*\/ide_terminals/).reply(400);
+
+ return testAction(
+ actions.startSession,
+ null,
+ { ...rootGetters, ...rootState },
+ [],
+ [
+ { type: 'requestStartSession' },
+ { type: 'receiveStartSessionError', payload: expect.any(Error) },
+ ],
+ );
+ });
+ });
+
+ describe('requestStopSession', () => {
+ it('sets session status', () => {
+ return testAction(
+ actions.requestStopSession,
+ null,
+ {},
+ [{ type: mutationTypes.SET_SESSION_STATUS, payload: STOPPING }],
+ [],
+ );
+ });
+ });
+
+ describe('receiveStopSessionSuccess', () => {
+ it('kills the session', () => {
+ return testAction(actions.receiveStopSessionSuccess, null, {}, [], [{ type: 'killSession' }]);
+ });
+ });
+
+ describe('receiveStopSessionError', () => {
+ it('flashes message', () => {
+ actions.receiveStopSessionError({ dispatch });
+
+ expect(createFlash).toHaveBeenCalledWith(messages.UNEXPECTED_ERROR_STOPPING);
+ });
+
+ it('kills the session', () => {
+ return testAction(actions.receiveStopSessionError, null, {}, [], [{ type: 'killSession' }]);
+ });
+ });
+
+ describe('stopSession', () => {
+ it('dispatches request and receive on success', () => {
+ mock.onPost(TEST_SESSION.cancel_path).reply(200, {});
+
+ const state = {
+ session: { cancelPath: TEST_SESSION.cancel_path },
+ };
+
+ return testAction(
+ actions.stopSession,
+ null,
+ state,
+ [],
+ [{ type: 'requestStopSession' }, { type: 'receiveStopSessionSuccess' }],
+ );
+ });
+
+ it('dispatches request and receive on error', () => {
+ mock.onPost(TEST_SESSION.cancel_path).reply(400);
+
+ const state = {
+ session: { cancelPath: TEST_SESSION.cancel_path },
+ };
+
+ return testAction(
+ actions.stopSession,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestStopSession' },
+ { type: 'receiveStopSessionError', payload: expect.any(Error) },
+ ],
+ );
+ });
+ });
+
+ describe('killSession', () => {
+ it('stops polling and sets status', () => {
+ return testAction(
+ actions.killSession,
+ null,
+ {},
+ [{ type: mutationTypes.SET_SESSION_STATUS, payload: STOPPED }],
+ [{ type: 'stopPollingSessionStatus' }],
+ );
+ });
+ });
+
+ describe('restartSession', () => {
+ let state;
+
+ beforeEach(() => {
+ state = {
+ session: { status: STOPPED, retryPath: 'test/retry' },
+ };
+ });
+
+ it('does nothing if current not stopped', () => {
+ state.session.status = STOPPING;
+
+ actions.restartSession({ state, dispatch, rootState });
+
+ expect(dispatch).not.toHaveBeenCalled();
+ });
+
+ it('dispatches startSession if retryPath is empty', () => {
+ state.session.retryPath = '';
+
+ return testAction(
+ actions.restartSession,
+ null,
+ { ...state, ...rootState },
+ [],
+ [{ type: 'startSession' }],
+ );
+ });
+
+ it('dispatches request and receive on success', () => {
+ mock
+ .onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' })
+ .reply(200, TEST_SESSION);
+
+ return testAction(
+ actions.restartSession,
+ null,
+ { ...state, ...rootState },
+ [],
+ [
+ { type: 'requestStartSession' },
+ { type: 'receiveStartSessionSuccess', payload: TEST_SESSION },
+ ],
+ );
+ });
+
+ it('dispatches request and receive on error', () => {
+ mock
+ .onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' })
+ .reply(400);
+
+ return testAction(
+ actions.restartSession,
+ null,
+ { ...state, ...rootState },
+ [],
+ [
+ { type: 'requestStartSession' },
+ { type: 'receiveStartSessionError', payload: expect.any(Error) },
+ ],
+ );
+ });
+
+ [httpStatus.NOT_FOUND, httpStatus.UNPROCESSABLE_ENTITY].forEach(status => {
+ it(`dispatches request and startSession on ${status}`, () => {
+ mock
+ .onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' })
+ .reply(status);
+
+ return testAction(
+ actions.restartSession,
+ null,
+ { ...state, ...rootState },
+ [],
+ [{ type: 'requestStartSession' }, { type: 'startSession' }],
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
new file mode 100644
index 00000000000..7909f828124
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
@@ -0,0 +1,169 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import { PENDING, RUNNING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
+import * as messages from '~/ide/stores/modules/terminal/messages';
+import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
+import * as actions from '~/ide/stores/modules/terminal/actions/session_status';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+
+jest.mock('~/flash');
+
+const TEST_SESSION = {
+ id: 7,
+ status: PENDING,
+ show_path: 'path/show',
+ cancel_path: 'path/cancel',
+ retry_path: 'path/retry',
+ terminal_path: 'path/terminal',
+};
+
+describe('IDE store terminal session controls actions', () => {
+ let mock;
+ let dispatch;
+ let commit;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ dispatch = jest.fn().mockName('dispatch');
+ commit = jest.fn().mockName('commit');
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('pollSessionStatus', () => {
+ it('starts interval to poll status', () => {
+ return testAction(
+ actions.pollSessionStatus,
+ null,
+ {},
+ [{ type: mutationTypes.SET_SESSION_STATUS_INTERVAL, payload: expect.any(Number) }],
+ [{ type: 'stopPollingSessionStatus' }, { type: 'fetchSessionStatus' }],
+ );
+ });
+
+ it('on interval, stops polling if no session', () => {
+ const state = {
+ session: null,
+ };
+
+ actions.pollSessionStatus({ state, dispatch, commit });
+ dispatch.mockClear();
+
+ jest.advanceTimersByTime(5001);
+
+ expect(dispatch).toHaveBeenCalledWith('stopPollingSessionStatus');
+ });
+
+ it('on interval, fetches status', () => {
+ const state = {
+ session: TEST_SESSION,
+ };
+
+ actions.pollSessionStatus({ state, dispatch, commit });
+ dispatch.mockClear();
+
+ jest.advanceTimersByTime(5001);
+
+ expect(dispatch).toHaveBeenCalledWith('fetchSessionStatus');
+ });
+ });
+
+ describe('stopPollingSessionStatus', () => {
+ it('does nothing if sessionStatusInterval is empty', () => {
+ return testAction(actions.stopPollingSessionStatus, null, {}, [], []);
+ });
+
+ it('clears interval', () => {
+ return testAction(
+ actions.stopPollingSessionStatus,
+ null,
+ { sessionStatusInterval: 7 },
+ [{ type: mutationTypes.SET_SESSION_STATUS_INTERVAL, payload: 0 }],
+ [],
+ );
+ });
+ });
+
+ describe('receiveSessionStatusSuccess', () => {
+ it('sets session status', () => {
+ return testAction(
+ actions.receiveSessionStatusSuccess,
+ { status: RUNNING },
+ {},
+ [{ type: mutationTypes.SET_SESSION_STATUS, payload: RUNNING }],
+ [],
+ );
+ });
+
+ [STOPPING, STOPPED, 'unexpected'].forEach(status => {
+ it(`kills session if status is ${status}`, () => {
+ return testAction(
+ actions.receiveSessionStatusSuccess,
+ { status },
+ {},
+ [{ type: mutationTypes.SET_SESSION_STATUS, payload: status }],
+ [{ type: 'killSession' }],
+ );
+ });
+ });
+ });
+
+ describe('receiveSessionStatusError', () => {
+ it('flashes message', () => {
+ actions.receiveSessionStatusError({ dispatch });
+
+ expect(createFlash).toHaveBeenCalledWith(messages.UNEXPECTED_ERROR_STATUS);
+ });
+
+ it('kills the session', () => {
+ return testAction(actions.receiveSessionStatusError, null, {}, [], [{ type: 'killSession' }]);
+ });
+ });
+
+ describe('fetchSessionStatus', () => {
+ let state;
+
+ beforeEach(() => {
+ state = {
+ session: {
+ showPath: TEST_SESSION.show_path,
+ },
+ };
+ });
+
+ it('does nothing if session is falsey', () => {
+ state.session = null;
+
+ actions.fetchSessionStatus({ dispatch, state });
+
+ expect(dispatch).not.toHaveBeenCalled();
+ });
+
+ it('dispatches success on success', () => {
+ mock.onGet(state.session.showPath).reply(200, TEST_SESSION);
+
+ return testAction(
+ actions.fetchSessionStatus,
+ null,
+ state,
+ [],
+ [{ type: 'receiveSessionStatusSuccess', payload: TEST_SESSION }],
+ );
+ });
+
+ it('dispatches error on error', () => {
+ mock.onGet(state.session.showPath).reply(400);
+
+ return testAction(
+ actions.fetchSessionStatus,
+ null,
+ state,
+ [],
+ [{ type: 'receiveSessionStatusError', payload: expect.any(Error) }],
+ );
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/setup_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/setup_spec.js
new file mode 100644
index 00000000000..8bf3b58228e
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/terminal/actions/setup_spec.js
@@ -0,0 +1,40 @@
+import testAction from 'helpers/vuex_action_helper';
+import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
+import * as actions from '~/ide/stores/modules/terminal/actions/setup';
+
+describe('IDE store terminal setup actions', () => {
+ describe('init', () => {
+ it('dispatches checks', () => {
+ return testAction(
+ actions.init,
+ null,
+ {},
+ [],
+ [{ type: 'fetchConfigCheck' }, { type: 'fetchRunnersCheck' }],
+ );
+ });
+ });
+
+ describe('hideSplash', () => {
+ it('commits HIDE_SPLASH', () => {
+ return testAction(actions.hideSplash, null, {}, [{ type: mutationTypes.HIDE_SPLASH }], []);
+ });
+ });
+
+ describe('setPaths', () => {
+ it('commits SET_PATHS', () => {
+ const paths = {
+ foo: 'bar',
+ lorem: 'ipsum',
+ };
+
+ return testAction(
+ actions.setPaths,
+ paths,
+ {},
+ [{ type: mutationTypes.SET_PATHS, payload: paths }],
+ [],
+ );
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/modules/terminal/getters_spec.js b/spec/frontend/ide/stores/modules/terminal/getters_spec.js
new file mode 100644
index 00000000000..b5d6a4bc746
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/terminal/getters_spec.js
@@ -0,0 +1,50 @@
+import { CHECK_CONFIG, CHECK_RUNNERS } from '~/ide/stores/modules/terminal/constants';
+import * as getters from '~/ide/stores/modules/terminal/getters';
+
+describe('IDE store terminal getters', () => {
+ describe('allCheck', () => {
+ it('is loading if one check is loading', () => {
+ const checks = {
+ [CHECK_CONFIG]: { isLoading: false, isValid: true },
+ [CHECK_RUNNERS]: { isLoading: true },
+ };
+
+ const result = getters.allCheck({ checks });
+
+ expect(result).toEqual({
+ isLoading: true,
+ });
+ });
+
+ it('is invalid if one check is invalid', () => {
+ const message = 'lorem ipsum';
+ const checks = {
+ [CHECK_CONFIG]: { isLoading: false, isValid: false, message },
+ [CHECK_RUNNERS]: { isLoading: false, isValid: true },
+ };
+
+ const result = getters.allCheck({ checks });
+
+ expect(result).toEqual({
+ isLoading: false,
+ isValid: false,
+ message,
+ });
+ });
+
+ it('is valid if all checks are valid', () => {
+ const checks = {
+ [CHECK_CONFIG]: { isLoading: false, isValid: true },
+ [CHECK_RUNNERS]: { isLoading: false, isValid: true },
+ };
+
+ const result = getters.allCheck({ checks });
+
+ expect(result).toEqual({
+ isLoading: false,
+ isValid: true,
+ message: '',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/modules/terminal/messages_spec.js b/spec/frontend/ide/stores/modules/terminal/messages_spec.js
new file mode 100644
index 00000000000..966158999da
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/terminal/messages_spec.js
@@ -0,0 +1,38 @@
+import { escape } from 'lodash';
+import { TEST_HOST } from 'spec/test_constants';
+import * as messages from '~/ide/stores/modules/terminal/messages';
+import { sprintf } from '~/locale';
+import httpStatus from '~/lib/utils/http_status';
+
+const TEST_HELP_URL = `${TEST_HOST}/help`;
+
+describe('IDE store terminal messages', () => {
+ describe('configCheckError', () => {
+ it('returns job error, with status UNPROCESSABLE_ENTITY', () => {
+ const result = messages.configCheckError(httpStatus.UNPROCESSABLE_ENTITY, TEST_HELP_URL);
+
+ expect(result).toBe(
+ sprintf(
+ messages.ERROR_CONFIG,
+ {
+ helpStart: `<a href="${escape(TEST_HELP_URL)}" target="_blank">`,
+ helpEnd: '</a>',
+ },
+ false,
+ ),
+ );
+ });
+
+ it('returns permission error, with status FORBIDDEN', () => {
+ const result = messages.configCheckError(httpStatus.FORBIDDEN, TEST_HELP_URL);
+
+ expect(result).toBe(messages.ERROR_PERMISSION);
+ });
+
+ it('returns unexpected error, with unexpected status', () => {
+ const result = messages.configCheckError(httpStatus.NOT_FOUND, TEST_HELP_URL);
+
+ expect(result).toBe(messages.UNEXPECTED_ERROR_CONFIG);
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/modules/terminal/mutations_spec.js b/spec/frontend/ide/stores/modules/terminal/mutations_spec.js
new file mode 100644
index 00000000000..e9933bdd7be
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/terminal/mutations_spec.js
@@ -0,0 +1,142 @@
+import {
+ CHECK_CONFIG,
+ CHECK_RUNNERS,
+ RUNNING,
+ STOPPING,
+} from '~/ide/stores/modules/terminal/constants';
+import createState from '~/ide/stores/modules/terminal/state';
+import * as types from '~/ide/stores/modules/terminal/mutation_types';
+import mutations from '~/ide/stores/modules/terminal/mutations';
+
+describe('IDE store terminal mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ });
+
+ describe(types.SET_VISIBLE, () => {
+ it('sets isVisible', () => {
+ state.isVisible = false;
+
+ mutations[types.SET_VISIBLE](state, true);
+
+ expect(state.isVisible).toBe(true);
+ });
+ });
+
+ describe(types.HIDE_SPLASH, () => {
+ it('sets isShowSplash', () => {
+ state.isShowSplash = true;
+
+ mutations[types.HIDE_SPLASH](state);
+
+ expect(state.isShowSplash).toBe(false);
+ });
+ });
+
+ describe(types.SET_PATHS, () => {
+ it('sets paths', () => {
+ const paths = {
+ test: 'foo',
+ };
+
+ mutations[types.SET_PATHS](state, paths);
+
+ expect(state.paths).toBe(paths);
+ });
+ });
+
+ describe(types.REQUEST_CHECK, () => {
+ it('sets isLoading for check', () => {
+ const type = CHECK_CONFIG;
+
+ state.checks[type] = {};
+ mutations[types.REQUEST_CHECK](state, type);
+
+ expect(state.checks[type]).toEqual({
+ isLoading: true,
+ });
+ });
+ });
+
+ describe(types.RECEIVE_CHECK_ERROR, () => {
+ it('sets error for check', () => {
+ const type = CHECK_RUNNERS;
+ const message = 'lorem ipsum';
+
+ state.checks[type] = {};
+ mutations[types.RECEIVE_CHECK_ERROR](state, { type, message });
+
+ expect(state.checks[type]).toEqual({
+ isLoading: false,
+ isValid: false,
+ message,
+ });
+ });
+ });
+
+ describe(types.RECEIVE_CHECK_SUCCESS, () => {
+ it('sets success for check', () => {
+ const type = CHECK_CONFIG;
+
+ state.checks[type] = {};
+ mutations[types.RECEIVE_CHECK_SUCCESS](state, type);
+
+ expect(state.checks[type]).toEqual({
+ isLoading: false,
+ isValid: true,
+ message: null,
+ });
+ });
+ });
+
+ describe(types.SET_SESSION, () => {
+ it('sets session', () => {
+ const session = {
+ terminalPath: 'terminal/foo',
+ status: RUNNING,
+ };
+
+ mutations[types.SET_SESSION](state, session);
+
+ expect(state.session).toBe(session);
+ });
+ });
+
+ describe(types.SET_SESSION_STATUS, () => {
+ it('sets session if a session does not exists', () => {
+ const status = RUNNING;
+
+ mutations[types.SET_SESSION_STATUS](state, status);
+
+ expect(state.session).toEqual({
+ status,
+ });
+ });
+
+ it('sets session status', () => {
+ state.session = {
+ terminalPath: 'terminal/foo',
+ status: RUNNING,
+ };
+
+ mutations[types.SET_SESSION_STATUS](state, STOPPING);
+
+ expect(state.session).toEqual({
+ terminalPath: 'terminal/foo',
+ status: STOPPING,
+ });
+ });
+ });
+
+ describe(types.SET_SESSION_STATUS_INTERVAL, () => {
+ it('sets sessionStatusInterval', () => {
+ const val = 7;
+
+ mutations[types.SET_SESSION_STATUS_INTERVAL](state, val);
+
+ expect(state.sessionStatusInterval).toEqual(val);
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js b/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js
new file mode 100644
index 00000000000..ac976300ed0
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js
@@ -0,0 +1,118 @@
+import * as actions from '~/ide/stores/modules/terminal_sync/actions';
+import mirror, { canConnect, SERVICE_NAME } from '~/ide/lib/mirror';
+import * as types from '~/ide/stores/modules/terminal_sync/mutation_types';
+import testAction from 'helpers/vuex_action_helper';
+
+jest.mock('~/ide/lib/mirror');
+
+const TEST_SESSION = {
+ proxyWebsocketPath: 'test/path',
+ services: [SERVICE_NAME],
+};
+
+describe('ide/stores/modules/terminal_sync/actions', () => {
+ let rootState;
+
+ beforeEach(() => {
+ canConnect.mockReturnValue(true);
+ rootState = {
+ changedFiles: [],
+ terminal: {},
+ };
+ });
+
+ describe('upload', () => {
+ it('uploads to mirror and sets success', done => {
+ mirror.upload.mockReturnValue(Promise.resolve());
+
+ testAction(
+ actions.upload,
+ null,
+ rootState,
+ [{ type: types.START_LOADING }, { type: types.SET_SUCCESS }],
+ [],
+ () => {
+ expect(mirror.upload).toHaveBeenCalledWith(rootState);
+ done();
+ },
+ );
+ });
+
+ it('sets error when failed', done => {
+ const err = { message: 'it failed!' };
+ mirror.upload.mockReturnValue(Promise.reject(err));
+
+ testAction(
+ actions.upload,
+ null,
+ rootState,
+ [{ type: types.START_LOADING }, { type: types.SET_ERROR, payload: err }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('stop', () => {
+ it('disconnects from mirror', done => {
+ testAction(actions.stop, null, rootState, [{ type: types.STOP }], [], () => {
+ expect(mirror.disconnect).toHaveBeenCalled();
+ done();
+ });
+ });
+ });
+
+ describe('start', () => {
+ it.each`
+ session | canConnectMock | description
+ ${null} | ${true} | ${'does not exist'}
+ ${{}} | ${true} | ${'does not have proxyWebsocketPath'}
+ ${{ proxyWebsocketPath: 'test/path' }} | ${false} | ${'can not connect service'}
+ `('rejects if session $description', ({ session, canConnectMock }) => {
+ canConnect.mockReturnValue(canConnectMock);
+
+ const result = actions.start({ rootState: { terminal: { session } } });
+
+ return expect(result).rejects.toBe(undefined);
+ });
+
+ describe('with terminal session in state', () => {
+ beforeEach(() => {
+ rootState = {
+ terminal: { session: TEST_SESSION },
+ };
+ });
+
+ it('connects to mirror and sets success', done => {
+ mirror.connect.mockReturnValue(Promise.resolve());
+
+ testAction(
+ actions.start,
+ null,
+ rootState,
+ [{ type: types.START_LOADING }, { type: types.SET_SUCCESS }],
+ [],
+ () => {
+ expect(mirror.connect).toHaveBeenCalledWith(TEST_SESSION.proxyWebsocketPath);
+ done();
+ },
+ );
+ });
+
+ it('sets error if connection fails', () => {
+ const commit = jest.fn();
+ const err = new Error('test');
+ mirror.connect.mockReturnValue(Promise.reject(err));
+
+ const result = actions.start({ rootState, commit });
+
+ return Promise.all([
+ expect(result).rejects.toEqual(err),
+ result.catch(() => {
+ expect(commit).toHaveBeenCalledWith(types.SET_ERROR, err);
+ }),
+ ]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/modules/terminal_sync/mutations_spec.js b/spec/frontend/ide/stores/modules/terminal_sync/mutations_spec.js
new file mode 100644
index 00000000000..ecf35d60e96
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/terminal_sync/mutations_spec.js
@@ -0,0 +1,89 @@
+import createState from '~/ide/stores/modules/terminal_sync/state';
+import * as types from '~/ide/stores/modules/terminal_sync/mutation_types';
+import mutations from '~/ide/stores/modules/terminal_sync/mutations';
+
+const TEST_MESSAGE = 'lorem ipsum dolar sit';
+
+describe('ide/stores/modules/terminal_sync/mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ });
+
+ describe(types.START_LOADING, () => {
+ it('sets isLoading and resets error', () => {
+ Object.assign(state, {
+ isLoading: false,
+ isError: true,
+ });
+
+ mutations[types.START_LOADING](state);
+
+ expect(state).toEqual(
+ expect.objectContaining({
+ isLoading: true,
+ isError: false,
+ }),
+ );
+ });
+ });
+
+ describe(types.SET_ERROR, () => {
+ it('sets isLoading and error message', () => {
+ Object.assign(state, {
+ isLoading: true,
+ isError: false,
+ message: '',
+ });
+
+ mutations[types.SET_ERROR](state, { message: TEST_MESSAGE });
+
+ expect(state).toEqual(
+ expect.objectContaining({
+ isLoading: false,
+ isError: true,
+ message: TEST_MESSAGE,
+ }),
+ );
+ });
+ });
+
+ describe(types.SET_SUCCESS, () => {
+ it('sets isLoading and resets error and is started', () => {
+ Object.assign(state, {
+ isLoading: true,
+ isError: true,
+ isStarted: false,
+ });
+
+ mutations[types.SET_SUCCESS](state);
+
+ expect(state).toEqual(
+ expect.objectContaining({
+ isLoading: false,
+ isError: false,
+ isStarted: true,
+ }),
+ );
+ });
+ });
+
+ describe(types.STOP, () => {
+ it('sets stop values', () => {
+ Object.assign(state, {
+ isLoading: true,
+ isStarted: true,
+ });
+
+ mutations[types.STOP](state);
+
+ expect(state).toEqual(
+ expect.objectContaining({
+ isLoading: false,
+ isStarted: false,
+ }),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/mutations/file_spec.js b/spec/frontend/ide/stores/mutations/file_spec.js
index 9b96b910fcb..ff904bbc9cd 100644
--- a/spec/frontend/ide/stores/mutations/file_spec.js
+++ b/spec/frontend/ide/stores/mutations/file_spec.js
@@ -60,22 +60,14 @@ describe('IDE store file mutations', () => {
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();
});
@@ -356,14 +348,6 @@ describe('IDE store file mutations', () => {
expect(localState.changedFiles.length).toBe(1);
});
-
- it('bursts unused seal', () => {
- expect(localState.unusedSeal).toBe(true);
-
- mutations.ADD_FILE_TO_CHANGED(localState, localFile.path);
-
- expect(localState.unusedSeal).toBe(false);
- });
});
describe('REMOVE_FILE_FROM_CHANGED', () => {
@@ -374,14 +358,6 @@ describe('IDE store file mutations', () => {
expect(localState.changedFiles.length).toBe(0);
});
-
- it('bursts unused seal', () => {
- expect(localState.unusedSeal).toBe(true);
-
- mutations.REMOVE_FILE_FROM_CHANGED(localState, localFile.path);
-
- expect(localState.unusedSeal).toBe(false);
- });
});
describe.each`
@@ -533,19 +509,6 @@ describe('IDE store file mutations', () => {
},
);
- describe('STAGE_CHANGE', () => {
- it('bursts unused seal', () => {
- expect(localState.unusedSeal).toBe(true);
-
- mutations.STAGE_CHANGE(localState, {
- path: localFile.path,
- diffInfo: localStore.getters.getDiffInfo(localFile.path),
- });
-
- expect(localState.unusedSeal).toBe(false);
- });
- });
-
describe('TOGGLE_FILE_CHANGED', () => {
it('updates file changed status', () => {
mutations.TOGGLE_FILE_CHANGED(localState, {
diff --git a/spec/frontend/ide/stores/mutations_spec.js b/spec/frontend/ide/stores/mutations_spec.js
index 2eca9acb8d8..1b29648fb8b 100644
--- a/spec/frontend/ide/stores/mutations_spec.js
+++ b/spec/frontend/ide/stores/mutations_spec.js
@@ -120,24 +120,6 @@ describe('Multi-file store mutations', () => {
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', () => {
@@ -265,16 +247,6 @@ describe('Multi-file store mutations', () => {
expect(localState.changedFiles).toEqual([]);
expect(localState.stagedFiles).toEqual([]);
});
-
- it('bursts unused seal', () => {
- localState.entries.test = file('test');
-
- expect(localState.unusedSeal).toBe(true);
-
- mutations.DELETE_ENTRY(localState, 'test');
-
- expect(localState.unusedSeal).toBe(false);
- });
});
describe('UPDATE_FILE_AFTER_COMMIT', () => {
@@ -283,10 +255,6 @@ describe('Multi-file store mutations', () => {
...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);
@@ -301,10 +269,6 @@ describe('Multi-file store mutations', () => {
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,
diff --git a/spec/frontend/ide/stores/plugins/terminal_spec.js b/spec/frontend/ide/stores/plugins/terminal_spec.js
new file mode 100644
index 00000000000..948c2131fd8
--- /dev/null
+++ b/spec/frontend/ide/stores/plugins/terminal_spec.js
@@ -0,0 +1,58 @@
+import { createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { TEST_HOST } from 'helpers/test_constants';
+import terminalModule from '~/ide/stores/modules/terminal';
+import createTerminalPlugin from '~/ide/stores/plugins/terminal';
+import { SET_BRANCH_WORKING_REFERENCE } from '~/ide/stores/mutation_types';
+
+const TEST_DATASET = {
+ eeWebTerminalSvgPath: `${TEST_HOST}/web/terminal/svg`,
+ eeWebTerminalHelpPath: `${TEST_HOST}/web/terminal/help`,
+ eeWebTerminalConfigHelpPath: `${TEST_HOST}/web/terminal/config/help`,
+ eeWebTerminalRunnersHelpPath: `${TEST_HOST}/web/terminal/runners/help`,
+};
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('ide/stores/extend', () => {
+ let store;
+
+ beforeEach(() => {
+ const el = document.createElement('div');
+ Object.assign(el.dataset, TEST_DATASET);
+
+ store = new Vuex.Store({
+ mutations: {
+ [SET_BRANCH_WORKING_REFERENCE]: () => {},
+ },
+ });
+
+ jest.spyOn(store, 'registerModule').mockImplementation();
+ jest.spyOn(store, 'dispatch').mockImplementation();
+
+ const plugin = createTerminalPlugin(el);
+
+ plugin(store);
+ });
+
+ it('registers terminal module', () => {
+ expect(store.registerModule).toHaveBeenCalledWith('terminal', terminalModule());
+ });
+
+ it('dispatches terminal/setPaths', () => {
+ expect(store.dispatch).toHaveBeenCalledWith('terminal/setPaths', {
+ webTerminalSvgPath: TEST_DATASET.eeWebTerminalSvgPath,
+ webTerminalHelpPath: TEST_DATASET.eeWebTerminalHelpPath,
+ webTerminalConfigHelpPath: TEST_DATASET.eeWebTerminalConfigHelpPath,
+ webTerminalRunnersHelpPath: TEST_DATASET.eeWebTerminalRunnersHelpPath,
+ });
+ });
+
+ it(`dispatches terminal/init on ${SET_BRANCH_WORKING_REFERENCE}`, () => {
+ store.dispatch.mockReset();
+
+ store.commit(SET_BRANCH_WORKING_REFERENCE);
+
+ expect(store.dispatch).toHaveBeenCalledWith('terminal/init');
+ });
+});
diff --git a/spec/frontend/ide/stores/plugins/terminal_sync_spec.js b/spec/frontend/ide/stores/plugins/terminal_sync_spec.js
new file mode 100644
index 00000000000..2aa3e770e7d
--- /dev/null
+++ b/spec/frontend/ide/stores/plugins/terminal_sync_spec.js
@@ -0,0 +1,72 @@
+import createTerminalPlugin from '~/ide/stores/plugins/terminal';
+import createTerminalSyncPlugin from '~/ide/stores/plugins/terminal_sync';
+import { SET_SESSION_STATUS } from '~/ide/stores/modules/terminal/mutation_types';
+import { RUNNING, STOPPING } from '~/ide/stores/modules/terminal/constants';
+import { createStore } from '~/ide/stores';
+import eventHub from '~/ide/eventhub';
+
+jest.mock('~/ide/lib/mirror');
+
+const ACTION_START = 'terminalSync/start';
+const ACTION_STOP = 'terminalSync/stop';
+const ACTION_UPLOAD = 'terminalSync/upload';
+const FILES_CHANGE_EVENT = 'ide.files.change';
+
+describe('IDE stores/plugins/mirror', () => {
+ let store;
+
+ beforeEach(() => {
+ const root = document.createElement('div');
+
+ store = createStore();
+ createTerminalPlugin(root)(store);
+
+ store.dispatch = jest.fn(() => Promise.resolve());
+
+ createTerminalSyncPlugin(root)(store);
+ });
+
+ it('does nothing on ide.files.change event', () => {
+ eventHub.$emit(FILES_CHANGE_EVENT);
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+
+ describe('when session starts running', () => {
+ beforeEach(() => {
+ store.commit(`terminal/${SET_SESSION_STATUS}`, RUNNING);
+ });
+
+ it('starts', () => {
+ expect(store.dispatch).toHaveBeenCalledWith(ACTION_START);
+ });
+
+ it('uploads when ide.files.change is emitted', () => {
+ expect(store.dispatch).not.toHaveBeenCalledWith(ACTION_UPLOAD);
+
+ eventHub.$emit(FILES_CHANGE_EVENT);
+
+ jest.runAllTimers();
+
+ expect(store.dispatch).toHaveBeenCalledWith(ACTION_UPLOAD);
+ });
+
+ describe('when session stops', () => {
+ beforeEach(() => {
+ store.commit(`terminal/${SET_SESSION_STATUS}`, STOPPING);
+ });
+
+ it('stops', () => {
+ expect(store.dispatch).toHaveBeenCalledWith(ACTION_STOP);
+ });
+
+ it('does not upload anymore', () => {
+ eventHub.$emit(FILES_CHANGE_EVENT);
+
+ jest.runAllTimers();
+
+ expect(store.dispatch).not.toHaveBeenCalledWith(ACTION_UPLOAD);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/utils_spec.js b/spec/frontend/ide/stores/utils_spec.js
index b87f6c1f05a..d1eb4304c79 100644
--- a/spec/frontend/ide/stores/utils_spec.js
+++ b/spec/frontend/ide/stores/utils_spec.js
@@ -28,61 +28,6 @@ describe('Multi-file store utils', () => {
});
});
- 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 = {
@@ -101,12 +46,11 @@ describe('Multi-file store utils', () => {
path: 'added',
tempFile: true,
content: 'new file content',
- base64: true,
+ rawPath: 'data:image/png;base64,abc',
lastCommitSha: '123456789',
},
{ ...file('deletedFile'), path: 'deletedFile', deleted: true },
{ ...file('renamedFile'), path: 'renamedFile', prevPath: 'prevPath' },
- { ...file('replacingFile'), path: 'replacingFile', replaces: true },
],
currentBranchId: 'master',
};
@@ -154,14 +98,6 @@ describe('Multi-file store utils', () => {
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,
});
@@ -181,7 +117,7 @@ describe('Multi-file store utils', () => {
path: 'added',
tempFile: true,
content: 'new file content',
- base64: true,
+ rawPath: 'data:image/png;base64,abc',
lastCommitSha: '123456789',
},
],
@@ -661,31 +597,6 @@ describe('Multi-file store utils', () => {
});
});
- 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);
- });
- });
- });
-
describe('extractMarkdownImagesFromEntries', () => {
let mdFile;
let entries;