diff options
Diffstat (limited to 'spec/javascripts/ide/stores')
-rw-r--r-- | spec/javascripts/ide/stores/actions/project_spec.js | 197 | ||||
-rw-r--r-- | spec/javascripts/ide/stores/actions_spec.js | 351 | ||||
-rw-r--r-- | spec/javascripts/ide/stores/modules/commit/actions_spec.js | 92 | ||||
-rw-r--r-- | spec/javascripts/ide/stores/mutations/file_spec.js | 19 | ||||
-rw-r--r-- | spec/javascripts/ide/stores/mutations_spec.js | 389 | ||||
-rw-r--r-- | spec/javascripts/ide/stores/utils_spec.js | 225 |
6 files changed, 1021 insertions, 252 deletions
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js index 8ecb6129c63..bcc7b5d5e46 100644 --- a/spec/javascripts/ide/stores/actions/project_spec.js +++ b/spec/javascripts/ide/stores/actions/project_spec.js @@ -6,8 +6,10 @@ import { createNewBranchFromDefault, showEmptyState, openBranch, + loadFile, + loadBranch, } from '~/ide/stores/actions'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import service from '~/ide/services'; import api from '~/api'; import router from '~/ide/ide_router'; @@ -16,8 +18,10 @@ import testAction from '../../../helpers/vuex_action_helper'; describe('IDE store project actions', () => { let mock; + let store; beforeEach(() => { + store = createStore(); mock = new MockAdapter(axios); store.state.projects['abc/def'] = { @@ -231,28 +235,139 @@ describe('IDE store project actions', () => { }); }); + describe('loadFile', () => { + beforeEach(() => { + Object.assign(store.state, { + entries: { + foo: { pending: false }, + 'foo/bar-pending': { pending: true }, + 'foo/bar': { pending: false }, + }, + }); + spyOn(store, 'dispatch'); + }); + + 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', jasmine.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.calls.count()).toBe(1); + + expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', jasmine.anything()); + + expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', { + name: 'not-existent.md', + type: 'blob', + }); + }); + }); + + describe('loadBranch', () => { + const projectId = 'abc/def'; + const branchId = '123-lorem'; + + it('fetches branch data', done => { + spyOn(store, 'dispatch').and.returnValue(Promise.resolve()); + + loadBranch(store, { projectId, branchId }) + .then(() => { + expect(store.dispatch.calls.allArgs()).toEqual([ + ['getBranchData', { projectId, branchId }], + ['getMergeRequestsForBranch', { projectId, branchId }], + ['getFiles', { projectId, branchId }], + ]); + }) + .then(done) + .catch(done.fail); + }); + + it('shows an error if branch can not be fetched', done => { + spyOn(store, 'dispatch').and.returnValue(Promise.reject()); + + loadBranch(store, { projectId, branchId }) + .then(done.fail) + .catch(() => { + expect(store.dispatch.calls.allArgs()).toEqual([ + ['getBranchData', { projectId, branchId }], + ['showBranchNotFoundError', branchId], + ]); + done(); + }); + }); + }); + describe('openBranch', () => { + const projectId = 'abc/def'; + const branchId = '123-lorem'; + const branch = { - projectId: 'abc/def', - branchId: '123-lorem', + projectId, + branchId, }; beforeEach(() => { - store.state.entries = { - foo: { pending: false }, - 'foo/bar-pending': { pending: true }, - 'foo/bar': { pending: false }, - }; + Object.assign(store.state, { + entries: { + foo: { pending: false }, + 'foo/bar-pending': { pending: true }, + 'foo/bar': { pending: false }, + }, + }); + }); + + it('loads file right away if the branch has already been fetched', done => { + spyOn(store, 'dispatch'); + + Object.assign(store.state, { + projects: { + [projectId]: { + branches: { + [branchId]: { foo: 'bar' }, + }, + }, + }, + }); + + openBranch(store, branch) + .then(() => { + expect(store.dispatch.calls.allArgs()).toEqual([['loadFile', { basePath: undefined }]]); + }) + .then(done) + .catch(done.fail); }); describe('empty repo', () => { beforeEach(() => { spyOn(store, 'dispatch').and.returnValue(Promise.resolve()); - store.state.currentProjectId = 'abc/def'; - store.state.projects['abc/def'] = { - empty_repo: true, - }; + Object.assign(store.state, { + currentProjectId: 'abc/def', + projects: { + 'abc/def': { + empty_repo: true, + }, + }, + }); }); afterEach(() => { @@ -262,10 +377,7 @@ describe('IDE store project actions', () => { it('dispatches showEmptyState action right away', done => { openBranch(store, branch) .then(() => { - expect(store.dispatch.calls.allArgs()).toEqual([ - ['setCurrentBranchId', branch.branchId], - ['showEmptyState', branch], - ]); + expect(store.dispatch.calls.allArgs()).toEqual([['showEmptyState', branch]]); done(); }) .catch(done.fail); @@ -281,56 +393,14 @@ describe('IDE store project actions', () => { openBranch(store, branch) .then(() => { expect(store.dispatch.calls.allArgs()).toEqual([ - ['setCurrentBranchId', branch.branchId], - ['getBranchData', branch], - ['getMergeRequestsForBranch', branch], - ['getFiles', branch], + ['setCurrentBranchId', branchId], + ['loadBranch', { projectId, branchId }], + ['loadFile', { basePath: undefined }], ]); }) .then(done) .catch(done.fail); }); - - it('handles tree entry action, if basePath is given', done => { - openBranch(store, { ...branch, basePath: 'foo/bar/' }) - .then(() => { - expect(store.dispatch).toHaveBeenCalledWith( - 'handleTreeEntryAction', - store.state.entries['foo/bar'], - ); - }) - .then(done) - .catch(done.fail); - }); - - it('does not handle tree entry action, if entry is pending', done => { - openBranch(store, { ...branch, basePath: 'foo/bar-pending' }) - .then(() => { - expect(store.dispatch).not.toHaveBeenCalledWith( - 'handleTreeEntryAction', - jasmine.anything(), - ); - }) - .then(done) - .catch(done.fail); - }); - - it('creates a new file supplied via URL if the file does not exist yet', done => { - openBranch(store, { ...branch, basePath: 'not-existent.md' }) - .then(() => { - expect(store.dispatch).not.toHaveBeenCalledWith( - 'handleTreeEntryAction', - jasmine.anything(), - ); - - expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', { - name: 'not-existent.md', - type: 'blob', - }); - }) - .then(done) - .catch(done.fail); - }); }); describe('non-existent branch', () => { @@ -342,9 +412,8 @@ describe('IDE store project actions', () => { openBranch(store, branch) .then(() => { expect(store.dispatch.calls.allArgs()).toEqual([ - ['setCurrentBranchId', branch.branchId], - ['getBranchData', branch], - ['showBranchNotFoundError', branch.branchId], + ['setCurrentBranchId', branchId], + ['loadBranch', { projectId, branchId }], ]); }) .then(done) diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index 8504fb3f42b..7e77b859fdd 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -13,12 +13,15 @@ import actions, { createTempEntry, } from '~/ide/stores/actions'; import axios from '~/lib/utils/axios_utils'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import * as types from '~/ide/stores/mutation_types'; import router from '~/ide/ide_router'; import { resetStore, file } from '../helpers'; import testAction from '../../helpers/vuex_action_helper'; import MockAdapter from 'axios-mock-adapter'; +import eventHub from '~/ide/eventhub'; + +const store = createStore(); describe('Multi-file store actions', () => { beforeEach(() => { @@ -451,6 +454,24 @@ describe('Multi-file store actions', () => { 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', () => { @@ -540,82 +561,298 @@ describe('Multi-file store actions', () => { done, ); }); - }); - describe('renameEntry', () => { - it('renames entry', done => { - store.state.entries.test = { - tree: [], + it('if renamed, reverts the rename before deleting', () => { + const testEntry = { + path: 'test', + name: 'test', + prevPath: 'lorem/ipsum', + prevName: 'ipsum', + prevParentPath: 'lorem', }; + store.state.entries = { test: testEntry }; testAction( - renameEntry, - { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' }, + deleteEntry, + testEntry.path, store.state, + [], [ { - type: types.RENAME_ENTRY, - payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' }, - }, - { - type: types.TOGGLE_FILE_CHANGED, + type: 'renameEntry', payload: { - file: store.state.entries['parent-path/new-name'], - changed: true, + path: testEntry.path, + name: testEntry.prevName, + parentPath: testEntry.prevParentPath, }, }, + { + type: 'deleteEntry', + payload: testEntry.prevPath, + }, ], - [{ type: 'triggerFilesChange' }], - done, ); }); + }); - it('renames all entries in tree', done => { - store.state.entries.test = { - type: 'tree', - tree: [ - { - path: 'tree-1', - }, - { - path: 'tree-2', + describe('renameEntry', () => { + describe('purging of file model cache', () => { + beforeEach(() => { + spyOn(eventHub, '$emit'); + }); + + 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, }, - ], - }; + }); - testAction( - renameEntry, - { path: 'test', name: 'new-name', parentPath: 'parent-path' }, - store.state, - [ - { - type: types.RENAME_ENTRY, - payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' }, + store + .dispatch('renameEntry', { + path: 'test', + name: 'new', + }) + .then(() => { + expect(eventHub.$emit.calls.allArgs()).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, }, - ], - [ - { - type: 'renameEntry', - payload: { - path: 'test', - name: 'new-name', - entryPath: 'tree-1', - parentPath: 'parent-path/new-name', + }); + + 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, + }); + }); + + afterEach(() => { + resetStore(store); + }); + + it('by default renames an entry and adds to changed', done => { + testAction( + renameEntry, + { path: 'orig', name: 'renamed' }, + store.state, + [ + { + type: types.RENAME_ENTRY, + payload: { + path: 'orig', + name: 'renamed', + parentPath: undefined, + }, }, - }, - { - type: 'renameEntry', - payload: { - path: 'test', - name: 'new-name', - entryPath: 'tree-2', - parentPath: 'parent-path/new-name', + { + type: types.ADD_FILE_TO_CHANGED, + payload: 'renamed', + }, + ], + [{ type: 'triggerFilesChange' }], + done, + ); + }); + + it('if not changed, completely unstages 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, + [jasmine.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.calls.count()).toBe(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, }, - { type: 'triggerFilesChange' }, - ], - done, - ); + }); + + 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( + jasmine.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( + jasmine.objectContaining({ + path: 'old-folder/test', + name: 'test', + prevPath: undefined, + prevName: undefined, + }), + ); + }) + .then(done) + .catch(done.fail); + }); }); }); diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index ffb97c85326..95d927065f0 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -1,5 +1,5 @@ import rootActions from '~/ide/stores/actions'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import service from '~/ide/services'; import router from '~/ide/ide_router'; import eventHub from '~/ide/eventhub'; @@ -11,6 +11,7 @@ import { resetStore, file } from 'spec/ide/helpers'; import testAction from '../../../../helpers/vuex_action_helper'; const TEST_COMMIT_SHA = '123456789'; +const store = createStore(); describe('IDE commit module actions', () => { beforeEach(() => { @@ -59,7 +60,9 @@ describe('IDE commit module actions', () => { }); it('sets shouldCreateMR to true if "Create new MR" option is visible', done => { - store.state.shouldHideNewMrOption = false; + Object.assign(store.state, { + shouldHideNewMrOption: false, + }); testAction( actions.updateCommitAction, @@ -78,7 +81,9 @@ describe('IDE commit module actions', () => { }); it('sets shouldCreateMR to false if "Create new MR" option is hidden', done => { - store.state.shouldHideNewMrOption = true; + Object.assign(store.state, { + shouldHideNewMrOption: true, + }); testAction( actions.updateCommitAction, @@ -172,24 +177,31 @@ describe('IDE commit module actions', () => { content: 'file content', }); - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'master'; - store.state.projects.abcproject = { - web_url: 'web_url', - branches: { - master: { - workingReference: '', - commit: { - short_id: TEST_COMMIT_SHA, + Object.assign(store.state, { + currentProjectId: 'abcproject', + currentBranchId: 'master', + projects: { + abcproject: { + web_url: 'web_url', + branches: { + master: { + workingReference: '', + commit: { + short_id: TEST_COMMIT_SHA, + }, + }, }, }, }, - }; - store.state.stagedFiles.push(f, { - ...file('changedFile2'), - changed: true, + stagedFiles: [ + f, + { + ...file('changedFile2'), + changed: true, + }, + ], + openFiles: store.state.stagedFiles, }); - store.state.openFiles = store.state.stagedFiles; store.state.stagedFiles.forEach(stagedFile => { store.state.entries[stagedFile.path] = stagedFile; @@ -275,40 +287,40 @@ describe('IDE commit module actions', () => { document.body.innerHTML += '<div class="flash-container"></div>'; - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'master'; - store.state.projects.abcproject = { - web_url: 'webUrl', - branches: { - master: { - workingReference: '1', - commit: { - id: TEST_COMMIT_SHA, - }, - }, - }, - }; - const f = { ...file('changed'), type: 'blob', active: true, lastCommitSha: TEST_COMMIT_SHA, }; - store.state.stagedFiles.push(f); - store.state.changedFiles = [ - { - ...f, - }, - ]; - store.state.openFiles = store.state.changedFiles; - store.state.openFiles.forEach(localF => { - store.state.entries[localF.path] = localF; + 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, + }, + }, + }, + }, + }, }); store.state.commit.commitAction = '2'; store.state.commit.commitMessage = 'testing 123'; + + store.state.openFiles.forEach(localF => { + store.state.entries[localF.path] = localF; + }); }); afterEach(() => { diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js index 064e66cef64..7c46bf55318 100644 --- a/spec/javascripts/ide/stores/mutations/file_spec.js +++ b/spec/javascripts/ide/stores/mutations/file_spec.js @@ -356,16 +356,16 @@ describe('IDE store file mutations', () => { }); describe('STAGE_CHANGE', () => { - it('adds file into stagedFiles array', () => { + beforeEach(() => { mutations.STAGE_CHANGE(localState, localFile.path); + }); + it('adds file into stagedFiles array', () => { expect(localState.stagedFiles.length).toBe(1); expect(localState.stagedFiles[0]).toEqual(localFile); }); it('updates stagedFile if it is already staged', () => { - mutations.STAGE_CHANGE(localState, localFile.path); - localFile.raw = 'testing 123'; mutations.STAGE_CHANGE(localState, localFile.path); @@ -373,19 +373,6 @@ describe('IDE store file mutations', () => { expect(localState.stagedFiles.length).toBe(1); expect(localState.stagedFiles[0].raw).toEqual('testing 123'); }); - - it('adds already-staged file to `replacedFiles`', () => { - localFile.raw = 'already-staged'; - - mutations.STAGE_CHANGE(localState, localFile.path); - - localFile.raw = 'testing 123'; - - mutations.STAGE_CHANGE(localState, localFile.path); - - expect(localState.replacedFiles.length).toBe(1); - expect(localState.replacedFiles[0].raw).toEqual('already-staged'); - }); }); describe('UNSTAGE_CHANGE', () => { diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 2470c99e300..7dd5d323f69 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -79,16 +79,6 @@ describe('Multi-file store mutations', () => { }); }); - describe('CLEAR_REPLACED_FILES', () => { - it('clears replacedFiles array', () => { - localState.replacedFiles.push('a'); - - mutations.CLEAR_REPLACED_FILES(localState); - - expect(localState.replacedFiles.length).toBe(0); - }); - }); - describe('UPDATE_VIEWER', () => { it('sets viewer state', () => { mutations.UPDATE_VIEWER(localState, 'diff'); @@ -311,8 +301,7 @@ describe('Multi-file store mutations', () => { describe('UPDATE_FILE_AFTER_COMMIT', () => { it('updates URLs if prevPath is set', () => { const f = { - ...file(), - path: 'test', + ...file('test'), prevPath: 'testing-123', rawPath: `${gl.TEST_HOST}/testing-123`, permalink: `${gl.TEST_HOST}/testing-123`, @@ -325,19 +314,26 @@ describe('Multi-file store mutations', () => { mutations.UPDATE_FILE_AFTER_COMMIT(localState, { file: f, lastCommit: { commit: {} } }); - expect(f.rawPath).toBe(`${gl.TEST_HOST}/test`); - expect(f.permalink).toBe(`${gl.TEST_HOST}/test`); - expect(f.commitsPath).toBe(`${gl.TEST_HOST}/test`); - expect(f.blamePath).toBe(`${gl.TEST_HOST}/test`); - expect(f.replaces).toBe(false); + expect(f).toEqual( + jasmine.objectContaining({ + rawPath: `${gl.TEST_HOST}/test`, + permalink: `${gl.TEST_HOST}/test`, + commitsPath: `${gl.TEST_HOST}/test`, + blamePath: `${gl.TEST_HOST}/test`, + replaces: false, + prevId: undefined, + prevPath: undefined, + prevName: undefined, + prevUrl: undefined, + prevKey: undefined, + }), + ); }); }); describe('OPEN_NEW_ENTRY_MODAL', () => { it('sets entryModal', () => { - localState.entries.testPath = { - ...file(), - }; + localState.entries.testPath = file(); mutations.OPEN_NEW_ENTRY_MODAL(localState, { type: 'test', path: 'testPath' }); @@ -356,58 +352,178 @@ describe('Multi-file store mutations', () => { }; localState.currentProjectId = 'gitlab-ce'; localState.currentBranchId = 'master'; - localState.entries.oldPath = { - ...file(), - type: 'blob', - name: 'oldPath', - path: 'oldPath', - url: `${gl.TEST_HOST}/oldPath`, + localState.entries = { + oldPath: file('oldPath', 'oldPath', 'blob'), }; }); - it('creates new renamed entry', () => { + it('updates existing entry without creating a new one', () => { + mutations.RENAME_ENTRY(localState, { + path: 'oldPath', + name: 'newPath', + parentPath: '', + }); + + expect(localState.entries).toEqual({ + newPath: jasmine.objectContaining({ + path: 'newPath', + prevPath: 'oldPath', + }), + }); + }); + + it('correctly handles consecutive renames for the same entry', () => { mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath', + parentPath: '', + }); + + mutations.RENAME_ENTRY(localState, { + path: 'newPath', + name: 'newestPath', + parentPath: '', + }); + + expect(localState.entries).toEqual({ + newestPath: jasmine.objectContaining({ + path: 'newestPath', + prevPath: 'oldPath', + }), + }); + }); + + it('correctly handles the same entry within a consecutively renamed folder', () => { + const oldPath = file('root-folder/oldPath', 'root-folder/oldPath', 'blob'); + localState.entries = { + 'root-folder': { + ...file('root-folder', 'root-folder', 'tree'), + tree: [oldPath], + }, + 'root-folder/oldPath': oldPath, + }; + Object.assign(localState.entries['root-folder/oldPath'], { + parentPath: 'root-folder', + url: 'root-folder/oldPath-blob-root-folder/oldPath', + }); + + mutations.RENAME_ENTRY(localState, { + path: 'root-folder/oldPath', + name: 'renamed-folder/oldPath', entryPath: null, parentPath: '', }); + mutations.RENAME_ENTRY(localState, { + path: 'renamed-folder/oldPath', + name: 'simply-renamed/oldPath', + entryPath: null, + parentPath: '', + }); + + expect(localState.entries).toEqual({ + 'root-folder': jasmine.objectContaining({ + path: 'root-folder', + }), + 'simply-renamed/oldPath': jasmine.objectContaining({ + path: 'simply-renamed/oldPath', + prevPath: 'root-folder/oldPath', + }), + }); + }); + + it('renames entry, preserving old parameters', () => { + Object.assign(localState.entries.oldPath, { + url: `project/-/oldPath`, + }); + const oldPathData = localState.entries.oldPath; + + mutations.RENAME_ENTRY(localState, { + path: 'oldPath', + name: 'newPath', + parentPath: '', + }); + expect(localState.entries.newPath).toEqual({ - ...localState.entries.oldPath, + ...oldPathData, id: 'newPath', - name: 'newPath', - key: 'newPath-blob-oldPath', path: 'newPath', - tempFile: true, + name: 'newPath', + url: `project/-/newPath`, + key: jasmine.stringMatching('newPath'), + + prevId: 'oldPath', + prevName: 'oldPath', prevPath: 'oldPath', - tree: [], - parentPath: '', - url: `${gl.TEST_HOST}/newPath`, - moved: jasmine.anything(), - movedPath: jasmine.anything(), - opened: false, + prevUrl: `project/-/oldPath`, + prevKey: oldPathData.key, + prevParentPath: oldPathData.parentPath, }); }); - it('adds new entry to changedFiles', () => { - mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + it('does not store previous attributes on temp files', () => { + Object.assign(localState.entries.oldPath, { + tempFile: true, + }); + mutations.RENAME_ENTRY(localState, { + path: 'oldPath', + name: 'newPath', + entryPath: null, + parentPath: '', + }); - expect(localState.changedFiles.length).toBe(1); - expect(localState.changedFiles[0].path).toBe('newPath'); - }); + expect(localState.entries.newPath).not.toEqual( + jasmine.objectContaining({ + prevId: jasmine.anything(), + prevName: jasmine.anything(), + prevPath: jasmine.anything(), + prevUrl: jasmine.anything(), + prevKey: jasmine.anything(), + prevParentPath: jasmine.anything(), + }), + ); + }); + + it('properly handles files with spaces in name', () => { + const path = 'my fancy path'; + const newPath = 'new path'; + const oldEntry = { + ...file(path, path, 'blob'), + url: `project/-/${encodeURI(path)}`, + }; - it('sets oldEntry as moved', () => { - mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + localState.entries[path] = oldEntry; - expect(localState.entries.oldPath.moved).toBe(true); + mutations.RENAME_ENTRY(localState, { + path, + name: newPath, + entryPath: null, + parentPath: '', + }); + + expect(localState.entries[newPath]).toEqual({ + ...oldEntry, + id: newPath, + path: newPath, + name: newPath, + url: `project/-/new%20path`, + key: jasmine.stringMatching(newPath), + + prevId: path, + prevName: path, + prevPath: path, + prevUrl: `project/-/my%20fancy%20path`, + prevKey: oldEntry.key, + prevParentPath: oldEntry.parentPath, + }); }); - it('adds to parents tree', () => { - localState.entries.oldPath.parentPath = 'parentPath'; - localState.entries.parentPath = { - ...file(), + it('adds to parent tree', () => { + const parentEntry = { + ...file('parentPath', 'parentPath', 'tree'), + tree: [localState.entries.oldPath], }; + localState.entries.parentPath = parentEntry; mutations.RENAME_ENTRY(localState, { path: 'oldPath', @@ -416,7 +532,180 @@ describe('Multi-file store mutations', () => { parentPath: 'parentPath', }); - expect(localState.entries.parentPath.tree.length).toBe(1); + expect(parentEntry.tree.length).toBe(1); + expect(parentEntry.tree[0].name).toBe('newPath'); + }); + + it('sorts tree after renaming an entry', () => { + const alpha = file('alpha', 'alpha', 'blob'); + const beta = file('beta', 'beta', 'blob'); + const gamma = file('gamma', 'gamma', 'blob'); + localState.entries = { alpha, beta, gamma }; + + localState.trees['gitlab-ce/master'].tree = [alpha, beta, gamma]; + + mutations.RENAME_ENTRY(localState, { + path: 'alpha', + name: 'theta', + entryPath: null, + parentPath: '', + }); + + expect(localState.trees['gitlab-ce/master'].tree).toEqual([ + jasmine.objectContaining({ name: 'beta' }), + jasmine.objectContaining({ name: 'gamma' }), + jasmine.objectContaining({ + path: 'theta', + name: 'theta', + }), + ]); + }); + + it('updates openFiles with the renamed one if the original one is open', () => { + Object.assign(localState.entries.oldPath, { + opened: true, + type: 'blob', + }); + Object.assign(localState, { + openFiles: [localState.entries.oldPath], + }); + + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.openFiles.length).toBe(1); + expect(localState.openFiles[0].path).toBe('newPath'); + }); + + it('does not add renamed entry to changedFiles', () => { + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.changedFiles.length).toBe(0); + }); + + it('updates existing changedFiles entry with the renamed one', () => { + const origFile = { + ...file('oldPath', 'oldPath', 'blob'), + content: 'Foo', + }; + + Object.assign(localState, { + changedFiles: [origFile], + }); + Object.assign(localState.entries, { + oldPath: origFile, + }); + + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.changedFiles).toEqual([ + jasmine.objectContaining({ + path: 'newPath', + content: 'Foo', + }), + ]); + }); + + it('correctly saves original values if an entry is renamed multiple times', () => { + const original = { ...localState.entries.oldPath }; + const paramsToCheck = ['prevId', 'prevPath', 'prevName', 'prevUrl']; + const expectedObj = paramsToCheck.reduce( + (o, param) => ({ ...o, [param]: original[param.replace('prev', '').toLowerCase()] }), + {}, + ); + + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.entries.newPath).toEqual(jasmine.objectContaining(expectedObj)); + + mutations.RENAME_ENTRY(localState, { path: 'newPath', name: 'newer' }); + + expect(localState.entries.newer).toEqual(jasmine.objectContaining(expectedObj)); + }); + + describe('renaming back to original', () => { + beforeEach(() => { + const renamedEntry = { + ...file('renamed', 'renamed', 'blob'), + prevId: 'lorem/orig', + prevPath: 'lorem/orig', + prevName: 'orig', + prevUrl: 'project/-/loren/orig', + prevKey: 'lorem/orig', + prevParentPath: 'lorem', + }; + + localState.entries = { + renamed: renamedEntry, + }; + + mutations.RENAME_ENTRY(localState, { path: 'renamed', name: 'orig', parentPath: 'lorem' }); + }); + + it('renames entry and clears prev properties', () => { + expect(localState.entries).toEqual({ + 'lorem/orig': jasmine.objectContaining({ + id: 'lorem/orig', + path: 'lorem/orig', + name: 'orig', + prevId: undefined, + prevPath: undefined, + prevName: undefined, + prevUrl: undefined, + prevKey: undefined, + prevParentPath: undefined, + }), + }); + }); + }); + + describe('key updates', () => { + beforeEach(() => { + const rootFolder = file('rootFolder', 'rootFolder', 'tree'); + localState.entries = { + rootFolder, + oldPath: file('oldPath', 'oldPath', 'blob'), + 'oldPath.txt': file('oldPath.txt', 'oldPath.txt', 'blob'), + 'rootFolder/oldPath.md': file('oldPath.md', 'oldPath.md', 'blob', rootFolder), + }; + }); + + it('sets properly constucted key while preserving the original one', () => { + const key = 'oldPath.txt-blob-oldPath.txt'; + localState.entries['oldPath.txt'].key = key; + mutations.RENAME_ENTRY(localState, { path: 'oldPath.txt', name: 'newPath.md' }); + + expect(localState.entries['newPath.md'].key).toBe('newPath.md-blob-newPath.md'); + expect(localState.entries['newPath.md'].prevKey).toBe(key); + }); + + it('correctly updates key for an entry without an extension', () => { + localState.entries.oldPath.key = 'oldPath-blob-oldPath'; + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath.md' }); + + expect(localState.entries['newPath.md'].key).toBe('newPath.md-blob-newPath.md'); + }); + + it('correctly updates key when new name does not have an extension', () => { + localState.entries['oldPath.txt'].key = 'oldPath.txt-blob-oldPath.txt'; + mutations.RENAME_ENTRY(localState, { path: 'oldPath.txt', name: 'newPath' }); + + expect(localState.entries.newPath.key).toBe('newPath-blob-newPath'); + }); + + it('correctly updates key when renaming an entry in a folder', () => { + localState.entries['rootFolder/oldPath.md'].key = + 'rootFolder/oldPath.md-blob-rootFolder/oldPath.md'; + mutations.RENAME_ENTRY(localState, { + path: 'rootFolder/oldPath.md', + name: 'newPath.md', + entryPath: null, + parentPath: 'rootFolder', + }); + + expect(localState.entries['rootFolder/newPath.md'].key).toBe( + 'rootFolder/newPath.md-blob-rootFolder/newPath.md', + ); + }); }); }); }); diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js index 0fc9519a6bf..a477d4fc200 100644 --- a/spec/javascripts/ide/stores/utils_spec.js +++ b/spec/javascripts/ide/stores/utils_spec.js @@ -237,31 +237,6 @@ describe('Multi-file store utils', () => { }); describe('getCommitFiles', () => { - it('returns list of files excluding moved files', () => { - const files = [ - { - path: 'a', - type: 'blob', - deleted: true, - }, - { - path: 'c', - type: 'blob', - moved: true, - }, - ]; - - const flattendFiles = utils.getCommitFiles(files); - - expect(flattendFiles).toEqual([ - { - path: 'a', - type: 'blob', - deleted: true, - }, - ]); - }); - it('filters out folders from the list', () => { const files = [ { @@ -422,4 +397,204 @@ describe('Multi-file store utils', () => { expect(res[1].tree[0].opened).toEqual(true); }); }); + + describe('escapeFileUrl', () => { + it('encodes URL excluding the slashes', () => { + expect(utils.escapeFileUrl('/foo-bar/file.md')).toBe('/foo-bar/file.md'); + expect(utils.escapeFileUrl('foo bar/file.md')).toBe('foo%20bar/file.md'); + expect(utils.escapeFileUrl('foo/bar/file.md')).toBe('foo/bar/file.md'); + }); + }); + + describe('swapInStateArray', () => { + let localState; + + beforeEach(() => { + localState = []; + }); + + it('swaps existing entry with a new one', () => { + const file1 = { + ...file('old'), + key: 'foo', + }; + const file2 = file('new'); + const arr = [file1]; + + Object.assign(localState, { + dummyArray: arr, + entries: { + new: file2, + }, + }); + + utils.swapInStateArray(localState, 'dummyArray', 'foo', 'new'); + + expect(localState.dummyArray.length).toBe(1); + expect(localState.dummyArray[0]).toBe(file2); + }); + + it('does not add an item if it does not exist yet in array', () => { + const file1 = file('file'); + Object.assign(localState, { + dummyArray: [], + entries: { + file: file1, + }, + }); + + utils.swapInStateArray(localState, 'dummyArray', 'foo', 'file'); + + expect(localState.dummyArray.length).toBe(0); + }); + }); + + describe('swapInParentTreeWithSorting', () => { + let localState; + let branchInfo; + const currentProjectId = '123-foo'; + const currentBranchId = 'master'; + + beforeEach(() => { + localState = { + currentBranchId, + currentProjectId, + trees: { + [`${currentProjectId}/${currentBranchId}`]: { + tree: [], + }, + }, + entries: { + oldPath: file('oldPath', 'oldPath', 'blob'), + newPath: file('newPath', 'newPath', 'blob'), + parentPath: file('parentPath', 'parentPath', 'tree'), + }, + }; + branchInfo = localState.trees[`${currentProjectId}/${currentBranchId}`]; + }); + + it('does not change tree if newPath is not supplied', () => { + branchInfo.tree = [localState.entries.oldPath]; + + utils.swapInParentTreeWithSorting(localState, 'oldPath', undefined, undefined); + + expect(branchInfo.tree).toEqual([localState.entries.oldPath]); + }); + + describe('oldPath to replace is not defined: simple addition to tree', () => { + it('adds to tree on the state if there is no parent for the entry', () => { + expect(branchInfo.tree.length).toBe(0); + + utils.swapInParentTreeWithSorting(localState, undefined, 'oldPath', undefined); + + expect(branchInfo.tree.length).toBe(1); + expect(branchInfo.tree[0].name).toBe('oldPath'); + + utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', undefined); + + expect(branchInfo.tree.length).toBe(2); + expect(branchInfo.tree).toEqual([ + jasmine.objectContaining({ name: 'newPath' }), + jasmine.objectContaining({ name: 'oldPath' }), + ]); + }); + + it('adds to parent tree if it is supplied', () => { + utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', 'parentPath'); + + expect(localState.entries.parentPath.tree.length).toBe(1); + expect(localState.entries.parentPath.tree).toEqual([ + jasmine.objectContaining({ name: 'newPath' }), + ]); + + localState.entries.parentPath.tree = [localState.entries.oldPath]; + + utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', 'parentPath'); + + expect(localState.entries.parentPath.tree.length).toBe(2); + expect(localState.entries.parentPath.tree).toEqual([ + jasmine.objectContaining({ name: 'newPath' }), + jasmine.objectContaining({ name: 'oldPath' }), + ]); + }); + }); + + describe('swapping of the items', () => { + it('swaps entries if both paths are supplied', () => { + branchInfo.tree = [localState.entries.oldPath]; + + utils.swapInParentTreeWithSorting(localState, localState.entries.oldPath.key, 'newPath'); + + expect(branchInfo.tree).toEqual([jasmine.objectContaining({ name: 'newPath' })]); + + utils.swapInParentTreeWithSorting(localState, localState.entries.newPath.key, 'oldPath'); + + expect(branchInfo.tree).toEqual([jasmine.objectContaining({ name: 'oldPath' })]); + }); + + it('sorts tree after swapping the entries', () => { + const alpha = file('alpha', 'alpha', 'blob'); + const beta = file('beta', 'beta', 'blob'); + const gamma = file('gamma', 'gamma', 'blob'); + const theta = file('theta', 'theta', 'blob'); + localState.entries = { alpha, beta, gamma, theta }; + + branchInfo.tree = [alpha, beta, gamma]; + + utils.swapInParentTreeWithSorting(localState, alpha.key, 'theta'); + + expect(branchInfo.tree).toEqual([ + jasmine.objectContaining({ name: 'beta' }), + jasmine.objectContaining({ name: 'gamma' }), + jasmine.objectContaining({ name: 'theta' }), + ]); + + utils.swapInParentTreeWithSorting(localState, gamma.key, 'alpha'); + + expect(branchInfo.tree).toEqual([ + jasmine.objectContaining({ name: 'alpha' }), + jasmine.objectContaining({ name: 'beta' }), + jasmine.objectContaining({ name: 'theta' }), + ]); + + utils.swapInParentTreeWithSorting(localState, beta.key, 'gamma'); + + expect(branchInfo.tree).toEqual([ + jasmine.objectContaining({ name: 'alpha' }), + jasmine.objectContaining({ name: 'gamma' }), + jasmine.objectContaining({ name: 'theta' }), + ]); + }); + }); + }); + + describe('cleanTrailingSlash', () => { + [ + { input: '', output: '' }, + { input: 'abc', output: 'abc' }, + { input: 'abc/', output: 'abc' }, + { input: 'abc/def', output: 'abc/def' }, + { input: 'abc/def/', output: 'abc/def' }, + ].forEach(({ input, output }) => { + it(`cleans trailing slash from string "${input}"`, () => { + expect(utils.cleanTrailingSlash(input)).toEqual(output); + }); + }); + }); + + describe('pathsAreEqual', () => { + [ + { args: ['abc', 'abc'], output: true }, + { args: ['abc', 'def'], output: false }, + { args: ['abc/', 'abc'], output: true }, + { args: ['abc/abc', 'abc'], output: false }, + { args: ['/', ''], output: true }, + { args: ['', '/'], output: true }, + { args: [false, '/'], output: true }, + ].forEach(({ args, output }) => { + it(`cleans and tests equality (${JSON.stringify(args)})`, () => { + expect(utils.pathsAreEqual(...args)).toEqual(output); + }); + }); + }); }); |