summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorTim Zallmann <tzallmann@gitlab.com>2018-03-21 09:34:42 +0000
committerTim Zallmann <tzallmann@gitlab.com>2018-03-21 09:34:42 +0000
commit8ff2d899dd5f960c6fd5fbf77794a702bd35eb9e (patch)
tree44fa0ba2e4d7ab70a2c8856f5336e945d43b244d /spec
parent2025f361e7520e00962f6964aa77c4f4e6c3d56b (diff)
parentedf2060c6d91db079e30ed56178be2eabc352132 (diff)
downloadgitlab-ce-8ff2d899dd5f960c6fd5fbf77794a702bd35eb9e.tar.gz
Merge branch 'make-ide-ce' into 'master'
Move IDE to CE Closes #44157 See merge request gitlab-org/gitlab-ce!17887
Diffstat (limited to 'spec')
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb53
-rw-r--r--spec/features/projects/tree/create_file_spec.rb43
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb51
-rw-r--r--spec/javascripts/ide/components/changed_file_icon_spec.js45
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/actions_spec.js35
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js28
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_item_spec.js85
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_spec.js53
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js130
-rw-r--r--spec/javascripts/ide/components/ide_context_bar_spec.js37
-rw-r--r--spec/javascripts/ide/components/ide_external_links_spec.js43
-rw-r--r--spec/javascripts/ide/components/ide_repo_tree_spec.js43
-rw-r--r--spec/javascripts/ide/components/ide_side_bar_spec.js42
-rw-r--r--spec/javascripts/ide/components/ide_spec.js41
-rw-r--r--spec/javascripts/ide/components/new_dropdown/index_spec.js84
-rw-r--r--spec/javascripts/ide/components/new_dropdown/modal_spec.js82
-rw-r--r--spec/javascripts/ide/components/new_dropdown/upload_spec.js87
-rw-r--r--spec/javascripts/ide/components/repo_commit_section_spec.js173
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js137
-rw-r--r--spec/javascripts/ide/components/repo_file_buttons_spec.js47
-rw-r--r--spec/javascripts/ide/components/repo_file_spec.js80
-rw-r--r--spec/javascripts/ide/components/repo_loading_file_spec.js63
-rw-r--r--spec/javascripts/ide/components/repo_tab_spec.js165
-rw-r--r--spec/javascripts/ide/components/repo_tabs_spec.js81
-rw-r--r--spec/javascripts/ide/helpers.js22
-rw-r--r--spec/javascripts/ide/lib/common/disposable_spec.js44
-rw-r--r--spec/javascripts/ide/lib/common/model_manager_spec.js129
-rw-r--r--spec/javascripts/ide/lib/common/model_spec.js113
-rw-r--r--spec/javascripts/ide/lib/decorations/controller_spec.js139
-rw-r--r--spec/javascripts/ide/lib/diff/controller_spec.js196
-rw-r--r--spec/javascripts/ide/lib/diff/diff_spec.js80
-rw-r--r--spec/javascripts/ide/lib/editor_options_spec.js11
-rw-r--r--spec/javascripts/ide/lib/editor_spec.js197
-rw-r--r--spec/javascripts/ide/monaco_loader_spec.js15
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js421
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js172
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js306
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js55
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js505
-rw-r--r--spec/javascripts/ide/stores/modules/commit/getters_spec.js128
-rw-r--r--spec/javascripts/ide/stores/modules/commit/mutations_spec.js42
-rw-r--r--spec/javascripts/ide/stores/mutations/branch_spec.js18
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js157
-rw-r--r--spec/javascripts/ide/stores/mutations/tree_spec.js69
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js79
-rw-r--r--spec/javascripts/ide/stores/utils_spec.js66
46 files changed, 4692 insertions, 0 deletions
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
new file mode 100644
index 00000000000..d96c7e655ba
--- /dev/null
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+feature 'Multi-file editor new directory', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit project_tree_path(project, :master)
+
+ wait_for_requests
+
+ click_link('Web IDE')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
+ end
+
+ it 'creates directory in current directory' do
+ find('.add-to-tree').click
+
+ click_link('New directory')
+
+ page.within('.modal') do
+ find('.form-control').set('folder name')
+
+ click_button('Create directory')
+ end
+
+ find('.add-to-tree').click
+
+ click_link('New file')
+
+ page.within('.modal-dialog') do
+ find('.form-control').set('file name')
+
+ click_button('Create file')
+ end
+
+ wait_for_requests
+
+ fill_in('commit-message', with: 'commit message ide')
+
+ click_button('Commit')
+
+ expect(page).to have_content('folder name')
+ end
+end
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
new file mode 100644
index 00000000000..a4cbd5cf766
--- /dev/null
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+feature 'Multi-file editor new file', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit project_path(project)
+
+ wait_for_requests
+
+ click_link('Web IDE')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
+ end
+
+ it 'creates file in current directory' do
+ find('.add-to-tree').click
+
+ click_link('New file')
+
+ page.within('.modal') do
+ find('.form-control').set('file name')
+
+ click_button('Create file')
+ end
+
+ wait_for_requests
+
+ fill_in('commit-message', with: 'commit message ide')
+
+ click_button('Commit')
+
+ expect(page).to have_content('file name')
+ end
+end
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
new file mode 100644
index 00000000000..8e53ae15700
--- /dev/null
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+feature 'Multi-file editor upload file', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') }
+ let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit project_tree_path(project, :master)
+
+ wait_for_requests
+
+ click_link('Web IDE')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
+ end
+
+ it 'uploads text file' do
+ find('.add-to-tree').click
+
+ # make the field visible so capybara can use it
+ execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
+ attach_file('file-upload', txt_file)
+
+ find('.add-to-tree').click
+
+ expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt')
+ expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
+ end
+
+ it 'uploads image file' do
+ find('.add-to-tree').click
+
+ # make the field visible so capybara can use it
+ execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
+ attach_file('file-upload', img_file)
+
+ find('.add-to-tree').click
+
+ expect(page).to have_selector('.multi-file-tab', text: 'dk.png')
+ expect(page).not_to have_selector('.monaco-editor')
+ end
+end
diff --git a/spec/javascripts/ide/components/changed_file_icon_spec.js b/spec/javascripts/ide/components/changed_file_icon_spec.js
new file mode 100644
index 00000000000..987aea7befc
--- /dev/null
+++ b/spec/javascripts/ide/components/changed_file_icon_spec.js
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import changedFileIcon from '~/ide/components/changed_file_icon.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('IDE changed file icon', () => {
+ let vm;
+
+ beforeEach(() => {
+ const component = Vue.extend(changedFileIcon);
+
+ vm = createComponent(component, {
+ file: {
+ tempFile: false,
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('changedIcon', () => {
+ it('equals file-modified when not a temp file', () => {
+ expect(vm.changedIcon).toBe('file-modified');
+ });
+
+ it('equals file-addition when a temp file', () => {
+ vm.file.tempFile = true;
+
+ expect(vm.changedIcon).toBe('file-addition');
+ });
+ });
+
+ describe('changedIconClass', () => {
+ it('includes multi-file-modified when not a temp file', () => {
+ expect(vm.changedIconClass).toContain('multi-file-modified');
+ });
+
+ it('includes multi-file-addition when a temp file', () => {
+ vm.file.tempFile = true;
+
+ expect(vm.changedIconClass).toContain('multi-file-addition');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
new file mode 100644
index 00000000000..144e78d14b5
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import commitActions from '~/ide/components/commit_sidebar/actions.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from 'spec/ide/helpers';
+
+describe('IDE commit sidebar actions', () => {
+ let vm;
+
+ beforeEach(done => {
+ const Component = Vue.extend(commitActions);
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.currentBranchId = 'master';
+
+ vm.$mount();
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders 3 groups', () => {
+ expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(3);
+ });
+
+ it('renders current branch text', () => {
+ expect(vm.$el.textContent).toContain('Commit to master branch');
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
new file mode 100644
index 00000000000..5b402886b55
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list collapsed', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(listCollapsed);
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.changedFiles.push(file('file1'), file('file2'));
+ vm.$store.state.changedFiles[0].tempFile = true;
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders added & modified files count', () => {
+ expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1');
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
new file mode 100644
index 00000000000..15b66952d99
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -0,0 +1,85 @@
+import Vue from 'vue';
+import listItem from '~/ide/components/commit_sidebar/list_item.vue';
+import router from '~/ide/ide_router';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list item', () => {
+ let vm;
+ let f;
+
+ beforeEach(() => {
+ const Component = Vue.extend(listItem);
+
+ f = file('test-file');
+
+ vm = mountComponent(Component, {
+ file: f,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders file path', () => {
+ expect(
+ vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim(),
+ ).toBe(f.path);
+ });
+
+ it('calls discardFileChanges when clicking discard button', () => {
+ spyOn(vm, 'discardFileChanges');
+
+ vm.$el.querySelector('.multi-file-discard-btn').click();
+
+ expect(vm.discardFileChanges).toHaveBeenCalled();
+ });
+
+ it('opens a closed file in the editor when clicking the file path', () => {
+ spyOn(vm, 'openFileInEditor').and.callThrough();
+ spyOn(vm, 'updateViewer');
+ spyOn(router, 'push');
+
+ vm.$el.querySelector('.multi-file-commit-list-path').click();
+
+ expect(vm.openFileInEditor).toHaveBeenCalled();
+ expect(router.push).toHaveBeenCalled();
+ });
+
+ it('calls updateViewer with diff when clicking file', () => {
+ spyOn(vm, 'openFileInEditor').and.callThrough();
+ spyOn(vm, 'updateViewer');
+ spyOn(router, 'push');
+
+ vm.$el.querySelector('.multi-file-commit-list-path').click();
+
+ expect(vm.updateViewer).toHaveBeenCalledWith('diff');
+ });
+
+ describe('computed', () => {
+ describe('iconName', () => {
+ it('returns modified when not a tempFile', () => {
+ expect(vm.iconName).toBe('file-modified');
+ });
+
+ it('returns addition when not a tempFile', () => {
+ f.tempFile = true;
+
+ expect(vm.iconName).toBe('file-addition');
+ });
+ });
+
+ describe('iconClass', () => {
+ it('returns modified when not a tempFile', () => {
+ expect(vm.iconClass).toContain('multi-file-modified');
+ });
+
+ it('returns addition when not a tempFile', () => {
+ f.tempFile = true;
+
+ expect(vm.iconClass).toContain('multi-file-addition');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
new file mode 100644
index 00000000000..a62c0a28340
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
@@ -0,0 +1,53 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(commitSidebarList);
+
+ vm = createComponentWithStore(Component, store, {
+ title: 'Staged',
+ fileList: [],
+ });
+
+ vm.$store.state.rightPanelCollapsed = false;
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('with a list of files', () => {
+ beforeEach(done => {
+ const f = file('file name');
+ f.changed = true;
+ vm.fileList.push(f);
+
+ Vue.nextTick(done);
+ });
+
+ it('renders list', () => {
+ expect(vm.$el.querySelectorAll('li').length).toBe(1);
+ });
+ });
+
+ describe('collapsed', () => {
+ beforeEach(done => {
+ vm.$store.state.rightPanelCollapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('hides list', () => {
+ expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
+ expect(vm.$el.querySelector('.help-block')).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
new file mode 100644
index 00000000000..4e8243439f3
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
@@ -0,0 +1,130 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from 'spec/ide/helpers';
+
+describe('IDE commit sidebar radio group', () => {
+ let vm;
+
+ beforeEach(done => {
+ const Component = Vue.extend(radioGroup);
+
+ store.state.commit.commitAction = '2';
+
+ vm = createComponentWithStore(Component, store, {
+ value: '1',
+ label: 'test',
+ checked: true,
+ });
+
+ vm.$mount();
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('uses label if present', () => {
+ expect(vm.$el.textContent).toContain('test');
+ });
+
+ it('uses slot if label is not present', done => {
+ vm.$destroy();
+
+ vm = new Vue({
+ components: {
+ radioGroup,
+ },
+ store,
+ template: `
+ <radio-group
+ value="1"
+ >
+ Testing slot
+ </radio-group>
+ `,
+ });
+
+ vm.$mount();
+
+ Vue.nextTick(() => {
+ expect(vm.$el.textContent).toContain('Testing slot');
+
+ done();
+ });
+ });
+
+ it('updates store when changing radio button', done => {
+ vm.$el.querySelector('input').dispatchEvent(new Event('change'));
+
+ Vue.nextTick(() => {
+ expect(store.state.commit.commitAction).toBe('1');
+
+ done();
+ });
+ });
+
+ it('renders helpText tooltip', done => {
+ vm.helpText = 'help text';
+
+ Vue.nextTick(() => {
+ const help = vm.$el.querySelector('.help-block');
+
+ expect(help).not.toBeNull();
+ expect(help.getAttribute('data-original-title')).toBe('help text');
+
+ done();
+ });
+ });
+
+ describe('with input', () => {
+ beforeEach(done => {
+ vm.$destroy();
+
+ const Component = Vue.extend(radioGroup);
+
+ store.state.commit.commitAction = '1';
+
+ vm = createComponentWithStore(Component, store, {
+ value: '1',
+ label: 'test',
+ checked: true,
+ showInput: true,
+ });
+
+ vm.$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders input box when commitAction matches value', () => {
+ expect(vm.$el.querySelector('.form-control')).not.toBeNull();
+ });
+
+ it('hides input when commitAction doesnt match value', done => {
+ store.state.commit.commitAction = '2';
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.form-control')).toBeNull();
+ done();
+ });
+ });
+
+ it('updates branch name in store on input', done => {
+ const input = vm.$el.querySelector('.form-control');
+ input.value = 'testing-123';
+ input.dispatchEvent(new Event('input'));
+
+ Vue.nextTick(() => {
+ expect(store.state.commit.newBranchName).toBe('testing-123');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_context_bar_spec.js b/spec/javascripts/ide/components/ide_context_bar_spec.js
new file mode 100644
index 00000000000..e17b051f137
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_context_bar_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ideContextBar from '~/ide/components/ide_context_bar.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+
+describe('Multi-file editor right context bar', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ideContextBar);
+
+ vm = createComponentWithStore(Component, store, {
+ noChangesStateSvgPath: 'svg',
+ committedStateSvgPath: 'svg',
+ });
+
+ vm.$store.state.rightPanelCollapsed = false;
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('collapsed', () => {
+ beforeEach(done => {
+ vm.$store.state.rightPanelCollapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('adds collapsed class', () => {
+ expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_external_links_spec.js b/spec/javascripts/ide/components/ide_external_links_spec.js
new file mode 100644
index 00000000000..9f6cb459f3b
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_external_links_spec.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import ideExternalLinks from '~/ide/components/ide_external_links.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('ide external links component', () => {
+ let vm;
+ let fakeReferrer;
+ let Component;
+
+ const fakeProjectUrl = '/project/';
+
+ beforeEach(() => {
+ Component = Vue.extend(ideExternalLinks);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('goBackUrl', () => {
+ it('renders the Go Back link with the referrer when present', () => {
+ fakeReferrer = '/example/README.md';
+ spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
+
+ vm = createComponent(Component, {
+ projectUrl: fakeProjectUrl,
+ }).$mount();
+
+ expect(vm.goBackUrl).toEqual(fakeReferrer);
+ });
+
+ it('renders the Go Back link with the project url when referrer is not present', () => {
+ fakeReferrer = '';
+ spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
+
+ vm = createComponent(Component, {
+ projectUrl: fakeProjectUrl,
+ }).$mount();
+
+ expect(vm.goBackUrl).toEqual(fakeProjectUrl);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_repo_tree_spec.js b/spec/javascripts/ide/components/ide_repo_tree_spec.js
new file mode 100644
index 00000000000..e0fbc90ca61
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_repo_tree_spec.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import ideRepoTree from '~/ide/components/ide_repo_tree.vue';
+import createComponent from '../../helpers/vue_mount_component_helper';
+import { file } from '../helpers';
+
+describe('IdeRepoTree', () => {
+ let vm;
+ let tree;
+
+ beforeEach(() => {
+ const IdeRepoTree = Vue.extend(ideRepoTree);
+
+ tree = {
+ tree: [file()],
+ loading: false,
+ };
+
+ vm = createComponent(IdeRepoTree, {
+ tree,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a sidebar', () => {
+ expect(vm.$el.querySelector('.loading-file')).toBeNull();
+ expect(vm.$el.querySelector('.file')).not.toBeNull();
+ });
+
+ it('renders 3 loading files if tree is loading', done => {
+ tree.loading = true;
+
+ vm.$nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.multi-file-loading-container').length,
+ ).toEqual(3);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_side_bar_spec.js b/spec/javascripts/ide/components/ide_side_bar_spec.js
new file mode 100644
index 00000000000..699dae1ce2f
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_side_bar_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ideSidebar from '~/ide/components/ide_side_bar.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from '../helpers';
+
+describe('IdeSidebar', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ideSidebar);
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders a sidebar', () => {
+ expect(
+ vm.$el.querySelector('.multi-file-commit-panel-inner'),
+ ).not.toBeNull();
+ });
+
+ it('renders loading icon component', done => {
+ vm.$store.state.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);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
new file mode 100644
index 00000000000..5bd890094cc
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ide from '~/ide/components/ide.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { file, resetStore } from '../helpers';
+
+describe('ide component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ide);
+
+ vm = createComponentWithStore(Component, store, {
+ emptyStateSvgPath: 'svg',
+ noChangesStateSvgPath: 'svg',
+ committedStateSvgPath: 'svg',
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('does not render panel right when no files open', () => {
+ expect(vm.$el.querySelector('.panel-right')).toBeNull();
+ });
+
+ it('renders panel right when files are open', done => {
+ vm.$store.state.trees['abcproject/mybranch'] = {
+ tree: [file()],
+ };
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.panel-right')).toBeNull();
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js
new file mode 100644
index 00000000000..e08abe7d849
--- /dev/null
+++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js
@@ -0,0 +1,84 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import newDropdown from '~/ide/components/new_dropdown/index.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
+
+describe('new dropdown component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const component = Vue.extend(newDropdown);
+
+ vm = createComponentWithStore(component, store, {
+ branch: 'master',
+ path: '',
+ });
+
+ vm.$store.state.currentProjectId = 'abcproject';
+ vm.$store.state.path = '';
+ vm.$store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders new file, upload and new directory links', () => {
+ expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
+ expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe(
+ 'Upload file',
+ );
+ expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe(
+ 'New directory',
+ );
+ });
+
+ describe('createNewItem', () => {
+ it('sets modalType to blob when new file is clicked', () => {
+ vm.$el.querySelectorAll('a')[0].click();
+
+ expect(vm.modalType).toBe('blob');
+ });
+
+ it('sets modalType to tree when new directory is clicked', () => {
+ vm.$el.querySelectorAll('a')[2].click();
+
+ expect(vm.modalType).toBe('tree');
+ });
+
+ it('opens modal when link is clicked', done => {
+ vm.$el.querySelectorAll('a')[0].click();
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.modal')).not.toBeNull();
+
+ done();
+ });
+ });
+ });
+
+ describe('hideModal', () => {
+ beforeAll(done => {
+ vm.openModal = true;
+ Vue.nextTick(done);
+ });
+
+ it('closes modal after toggling', done => {
+ vm.hideModal();
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.modal')).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
new file mode 100644
index 00000000000..a6e1e5a0d35
--- /dev/null
+++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
@@ -0,0 +1,82 @@
+import Vue from 'vue';
+import modal from '~/ide/components/new_dropdown/modal.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('new file modal component', () => {
+ const Component = Vue.extend(modal);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ ['tree', 'blob'].forEach(type => {
+ describe(type, () => {
+ beforeEach(() => {
+ vm = createComponent(Component, {
+ type,
+ branchId: 'master',
+ path: '',
+ });
+
+ vm.entryName = 'testing';
+ });
+
+ it(`sets modal title as ${type}`, () => {
+ const title = type === 'tree' ? 'directory' : 'file';
+
+ expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(
+ `Create new ${title}`,
+ );
+ });
+
+ it(`sets button label as ${type}`, () => {
+ const title = type === 'tree' ? 'directory' : 'file';
+
+ expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(
+ `Create ${title}`,
+ );
+ });
+
+ it(`sets form label as ${type}`, () => {
+ const title = type === 'tree' ? 'Directory' : 'File';
+
+ expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(
+ `${title} name`,
+ );
+ });
+
+ describe('createEntryInStore', () => {
+ it('$emits create', () => {
+ spyOn(vm, '$emit');
+
+ vm.createEntryInStore();
+
+ expect(vm.$emit).toHaveBeenCalledWith('create', {
+ branchId: 'master',
+ name: 'testing',
+ type,
+ });
+ });
+ });
+ });
+ });
+
+ it('focuses field on mount', () => {
+ document.body.innerHTML += '<div class="js-test"></div>';
+
+ vm = createComponent(
+ Component,
+ {
+ type: 'tree',
+ branchId: 'master',
+ path: '',
+ },
+ '.js-test',
+ );
+
+ expect(document.activeElement).toBe(vm.$refs.fieldName);
+
+ vm.$el.remove();
+ });
+});
diff --git a/spec/javascripts/ide/components/new_dropdown/upload_spec.js b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
new file mode 100644
index 00000000000..2bc5d701601
--- /dev/null
+++ b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
@@ -0,0 +1,87 @@
+import Vue from 'vue';
+import upload from '~/ide/components/new_dropdown/upload.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('new dropdown upload', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(upload);
+
+ vm = createComponent(Component, {
+ branchId: 'master',
+ path: '',
+ });
+
+ vm.entryName = 'testing';
+
+ spyOn(vm, '$emit');
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('readFile', () => {
+ beforeEach(() => {
+ spyOn(FileReader.prototype, 'readAsText');
+ spyOn(FileReader.prototype, 'readAsDataURL');
+ });
+
+ it('calls readAsText for text files', () => {
+ const file = {
+ type: 'text/html',
+ };
+
+ vm.readFile(file);
+
+ expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(file);
+ });
+
+ it('calls readAsDataURL for non-text files', () => {
+ const file = {
+ type: 'images/png',
+ };
+
+ vm.readFile(file);
+
+ expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file);
+ });
+ });
+
+ describe('createFile', () => {
+ const target = {
+ result: 'content',
+ };
+ const binaryTarget = {
+ result: 'base64,base64content',
+ };
+ const file = {
+ name: 'file',
+ };
+
+ it('creates new file', () => {
+ vm.createFile(target, file, true);
+
+ expect(vm.$emit).toHaveBeenCalledWith('create', {
+ name: file.name,
+ branchId: 'master',
+ type: 'blob',
+ content: target.result,
+ base64: false,
+ });
+ });
+
+ it('splits content on base64 if binary', () => {
+ vm.createFile(binaryTarget, file, false);
+
+ expect(vm.$emit).toHaveBeenCalledWith('create', {
+ name: file.name,
+ branchId: 'master',
+ type: 'blob',
+ content: binaryTarget.result.split('base64,')[1],
+ base64: true,
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js
new file mode 100644
index 00000000000..113ade269e9
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_commit_section_spec.js
@@ -0,0 +1,173 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import repoCommitSection from '~/ide/components/repo_commit_section.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
+import { file, resetStore } from '../helpers';
+
+describe('RepoCommitSection', () => {
+ let vm;
+
+ function createComponent() {
+ const Component = Vue.extend(repoCommitSection);
+
+ vm = createComponentWithStore(Component, store, {
+ noChangesStateSvgPath: 'svg',
+ committedStateSvgPath: 'commitsvg',
+ });
+
+ vm.$store.state.currentProjectId = 'abcproject';
+ vm.$store.state.currentBranchId = 'master';
+ vm.$store.state.projects.abcproject = {
+ web_url: '',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+
+ vm.$store.state.rightPanelCollapsed = false;
+ vm.$store.state.currentBranch = 'master';
+ vm.$store.state.changedFiles = [file('file1'), file('file2')];
+ vm.$store.state.changedFiles.forEach(f =>
+ Object.assign(f, {
+ changed: true,
+ content: 'testing',
+ }),
+ );
+
+ return vm.$mount();
+ }
+
+ beforeEach(done => {
+ vm = createComponent();
+
+ spyOn(service, 'getTreeData').and.returnValue(
+ Promise.resolve({
+ headers: {
+ 'page-title': 'test',
+ },
+ json: () =>
+ Promise.resolve({
+ last_commit_path: 'last_commit_path',
+ parent_tree_url: 'parent_tree_url',
+ path: '/',
+ trees: [{ name: 'tree' }],
+ blobs: [{ name: 'blob' }],
+ submodules: [{ name: 'submodule' }],
+ }),
+ }),
+ );
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('empty Stage', () => {
+ it('renders no changes text', () => {
+ resetStore(vm.$store);
+ const Component = Vue.extend(repoCommitSection);
+
+ vm = createComponentWithStore(Component, store, {
+ noChangesStateSvgPath: 'nochangessvg',
+ committedStateSvgPath: 'svg',
+ }).$mount();
+
+ expect(
+ vm.$el.querySelector('.js-empty-state').textContent.trim(),
+ ).toContain('No changes');
+ expect(
+ vm.$el.querySelector('.js-empty-state img').getAttribute('src'),
+ ).toBe('nochangessvg');
+ });
+ });
+
+ it('renders a commit section', () => {
+ const changedFileElements = [
+ ...vm.$el.querySelectorAll('.multi-file-commit-list li'),
+ ];
+ const submitCommit = vm.$el.querySelector('form .btn');
+
+ expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
+ expect(changedFileElements.length).toEqual(2);
+
+ changedFileElements.forEach((changedFile, i) => {
+ expect(changedFile.textContent.trim()).toContain(
+ vm.$store.state.changedFiles[i].path,
+ );
+ });
+
+ expect(submitCommit.disabled).toBeTruthy();
+ expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull();
+ });
+
+ it('updates commitMessage in store on input', done => {
+ const textarea = vm.$el.querySelector('textarea');
+
+ textarea.value = 'testing commit message';
+
+ textarea.dispatchEvent(new Event('input'));
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(vm.$store.state.commit.commitMessage).toBe(
+ 'testing commit message',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('discard draft button', () => {
+ it('hidden when commitMessage is empty', () => {
+ expect(
+ vm.$el.querySelector('.multi-file-commit-form .btn-default'),
+ ).toBeNull();
+ });
+
+ it('resets commitMessage when clicking discard button', done => {
+ vm.$store.state.commit.commitMessage = 'testing commit message';
+
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.multi-file-commit-form .btn-default').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.$store.state.commit.commitMessage).not.toBe(
+ 'testing commit message',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('when submitting', () => {
+ beforeEach(() => {
+ spyOn(vm, 'commitChanges');
+ });
+
+ it('calls commitChanges', done => {
+ vm.$store.state.commit.commitMessage = 'testing commit message';
+
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.multi-file-commit-form .btn-success').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.commitChanges).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
new file mode 100644
index 00000000000..ae657e8c881
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -0,0 +1,137 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import repoEditor from '~/ide/components/repo_editor.vue';
+import monacoLoader from '~/ide/monaco_loader';
+import Editor from '~/ide/lib/editor';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../helpers';
+
+describe('RepoEditor', () => {
+ let vm;
+
+ beforeEach(done => {
+ const f = file();
+ const RepoEditor = Vue.extend(repoEditor);
+
+ vm = createComponentWithStore(RepoEditor, store, {
+ file: f,
+ });
+
+ f.active = true;
+ f.tempFile = true;
+ f.html = 'testing';
+ vm.$store.state.openFiles.push(f);
+ vm.$store.state.entries[f.path] = f;
+ vm.monaco = true;
+
+ vm.$mount();
+
+ monacoLoader(['vs/editor/editor.main'], () => {
+ setTimeout(done, 0);
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+
+ Editor.editorInstance.modelManager.dispose();
+ });
+
+ it('renders an ide container', done => {
+ Vue.nextTick(() => {
+ expect(vm.shouldHideEditor).toBeFalsy();
+
+ done();
+ });
+ });
+
+ describe('when open file is binary and not raw', () => {
+ beforeEach(done => {
+ vm.file.binary = true;
+
+ vm.$nextTick(done);
+ });
+
+ it('does not render the IDE', () => {
+ expect(vm.shouldHideEditor).toBeTruthy();
+ });
+
+ it('shows activeFile html', () => {
+ expect(vm.$el.textContent).toContain('testing');
+ });
+ });
+
+ describe('createEditorInstance', () => {
+ it('calls createInstance when viewer is editor', done => {
+ spyOn(vm.editor, 'createInstance');
+
+ vm.createEditorInstance();
+
+ vm.$nextTick(() => {
+ expect(vm.editor.createInstance).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('calls createDiffInstance when viewer is diff', done => {
+ vm.$store.state.viewer = 'diff';
+
+ spyOn(vm.editor, 'createDiffInstance');
+
+ vm.createEditorInstance();
+
+ vm.$nextTick(() => {
+ expect(vm.editor.createDiffInstance).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+
+ describe('setupEditor', () => {
+ it('creates new model', () => {
+ spyOn(vm.editor, 'createModel').and.callThrough();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file);
+ expect(vm.model).not.toBeNull();
+ });
+
+ it('attaches model to editor', () => {
+ spyOn(vm.editor, 'attachModel').and.callThrough();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.attachModel).toHaveBeenCalledWith(vm.model);
+ });
+
+ it('adds callback methods', () => {
+ spyOn(vm.editor, 'onPositionChange').and.callThrough();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.onPositionChange).toHaveBeenCalled();
+ expect(vm.model.events.size).toBe(1);
+ });
+
+ it('updates state when model content changed', done => {
+ vm.model.setValue('testing 123');
+
+ setTimeout(() => {
+ expect(vm.file.content).toBe('testing 123');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_file_buttons_spec.js b/spec/javascripts/ide/components/repo_file_buttons_spec.js
new file mode 100644
index 00000000000..c86bdb132b4
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_file_buttons_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import repoFileButtons from '~/ide/components/repo_file_buttons.vue';
+import createVueComponent from '../../helpers/vue_mount_component_helper';
+import { file } from '../helpers';
+
+describe('RepoFileButtons', () => {
+ const activeFile = file();
+ let vm;
+
+ function createComponent() {
+ const RepoFileButtons = Vue.extend(repoFileButtons);
+
+ activeFile.rawPath = 'test';
+ activeFile.blamePath = 'test';
+ activeFile.commitsPath = 'test';
+
+ return createVueComponent(RepoFileButtons, {
+ file: activeFile,
+ });
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders Raw, Blame, History, Permalink and Preview toggle', done => {
+ vm = createComponent();
+
+ vm.$nextTick(() => {
+ const raw = vm.$el.querySelector('.raw');
+ const blame = vm.$el.querySelector('.blame');
+ const history = vm.$el.querySelector('.history');
+
+ expect(raw.href).toMatch(`/${activeFile.rawPath}`);
+ expect(raw.textContent.trim()).toEqual('Raw');
+ expect(blame.href).toMatch(`/${activeFile.blamePath}`);
+ expect(blame.textContent.trim()).toEqual('Blame');
+ expect(history.href).toMatch(`/${activeFile.commitsPath}`);
+ expect(history.textContent.trim()).toEqual('History');
+ expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual(
+ 'Permalink',
+ );
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_file_spec.js b/spec/javascripts/ide/components/repo_file_spec.js
new file mode 100644
index 00000000000..ff391cb4351
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_file_spec.js
@@ -0,0 +1,80 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import repoFile from '~/ide/components/repo_file.vue';
+import router from '~/ide/ide_router';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { file } from '../helpers';
+
+describe('RepoFile', () => {
+ let vm;
+
+ function createComponent(propsData) {
+ const RepoFile = Vue.extend(repoFile);
+
+ vm = createComponentWithStore(RepoFile, store, propsData);
+
+ vm.$mount();
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders link, icon and name', () => {
+ createComponent({
+ file: file('t4'),
+ level: 0,
+ });
+
+ const name = vm.$el.querySelector('.ide-file-name');
+
+ expect(name.href).toMatch('');
+ expect(name.textContent.trim()).toEqual(vm.file.name);
+ });
+
+ it('fires clickFile when the link is clicked', done => {
+ spyOn(router, 'push');
+ createComponent({
+ file: file('t3'),
+ level: 0,
+ });
+
+ vm.$el.querySelector('.file-name').click();
+
+ setTimeout(() => {
+ expect(router.push).toHaveBeenCalledWith(`/project${vm.file.url}`);
+
+ done();
+ });
+ });
+
+ describe('locked file', () => {
+ let f;
+
+ beforeEach(() => {
+ f = file('locked file');
+ f.file_lock = {
+ user: {
+ name: 'testuser',
+ updated_at: new Date(),
+ },
+ };
+
+ createComponent({
+ file: f,
+ level: 0,
+ });
+ });
+
+ it('renders lock icon', () => {
+ expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
+ });
+
+ it('renders a tooltip', () => {
+ expect(
+ vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset
+ .originalTitle,
+ ).toContain('Locked by testuser');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_loading_file_spec.js b/spec/javascripts/ide/components/repo_loading_file_spec.js
new file mode 100644
index 00000000000..8f9644216bc
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_loading_file_spec.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import repoLoadingFile from '~/ide/components/repo_loading_file.vue';
+import { resetStore } from '../helpers';
+
+describe('RepoLoadingFile', () => {
+ let vm;
+
+ function createComponent() {
+ const RepoLoadingFile = Vue.extend(repoLoadingFile);
+
+ return new RepoLoadingFile({
+ store,
+ }).$mount();
+ }
+
+ function assertLines(lines) {
+ lines.forEach((line, n) => {
+ const index = n + 1;
+ expect(line.classList.contains(`skeleton-line-${index}`)).toBeTruthy();
+ });
+ }
+
+ function assertColumns(columns) {
+ columns.forEach(column => {
+ const container = column.querySelector('.animation-container');
+ const lines = [...container.querySelectorAll(':scope > div')];
+
+ expect(container).toBeTruthy();
+ expect(lines.length).toEqual(6);
+ assertLines(lines);
+ });
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders 3 columns of animated LoC', () => {
+ vm = createComponent();
+ const columns = [...vm.$el.querySelectorAll('td')];
+
+ expect(columns.length).toEqual(3);
+ assertColumns(columns);
+ });
+
+ it('renders 1 column of animated LoC if isMini', done => {
+ vm = createComponent();
+ vm.$store.state.leftPanelCollapsed = true;
+ vm.$store.state.openFiles.push('test');
+
+ vm.$nextTick(() => {
+ const columns = [...vm.$el.querySelectorAll('td')];
+
+ expect(columns.length).toEqual(1);
+ assertColumns(columns);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js
new file mode 100644
index 00000000000..ddb5204e3a7
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_tab_spec.js
@@ -0,0 +1,165 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import repoTab from '~/ide/components/repo_tab.vue';
+import router from '~/ide/ide_router';
+import { file, resetStore } from '../helpers';
+
+describe('RepoTab', () => {
+ let vm;
+
+ function createComponent(propsData) {
+ const RepoTab = Vue.extend(repoTab);
+
+ return new RepoTab({
+ store,
+ propsData,
+ }).$mount();
+ }
+
+ beforeEach(() => {
+ spyOn(router, 'push');
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders a close link and a name link', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+ vm.$store.state.openFiles.push(vm.tab);
+ const close = vm.$el.querySelector('.multi-file-tab-close');
+ const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`);
+
+ expect(close.innerHTML).toContain('#close');
+ expect(name.textContent.trim()).toEqual(vm.tab.name);
+ });
+
+ it('fires clickFile when the link is clicked', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ spyOn(vm, 'clickFile');
+
+ vm.$el.click();
+
+ expect(vm.clickFile).toHaveBeenCalledWith(vm.tab);
+ });
+
+ it('calls closeFile when clicking close button', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ spyOn(vm, 'closeFile');
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ expect(vm.closeFile).toHaveBeenCalledWith(vm.tab.path);
+ });
+
+ it('changes icon on hover', done => {
+ const tab = file();
+ tab.changed = true;
+ vm = createComponent({
+ tab,
+ });
+
+ vm.$el.dispatchEvent(new Event('mouseover'));
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.multi-file-modified')).toBeNull();
+
+ vm.$el.dispatchEvent(new Event('mouseout'));
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.$el.querySelector('.multi-file-modified')).not.toBeNull();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ describe('locked file', () => {
+ let f;
+
+ beforeEach(() => {
+ f = file('locked file');
+ f.file_lock = {
+ user: {
+ name: 'testuser',
+ updated_at: new Date(),
+ },
+ };
+
+ vm = createComponent({
+ tab: f,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders lock icon', () => {
+ expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
+ });
+
+ it('renders a tooltip', () => {
+ expect(
+ vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle,
+ ).toContain('Locked by testuser');
+ });
+ });
+
+ describe('methods', () => {
+ describe('closeTab', () => {
+ it('closes tab if file has changed', done => {
+ const tab = file();
+ tab.changed = true;
+ tab.opened = true;
+ vm = createComponent({
+ tab,
+ });
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.state.changedFiles.push(tab);
+ vm.$store.state.entries[tab.path] = tab;
+ vm.$store.dispatch('setFileActive', tab.path);
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeFalsy();
+ expect(vm.$store.state.changedFiles.length).toBe(1);
+
+ done();
+ });
+ });
+
+ it('closes tab when clicking close btn', done => {
+ const tab = file('lose');
+ tab.opened = true;
+ vm = createComponent({
+ tab,
+ });
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.state.entries[tab.path] = tab;
+ vm.$store.dispatch('setFileActive', tab.path);
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeFalsy();
+
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js
new file mode 100644
index 00000000000..ceb0416aff8
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_tabs_spec.js
@@ -0,0 +1,81 @@
+import Vue from 'vue';
+import repoTabs from '~/ide/components/repo_tabs.vue';
+import createComponent from '../../helpers/vue_mount_component_helper';
+import { file } from '../helpers';
+
+describe('RepoTabs', () => {
+ const openedFiles = [file('open1'), file('open2')];
+ const RepoTabs = Vue.extend(repoTabs);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a list of tabs', done => {
+ vm = createComponent(RepoTabs, {
+ files: openedFiles,
+ viewer: 'editor',
+ hasChanges: false,
+ });
+ openedFiles[0].active = true;
+
+ vm.$nextTick(() => {
+ const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')];
+
+ expect(tabs.length).toEqual(2);
+ expect(tabs[0].classList.contains('active')).toEqual(true);
+ expect(tabs[1].classList.contains('active')).toEqual(false);
+
+ done();
+ });
+ });
+
+ describe('updated', () => {
+ it('sets showShadow as true when scroll width is larger than width', done => {
+ const el = document.createElement('div');
+ el.innerHTML = '<div id="test-app"></div>';
+ document.body.appendChild(el);
+
+ const style = document.createElement('style');
+ style.innerText = `
+ .multi-file-tabs {
+ width: 100px;
+ }
+
+ .multi-file-tabs .list-unstyled {
+ display: flex;
+ overflow-x: auto;
+ }
+ `;
+ document.head.appendChild(style);
+
+ vm = createComponent(
+ RepoTabs,
+ {
+ files: [],
+ viewer: 'editor',
+ hasChanges: false,
+ },
+ '#test-app',
+ );
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.showShadow).toEqual(false);
+
+ vm.files = openedFiles;
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.showShadow).toEqual(true);
+
+ style.remove();
+ el.remove();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js
new file mode 100644
index 00000000000..98db6defc7a
--- /dev/null
+++ b/spec/javascripts/ide/helpers.js
@@ -0,0 +1,22 @@
+import { decorateData } from '~/ide/stores/utils';
+import state from '~/ide/stores/state';
+import commitState from '~/ide/stores/modules/commit/state';
+
+export const resetStore = store => {
+ const newState = {
+ ...state(),
+ commit: commitState(),
+ };
+ store.replaceState(newState);
+};
+
+export const file = (name = 'name', id = name, type = '') =>
+ decorateData({
+ id,
+ type,
+ icon: 'icon',
+ url: 'url',
+ name,
+ path: name,
+ lastCommit: {},
+ });
diff --git a/spec/javascripts/ide/lib/common/disposable_spec.js b/spec/javascripts/ide/lib/common/disposable_spec.js
new file mode 100644
index 00000000000..af12ca15369
--- /dev/null
+++ b/spec/javascripts/ide/lib/common/disposable_spec.js
@@ -0,0 +1,44 @@
+import Disposable from '~/ide/lib/common/disposable';
+
+describe('Multi-file editor library disposable class', () => {
+ let instance;
+ let disposableClass;
+
+ beforeEach(() => {
+ instance = new Disposable();
+
+ disposableClass = {
+ dispose: jasmine.createSpy('dispose'),
+ };
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ describe('add', () => {
+ it('adds disposable classes', () => {
+ instance.add(disposableClass);
+
+ expect(instance.disposers.size).toBe(1);
+ });
+ });
+
+ describe('dispose', () => {
+ beforeEach(() => {
+ instance.add(disposableClass);
+ });
+
+ it('calls dispose on all cached disposers', () => {
+ instance.dispose();
+
+ expect(disposableClass.dispose).toHaveBeenCalled();
+ });
+
+ it('clears cached disposers', () => {
+ instance.dispose();
+
+ expect(instance.disposers.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/common/model_manager_spec.js b/spec/javascripts/ide/lib/common/model_manager_spec.js
new file mode 100644
index 00000000000..4381f6fcfd0
--- /dev/null
+++ b/spec/javascripts/ide/lib/common/model_manager_spec.js
@@ -0,0 +1,129 @@
+/* global monaco */
+import eventHub from '~/ide/eventhub';
+import monacoLoader from '~/ide/monaco_loader';
+import ModelManager from '~/ide/lib/common/model_manager';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model manager', () => {
+ let instance;
+
+ beforeEach(done => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ instance = new ModelManager(monaco);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ describe('addModel', () => {
+ it('caches model', () => {
+ instance.addModel(file());
+
+ expect(instance.models.size).toBe(1);
+ });
+
+ it('caches model by file path', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.models.keys().next().value).toBe('path-name');
+ });
+
+ it('adds model into disposable', () => {
+ spyOn(instance.disposable, 'add').and.callThrough();
+
+ instance.addModel(file());
+
+ expect(instance.disposable.add).toHaveBeenCalled();
+ });
+
+ it('returns cached model', () => {
+ spyOn(instance.models, 'get').and.callThrough();
+
+ instance.addModel(file());
+ instance.addModel(file());
+
+ expect(instance.models.get).toHaveBeenCalled();
+ });
+
+ it('adds eventHub listener', () => {
+ const f = file();
+ spyOn(eventHub, '$on').and.callThrough();
+
+ instance.addModel(f);
+
+ expect(eventHub.$on).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${f.path}`,
+ jasmine.anything(),
+ );
+ });
+ });
+
+ describe('hasCachedModel', () => {
+ it('returns false when no models exist', () => {
+ expect(instance.hasCachedModel('path')).toBeFalsy();
+ });
+
+ it('returns true when model exists', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.hasCachedModel('path-name')).toBeTruthy();
+ });
+ });
+
+ describe('getModel', () => {
+ it('returns cached model', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.getModel('path-name')).not.toBeNull();
+ });
+ });
+
+ describe('removeCachedModel', () => {
+ let f;
+
+ beforeEach(() => {
+ f = file();
+
+ instance.addModel(f);
+ });
+
+ it('clears cached model', () => {
+ instance.removeCachedModel(f);
+
+ expect(instance.models.size).toBe(0);
+ });
+
+ it('removes eventHub listener', () => {
+ spyOn(eventHub, '$off').and.callThrough();
+
+ instance.removeCachedModel(f);
+
+ expect(eventHub.$off).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${f.path}`,
+ jasmine.anything(),
+ );
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached models', () => {
+ instance.addModel(file());
+
+ instance.dispose();
+
+ expect(instance.models.size).toBe(0);
+ });
+
+ it('calls disposable dispose', () => {
+ spyOn(instance.disposable, 'dispose').and.callThrough();
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
new file mode 100644
index 00000000000..adc6a93c06b
--- /dev/null
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -0,0 +1,113 @@
+/* global monaco */
+import eventHub from '~/ide/eventhub';
+import monacoLoader from '~/ide/monaco_loader';
+import Model from '~/ide/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model', () => {
+ let model;
+
+ beforeEach(done => {
+ spyOn(eventHub, '$on').and.callThrough();
+
+ monacoLoader(['vs/editor/editor.main'], () => {
+ model = new Model(monaco, file('path'));
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ model.dispose();
+ });
+
+ it('creates original model & new model', () => {
+ expect(model.originalModel).not.toBeNull();
+ expect(model.model).not.toBeNull();
+ });
+
+ it('adds eventHub listener', () => {
+ expect(eventHub.$on).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${model.file.path}`,
+ jasmine.anything(),
+ );
+ });
+
+ describe('path', () => {
+ it('returns file path', () => {
+ expect(model.path).toBe('path');
+ });
+ });
+
+ describe('getModel', () => {
+ it('returns model', () => {
+ expect(model.getModel()).toBe(model.model);
+ });
+ });
+
+ describe('getOriginalModel', () => {
+ it('returns original model', () => {
+ expect(model.getOriginalModel()).toBe(model.originalModel);
+ });
+ });
+
+ describe('setValue', () => {
+ it('updates models value', () => {
+ model.setValue('testing 123');
+
+ expect(model.getModel().getValue()).toBe('testing 123');
+ });
+ });
+
+ describe('onChange', () => {
+ it('caches event by path', () => {
+ model.onChange(() => {});
+
+ expect(model.events.size).toBe(1);
+ expect(model.events.keys().next().value).toBe('path');
+ });
+
+ it('calls callback on change', done => {
+ const spy = jasmine.createSpy();
+ model.onChange(spy);
+
+ model.getModel().setValue('123');
+
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalledWith(model, jasmine.anything());
+ done();
+ });
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ spyOn(model.disposable, 'dispose').and.callThrough();
+
+ model.dispose();
+
+ expect(model.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('clears events', () => {
+ model.onChange(() => {});
+
+ expect(model.events.size).toBe(1);
+
+ model.dispose();
+
+ expect(model.events.size).toBe(0);
+ });
+
+ it('removes eventHub listener', () => {
+ spyOn(eventHub, '$off').and.callThrough();
+
+ model.dispose();
+
+ expect(eventHub.$off).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${model.file.path}`,
+ jasmine.anything(),
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js
new file mode 100644
index 00000000000..092170d086a
--- /dev/null
+++ b/spec/javascripts/ide/lib/decorations/controller_spec.js
@@ -0,0 +1,139 @@
+/* global monaco */
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
+import DecorationsController from '~/ide/lib/decorations/controller';
+import Model from '~/ide/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library decorations controller', () => {
+ let editorInstance;
+ let controller;
+ let model;
+
+ beforeEach(done => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ editorInstance = editor.create(monaco);
+ editorInstance.createInstance(document.createElement('div'));
+
+ controller = new DecorationsController(editorInstance);
+ model = new Model(monaco, file('path'));
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ model.dispose();
+ editorInstance.dispose();
+ controller.dispose();
+ });
+
+ describe('getAllDecorationsForModel', () => {
+ it('returns empty array when no decorations exist for model', () => {
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations).toEqual([]);
+ });
+
+ it('returns decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations[0]).toEqual({ decoration: 'decorationValue' });
+ });
+ });
+
+ describe('addDecorations', () => {
+ it('caches decorations in a new map', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('does not create new cache model', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue2' },
+ ]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ expect(controller.decorations.size).toBe(1);
+ expect(controller.decorations.keys().next().value).toBe('path');
+ });
+
+ it('calls decorate method', () => {
+ spyOn(controller, 'decorate');
+
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ expect(controller.decorate).toHaveBeenCalled();
+ });
+ });
+
+ describe('decorate', () => {
+ it('sets decorations on editor instance', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations');
+
+ controller.decorate(model);
+
+ expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith(
+ [],
+ [],
+ );
+ });
+
+ it('caches decorations', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.keys().next().value).toBe('path');
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached decorations', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ controller.dispose();
+
+ expect(controller.decorations.size).toBe(0);
+ });
+
+ it('clears cached editorDecorations', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ controller.dispose();
+
+ expect(controller.editorDecorations.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js
new file mode 100644
index 00000000000..c8f3e9f4830
--- /dev/null
+++ b/spec/javascripts/ide/lib/diff/controller_spec.js
@@ -0,0 +1,196 @@
+/* global monaco */
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
+import ModelManager from '~/ide/lib/common/model_manager';
+import DecorationsController from '~/ide/lib/decorations/controller';
+import DirtyDiffController, {
+ getDiffChangeType,
+ getDecorator,
+} from '~/ide/lib/diff/controller';
+import { computeDiff } from '~/ide/lib/diff/diff';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library dirty diff controller', () => {
+ let editorInstance;
+ let controller;
+ let modelManager;
+ let decorationsController;
+ let model;
+
+ beforeEach(done => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ editorInstance = editor.create(monaco);
+ editorInstance.createInstance(document.createElement('div'));
+
+ modelManager = new ModelManager(monaco);
+ decorationsController = new DecorationsController(editorInstance);
+
+ model = modelManager.addModel(file('path'));
+
+ controller = new DirtyDiffController(modelManager, decorationsController);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ controller.dispose();
+ model.dispose();
+ decorationsController.dispose();
+ editorInstance.dispose();
+ });
+
+ describe('getDiffChangeType', () => {
+ ['added', 'removed', 'modified'].forEach(type => {
+ it(`returns ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(getDiffChangeType(change)).toBe(type);
+ });
+ });
+ });
+
+ describe('getDecorator', () => {
+ ['added', 'removed', 'modified'].forEach(type => {
+ it(`returns with linesDecorationsClassName for ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(getDecorator(change).options.linesDecorationsClassName).toBe(
+ `dirty-diff dirty-diff-${type}`,
+ );
+ });
+
+ it('returns with line numbers', () => {
+ const change = {
+ lineNumber: 1,
+ endLineNumber: 2,
+ [type]: true,
+ };
+
+ const range = getDecorator(change).range;
+
+ expect(range.startLineNumber).toBe(1);
+ expect(range.endLineNumber).toBe(2);
+ expect(range.startColumn).toBe(1);
+ expect(range.endColumn).toBe(1);
+ });
+ });
+ });
+
+ describe('attachModel', () => {
+ it('adds change event callback', () => {
+ spyOn(model, 'onChange');
+
+ controller.attachModel(model);
+
+ expect(model.onChange).toHaveBeenCalled();
+ });
+
+ it('calls throttledComputeDiff on change', () => {
+ spyOn(controller, 'throttledComputeDiff');
+
+ controller.attachModel(model);
+
+ model.getModel().setValue('123');
+
+ expect(controller.throttledComputeDiff).toHaveBeenCalled();
+ });
+ });
+
+ describe('computeDiff', () => {
+ it('posts to worker', () => {
+ spyOn(controller.dirtyDiffWorker, 'postMessage');
+
+ controller.computeDiff(model);
+
+ expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({
+ path: model.path,
+ originalContent: '',
+ newContent: '',
+ });
+ });
+ });
+
+ describe('reDecorate', () => {
+ it('calls decorations controller decorate', () => {
+ spyOn(controller.decorationsController, 'decorate');
+
+ controller.reDecorate(model);
+
+ expect(controller.decorationsController.decorate).toHaveBeenCalledWith(
+ model,
+ );
+ });
+ });
+
+ describe('decorate', () => {
+ it('adds decorations into decorations controller', () => {
+ spyOn(controller.decorationsController, 'addDecorations');
+
+ controller.decorate({ data: { changes: [], path: 'path' } });
+
+ expect(
+ controller.decorationsController.addDecorations,
+ ).toHaveBeenCalledWith(model, 'dirtyDiff', jasmine.anything());
+ });
+
+ it('adds decorations into editor', () => {
+ const spy = spyOn(
+ controller.decorationsController.editor.instance,
+ 'deltaDecorations',
+ );
+
+ controller.decorate({
+ data: { changes: computeDiff('123', '1234'), path: 'path' },
+ });
+
+ expect(spy).toHaveBeenCalledWith(
+ [],
+ [
+ {
+ range: new monaco.Range(1, 1, 1, 1),
+ options: {
+ isWholeLine: true,
+ linesDecorationsClassName: 'dirty-diff dirty-diff-modified',
+ },
+ },
+ ],
+ );
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ spyOn(controller.disposable, 'dispose').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('terminates worker', () => {
+ spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled();
+ });
+
+ it('removes worker event listener', () => {
+ spyOn(
+ controller.dirtyDiffWorker,
+ 'removeEventListener',
+ ).and.callThrough();
+
+ controller.dispose();
+
+ expect(
+ controller.dirtyDiffWorker.removeEventListener,
+ ).toHaveBeenCalledWith('message', jasmine.anything());
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/diff/diff_spec.js b/spec/javascripts/ide/lib/diff/diff_spec.js
new file mode 100644
index 00000000000..57f3ac3d365
--- /dev/null
+++ b/spec/javascripts/ide/lib/diff/diff_spec.js
@@ -0,0 +1,80 @@
+import { computeDiff } from '~/ide/lib/diff/diff';
+
+describe('Multi-file editor library diff calculator', () => {
+ describe('computeDiff', () => {
+ it('returns empty array if no changes', () => {
+ const diff = computeDiff('123', '123');
+
+ expect(diff).toEqual([]);
+ });
+
+ describe('modified', () => {
+ it('', () => {
+ const diff = computeDiff('123', '1234')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(2);
+ });
+ });
+
+ describe('added', () => {
+ it('', () => {
+ const diff = computeDiff('123', '123\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(3);
+ });
+ });
+
+ describe('removed', () => {
+ it('', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeTruthy();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n123')[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeTruthy();
+ expect(diff.lineNumber).toBe(2);
+ });
+ });
+
+ it('includes line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.lineNumber).toBe(1);
+ });
+
+ it('includes end line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.endLineNumber).toBe(1);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/editor_options_spec.js b/spec/javascripts/ide/lib/editor_options_spec.js
new file mode 100644
index 00000000000..d149a883166
--- /dev/null
+++ b/spec/javascripts/ide/lib/editor_options_spec.js
@@ -0,0 +1,11 @@
+import editorOptions from '~/ide/lib/editor_options';
+
+describe('Multi-file editor library editor options', () => {
+ it('returns an array', () => {
+ expect(editorOptions).toEqual(jasmine.any(Array));
+ });
+
+ it('contains readOnly option', () => {
+ expect(editorOptions[0].readOnly).toBeDefined();
+ });
+});
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
new file mode 100644
index 00000000000..d6df35c90e8
--- /dev/null
+++ b/spec/javascripts/ide/lib/editor_spec.js
@@ -0,0 +1,197 @@
+/* global monaco */
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
+import { file } from '../helpers';
+
+describe('Multi-file editor library', () => {
+ let instance;
+ let el;
+ let holder;
+
+ beforeEach(done => {
+ el = document.createElement('div');
+ holder = document.createElement('div');
+ el.appendChild(holder);
+
+ document.body.appendChild(el);
+
+ monacoLoader(['vs/editor/editor.main'], () => {
+ instance = editor.create(monaco);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ instance.dispose();
+
+ el.remove();
+ });
+
+ it('creates instance of editor', () => {
+ expect(editor.editorInstance).not.toBeNull();
+ });
+
+ it('creates instance returns cached instance', () => {
+ expect(editor.create(monaco)).toEqual(instance);
+ });
+
+ describe('createInstance', () => {
+ it('creates editor instance', () => {
+ spyOn(instance.monaco.editor, 'create').and.callThrough();
+
+ instance.createInstance(holder);
+
+ expect(instance.monaco.editor.create).toHaveBeenCalled();
+ });
+
+ it('creates dirty diff controller', () => {
+ instance.createInstance(holder);
+
+ expect(instance.dirtyDiffController).not.toBeNull();
+ });
+
+ it('creates model manager', () => {
+ instance.createInstance(holder);
+
+ expect(instance.modelManager).not.toBeNull();
+ });
+ });
+
+ describe('createDiffInstance', () => {
+ it('creates editor instance', () => {
+ spyOn(instance.monaco.editor, 'createDiffEditor').and.callThrough();
+
+ instance.createDiffInstance(holder);
+
+ expect(instance.monaco.editor.createDiffEditor).toHaveBeenCalledWith(
+ holder,
+ {
+ model: null,
+ contextmenu: true,
+ minimap: {
+ enabled: false,
+ },
+ readOnly: true,
+ scrollBeyondLastLine: false,
+ },
+ );
+ });
+ });
+
+ describe('createModel', () => {
+ it('calls model manager addModel', () => {
+ spyOn(instance.modelManager, 'addModel');
+
+ instance.createModel('FILE');
+
+ expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE');
+ });
+ });
+
+ describe('attachModel', () => {
+ let model;
+
+ beforeEach(() => {
+ instance.createInstance(document.createElement('div'));
+
+ model = instance.createModel(file());
+ });
+
+ it('sets the current model on the instance', () => {
+ instance.attachModel(model);
+
+ expect(instance.currentModel).toBe(model);
+ });
+
+ it('attaches the model to the current instance', () => {
+ spyOn(instance.instance, 'setModel');
+
+ instance.attachModel(model);
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel());
+ });
+
+ it('sets original & modified when diff editor', () => {
+ spyOn(instance.instance, 'getEditorType').and.returnValue(
+ 'vs.editor.IDiffEditor',
+ );
+ spyOn(instance.instance, 'setModel');
+
+ instance.attachModel(model);
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith({
+ original: model.getOriginalModel(),
+ modified: model.getModel(),
+ });
+ });
+
+ it('attaches the model to the dirty diff controller', () => {
+ spyOn(instance.dirtyDiffController, 'attachModel');
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(
+ model,
+ );
+ });
+
+ it('re-decorates with the dirty diff controller', () => {
+ spyOn(instance.dirtyDiffController, 'reDecorate');
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(
+ model,
+ );
+ });
+ });
+
+ describe('clearEditor', () => {
+ it('resets the editor model', () => {
+ instance.createInstance(document.createElement('div'));
+
+ spyOn(instance.instance, 'setModel');
+
+ instance.clearEditor();
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(null);
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposble dispose method', () => {
+ spyOn(instance.disposable, 'dispose').and.callThrough();
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('resets instance', () => {
+ instance.createInstance(document.createElement('div'));
+
+ expect(instance.instance).not.toBeNull();
+
+ instance.dispose();
+
+ expect(instance.instance).toBeNull();
+ });
+
+ it('does not dispose modelManager', () => {
+ spyOn(instance.modelManager, 'dispose');
+
+ instance.dispose();
+
+ expect(instance.modelManager.dispose).not.toHaveBeenCalled();
+ });
+
+ it('does not dispose decorationsController', () => {
+ spyOn(instance.decorationsController, 'dispose');
+
+ instance.dispose();
+
+ expect(instance.decorationsController.dispose).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/monaco_loader_spec.js b/spec/javascripts/ide/monaco_loader_spec.js
new file mode 100644
index 00000000000..7ab315aa8c8
--- /dev/null
+++ b/spec/javascripts/ide/monaco_loader_spec.js
@@ -0,0 +1,15 @@
+import monacoContext from 'monaco-editor/dev/vs/loader';
+import monacoLoader from '~/ide/monaco_loader';
+
+describe('MonacoLoader', () => {
+ it('calls require.config and exports require', () => {
+ expect(monacoContext.require.getConfig()).toEqual(
+ jasmine.objectContaining({
+ paths: {
+ vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
+ },
+ }),
+ );
+ expect(monacoLoader).toBe(monacoContext.require);
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
new file mode 100644
index 00000000000..5b7c8365641
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -0,0 +1,421 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import router from '~/ide/ide_router';
+import eventHub from '~/ide/eventhub';
+import { file, resetStore } from '../../helpers';
+
+describe('Multi-file store file actions', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('closeFile', () => {
+ let localFile;
+
+ beforeEach(() => {
+ localFile = file('testFile');
+ localFile.active = true;
+ localFile.opened = true;
+ localFile.parentTreeUrl = 'parentTreeUrl';
+
+ store.state.openFiles.push(localFile);
+ store.state.entries[localFile.path] = localFile;
+ });
+
+ it('closes open files', done => {
+ store
+ .dispatch('closeFile', localFile.path)
+ .then(() => {
+ expect(localFile.opened).toBeFalsy();
+ expect(localFile.active).toBeFalsy();
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('closes file even if file has changes', done => {
+ store.state.changedFiles.push(localFile);
+
+ store
+ .dispatch('closeFile', localFile.path)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+ expect(store.state.changedFiles.length).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('closes file & opens next available file', done => {
+ const f = {
+ ...file('newOpenFile'),
+ url: '/newOpenFile',
+ };
+
+ store.state.openFiles.push(f);
+ store.state.entries[f.path] = f;
+
+ store
+ .dispatch('closeFile', localFile.path)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(router.push).toHaveBeenCalledWith(`/project${f.url}`);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('setFileActive', () => {
+ let localFile;
+ let scrollToTabSpy;
+ let oldScrollToTab;
+
+ beforeEach(() => {
+ scrollToTabSpy = jasmine.createSpy('scrollToTab');
+ oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
+ store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
+
+ localFile = file('setThisActive');
+
+ store.state.entries[localFile.path] = localFile;
+ });
+
+ afterEach(() => {
+ store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
+ });
+
+ it('calls scrollToTab', done => {
+ store
+ .dispatch('setFileActive', localFile.path)
+ .then(() => {
+ expect(scrollToTabSpy).toHaveBeenCalled();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the file active', done => {
+ store
+ .dispatch('setFileActive', localFile.path)
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns early if file is already active', done => {
+ localFile.active = true;
+
+ store
+ .dispatch('setFileActive', localFile.path)
+ .then(() => {
+ expect(scrollToTabSpy).not.toHaveBeenCalled();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets current active file to not active', done => {
+ const f = file('newActive');
+ store.state.entries[f.path] = f;
+ localFile.active = true;
+ store.state.openFiles.push(localFile);
+
+ store
+ .dispatch('setFileActive', f.path)
+ .then(() => {
+ expect(localFile.active).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('resets location.hash for line highlighting', done => {
+ location.hash = 'test';
+
+ store
+ .dispatch('setFileActive', localFile.path)
+ .then(() => {
+ expect(location.hash).not.toBe('test');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('getFileData', () => {
+ let localFile;
+
+ beforeEach(() => {
+ spyOn(service, 'getFileData').and.returnValue(
+ Promise.resolve({
+ headers: {
+ 'page-title': 'testing getFileData',
+ },
+ json: () =>
+ Promise.resolve({
+ blame_path: 'blame_path',
+ commits_path: 'commits_path',
+ permalink: 'permalink',
+ raw_path: 'raw_path',
+ binary: false,
+ html: '123',
+ render_error: '',
+ }),
+ }),
+ );
+
+ localFile = file(`newCreate-${Math.random()}`);
+ localFile.url = 'getFileDataURL';
+ store.state.entries[localFile.path] = localFile;
+ });
+
+ it('calls the service', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the file data', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(localFile.blamePath).toBe('blame_path');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets document title', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(document.title).toBe('testing getFileData');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the file as active', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds the file to open files', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(localFile.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('getRawFileData', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw'));
+
+ tmpFile = file('tmpFile');
+ store.state.entries[tmpFile.path] = tmpFile;
+ });
+
+ it('calls getRawFileData service method', done => {
+ store
+ .dispatch('getRawFileData', tmpFile)
+ .then(() => {
+ expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('updates file raw data', done => {
+ store
+ .dispatch('getRawFileData', tmpFile)
+ .then(() => {
+ expect(tmpFile.raw).toBe('raw');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('changeFileContent', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ tmpFile = file('tmpFile');
+ store.state.entries[tmpFile.path] = tmpFile;
+ });
+
+ it('updates file content', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() => {
+ expect(tmpFile.content).toBe('content');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds file into changedFiles array', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds file once into changedFiles array', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() =>
+ store.dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content 123',
+ }),
+ )
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('removes file from changedFiles array if not changed', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() =>
+ store.dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: '',
+ }),
+ )
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('discardFileChanges', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ spyOn(eventHub, '$on');
+
+ tmpFile = file();
+ tmpFile.content = 'testing';
+
+ store.state.changedFiles.push(tmpFile);
+ store.state.entries[tmpFile.path] = tmpFile;
+ });
+
+ it('resets file content', done => {
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(tmpFile.content).not.toBe('testing');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('removes file from changedFiles array', done => {
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('closes temp file', done => {
+ tmpFile.tempFile = true;
+ tmpFile.opened = true;
+
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(tmpFile.opened).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not re-open a closed temp file', done => {
+ tmpFile.tempFile = true;
+
+ expect(tmpFile.opened).toBeFalsy();
+
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(tmpFile.opened).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
new file mode 100644
index 00000000000..381f038067b
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -0,0 +1,172 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import router from '~/ide/ide_router';
+import { file, resetStore } from '../../helpers';
+
+describe('Multi-file store tree actions', () => {
+ let projectTree;
+
+ const basicCallParameters = {
+ endpoint: 'rootEndpoint',
+ projectId: 'abcproject',
+ branch: 'master',
+ branchId: 'master',
+ };
+
+ beforeEach(() => {
+ spyOn(router, 'push');
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ web_url: '',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('getFiles', () => {
+ beforeEach(() => {
+ spyOn(service, 'getFiles').and.returnValue(
+ Promise.resolve({
+ json: () =>
+ Promise.resolve([
+ 'file.txt',
+ 'folder/fileinfolder.js',
+ 'folder/subfolder/fileinsubfolder.js',
+ ]),
+ }),
+ );
+ });
+
+ it('calls service getFiles', done => {
+ store
+ .dispatch('getFiles', basicCallParameters)
+ .then(() => {
+ expect(service.getFiles).toHaveBeenCalledWith('', 'master');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds data into tree', done => {
+ store
+ .dispatch('getFiles', basicCallParameters)
+ .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('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('getLastCommitData', () => {
+ beforeEach(() => {
+ spyOn(service, 'getTreeLastCommit').and.returnValue(
+ Promise.resolve({
+ headers: {
+ 'more-logs-url': null,
+ },
+ json: () =>
+ Promise.resolve([
+ {
+ type: 'tree',
+ file_name: 'testing',
+ commit: {
+ message: 'commit message',
+ authored_date: '123',
+ },
+ },
+ ]),
+ }),
+ );
+
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+
+ projectTree = store.state.trees['abcproject/mybranch'];
+ projectTree.tree.push(file('testing', '1', 'tree'));
+ projectTree.lastCommitPath = 'lastcommitpath';
+ });
+
+ it('calls service with lastCommitPath', done => {
+ store
+ .dispatch('getLastCommitData', projectTree)
+ .then(() => {
+ expect(service.getTreeLastCommit).toHaveBeenCalledWith(
+ 'lastcommitpath',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('updates trees last commit data', done => {
+ store
+ .dispatch('getLastCommitData', projectTree)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(projectTree.tree[0].lastCommit.message).toBe('commit message');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not update entry if not found', done => {
+ projectTree.tree[0].name = 'a';
+
+ store
+ .dispatch('getLastCommitData', projectTree)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(projectTree.tree[0].lastCommit.message).not.toBe(
+ 'commit message',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
new file mode 100644
index 00000000000..cec572f4507
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -0,0 +1,306 @@
+import * as urlUtils from '~/lib/utils/url_utility';
+import store from '~/ide/stores';
+import router from '~/ide/ide_router';
+import { resetStore, file } from '../helpers';
+
+describe('Multi-file store actions', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('redirectToUrl', () => {
+ it('calls visitUrl', done => {
+ spyOn(urlUtils, 'visitUrl');
+
+ store
+ .dispatch('redirectToUrl', 'test')
+ .then(() => {
+ expect(urlUtils.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', () => {
+ beforeEach(() => {
+ const f = file('discardAll');
+ f.changed = true;
+
+ store.state.openFiles.push(f);
+ store.state.changedFiles.push(f);
+ store.state.entries[f.path] = f;
+ });
+
+ it('discards changes in file', done => {
+ store
+ .dispatch('discardAllChanges')
+ .then(() => {
+ expect(store.state.openFiles.changed).toBeFalsy();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ 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(1);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('closeAllFiles', () => {
+ beforeEach(() => {
+ const f = file('closeAll');
+ store.state.openFiles.push(f);
+ store.state.openFiles[0].opened = true;
+ store.state.entries[f.path] = f;
+ });
+
+ it('closes all open files', done => {
+ store
+ .dispatch('closeAllFiles')
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+
+ 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 => {
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(f => {
+ 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 => {
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(f => {
+ 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 changed files', done => {
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(f => {
+ expect(store.state.changedFiles.length).toBe(1);
+ expect(store.state.changedFiles[0].name).toBe(f.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets tmp file as active', done => {
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(f => {
+ expect(f.active).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ 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')).not.toBeNull();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('popHistoryState', () => {});
+
+ 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');
+ spyOn(el, 'focus');
+
+ store
+ .dispatch('scrollToTab')
+ .then(() => {
+ setTimeout(() => {
+ expect(el.focus).toHaveBeenCalled();
+
+ document.getElementById('tabs').remove();
+
+ done();
+ });
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateViewer', () => {
+ it('updates viewer state', done => {
+ store
+ .dispatch('updateViewer', 'diff')
+ .then(() => {
+ expect(store.state.viewer).toBe('diff');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
new file mode 100644
index 00000000000..a613f3a21cc
--- /dev/null
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -0,0 +1,55 @@
+import * as getters from '~/ide/stores/getters';
+import state from '~/ide/stores/state';
+import { file } from '../helpers';
+
+describe('Multi-file store getters', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('activeFile', () => {
+ it('returns the current active file', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('active'));
+ localState.openFiles[1].active = true;
+
+ expect(getters.activeFile(localState).name).toBe('active');
+ });
+
+ it('returns undefined if no active files are found', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('active'));
+
+ expect(getters.activeFile(localState)).toBeNull();
+ });
+ });
+
+ describe('modifiedFiles', () => {
+ it('returns a list of modified files', () => {
+ localState.openFiles.push(file());
+ localState.changedFiles.push(file('changed'));
+ localState.changedFiles[0].changed = true;
+
+ const modifiedFiles = getters.modifiedFiles(localState);
+
+ expect(modifiedFiles.length).toBe(1);
+ expect(modifiedFiles[0].name).toBe('changed');
+ });
+ });
+
+ describe('addedFiles', () => {
+ it('returns a list of added files', () => {
+ localState.openFiles.push(file());
+ localState.changedFiles.push(file('added'));
+ localState.changedFiles[0].changed = true;
+ localState.changedFiles[0].tempFile = true;
+
+ const modifiedFiles = getters.addedFiles(localState);
+
+ expect(modifiedFiles.length).toBe(1);
+ expect(modifiedFiles[0].name).toBe('added');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
new file mode 100644
index 00000000000..90ded940227
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -0,0 +1,505 @@
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import router from '~/ide/ide_router';
+import * as urlUtils from '~/lib/utils/url_utility';
+import eventHub from '~/ide/eventhub';
+import * as consts from '~/ide/stores/modules/commit/constants';
+import { resetStore, file } from 'spec/ide/helpers';
+
+describe('IDE commit module actions', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ 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);
+ });
+ });
+
+ 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('checkCommitStatus', () => {
+ beforeEach(() => {
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+ });
+
+ it('calls service', done => {
+ spyOn(service, 'getBranchData').and.returnValue(
+ Promise.resolve({
+ data: {
+ commit: { id: '123' },
+ },
+ }),
+ );
+
+ store
+ .dispatch('commit/checkCommitStatus')
+ .then(() => {
+ expect(service.getBranchData).toHaveBeenCalledWith(
+ 'abcproject',
+ 'master',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns true if current ref does not equal returned ID', done => {
+ spyOn(service, 'getBranchData').and.returnValue(
+ Promise.resolve({
+ data: {
+ commit: { id: '123' },
+ },
+ }),
+ );
+
+ store
+ .dispatch('commit/checkCommitStatus')
+ .then(val => {
+ expect(val).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns false if current ref equals returned ID', done => {
+ spyOn(service, 'getBranchData').and.returnValue(
+ Promise.resolve({
+ data: {
+ commit: { id: '1' },
+ },
+ }),
+ );
+
+ store
+ .dispatch('commit/checkCommitStatus')
+ .then(val => {
+ expect(val).toBeFalsy();
+
+ 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(() => {
+ spyOn(eventHub, '$emit');
+
+ f = file('changedFile');
+ Object.assign(f, {
+ active: true,
+ changed: true,
+ content: 'file content',
+ });
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ web_url: 'web_url',
+ branches: {
+ master: {
+ workingReference: '',
+ },
+ },
+ };
+ store.state.changedFiles.push(f, {
+ ...file('changedFile2'),
+ changed: true,
+ });
+ store.state.openFiles = store.state.changedFiles;
+
+ store.state.changedFiles.forEach(changedFile => {
+ store.state.entries[changedFile.path] = changedFile;
+ });
+ });
+
+ 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('removes all changed files', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(0);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('sets files commit data', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(f.lastCommit.message).toBe(data.message);
+ })
+ .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.path}`,
+ f.content,
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('pushes route to new branch if commitAction is new branch', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(router.push).toHaveBeenCalledWith(
+ `/project/abcproject/blob/master/${f.path}`,
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('resets stores commit actions', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(store.state.commit.commitAction).not.toBe(
+ consts.COMMIT_TO_NEW_BRANCH,
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('commitChanges', () => {
+ beforeEach(() => {
+ spyOn(urlUtils, 'visitUrl');
+
+ 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',
+ },
+ },
+ };
+ store.state.changedFiles.push(file('changed'));
+ store.state.changedFiles[0].active = true;
+ store.state.openFiles = store.state.changedFiles;
+
+ store.state.openFiles.forEach(f => {
+ store.state.entries[f.path] = f;
+ });
+
+ store.state.commit.commitAction = '2';
+ store.state.commit.commitMessage = 'testing 123';
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'commit').and.returnValue(
+ Promise.resolve({
+ data: {
+ id: '123456',
+ short_id: '123',
+ message: 'test message',
+ committed_date: 'date',
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ },
+ }),
+ );
+ });
+
+ it('calls service', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(service.commit).toHaveBeenCalledWith('abcproject', {
+ branch: jasmine.anything(),
+ commit_message: 'testing 123',
+ actions: [
+ {
+ action: 'update',
+ file_path: jasmine.anything(),
+ content: jasmine.anything(),
+ encoding: jasmine.anything(),
+ },
+ ],
+ start_branch: 'master',
+ });
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('pushes router to new route', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(router.push).toHaveBeenCalledWith(
+ `/project/${store.state.currentProjectId}/blob/${
+ store.getters['commit/newBranchName']
+ }/changed`,
+ );
+
+ 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 changed files', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.state.openFiles[0].lastCommit.message).toBe(
+ 'test message',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('redirects to new merge request page', done => {
+ spyOn(eventHub, '$on');
+
+ store.state.commit.commitAction = '3';
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith(
+ `webUrl/merge_requests/new?merge_request[source_branch]=${
+ store.getters['commit/newBranchName']
+ }&merge_request[target_branch]=master`,
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('failed', () => {
+ beforeEach(() => {
+ spyOn(service, 'commit').and.returnValue(
+ Promise.resolve({
+ 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);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
new file mode 100644
index 00000000000..e396284ec2c
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
@@ -0,0 +1,128 @@
+import commitState from '~/ide/stores/modules/commit/state';
+import * as consts from '~/ide/stores/modules/commit/constants';
+import * as getters from '~/ide/stores/modules/commit/getters';
+
+describe('IDE commit module getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = commitState();
+ });
+
+ describe('discardDraftButtonDisabled', () => {
+ it('returns true when commitMessage is empty', () => {
+ expect(getters.discardDraftButtonDisabled(state)).toBeTruthy();
+ });
+
+ it('returns false when commitMessage is not empty & loading is false', () => {
+ state.commitMessage = 'test';
+ state.submitCommitLoading = false;
+
+ expect(getters.discardDraftButtonDisabled(state)).toBeFalsy();
+ });
+
+ it('returns true when commitMessage is not empty & loading is true', () => {
+ state.commitMessage = 'test';
+ state.submitCommitLoading = true;
+
+ expect(getters.discardDraftButtonDisabled(state)).toBeTruthy();
+ });
+ });
+
+ describe('commitButtonDisabled', () => {
+ const localGetters = {
+ discardDraftButtonDisabled: false,
+ };
+ const rootState = {
+ changedFiles: ['a'],
+ };
+
+ it('returns false when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
+ expect(
+ getters.commitButtonDisabled(state, localGetters, rootState),
+ ).toBeFalsy();
+ });
+
+ it('returns true when discardDraftButtonDisabled is false & changedFiles is empty', () => {
+ rootState.changedFiles.length = 0;
+
+ expect(
+ getters.commitButtonDisabled(state, localGetters, rootState),
+ ).toBeTruthy();
+ });
+
+ it('returns true when discardDraftButtonDisabled is true', () => {
+ localGetters.discardDraftButtonDisabled = true;
+
+ expect(
+ getters.commitButtonDisabled(state, localGetters, rootState),
+ ).toBeTruthy();
+ });
+
+ it('returns true when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
+ localGetters.discardDraftButtonDisabled = false;
+ rootState.changedFiles.length = 0;
+
+ expect(
+ getters.commitButtonDisabled(state, localGetters, rootState),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('newBranchName', () => {
+ it('includes username, currentBranchId, patch & random number', () => {
+ gon.current_username = 'username';
+
+ const branch = getters.newBranchName(state, null, {
+ currentBranchId: 'testing',
+ });
+
+ expect(branch).toMatch(/username-testing-patch-\d{5}$/);
+ });
+ });
+
+ describe('branchName', () => {
+ const rootState = {
+ currentBranchId: 'master',
+ };
+ const localGetters = {
+ newBranchName: 'newBranchName',
+ };
+
+ beforeEach(() => {
+ Object.assign(state, {
+ newBranchName: 'state-newBranchName',
+ });
+ });
+
+ it('defualts to currentBranchId', () => {
+ expect(getters.branchName(state, null, rootState)).toBe('master');
+ });
+
+ ['COMMIT_TO_NEW_BRANCH', 'COMMIT_TO_NEW_BRANCH_MR'].forEach(type => {
+ describe(type, () => {
+ beforeEach(() => {
+ Object.assign(state, {
+ commitAction: consts[type],
+ });
+ });
+
+ it('uses newBranchName when not empty', () => {
+ expect(getters.branchName(state, localGetters, rootState)).toBe(
+ 'state-newBranchName',
+ );
+ });
+
+ it('uses getters newBranchName when state newBranchName is empty', () => {
+ Object.assign(state, {
+ newBranchName: '',
+ });
+
+ expect(getters.branchName(state, localGetters, rootState)).toBe(
+ 'newBranchName',
+ );
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/commit/mutations_spec.js b/spec/javascripts/ide/stores/modules/commit/mutations_spec.js
new file mode 100644
index 00000000000..5de7a281d34
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/commit/mutations_spec.js
@@ -0,0 +1,42 @@
+import commitState from '~/ide/stores/modules/commit/state';
+import mutations from '~/ide/stores/modules/commit/mutations';
+
+describe('IDE commit module mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = commitState();
+ });
+
+ describe('UPDATE_COMMIT_MESSAGE', () => {
+ it('updates commitMessage', () => {
+ mutations.UPDATE_COMMIT_MESSAGE(state, 'testing');
+
+ expect(state.commitMessage).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_COMMIT_ACTION', () => {
+ it('updates commitAction', () => {
+ mutations.UPDATE_COMMIT_ACTION(state, 'testing');
+
+ expect(state.commitAction).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_NEW_BRANCH_NAME', () => {
+ it('updates newBranchName', () => {
+ mutations.UPDATE_NEW_BRANCH_NAME(state, 'testing');
+
+ expect(state.newBranchName).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_LOADING', () => {
+ it('updates submitCommitLoading', () => {
+ mutations.UPDATE_LOADING(state, true);
+
+ expect(state.submitCommitLoading).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations/branch_spec.js b/spec/javascripts/ide/stores/mutations/branch_spec.js
new file mode 100644
index 00000000000..a7167537ef2
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations/branch_spec.js
@@ -0,0 +1,18 @@
+import mutations from '~/ide/stores/mutations/branch';
+import state from '~/ide/stores/state';
+
+describe('Multi-file store branch mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('SET_CURRENT_BRANCH', () => {
+ it('sets currentBranch', () => {
+ mutations.SET_CURRENT_BRANCH(localState, 'master');
+
+ expect(localState.currentBranchId).toBe('master');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
new file mode 100644
index 00000000000..131380248e8
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -0,0 +1,157 @@
+import mutations from '~/ide/stores/mutations/file';
+import state from '~/ide/stores/state';
+import { file } from '../../helpers';
+
+describe('Multi-file store file mutations', () => {
+ let localState;
+ let localFile;
+
+ beforeEach(() => {
+ localState = state();
+ localFile = file();
+
+ localState.entries[localFile.path] = localFile;
+ });
+
+ describe('SET_FILE_ACTIVE', () => {
+ it('sets the file active', () => {
+ mutations.SET_FILE_ACTIVE(localState, {
+ path: localFile.path,
+ active: true,
+ });
+
+ expect(localFile.active).toBeTruthy();
+ });
+ });
+
+ describe('TOGGLE_FILE_OPEN', () => {
+ beforeEach(() => {
+ mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
+ });
+
+ it('adds into opened files', () => {
+ expect(localFile.opened).toBeTruthy();
+ expect(localState.openFiles.length).toBe(1);
+ });
+
+ it('removes from opened files', () => {
+ mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
+
+ expect(localFile.opened).toBeFalsy();
+ expect(localState.openFiles.length).toBe(0);
+ });
+ });
+
+ describe('SET_FILE_DATA', () => {
+ it('sets extra file data', () => {
+ mutations.SET_FILE_DATA(localState, {
+ data: {
+ blame_path: 'blame',
+ commits_path: 'commits',
+ permalink: 'permalink',
+ raw_path: 'raw',
+ binary: true,
+ render_error: 'render_error',
+ },
+ file: localFile,
+ });
+
+ expect(localFile.blamePath).toBe('blame');
+ expect(localFile.commitsPath).toBe('commits');
+ expect(localFile.permalink).toBe('permalink');
+ expect(localFile.rawPath).toBe('raw');
+ expect(localFile.binary).toBeTruthy();
+ expect(localFile.renderError).toBe('render_error');
+ });
+ });
+
+ describe('SET_FILE_RAW_DATA', () => {
+ it('sets raw data', () => {
+ mutations.SET_FILE_RAW_DATA(localState, {
+ file: localFile,
+ raw: 'testing',
+ });
+
+ expect(localFile.raw).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_FILE_CONTENT', () => {
+ beforeEach(() => {
+ localFile.raw = 'test';
+ });
+
+ it('sets content', () => {
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ path: localFile.path,
+ content: 'test',
+ });
+
+ expect(localFile.content).toBe('test');
+ });
+
+ it('sets changed if content does not match raw', () => {
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ path: localFile.path,
+ content: 'testing',
+ });
+
+ expect(localFile.content).toBe('testing');
+ expect(localFile.changed).toBeTruthy();
+ });
+
+ it('sets changed if file is a temp file', () => {
+ localFile.tempFile = true;
+
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ path: localFile.path,
+ content: '',
+ });
+
+ expect(localFile.changed).toBeTruthy();
+ });
+ });
+
+ describe('DISCARD_FILE_CHANGES', () => {
+ beforeEach(() => {
+ localFile.content = 'test';
+ localFile.changed = true;
+ });
+
+ it('resets content and changed', () => {
+ mutations.DISCARD_FILE_CHANGES(localState, localFile.path);
+
+ expect(localFile.content).toBe('');
+ expect(localFile.changed).toBeFalsy();
+ });
+ });
+
+ describe('ADD_FILE_TO_CHANGED', () => {
+ it('adds file into changed files array', () => {
+ mutations.ADD_FILE_TO_CHANGED(localState, localFile.path);
+
+ expect(localState.changedFiles.length).toBe(1);
+ });
+ });
+
+ describe('REMOVE_FILE_FROM_CHANGED', () => {
+ it('removes files from changed files array', () => {
+ localState.changedFiles.push(localFile);
+
+ mutations.REMOVE_FILE_FROM_CHANGED(localState, localFile.path);
+
+ expect(localState.changedFiles.length).toBe(0);
+ });
+ });
+
+ describe('TOGGLE_FILE_CHANGED', () => {
+ it('updates file changed status', () => {
+ mutations.TOGGLE_FILE_CHANGED(localState, {
+ file: localFile,
+ changed: true,
+ });
+
+ expect(localFile.changed).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations/tree_spec.js b/spec/javascripts/ide/stores/mutations/tree_spec.js
new file mode 100644
index 00000000000..e6c085eaff6
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations/tree_spec.js
@@ -0,0 +1,69 @@
+import mutations from '~/ide/stores/mutations/tree';
+import state from '~/ide/stores/state';
+import { file } from '../../helpers';
+
+describe('Multi-file store tree mutations', () => {
+ let localState;
+ let localTree;
+
+ beforeEach(() => {
+ localState = state();
+ localTree = file();
+
+ localState.entries[localTree.path] = localTree;
+ });
+
+ describe('TOGGLE_TREE_OPEN', () => {
+ it('toggles tree open', () => {
+ mutations.TOGGLE_TREE_OPEN(localState, localTree.path);
+
+ expect(localTree.opened).toBeTruthy();
+
+ mutations.TOGGLE_TREE_OPEN(localState, localTree.path);
+
+ expect(localTree.opened).toBeFalsy();
+ });
+ });
+
+ describe('SET_DIRECTORY_DATA', () => {
+ const data = [
+ {
+ name: 'tree',
+ },
+ {
+ name: 'submodule',
+ },
+ {
+ name: 'blob',
+ },
+ ];
+
+ it('adds directory data', () => {
+ localState.trees['project/master'] = {
+ tree: [],
+ };
+
+ mutations.SET_DIRECTORY_DATA(localState, {
+ data,
+ treePath: 'project/master',
+ });
+
+ const tree = localState.trees['project/master'];
+
+ expect(tree.tree.length).toBe(3);
+ expect(tree.tree[0].name).toBe('tree');
+ expect(tree.tree[1].name).toBe('submodule');
+ expect(tree.tree[2].name).toBe('blob');
+ });
+ });
+
+ describe('REMOVE_ALL_CHANGES_FILES', () => {
+ it('removes all files from changedFiles state', () => {
+ localState.changedFiles.push(file('REMOVE_ALL_CHANGES_FILES'));
+
+ mutations.REMOVE_ALL_CHANGES_FILES(localState);
+
+ expect(localState.changedFiles.length).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
new file mode 100644
index 00000000000..38162a470ad
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -0,0 +1,79 @@
+import mutations from '~/ide/stores/mutations';
+import state from '~/ide/stores/state';
+import { file } from '../helpers';
+
+describe('Multi-file store mutations', () => {
+ let localState;
+ let entry;
+
+ beforeEach(() => {
+ localState = state();
+ entry = file();
+
+ localState.entries[entry.path] = entry;
+ });
+
+ describe('SET_INITIAL_DATA', () => {
+ it('sets all initial data', () => {
+ mutations.SET_INITIAL_DATA(localState, {
+ test: 'test',
+ });
+
+ expect(localState.test).toBe('test');
+ });
+ });
+
+ describe('TOGGLE_LOADING', () => {
+ it('toggles loading of entry', () => {
+ mutations.TOGGLE_LOADING(localState, { entry });
+
+ expect(entry.loading).toBeTruthy();
+
+ mutations.TOGGLE_LOADING(localState, { entry });
+
+ expect(entry.loading).toBeFalsy();
+ });
+
+ it('toggles loading of entry and sets specific value', () => {
+ mutations.TOGGLE_LOADING(localState, { entry });
+
+ expect(entry.loading).toBeTruthy();
+
+ mutations.TOGGLE_LOADING(localState, { entry, forceValue: true });
+
+ expect(entry.loading).toBeTruthy();
+ });
+ });
+
+ describe('SET_LEFT_PANEL_COLLAPSED', () => {
+ it('sets left panel collapsed', () => {
+ mutations.SET_LEFT_PANEL_COLLAPSED(localState, true);
+
+ expect(localState.leftPanelCollapsed).toBeTruthy();
+
+ mutations.SET_LEFT_PANEL_COLLAPSED(localState, false);
+
+ expect(localState.leftPanelCollapsed).toBeFalsy();
+ });
+ });
+
+ describe('SET_RIGHT_PANEL_COLLAPSED', () => {
+ it('sets right panel collapsed', () => {
+ mutations.SET_RIGHT_PANEL_COLLAPSED(localState, true);
+
+ expect(localState.rightPanelCollapsed).toBeTruthy();
+
+ mutations.SET_RIGHT_PANEL_COLLAPSED(localState, false);
+
+ expect(localState.rightPanelCollapsed).toBeFalsy();
+ });
+ });
+
+ describe('UPDATE_VIEWER', () => {
+ it('sets viewer state', () => {
+ mutations.UPDATE_VIEWER(localState, 'diff');
+
+ expect(localState.viewer).toBe('diff');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js
new file mode 100644
index 00000000000..f38ac6dd82f
--- /dev/null
+++ b/spec/javascripts/ide/stores/utils_spec.js
@@ -0,0 +1,66 @@
+import * as utils from '~/ide/stores/utils';
+
+describe('Multi-file store utils', () => {
+ describe('setPageTitle', () => {
+ it('sets the document page title', () => {
+ utils.setPageTitle('test');
+
+ expect(document.title).toBe('test');
+ });
+ });
+
+ 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();
+ });
+ });
+});