Commit d017d2d9 authored by Fatih Acet's avatar Fatih Acet

Merge branch '45687-web-ide-empty-state' into 'master'

Resolve "WebIDE doesn't work on empty repositories"

Closes #60107 and #45687

See merge request gitlab-org/gitlab-ce!26556
parents 4ca791e6 0d7afb95
<script>
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
import NewModal from './new_dropdown/modal.vue';
......@@ -22,6 +23,8 @@ export default {
FindFile,
ErrorMessage,
CommitEditorHeader,
GlButton,
GlLoadingIcon,
},
props: {
rightPaneComponent: {
......@@ -47,13 +50,15 @@ export default {
'someUncommittedChanges',
'isCommitModeActive',
'allBlobs',
'emptyRepo',
'currentTree',
]),
},
mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e);
},
methods: {
...mapActions(['toggleFileFinder']),
...mapActions(['toggleFileFinder', 'openNewEntryModal']),
onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?');
......@@ -98,17 +103,40 @@ export default {
<repo-editor :file="activeFile" class="multi-file-edit-pane-content" />
</template>
<template v-else>
<div v-once class="ide-empty-state">
<div class="ide-empty-state">
<div class="row js-empty-state">
<div class="col-12">
<div class="svg-content svg-250"><img :src="emptyStateSvgPath" /></div>
</div>
<div class="col-12">
<div class="text-content text-center">
<h4>Welcome to the GitLab IDE</h4>
<p>
Select a file from the left sidebar to begin editing. Afterwards, you'll be able
to commit your changes.
<h4>
{{ __('Make and review changes in the browser with the Web IDE') }}
</h4>
<template v-if="emptyRepo">
<p>
{{
__(
"Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes.",
)
}}
</p>
<gl-button
variant="success"
:title="__('New file')"
:aria-label="__('New file')"
@click="openNewEntryModal({ type: 'blob' })"
>
{{ __('New file') }}
</gl-button>
</template>
<gl-loading-icon v-else-if="!currentTree || currentTree.loading" size="md" />
<p v-else>
{{
__(
"Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes.",
)
}}
</p>
</div>
</div>
......
......@@ -54,14 +54,17 @@ export default {
<slot name="header"></slot>
</header>
<div class="ide-tree-body h-100">
<file-row
v-for="file in currentTree.tree"
:key="file.key"
:file="file"
:level="0"
:extra-component="$options.FileRowExtra"
@toggleTreeOpen="toggleTreeOpen"
/>
<template v-if="currentTree.tree.length">
<file-row
v-for="file in currentTree.tree"
:key="file.key"
:file="file"
:level="0"
:extra-component="$options.FileRowExtra"
@toggleTreeOpen="toggleTreeOpen"
/>
</template>
<div v-else class="file-row">{{ __('No files') }}</div>
</div>
</template>
</div>
......
import $ from 'jquery';
import Vue from 'vue';
import { __, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
import _ from 'underscore';
import * as types from './mutation_types';
import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants';
import service from '../services';
export const redirectToUrl = (_, url) => visitUrl(url);
export const redirectToUrl = (self, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
......@@ -239,6 +242,53 @@ export const renameEntry = (
}
};
export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
new Promise((resolve, reject) => {
const currentProject = state.projects[projectId];
if (!currentProject || !currentProject.branches[branchId] || force) {
service
.getBranchData(projectId, branchId)
.then(({ data }) => {
const { id } = data.commit;
commit(types.SET_BRANCH, {
projectPath: projectId,
branchName: branchId,
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
resolve(data);
})
.catch(e => {
if (e.response.status === 404) {
reject(e);
} else {
flash(
__('Error loading branch data. Please try again.'),
'alert',
document,
null,
false,
true,
);
reject(
new Error(
sprintf(
__('Branch not loaded - %{branchId}'),
{
branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
},
false,
),
),
);
}
});
} else {
resolve(currentProject.branches[branchId]);
}
});
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
......
......@@ -35,48 +35,6 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force
}
});
export const getBranchData = (
{ commit, dispatch, state },
{ projectId, branchId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (
typeof state.projects[`${projectId}`] === 'undefined' ||
!state.projects[`${projectId}`].branches[branchId] ||
force
) {
service
.getBranchData(`${projectId}`, branchId)
.then(({ data }) => {
const { id } = data.commit;
commit(types.SET_BRANCH, {
projectPath: `${projectId}`,
branchName: branchId,
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
resolve(data);
})
.catch(e => {
if (e.response.status === 404) {
dispatch('showBranchNotFoundError', branchId);
} else {
flash(
__('Error loading branch data. Please try again.'),
'alert',
document,
null,
false,
true,
);
}
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) =>
service
.getBranchData(projectId, branchId)
......@@ -125,40 +83,66 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
});
};
export const openBranch = ({ dispatch, state }, { projectId, branchId, basePath }) => {
dispatch('setCurrentBranchId', branchId);
dispatch('getBranchData', {
projectId,
branchId,
export const showEmptyState = ({ commit, state }, { projectId, branchId }) => {
const treePath = `${projectId}/${branchId}`;
commit(types.CREATE_TREE, { treePath });
commit(types.TOGGLE_LOADING, {
entry: state.trees[treePath],
forceValue: false,
});
};
return dispatch('getFiles', {
export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => {
dispatch('setCurrentBranchId', branchId);
if (getters.emptyRepo) {
return dispatch('showEmptyState', { projectId, branchId });
}
return dispatch('getBranchData', {
projectId,
branchId,
})
.then(() => {
if (basePath) {
const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(state.entries).find(
key => key === path && !state.entries[key].pending,
);
const treeEntry = state.entries[treeEntryKey];
if (treeEntry) {
dispatch('handleTreeEntryAction', treeEntry);
} else {
dispatch('createTempEntry', {
name: path,
type: 'blob',
});
}
}
})
.then(() => {
dispatch('getMergeRequestsForBranch', {
projectId,
branchId,
});
dispatch('getFiles', {
projectId,
branchId,
})
.then(() => {
if (basePath) {
const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(state.entries).find(
key => key === path && !state.entries[key].pending,
);
const treeEntry = state.entries[treeEntryKey];
if (treeEntry) {
dispatch('handleTreeEntryAction', treeEntry);
} else {
dispatch('createTempEntry', {
name: path,
type: 'blob',
});
}
}
})
.catch(
() =>
new Error(
sprintf(
__('An error occurred whilst getting files for - %{branchId}'),
{
branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
},
false,
),
),
);
})
.catch(() => {
dispatch('showBranchNotFoundError', branchId);
});
};
......@@ -74,17 +74,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } =
resolve();
})
.catch(e => {
if (e.response.status === 404) {
dispatch('showBranchNotFoundError', branchId);
} else {
dispatch('setErrorMessage', {
text: __('An error occurred whilst loading all the files.'),
action: payload =>
dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
actionText: __('Please try again'),
actionPayload: { projectId, branchId },
});
}
dispatch('setErrorMessage', {
text: __('An error occurred whilst loading all the files.'),
action: payload =>
dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
actionText: __('Please try again'),
actionPayload: { projectId, branchId },
});
reject(e);
});
} else {
......
......@@ -36,6 +36,9 @@ export const currentMergeRequest = state => {
export const currentProject = state => state.projects[state.currentProjectId];
export const emptyRepo = state =>
state.projects[state.currentProjectId] && state.projects[state.currentProjectId].empty_repo;
export const currentTree = state =>
state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
......
......@@ -135,6 +135,17 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
return null;
}
if (!data.parent_ids.length) {
commit(
rootTypes.TOGGLE_EMPTY_STATE,
{
projectPath: rootState.currentProjectId,
value: false,
},
{ root: true },
);
}
dispatch('setLastCommitMessage', data);
dispatch('updateCommitMessage', '');
return dispatch('updateFilesAfterCommit', {
......
......@@ -12,6 +12,7 @@ export const SET_LINKS = 'SET_LINKS';
export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
// Merge Request Mutation Types
export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
......
......@@ -19,6 +19,12 @@ export default {
});
},
[types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
if (!state.projects[projectId].branches[branchId]) {
Object.assign(state.projects[projectId].branches, {
[branchId]: {},
});
}
Object.assign(state.projects[projectId].branches[branchId], {
workingReference: reference,
});
......
......@@ -21,4 +21,9 @@ export default {
}),
});
},
[types.TOGGLE_EMPTY_STATE](state, { projectPath, value }) {
Object.assign(state.projects[projectPath], {
empty_repo: value,
});
},
};
---
title: Empty project state for Web IDE
merge_request: 26556
author:
type: added
......@@ -239,6 +239,7 @@ module API
end
end
expose :empty_repo?, as: :empty_repo
expose :archived?, as: :archived
expose :visibility
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
......
......@@ -952,6 +952,9 @@ msgstr ""
msgid "An error occurred whilst fetching the latest pipeline."
msgstr ""
msgid "An error occurred whilst getting files for - %{branchId}"
msgstr ""
msgid "An error occurred whilst loading all the files."
msgstr ""
......@@ -1482,6 +1485,9 @@ msgstr ""
msgid "Branch name"
msgstr ""
msgid "Branch not loaded - %{branchId}"
msgstr ""
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr ""
......@@ -2993,6 +2999,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
msgid "Create a new issue"
msgstr ""
......@@ -5796,6 +5805,9 @@ msgstr ""
msgid "MRDiff|Show full file"
msgstr ""
msgid "Make and review changes in the browser with the Web IDE"
msgstr ""
msgid "Make issue confidential."
msgstr ""
......@@ -6452,6 +6464,9 @@ msgstr ""
msgid "No file selected"
msgstr ""
msgid "No files"
msgstr ""
msgid "No files found."
msgstr ""
......@@ -8615,6 +8630,9 @@ msgstr ""
msgid "Select Archive Format"
msgstr ""
msgid "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes."
msgstr ""
msgid "Select a group to invite"
msgstr ""
......
......@@ -37,4 +37,39 @@ describe('Multi-file store branch mutations', () => {
expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit');
});
});
describe('SET_BRANCH_WORKING_REFERENCE', () => {
beforeEach(() => {
localState.projects = {
Foo: {
branches: {
bar: {},
},
},
};
});
it('sets workingReference for existing branch', () => {
mutations.SET_BRANCH_WORKING_REFERENCE(localState, {
projectId: 'Foo',
branchId: 'bar',
reference: 'foo-bar-ref',
});
expect(localState.projects.Foo.branches.bar.workingReference).toBe('foo-bar-ref');
});
it('does not fail on non-existent just yet branch', () => {
expect(localState.projects.Foo.branches.unknown).toBeUndefined();
mutations.SET_BRANCH_WORKING_REFERENCE(localState, {
projectId: 'Foo',
branchId: 'unknown',
reference: 'fun-fun-ref',
});
expect(localState.projects.Foo.branches.unknown).not.toBeUndefined();
expect(localState.projects.Foo.branches.unknown.workingReference).toBe('fun-fun-ref');
});
});
});
import mutations from '~/ide/stores/mutations/project';
import state from '~/ide/stores/state';
describe('Multi-file store branch mutations', () => {
let localState;
beforeEach(() => {
localState = state();
localState.projects = { abcproject: { empty_repo: true } };
});
describe('TOGGLE_EMPTY_STATE', () => {
it('sets empty_repo for project to passed value', () => {
mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: false });
expect(localState.projects.abcproject.empty_repo).toBe(false);
mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: true });
expect(localState.projects.abcproject.empty_repo).toBe(true);
});
});
});
......@@ -5,21 +5,53 @@ import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helpe
import { file, resetStore } from '../helpers';
import { projectData } from '../mock_data';
describe('ide component', () => {
function bootstrap(projData) {
const Component = Vue.extend(ide);
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projData);
Vue.set(store.state.trees, 'abcproject/master', {
tree: [],
loading: false,
});
return createComponentWithStore(Component, store, {
emptyStateSvgPath: 'svg',
noChangesStateSvgPath: 'svg',
committedStateSvgPath: 'svg',
});
}
describe('ide component, empty repo', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(ide);
const emptyProjData = Object.assign({}, projectData, { empty_repo: true, branches: {} });
vm = bootstrap(emptyProjData);
vm.$mount();
});
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projectData);
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
vm = createComponentWithStore(Component, store, {
emptyStateSvgPath: 'svg',
noChangesStateSvgPath: 'svg',
committedStateSvgPath: 'svg',
}).$mount();
it('renders "New file" button in empty repo', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).not.toBeNull();
done();
});
});
});
describe('ide component, non-empty repo', () => {
let vm;
beforeEach(() => {
vm = bootstrap(projectData);
vm.$mount();
});
afterEach(() => {
......@@ -28,17 +60,15 @@ describe('ide component', () => {
resetStore(vm.$store);
});
it('does not render right when no files open', () => {
expect(vm.$el.querySelector('.panel-right')).toBeNull();
});
it('shows error message when set', done => {
expect(vm.$el.querySelector('.flash-container')).toBe(null);
it('renders right panel when files are open', done => {
vm.$store.state.trees['abcproject/mybranch'] = {
tree: [file()],
vm.$store.state.errorMessage = {
text: 'error',
};
Vue.nextTick(() => {
expect(vm.$el.querySelector('.panel-right')).toBeNull();
vm.$nextTick(() => {
expect(vm.$el.querySelector('.flash-container')).not.toBe(null);
done();
});
......@@ -71,17 +101,25 @@ describe('ide component', () => {
});
});
it('shows error message when set', done => {
expect(vm.$el.querySelector('.flash-container')).toBe(null);
vm.$store.state.errorMessage = {
text: 'error',
};
describe('non-existent branch', () => {
it('does not render "New file" button for non-existent branch when repo is not empty', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull();
done();
});
});
});
vm.$nextTick(() => {
expect(vm.$el.querySelector('.flash-container')).not.toBe(null);
describe('branch with files', () => {
beforeEach(() => {
store.state.trees['abcproject/master'].tree = [file()];
});
done();
it('does not render "New file" button', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull();
done();
});
});
});
});
......@@ -7,25 +7,23 @@ import { projectData } from '../mock_data';
describe('IDE tree list', () => {
const Component = Vue.extend(IdeTreeList);
const normalBranchTree = [file('fileName')];
const emptyBranchTree = [];
let vm;
beforeEach(() => {
const bootstrapWithTree = (tree = normalBranchTree) => {
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projectData);
Vue.set(store.state.trees, 'abcproject/master', {
tree: [file('fileName')],
tree,
loading: false,
});
vm = createComponentWithStore(Component, store, {
viewerType: 'edit',
});
spyOn(vm, 'updateViewer').and.callThrough();
vm.$mount();
});
};
afterEach(() => {
vm.$destroy();
......@@ -33,22 +31,47 @@ describe('IDE tree list', () => {
resetStore(vm.$store);
});
it('updates viewer on mount', () => {
expect(vm.updateViewer).toHaveBeenCalledWith('edit');
});
describe('normal branch', () => {
beforeEach(() => {
bootstrapWithTree();
spyOn(vm, 'updateViewer').and.callThrough();
vm.$mount();
});
it('updates viewer on mount', () => {
expect(vm.updateViewer).toHaveBeenCalledWith('edit');
});
it('renders loading indicator', done => {
store.state.trees['abcproject/master'].loading = true;
it('renders loading indicator', done => {
store.state.trees['abcproject/master'].loading = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
vm.$nextTick(() => {
expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
done();
});
});
done();
it('renders list of files', () => {
expect(vm.$el.textContent).toContain('fileName');
});
});
it('renders list of files', () => {
expect(vm.$el.textContent).toContain('fileName');
describe('empty-branch state', () => {
beforeEach(() => {
bootstrapWithTree(emptyBranchTree);
spyOn(vm, 'updateViewer').and.callThrough();
vm.$mount();
});
it('does not load files if the branch is empty', () => {
expect(vm.$el.textContent).not.toContain('fileName');
expect(vm.$el.textContent).toContain('No files');
});
});
});
......@@ -4,7 +4,7 @@ import {
refreshLastCommitData,
showBranchNotFoundError,
createNewBranchFromDefault,
getBranchData,
showEmptyState,
openBranch,
} from '~/ide/stores/actions';
import store from '~/ide/stores';
......@@ -196,39 +196,44 @@ describe('IDE store project actions', () => {
});
});
describe('getBranchData', () => {
describe('error', () => {
it('dispatches branch not found action when response is 404', done => {
const dispatch = jasmine.createSpy('dispatchSpy');
mock.onGet(/(.*)/).replyOnce(404);
getBranchData(
describe('showEmptyState', () => {
it('commits proper mutations when supplied error is 404', done => {
testAction(
showEmptyState,
{
err: {
response: {
status: 404,
},
},
projectId: 'abc/def',
branchId: 'master',
},
store.state,
[
{
commit() {},
dispatch,
state: store.state,
type: 'CREATE_TREE',
payload: {
treePath: 'abc/def/master',
},
},
{
projectId: 'abc/def',
branchId: 'master-testing',
type: 'TOGGLE_LOADING',
payload: {
entry: store.state.trees['abc/def/master'],
forceValue: false,
},
},
)
.then(done.fail)
.catch(() => {
expect(dispatch.calls.argsFor(0)).toEqual([
'showBranchNotFoundError',
'master-testing',
]);
done();
});
});
],
[],
done,
);
});
});
describe('openBranch', () => {
const branch = {
projectId: 'feature/lorem-ipsum',
projectId: 'abc/def',
branchId: '123-lorem',
};
......@@ -238,63 +243,113 @@ describe('IDE store project actions', () => {
'foo/bar-pending': { pending: true },
'foo/bar': { pending: false },
};
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
});
it('dispatches branch actions', done => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
['setCurrentBranchId', branch.branchId],
['getBranchData', branch],
['getFiles', branch],
['getMergeRequestsForBranch', branch],
]);
})
.then(done)
.catch(done.fail);
});
describe('empty repo', () => {
beforeEach(() => {
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
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);
store.state.currentProjectId = 'abc/def';
store.state.projects['abc/def'] = {
empty_repo: true,
};
});
afterEach(() => {
resetStore(store);
});
it('dispatches showEmptyState action right away', done => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
['setCurrentBranchId', branch.branchId],
['showEmptyState', branch],
]);
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);
describe('existing branch', () => {
beforeEach(() => {
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
});
it('dispatches branch actions', done => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
['setCurrentBranchId', branch.branchId],
['getBranchData', branch],
['getMergeRequestsForBranch', branch],
['getFiles', branch],
]);
})
.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);
});
});
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(),
);
describe('non-existent branch', () => {
beforeEach(() => {
spyOn(store, 'dispatch').and.returnValue(Promise.reject());
});
expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
name: 'not-existent.md',
type: 'blob',
});
})
.then(done)
.catch(done.fail);
it('dispatches correct branch actions', done => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
['setCurrentBranchId', branch.branchId],
['getBranchData', branch],
['showBranchNotFoundError', branch.branchId],
]);
})
.then(done)
.catch(done.fail);
});
});
});
});
......@@ -93,38 +93,6 @@ describe('Multi-file store tree actions', () => {
});
describe('error', () => {
it('dispatches branch not found actions when response is 404', done => {
const dispatch = jasmine.createSpy('dispatchSpy');
store.state.projects = {
'abc/def': {
web_url: `${gl.TEST_HOST}/files`,
},
};
mock.onGet(/(.*)/).replyOnce(404);
getFiles(
{
commit() {},
dispatch,
state: store.state,
},
{
projectId: 'abc/def',
branchId: 'master-testing',
},
)
.then(done.fail)
.catch(() => {
expect(dispatch.calls.argsFor(0)).toEqual([
'showBranchNotFoundError',
'master-testing',
]);
done();
});
});
it('dispatches error action', done => {
const dispatch = jasmine.createSpy('dispatchSpy');
......
......@@ -9,12 +9,15 @@ import actions, {
setErrorMessage,
deleteEntry,
renameEntry,
getBranchData,
} from '~/ide/stores/actions';
import axios from '~/lib/utils/axios_utils';
import store 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';
describe('Multi-file store actions', () => {
beforeEach(() => {
......@@ -560,4 +563,65 @@ describe('Multi-file store actions', () => {
);
});
});
describe('getBranchData', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('error', () => {
let dispatch;
const callParams = [
{
commit() {},
state: store.state,
},
{
projectId: 'abc/def',
branchId: 'master-testing',
},
];
beforeEach(() => {
dispatch = jasmine.createSpy('dispatchSpy');
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.calls.count()).toEqual(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.calls.count()).toEqual(0);
expect(e.response).toBeUndefined();
expect(document.querySelector('.flash-alert')).not.toBeNull();
done();
});
});
});
});
});
......@@ -272,6 +272,7 @@ describe('IDE commit module actions', () => {
short_id: '123',
message: 'test message',
committed_date: 'date',
parent_ids: '321',
stats: {
additions: '1',
deletions: '2',
......@@ -463,5 +464,63 @@ describe('IDE commit module actions', () => {
.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 => {
spyOn(service, 'commit').and.returnValue(
Promise.resolve({
data: COMMIT_RESPONSE,
}),
);
spyOn(store, 'commit').and.callThrough();
store
.dispatch('commit/commitChanges')
.then(() => {
expect(store.commit.calls.allArgs()).toEqual(
jasmine.arrayContaining([
['TOGGLE_EMPTY_STATE', jasmine.any(Object), jasmine.any(Object)],
]),
);
done();
})
.catch(done.fail);
});
it('does not commmit TOGGLE_EMPTY_STATE mutation on existing project', done => {
COMMIT_RESPONSE.parent_ids.push('1234');
spyOn(service, 'commit').and.returnValue(
Promise.resolve({
data: COMMIT_RESPONSE,
}),
);
spyOn(store, 'commit').and.callThrough();
store
.dispatch('commit/commitChanges')
.then(() => {
expect(store.commit.calls.allArgs()).not.toEqual(
jasmine.arrayContaining([
['TOGGLE_EMPTY_STATE', jasmine.any(Object), jasmine.any(Object)],
]),
);
done();
})
.catch(done.fail);
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment