diff options
Diffstat (limited to 'spec/frontend/ide/stores')
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; |