summaryrefslogtreecommitdiff
path: root/spec/frontend/ide/components
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/ide/components')
-rw-r--r--spec/frontend/ide/components/branches/item_spec.js11
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js136
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_item_spec.js13
-rw-r--r--spec/frontend/ide/components/commit_sidebar/message_field_spec.js170
-rw-r--r--spec/frontend/ide/components/ide_sidebar_nav_spec.js118
-rw-r--r--spec/frontend/ide/components/ide_spec.js9
-rw-r--r--spec/frontend/ide/components/ide_status_list_spec.js16
-rw-r--r--spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap4
-rw-r--r--spec/frontend/ide/components/jobs/detail_spec.js187
-rw-r--r--spec/frontend/ide/components/merge_requests/item_spec.js106
-rw-r--r--spec/frontend/ide/components/new_dropdown/modal_spec.js40
-rw-r--r--spec/frontend/ide/components/new_dropdown/upload_spec.js2
-rw-r--r--spec/frontend/ide/components/panes/collapsible_sidebar_spec.js113
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js57
-rw-r--r--spec/frontend/ide/components/pipelines/list_spec.js2
-rw-r--r--spec/frontend/ide/components/repo_commit_section_spec.js46
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js664
-rw-r--r--spec/frontend/ide/components/repo_tab_spec.js12
-rw-r--r--spec/frontend/ide/components/repo_tabs_spec.js2
-rw-r--r--spec/frontend/ide/components/resizable_panel_spec.js114
-rw-r--r--spec/frontend/ide/components/terminal/empty_state_spec.js107
-rw-r--r--spec/frontend/ide/components/terminal/session_spec.js96
-rw-r--r--spec/frontend/ide/components/terminal/terminal_controls_spec.js65
-rw-r--r--spec/frontend/ide/components/terminal/terminal_spec.js225
-rw-r--r--spec/frontend/ide/components/terminal/view_spec.js91
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js47
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js99
27 files changed, 2367 insertions, 185 deletions
diff --git a/spec/frontend/ide/components/branches/item_spec.js b/spec/frontend/ide/components/branches/item_spec.js
index 138443b715e..d8175025755 100644
--- a/spec/frontend/ide/components/branches/item_spec.js
+++ b/spec/frontend/ide/components/branches/item_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import router from '~/ide/ide_router';
+import { createStore } from '~/ide/stores';
+import { createRouter } from '~/ide/ide_router';
import Item from '~/ide/components/branches/item.vue';
import Icon from '~/vue_shared/components/icon.vue';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -13,6 +14,8 @@ const TEST_PROJECT_ID = projectData.name_with_namespace;
describe('IDE branch item', () => {
let wrapper;
+ let store;
+ let router;
function createComponent(props = {}) {
wrapper = shallowMount(Item, {
@@ -22,9 +25,15 @@ describe('IDE branch item', () => {
isActive: false,
...props,
},
+ router,
});
}
+ beforeEach(() => {
+ store = createStore();
+ router = createRouter(store);
+ });
+
afterEach(() => {
wrapper.destroy();
});
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index 129180bb46e..c62df4a3795 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -5,11 +5,14 @@ import store from '~/ide/stores';
import CommitForm from '~/ide/components/commit_sidebar/form.vue';
import { leftSidebarViews } from '~/ide/constants';
import { resetStore } from '../../helpers';
+import waitForPromises from 'helpers/wait_for_promises';
describe('IDE commit form', () => {
const Component = Vue.extend(CommitForm);
let vm;
+ const beginCommitButton = () => vm.$el.querySelector('[data-testid="begin-commit-button"]');
+
beforeEach(() => {
store.state.changedFiles.push('test');
store.state.currentProjectId = 'abcproject';
@@ -25,8 +28,15 @@ describe('IDE commit form', () => {
resetStore(vm.$store);
});
- it('enables button when has changes', () => {
- expect(vm.$el.querySelector('[disabled]')).toBe(null);
+ it('enables begin commit button when there are changes', () => {
+ expect(beginCommitButton()).not.toHaveAttr('disabled');
+ });
+
+ it('disables begin commit button when there are no changes', async () => {
+ store.state.changedFiles = [];
+ await vm.$nextTick();
+
+ expect(beginCommitButton()).toHaveAttr('disabled');
});
describe('compact', () => {
@@ -37,8 +47,8 @@ describe('IDE commit form', () => {
});
it('renders commit button in compact mode', () => {
- expect(vm.$el.querySelector('.btn-primary')).not.toBeNull();
- expect(vm.$el.querySelector('.btn-primary').textContent).toContain('Commit');
+ expect(beginCommitButton()).not.toBeNull();
+ expect(beginCommitButton().textContent).toContain('Commit');
});
it('does not render form', () => {
@@ -54,7 +64,7 @@ describe('IDE commit form', () => {
});
it('shows form when clicking commit button', () => {
- vm.$el.querySelector('.btn-primary').click();
+ beginCommitButton().click();
return vm.$nextTick(() => {
expect(vm.$el.querySelector('form')).not.toBeNull();
@@ -62,31 +72,117 @@ describe('IDE commit form', () => {
});
it('toggles activity bar view when clicking commit button', () => {
- vm.$el.querySelector('.btn-primary').click();
+ beginCommitButton().click();
return vm.$nextTick(() => {
expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
});
});
- it('collapses if lastCommitMsg is set to empty and current view is not commit view', () => {
+ it('collapses if lastCommitMsg is set to empty and current view is not commit view', async () => {
store.state.lastCommitMsg = 'abc';
store.state.currentActivityView = leftSidebarViews.edit.name;
+ await vm.$nextTick();
- return vm
- .$nextTick()
- .then(() => {
- // if commit message is set, form is uncollapsed
- expect(vm.isCompact).toBe(false);
+ // if commit message is set, form is uncollapsed
+ expect(vm.isCompact).toBe(false);
- store.state.lastCommitMsg = '';
+ store.state.lastCommitMsg = '';
+ await vm.$nextTick();
- return vm.$nextTick();
- })
- .then(() => {
- // collapsed when set to empty
- expect(vm.isCompact).toBe(true);
- });
+ // collapsed when set to empty
+ expect(vm.isCompact).toBe(true);
+ });
+
+ it('collapses if in commit view but there are no changes and vice versa', async () => {
+ store.state.currentActivityView = leftSidebarViews.commit.name;
+ await vm.$nextTick();
+
+ // expanded by default if there are changes
+ expect(vm.isCompact).toBe(false);
+
+ store.state.changedFiles = [];
+ await vm.$nextTick();
+
+ expect(vm.isCompact).toBe(true);
+
+ store.state.changedFiles.push('test');
+ await vm.$nextTick();
+
+ // uncollapsed once again
+ expect(vm.isCompact).toBe(false);
+ });
+
+ it('collapses if switched from commit view to edit view and vice versa', async () => {
+ store.state.currentActivityView = leftSidebarViews.edit.name;
+ await vm.$nextTick();
+
+ expect(vm.isCompact).toBe(true);
+
+ store.state.currentActivityView = leftSidebarViews.commit.name;
+ await vm.$nextTick();
+
+ expect(vm.isCompact).toBe(false);
+
+ store.state.currentActivityView = leftSidebarViews.edit.name;
+ await vm.$nextTick();
+
+ expect(vm.isCompact).toBe(true);
+ });
+
+ describe('when window height is less than MAX_WINDOW_HEIGHT', () => {
+ let oldHeight;
+
+ beforeEach(() => {
+ oldHeight = window.innerHeight;
+ window.innerHeight = 700;
+ });
+
+ afterEach(() => {
+ window.innerHeight = oldHeight;
+ });
+
+ it('stays collapsed when switching from edit view to commit view and back', async () => {
+ store.state.currentActivityView = leftSidebarViews.edit.name;
+ await vm.$nextTick();
+
+ expect(vm.isCompact).toBe(true);
+
+ store.state.currentActivityView = leftSidebarViews.commit.name;
+ await vm.$nextTick();
+
+ expect(vm.isCompact).toBe(true);
+
+ store.state.currentActivityView = leftSidebarViews.edit.name;
+ await vm.$nextTick();
+
+ expect(vm.isCompact).toBe(true);
+ });
+
+ it('stays uncollapsed if changes are added or removed', async () => {
+ store.state.currentActivityView = leftSidebarViews.commit.name;
+ await vm.$nextTick();
+
+ expect(vm.isCompact).toBe(true);
+
+ store.state.changedFiles = [];
+ await vm.$nextTick();
+
+ expect(vm.isCompact).toBe(true);
+
+ store.state.changedFiles.push('test');
+ await vm.$nextTick();
+
+ expect(vm.isCompact).toBe(true);
+ });
+
+ it('uncollapses when clicked on Commit button in the edit view', async () => {
+ store.state.currentActivityView = leftSidebarViews.edit.name;
+ beginCommitButton().click();
+ await waitForPromises();
+
+ expect(vm.isCompact).toBe(false);
+ });
});
});
@@ -118,7 +214,7 @@ describe('IDE commit form', () => {
});
it('always opens itself in full view current activity view is not commit view when clicking commit button', () => {
- vm.$el.querySelector('.btn-primary').click();
+ beginCommitButton().click();
return vm.$nextTick(() => {
expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
diff --git a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
index ebb41448905..7ce628d4da7 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
@@ -1,17 +1,22 @@
import Vue from 'vue';
import { trimText } from 'helpers/text_helper';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
-import router from '~/ide/ide_router';
-import { file, resetStore } from '../../helpers';
+import { createRouter } from '~/ide/ide_router';
+import { file } from '../../helpers';
describe('Multi-file editor commit sidebar list item', () => {
let vm;
let f;
let findPathEl;
+ let store;
+ let router;
beforeEach(() => {
+ store = createStore();
+ router = createRouter(store);
+
const Component = Vue.extend(listItem);
f = file('test-file');
@@ -28,8 +33,6 @@ describe('Multi-file editor commit sidebar list item', () => {
afterEach(() => {
vm.$destroy();
-
- resetStore(store);
});
const findPathText = () => trimText(findPathEl.textContent);
diff --git a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
new file mode 100644
index 00000000000..d6ea8b9a4bd
--- /dev/null
+++ b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
@@ -0,0 +1,170 @@
+import Vue from 'vue';
+import createComponent from 'helpers/vue_mount_component_helper';
+import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue';
+
+describe('IDE commit message field', () => {
+ const Component = Vue.extend(CommitMessageField);
+ let vm;
+
+ beforeEach(() => {
+ setFixtures('<div id="app"></div>');
+
+ vm = createComponent(
+ Component,
+ {
+ text: '',
+ placeholder: 'testing',
+ },
+ '#app',
+ );
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('adds is-focused class on focus', done => {
+ vm.$el.querySelector('textarea').focus();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('removed is-focused class on blur', done => {
+ vm.$el.querySelector('textarea').focus();
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
+
+ vm.$el.querySelector('textarea').blur();
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.$el.querySelector('.is-focused')).toBeNull();
+
+ done();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('emits input event on input', () => {
+ jest.spyOn(vm, '$emit').mockImplementation();
+
+ const textarea = vm.$el.querySelector('textarea');
+ textarea.value = 'testing';
+
+ textarea.dispatchEvent(new Event('input'));
+
+ expect(vm.$emit).toHaveBeenCalledWith('input', 'testing');
+ });
+
+ describe('highlights', () => {
+ describe('subject line', () => {
+ it('does not highlight less than 50 characters', done => {
+ vm.text = 'text less than 50 chars';
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.highlights span').textContent).toContain(
+ 'text less than 50 chars',
+ );
+
+ expect(vm.$el.querySelector('mark').style.display).toBe('none');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('highlights characters over 50 length', done => {
+ vm.text =
+ 'text less than 50 chars that should not highlighted. text more than 50 should be highlighted';
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.highlights span').textContent).toContain(
+ 'text less than 50 chars that should not highlighte',
+ );
+
+ expect(vm.$el.querySelector('mark').style.display).not.toBe('none');
+ expect(vm.$el.querySelector('mark').textContent).toBe(
+ 'd. text more than 50 should be highlighted',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('body text', () => {
+ it('does not highlight body text less tan 72 characters', done => {
+ vm.text = 'subject line\nbody content';
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
+ expect(vm.$el.querySelectorAll('mark')[1].style.display).toBe('none');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('highlights body text more than 72 characters', done => {
+ vm.text =
+ 'subject line\nbody content that will be highlighted when it is more than 72 characters in length';
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
+ expect(vm.$el.querySelectorAll('mark')[1].style.display).not.toBe('none');
+ expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('highlights body text & subject line', done => {
+ vm.text =
+ 'text less than 50 chars that should not highlighted\nbody content that will be highlighted when it is more than 72 characters in length';
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
+ expect(vm.$el.querySelectorAll('mark').length).toBe(2);
+
+ expect(vm.$el.querySelectorAll('mark')[0].textContent).toContain('d');
+ expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('scrolling textarea', () => {
+ it('updates transform of highlights', done => {
+ vm.text = 'subject line\n\n\n\n\n\n\n\n\n\n\nbody content';
+
+ vm.$nextTick()
+ .then(() => {
+ vm.$el.querySelector('textarea').scrollTo(0, 50);
+
+ vm.handleScroll();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.scrollTop).toBe(50);
+ expect(vm.$el.querySelector('.highlights').style.transform).toBe(
+ 'translate3d(0, -50px, 0)',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/ide_sidebar_nav_spec.js b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
new file mode 100644
index 00000000000..49d476b56e4
--- /dev/null
+++ b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
@@ -0,0 +1,118 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue';
+import { SIDE_RIGHT, SIDE_LEFT } from '~/ide/constants';
+
+const TEST_TABS = [
+ {
+ title: 'Lorem',
+ icon: 'angle-up',
+ views: [{ name: 'lorem-1' }, { name: 'lorem-2' }],
+ },
+ {
+ title: 'Ipsum',
+ icon: 'angle-down',
+ views: [{ name: 'ipsum-1' }, { name: 'ipsum-2' }],
+ },
+];
+const TEST_CURRENT_INDEX = 1;
+const TEST_CURRENT_VIEW = TEST_TABS[TEST_CURRENT_INDEX].views[1].name;
+const TEST_OPEN_VIEW = TEST_TABS[TEST_CURRENT_INDEX].views[0];
+
+describe('ide/components/ide_sidebar_nav', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ if (wrapper) {
+ throw new Error('wrapper already exists');
+ }
+
+ wrapper = shallowMount(IdeSidebarNav, {
+ propsData: {
+ tabs: TEST_TABS,
+ currentView: TEST_CURRENT_VIEW,
+ isOpen: false,
+ ...props,
+ },
+ directives: {
+ tooltip: createMockDirective(),
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findButtons = () => wrapper.findAll('li button');
+ const findButtonsData = () =>
+ findButtons().wrappers.map(button => {
+ return {
+ title: button.attributes('title'),
+ ariaLabel: button.attributes('aria-label'),
+ classes: button.classes(),
+ qaSelector: button.attributes('data-qa-selector'),
+ icon: button.find(GlIcon).props('name'),
+ tooltip: getBinding(button.element, 'tooltip').value,
+ };
+ });
+ const clickTab = () =>
+ findButtons()
+ .at(TEST_CURRENT_INDEX)
+ .trigger('click');
+
+ describe.each`
+ isOpen | side | otherSide | classes | classesObj | emitEvent | emitArg
+ ${false} | ${SIDE_LEFT} | ${SIDE_RIGHT} | ${[]} | ${{}} | ${'open'} | ${[TEST_OPEN_VIEW]}
+ ${false} | ${SIDE_RIGHT} | ${SIDE_LEFT} | ${['is-right']} | ${{}} | ${'open'} | ${[TEST_OPEN_VIEW]}
+ ${true} | ${SIDE_RIGHT} | ${SIDE_LEFT} | ${['is-right']} | ${{ [TEST_CURRENT_INDEX]: ['active'] }} | ${'close'} | ${[]}
+ `(
+ 'with side = $side, isOpen = $isOpen',
+ ({ isOpen, side, otherSide, classes, classesObj, emitEvent, emitArg }) => {
+ let bsTooltipHide;
+
+ beforeEach(() => {
+ createComponent({ isOpen, side });
+
+ bsTooltipHide = jest.fn();
+ wrapper.vm.$root.$on('bv::hide::tooltip', bsTooltipHide);
+ });
+
+ it('renders buttons', () => {
+ expect(findButtonsData()).toEqual(
+ TEST_TABS.map((tab, index) => ({
+ title: tab.title,
+ ariaLabel: tab.title,
+ classes: ['ide-sidebar-link', ...classes, ...(classesObj[index] || [])],
+ qaSelector: `${tab.title.toLowerCase()}_tab_button`,
+ icon: tab.icon,
+ tooltip: {
+ container: 'body',
+ placement: otherSide,
+ },
+ })),
+ );
+ });
+
+ it('when tab clicked, emits event', () => {
+ expect(wrapper.emitted()).toEqual({});
+
+ clickTab();
+
+ expect(wrapper.emitted()).toEqual({
+ [emitEvent]: [emitArg],
+ });
+ });
+
+ it('when tab clicked, hides tooltip', () => {
+ expect(bsTooltipHide).not.toHaveBeenCalled();
+
+ clickTab();
+
+ expect(bsTooltipHide).toHaveBeenCalled();
+ });
+ },
+ );
+});
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index 78a280e6304..efc1d984dec 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -1,11 +1,18 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import ide from '~/ide/components/ide.vue';
import { file, resetStore } from '../helpers';
import { projectData } from '../mock_data';
+import extendStore from '~/ide/stores/extend';
+
+let store;
function bootstrap(projData) {
+ store = createStore();
+
+ extendStore(store, document.createElement('div'));
+
const Component = Vue.extend(ide);
store.state.currentProjectId = 'abcproject';
diff --git a/spec/frontend/ide/components/ide_status_list_spec.js b/spec/frontend/ide/components/ide_status_list_spec.js
index 99c27ca30fb..847464ed806 100644
--- a/spec/frontend/ide/components/ide_status_list_spec.js
+++ b/spec/frontend/ide/components/ide_status_list_spec.js
@@ -1,13 +1,14 @@
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import IdeStatusList from '~/ide/components/ide_status_list.vue';
+import TerminalSyncStatusSafe from '~/ide/components/terminal_sync/terminal_sync_status_safe.vue';
const TEST_FILE = {
name: 'lorem.md',
- eol: 'LF',
editorRow: 3,
editorColumn: 23,
fileLanguage: 'markdown',
+ content: 'abc\nndef',
};
const localVue = createLocalVue();
@@ -55,7 +56,8 @@ describe('ide/components/ide_status_list', () => {
});
it('shows file eol', () => {
- expect(wrapper.text()).toContain(TEST_FILE.name);
+ expect(wrapper.text()).not.toContain('CRLF');
+ expect(wrapper.text()).toContain('LF');
});
it('shows file editor position', () => {
@@ -78,13 +80,9 @@ describe('ide/components/ide_status_list', () => {
});
});
- it('adds slot as child of list', () => {
- createComponent({
- slots: {
- default: ['<div class="js-test">Hello</div>', '<div class="js-test">World</div>'],
- },
- });
+ it('renders terminal sync status', () => {
+ createComponent();
- expect(wrapper.find('.ide-status-list').findAll('.js-test').length).toEqual(2);
+ expect(wrapper.find(TerminalSyncStatusSafe).exists()).toBe(true);
});
});
diff --git a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
index db5175c3f7b..bdd3d439fd4 100644
--- a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
+++ b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
@@ -14,7 +14,7 @@ exports[`IDE pipeline stage renders stage details & icon 1`] = `
/>
<strong
- class="prepend-left-8 text-truncate"
+ class="gl-ml-3 text-truncate"
data-container="body"
data-original-title=""
title=""
@@ -25,7 +25,7 @@ exports[`IDE pipeline stage renders stage details & icon 1`] = `
</strong>
<div
- class="append-right-8 prepend-left-4"
+ class="gl-mr-3 gl-ml-2"
>
<span
class="badge badge-pill"
diff --git a/spec/frontend/ide/components/jobs/detail_spec.js b/spec/frontend/ide/components/jobs/detail_spec.js
new file mode 100644
index 00000000000..8f3815d5aab
--- /dev/null
+++ b/spec/frontend/ide/components/jobs/detail_spec.js
@@ -0,0 +1,187 @@
+import Vue from 'vue';
+import JobDetail from '~/ide/components/jobs/detail.vue';
+import { createStore } from '~/ide/stores';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { jobs } from '../../mock_data';
+import { TEST_HOST } from 'helpers/test_constants';
+
+describe('IDE jobs detail view', () => {
+ let vm;
+
+ const createComponent = () => {
+ const store = createStore();
+
+ store.state.pipelines.detailJob = {
+ ...jobs[0],
+ isLoading: true,
+ output: 'testing',
+ rawPath: `${TEST_HOST}/raw`,
+ };
+
+ return createComponentWithStore(Vue.extend(JobDetail), store);
+ };
+
+ beforeEach(() => {
+ vm = createComponent();
+
+ jest.spyOn(vm, 'fetchJobTrace').mockResolvedValue();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('mounted', () => {
+ beforeEach(() => {
+ vm = vm.$mount();
+ });
+
+ it('calls fetchJobTrace', () => {
+ expect(vm.fetchJobTrace).toHaveBeenCalled();
+ });
+
+ it('scrolls to bottom', () => {
+ expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalled();
+ });
+
+ it('renders job output', () => {
+ expect(vm.$el.querySelector('.bash').textContent).toContain('testing');
+ });
+
+ it('renders empty message output', done => {
+ vm.$store.state.pipelines.detailJob.output = '';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.bash').textContent).toContain('No messages were logged');
+
+ done();
+ });
+ });
+
+ it('renders loading icon', () => {
+ expect(vm.$el.querySelector('.build-loader-animation')).not.toBe(null);
+ expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('');
+ });
+
+ it('hides output when loading', () => {
+ expect(vm.$el.querySelector('.bash')).not.toBe(null);
+ expect(vm.$el.querySelector('.bash').style.display).toBe('none');
+ });
+
+ it('hide loading icon when isLoading is false', done => {
+ vm.$store.state.pipelines.detailJob.isLoading = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('none');
+
+ done();
+ });
+ });
+
+ it('resets detailJob when clicking header button', () => {
+ jest.spyOn(vm, 'setDetailJob').mockImplementation();
+
+ vm.$el.querySelector('.btn').click();
+
+ expect(vm.setDetailJob).toHaveBeenCalledWith(null);
+ });
+
+ it('renders raw path link', () => {
+ expect(vm.$el.querySelector('.controllers-buttons').getAttribute('href')).toBe(
+ `${TEST_HOST}/raw`,
+ );
+ });
+ });
+
+ describe('scroll buttons', () => {
+ beforeEach(() => {
+ vm = createComponent();
+ jest.spyOn(vm, 'fetchJobTrace').mockResolvedValue();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it.each`
+ fnName | btnName | scrollPos
+ ${'scrollDown'} | ${'down'} | ${0}
+ ${'scrollUp'} | ${'up'} | ${1}
+ `('triggers $fnName when clicking $btnName button', ({ fnName, scrollPos }) => {
+ jest.spyOn(vm, fnName).mockImplementation();
+
+ vm = vm.$mount();
+
+ vm.scrollPos = scrollPos;
+
+ return vm.$nextTick().then(() => {
+ vm.$el.querySelector('.btn-scroll:not([disabled])').click();
+ expect(vm[fnName]).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('scrollDown', () => {
+ beforeEach(() => {
+ vm = vm.$mount();
+
+ jest.spyOn(vm.$refs.buildTrace, 'scrollTo').mockImplementation();
+ });
+
+ it('scrolls build trace to bottom', () => {
+ jest.spyOn(vm.$refs.buildTrace, 'scrollHeight', 'get').mockReturnValue(1000);
+
+ vm.scrollDown();
+
+ expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 1000);
+ });
+ });
+
+ describe('scrollUp', () => {
+ beforeEach(() => {
+ vm = vm.$mount();
+
+ jest.spyOn(vm.$refs.buildTrace, 'scrollTo').mockImplementation();
+ });
+
+ it('scrolls build trace to top', () => {
+ vm.scrollUp();
+
+ expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 0);
+ });
+ });
+
+ describe('scrollBuildLog', () => {
+ beforeEach(() => {
+ vm = vm.$mount();
+ jest.spyOn(vm.$refs.buildTrace, 'scrollTo').mockImplementation();
+ jest.spyOn(vm.$refs.buildTrace, 'offsetHeight', 'get').mockReturnValue(100);
+ jest.spyOn(vm.$refs.buildTrace, 'scrollHeight', 'get').mockReturnValue(200);
+ });
+
+ it('sets scrollPos to bottom when at the bottom', () => {
+ jest.spyOn(vm.$refs.buildTrace, 'scrollTop', 'get').mockReturnValue(100);
+
+ vm.scrollBuildLog();
+
+ expect(vm.scrollPos).toBe(1);
+ });
+
+ it('sets scrollPos to top when at the top', () => {
+ jest.spyOn(vm.$refs.buildTrace, 'scrollTop', 'get').mockReturnValue(0);
+ vm.scrollPos = 1;
+
+ vm.scrollBuildLog();
+
+ expect(vm.scrollPos).toBe(0);
+ });
+
+ it('resets scrollPos when not at top or bottom', () => {
+ jest.spyOn(vm.$refs.buildTrace, 'scrollTop', 'get').mockReturnValue(10);
+
+ vm.scrollBuildLog();
+
+ expect(vm.scrollPos).toBe('');
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/merge_requests/item_spec.js b/spec/frontend/ide/components/merge_requests/item_spec.js
index 6a2451ad263..b1da89d7a9b 100644
--- a/spec/frontend/ide/components/merge_requests/item_spec.js
+++ b/spec/frontend/ide/components/merge_requests/item_spec.js
@@ -1,63 +1,91 @@
-import Vue from 'vue';
-import router from '~/ide/ide_router';
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { createStore } from '~/ide/stores';
+import { createRouter } from '~/ide/ide_router';
import Item from '~/ide/components/merge_requests/item.vue';
-import mountCompontent from '../../../helpers/vue_mount_component_helper';
+
+const TEST_ITEM = {
+ iid: 1,
+ projectPathWithNamespace: 'gitlab-org/gitlab-ce',
+ title: 'Merge request title',
+};
describe('IDE merge request item', () => {
- const Component = Vue.extend(Item);
- let vm;
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
- beforeEach(() => {
- vm = mountCompontent(Component, {
- item: {
- iid: 1,
- projectPathWithNamespace: 'gitlab-org/gitlab-ce',
- title: 'Merge request title',
+ let wrapper;
+ let store;
+ let router;
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(Item, {
+ propsData: {
+ item: {
+ ...TEST_ITEM,
+ },
+ currentId: `${TEST_ITEM.iid}`,
+ currentProjectId: TEST_ITEM.projectPathWithNamespace,
+ ...props,
},
- currentId: '1',
- currentProjectId: 'gitlab-org/gitlab-ce',
+ localVue,
+ router,
+ store,
});
+ };
+ const findIcon = () => wrapper.find('.ic-mobile-issue-close');
+
+ beforeEach(() => {
+ store = createStore();
+ router = createRouter(store);
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
- it('renders merge requests data', () => {
- expect(vm.$el.textContent).toContain('Merge request title');
- expect(vm.$el.textContent).toContain('gitlab-org/gitlab-ce!1');
- });
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders merge requests data', () => {
+ expect(wrapper.text()).toContain('Merge request title');
+ expect(wrapper.text()).toContain('gitlab-org/gitlab-ce!1');
+ });
- it('renders link with href', () => {
- const expectedHref = router.resolve(
- `/project/${vm.item.projectPathWithNamespace}/merge_requests/${vm.item.iid}`,
- ).href;
+ it('renders link with href', () => {
+ const expectedHref = router.resolve(
+ `/project/${TEST_ITEM.projectPathWithNamespace}/merge_requests/${TEST_ITEM.iid}`,
+ ).href;
- expect(vm.$el.tagName.toLowerCase()).toBe('a');
- expect(vm.$el).toHaveAttr('href', expectedHref);
- });
+ expect(wrapper.element.tagName.toLowerCase()).toBe('a');
+ expect(wrapper.attributes('href')).toBe(expectedHref);
+ });
- it('renders icon if ID matches currentId', () => {
- expect(vm.$el.querySelector('.ic-mobile-issue-close')).not.toBe(null);
+ it('renders icon if ID matches currentId', () => {
+ expect(findIcon().exists()).toBe(true);
+ });
});
- it('does not render icon if ID does not match currentId', done => {
- vm.currentId = '2';
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null);
+ describe('with different currentId', () => {
+ beforeEach(() => {
+ createComponent({ currentId: `${TEST_ITEM.iid + 1}` });
+ });
- done();
+ it('does not render icon', () => {
+ expect(findIcon().exists()).toBe(false);
});
});
- it('does not render icon if project ID does not match', done => {
- vm.currentProjectId = 'test/test';
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null);
+ describe('with different project ID', () => {
+ beforeEach(() => {
+ createComponent({ currentProjectId: 'test/test' });
+ });
- done();
+ it('does not render icon', () => {
+ expect(findIcon().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js
index 62a59a76bf4..da17cc3601e 100644
--- a/spec/frontend/ide/components/new_dropdown/modal_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js
@@ -120,6 +120,46 @@ describe('new file modal component', () => {
});
});
+ describe('createFromTemplate', () => {
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ store.state.entries = {
+ 'test-path/test': {
+ name: 'test',
+ deleted: false,
+ },
+ };
+
+ vm = createComponentWithStore(Component, store).$mount();
+ vm.open('blob');
+
+ jest.spyOn(vm, 'createTempEntry').mockImplementation();
+ });
+
+ it.each`
+ entryName | newFilePath
+ ${''} | ${'.gitignore'}
+ ${'README.md'} | ${'.gitignore'}
+ ${'test-path/test/'} | ${'test-path/test/.gitignore'}
+ ${'test-path/test'} | ${'test-path/.gitignore'}
+ ${'test-path/test/abc.md'} | ${'test-path/test/.gitignore'}
+ `(
+ 'creates a new file with the given template name in appropriate directory for path: $path',
+ ({ entryName, newFilePath }) => {
+ vm.entryName = entryName;
+
+ vm.createFromTemplate({ name: '.gitignore' });
+
+ expect(vm.createTempEntry).toHaveBeenCalledWith({
+ name: newFilePath,
+ type: 'blob',
+ });
+ },
+ );
+ });
+
describe('submitForm', () => {
let store;
diff --git a/spec/frontend/ide/components/new_dropdown/upload_spec.js b/spec/frontend/ide/components/new_dropdown/upload_spec.js
index a418fdeb572..ad27954cd10 100644
--- a/spec/frontend/ide/components/new_dropdown/upload_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/upload_spec.js
@@ -85,7 +85,6 @@ describe('new dropdown upload', () => {
name: textFile.name,
type: 'blob',
content: 'plain text',
- base64: false,
binary: false,
rawPath: '',
});
@@ -103,7 +102,6 @@ describe('new dropdown upload', () => {
name: binaryFile.name,
type: 'blob',
content: binaryTarget.result.split('base64,')[1],
- base64: true,
binary: true,
rawPath: binaryTarget.result,
});
diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
index 3bc89996978..e32abc98aae 100644
--- a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
+++ b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
@@ -2,6 +2,7 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import { createStore } from '~/ide/stores';
import paneModule from '~/ide/stores/modules/pane';
import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
+import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue';
import Vuex from 'vuex';
const localVue = createLocalVue();
@@ -24,19 +25,15 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => {
width,
...props,
},
- slots: {
- 'header-icon': '<div class=".header-icon-slot">SLOT ICON</div>',
- header: '<div class=".header-slot"/>',
- footer: '<div class=".footer-slot"/>',
- },
});
};
- const findTabButton = () => wrapper.find(`[data-qa-selector="${fakeComponentName}_tab_button"]`);
+ const findSidebarNav = () => wrapper.find(IdeSidebarNav);
beforeEach(() => {
store = createStore();
store.registerModule('leftPane', paneModule());
+ jest.spyOn(store, 'dispatch').mockImplementation();
});
afterEach(() => {
@@ -75,92 +72,60 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => {
${'left'}
${'right'}
`('when side=$side', ({ side }) => {
- it('correctly renders side specific attributes', () => {
+ beforeEach(() => {
createComponent({ extensionTabs, side });
- const button = findTabButton();
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.classes()).toContain('multi-file-commit-panel');
- expect(wrapper.classes()).toContain(`ide-${side}-sidebar`);
- expect(wrapper.find('.multi-file-commit-panel-inner')).not.toBe(null);
- expect(wrapper.find(`.ide-${side}-sidebar-${fakeComponentName}`)).not.toBe(null);
- expect(button.attributes('data-placement')).toEqual(side === 'left' ? 'right' : 'left');
- if (side === 'right') {
- // this class is only needed on the right side; there is no 'is-left'
- expect(button.classes()).toContain('is-right');
- } else {
- expect(button.classes()).not.toContain('is-right');
- }
- });
});
- });
-
- describe('when default side', () => {
- let button;
- beforeEach(() => {
- createComponent({ extensionTabs });
-
- button = findTabButton();
+ it('correctly renders side specific attributes', () => {
+ expect(wrapper.classes()).toContain('multi-file-commit-panel');
+ expect(wrapper.classes()).toContain(`ide-${side}-sidebar`);
+ expect(wrapper.find('.multi-file-commit-panel-inner')).not.toBe(null);
+ expect(wrapper.find(`.ide-${side}-sidebar-${fakeComponentName}`)).not.toBe(null);
+ expect(findSidebarNav().props('side')).toBe(side);
});
- it('correctly renders tab-specific classes', () => {
- store.state.rightPane.currentView = fakeComponentName;
-
- return wrapper.vm.$nextTick().then(() => {
- expect(button.classes()).toContain('button-class-1');
- expect(button.classes()).toContain('button-class-2');
- });
+ it('nothing is dispatched', () => {
+ expect(store.dispatch).not.toHaveBeenCalled();
});
- it('can show an open pane tab with an active view', () => {
- store.state.rightPane.isOpen = true;
- store.state.rightPane.currentView = fakeComponentName;
+ it('when sidebar emits open, dispatch open', () => {
+ const view = 'lorem-view';
- return wrapper.vm.$nextTick().then(() => {
- expect(button.classes()).toEqual(expect.arrayContaining(['ide-sidebar-link', 'active']));
- expect(button.attributes('data-original-title')).toEqual(fakeComponentName);
- expect(wrapper.find('.js-tab-view').exists()).toBe(true);
- });
- });
-
- it('does not show a pane which is not open', () => {
- store.state.rightPane.isOpen = false;
- store.state.rightPane.currentView = fakeComponentName;
+ findSidebarNav().vm.$emit('open', view);
- return wrapper.vm.$nextTick().then(() => {
- expect(button.classes()).not.toEqual(
- expect.arrayContaining(['ide-sidebar-link', 'active']),
- );
- expect(wrapper.find('.js-tab-view').exists()).toBe(false);
- });
+ expect(store.dispatch).toHaveBeenCalledWith(`${side}Pane/open`, view);
});
- describe('when button is clicked', () => {
- it('opens view', () => {
- button.trigger('click');
- expect(store.state.rightPane.isOpen).toBeTruthy();
- });
-
- it('toggles open view if tab is currently active', () => {
- button.trigger('click');
- expect(store.state.rightPane.isOpen).toBeTruthy();
+ it('when sidebar emits close, dispatch toggleOpen', () => {
+ findSidebarNav().vm.$emit('close');
- button.trigger('click');
- expect(store.state.rightPane.isOpen).toBeFalsy();
- });
+ expect(store.dispatch).toHaveBeenCalledWith(`${side}Pane/toggleOpen`);
});
+ });
- it('shows header-icon', () => {
- expect(wrapper.find('.header-icon-slot')).not.toBeNull();
+ describe.each`
+ isOpen
+ ${true}
+ ${false}
+ `('when isOpen=$isOpen', ({ isOpen }) => {
+ beforeEach(() => {
+ store.state.rightPane.isOpen = isOpen;
+ store.state.rightPane.currentView = fakeComponentName;
+
+ createComponent({ extensionTabs });
});
- it('shows header', () => {
- expect(wrapper.find('.header-slot')).not.toBeNull();
+ it(`tab view is shown=${isOpen}`, () => {
+ expect(wrapper.find('.js-tab-view').exists()).toBe(isOpen);
});
- it('shows footer', () => {
- expect(wrapper.find('.footer-slot')).not.toBeNull();
+ it('renders sidebar nav', () => {
+ expect(findSidebarNav().props()).toEqual({
+ tabs: extensionTabs,
+ side: 'right',
+ currentView: fakeComponentName,
+ isOpen,
+ });
});
});
});
diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js
index 84b2d440b60..203d35ed335 100644
--- a/spec/frontend/ide/components/panes/right_spec.js
+++ b/spec/frontend/ide/components/panes/right_spec.js
@@ -5,6 +5,7 @@ import { createStore } from '~/ide/stores';
import RightPane from '~/ide/components/panes/right.vue';
import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
import { rightSidebarViews } from '~/ide/constants';
+import extendStore from '~/ide/stores/extend';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -14,6 +15,8 @@ describe('ide/components/panes/right.vue', () => {
let store;
const createComponent = props => {
+ extendStore(store, document.createElement('div'));
+
wrapper = shallowMount(RightPane, {
localVue,
store,
@@ -32,26 +35,6 @@ describe('ide/components/panes/right.vue', () => {
wrapper = null;
});
- it('allows tabs to be added via extensionTabs prop', () => {
- createComponent({
- extensionTabs: [
- {
- show: true,
- title: 'FakeTab',
- },
- ],
- });
-
- expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- show: true,
- title: 'FakeTab',
- }),
- ]),
- );
- });
-
describe('pipelines tab', () => {
it('is always shown', () => {
createComponent();
@@ -99,4 +82,38 @@ describe('ide/components/panes/right.vue', () => {
);
});
});
+
+ describe('terminal tab', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('adds terminal tab', () => {
+ store.state.terminal.isVisible = true;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ show: true,
+ title: 'Terminal',
+ }),
+ ]),
+ );
+ });
+ });
+
+ it('hides terminal tab when not visible', () => {
+ store.state.terminal.isVisible = false;
+
+ expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ show: false,
+ title: 'Terminal',
+ }),
+ ]),
+ );
+ });
+ });
});
diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js
index d909a5e478e..795ded35d20 100644
--- a/spec/frontend/ide/components/pipelines/list_spec.js
+++ b/spec/frontend/ide/components/pipelines/list_spec.js
@@ -6,7 +6,7 @@ import List from '~/ide/components/pipelines/list.vue';
import JobsList from '~/ide/components/jobs/list.vue';
import Tab from '~/vue_shared/components/tabs/tab.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import { pipelines } from '../../../../javascripts/ide/mock_data';
+import { pipelines } from 'jest/ide/mock_data';
import IDEServices from '~/ide/services';
const localVue = createLocalVue();
diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js
index 237be018807..3b837622720 100644
--- a/spec/frontend/ide/components/repo_commit_section_spec.js
+++ b/spec/frontend/ide/components/repo_commit_section_spec.js
@@ -1,7 +1,8 @@
import { mount } from '@vue/test-utils';
import { createStore } from '~/ide/stores';
-import router from '~/ide/ide_router';
+import { createRouter } from '~/ide/ide_router';
import RepoCommitSection from '~/ide/components/repo_commit_section.vue';
+import EmptyState from '~/ide/components/commit_sidebar/empty_state.vue';
import { stageKeys } from '~/ide/constants';
import { file } from '../helpers';
@@ -9,6 +10,7 @@ const TEST_NO_CHANGES_SVG = 'nochangessvg';
describe('RepoCommitSection', () => {
let wrapper;
+ let router;
let store;
function createComponent() {
@@ -54,6 +56,7 @@ describe('RepoCommitSection', () => {
beforeEach(() => {
store = createStore();
+ router = createRouter(store);
jest.spyOn(store, 'dispatch');
jest.spyOn(router, 'push').mockImplementation();
@@ -63,7 +66,7 @@ describe('RepoCommitSection', () => {
wrapper.destroy();
});
- describe('empty Stage', () => {
+ describe('empty state', () => {
beforeEach(() => {
store.state.noChangesStateSvgPath = TEST_NO_CHANGES_SVG;
store.state.committedStateSvgPath = 'svg';
@@ -74,11 +77,16 @@ describe('RepoCommitSection', () => {
it('renders no changes text', () => {
expect(
wrapper
- .find('.js-empty-state')
+ .find(EmptyState)
.text()
.trim(),
).toContain('No changes');
- expect(wrapper.find('.js-empty-state img').attributes('src')).toBe(TEST_NO_CHANGES_SVG);
+ expect(
+ wrapper
+ .find(EmptyState)
+ .find('img')
+ .attributes('src'),
+ ).toBe(TEST_NO_CHANGES_SVG);
});
});
@@ -109,6 +117,32 @@ describe('RepoCommitSection', () => {
expect(changedFileNames).toEqual(allFiles.map(x => x.path));
});
+
+ it('does not show empty state', () => {
+ expect(wrapper.find(EmptyState).exists()).toBe(false);
+ });
+ });
+
+ describe('if nothing is changed or staged', () => {
+ beforeEach(() => {
+ setupDefaultState();
+
+ store.state.openFiles = [...Object.values(store.state.entries)];
+ store.state.openFiles[0].active = true;
+ store.state.stagedFiles = [];
+
+ createComponent();
+ });
+
+ it('opens currently active file', () => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].pending).toBe(true);
+
+ expect(store.dispatch).toHaveBeenCalledWith('openPendingTab', {
+ file: store.state.entries[store.getters.activeFile.path],
+ keyPrefix: stageKeys.unstaged,
+ });
+ });
});
describe('with unstaged file', () => {
@@ -129,5 +163,9 @@ describe('RepoCommitSection', () => {
keyPrefix: stageKeys.unstaged,
});
});
+
+ it('does not show empty state', () => {
+ expect(wrapper.find(EmptyState).exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
new file mode 100644
index 00000000000..4967434dfd7
--- /dev/null
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -0,0 +1,664 @@
+import Vuex from 'vuex';
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import '~/behaviors/markdown/render_gfm';
+import { Range } from 'monaco-editor';
+import axios from '~/lib/utils/axios_utils';
+import { createStoreOptions } from '~/ide/stores';
+import RepoEditor from '~/ide/components/repo_editor.vue';
+import Editor from '~/ide/lib/editor';
+import { leftSidebarViews, FILE_VIEW_MODE_EDITOR, FILE_VIEW_MODE_PREVIEW } from '~/ide/constants';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { file } from '../helpers';
+import { exampleConfigs, exampleFiles } from '../lib/editorconfig/mock_data';
+
+describe('RepoEditor', () => {
+ let vm;
+ let store;
+ let mockActions;
+
+ const waitForEditorSetup = () =>
+ new Promise(resolve => {
+ vm.$once('editorSetup', resolve);
+ });
+
+ const createComponent = () => {
+ if (vm) {
+ throw new Error('vm already exists');
+ }
+ vm = createComponentWithStore(Vue.extend(RepoEditor), store, {
+ file: store.state.openFiles[0],
+ });
+ vm.$mount();
+ };
+
+ const createOpenFile = path => {
+ const origFile = store.state.openFiles[0];
+ const newFile = { ...origFile, path, key: path };
+
+ store.state.entries[path] = newFile;
+
+ store.state.openFiles = [newFile];
+ };
+
+ beforeEach(() => {
+ mockActions = {
+ getFileData: jest.fn().mockResolvedValue(),
+ getRawFileData: jest.fn().mockResolvedValue(),
+ };
+
+ const f = {
+ ...file(),
+ viewMode: FILE_VIEW_MODE_EDITOR,
+ };
+
+ const storeOptions = createStoreOptions();
+ storeOptions.actions = {
+ ...storeOptions.actions,
+ ...mockActions,
+ };
+ store = new Vuex.Store(storeOptions);
+
+ f.active = true;
+ f.tempFile = true;
+
+ store.state.openFiles.push(f);
+ store.state.projects = {
+ 'gitlab-org/gitlab': {
+ branches: {
+ master: {
+ name: 'master',
+ commit: {
+ id: 'abcdefgh',
+ },
+ },
+ },
+ },
+ };
+ store.state.currentProjectId = 'gitlab-org/gitlab';
+ store.state.currentBranchId = 'master';
+
+ Vue.set(store.state.entries, f.path, f);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ vm = null;
+
+ Editor.editorInstance.dispose();
+ });
+
+ const findEditor = () => vm.$el.querySelector('.multi-file-editor-holder');
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+
+ return waitForEditorSetup();
+ });
+
+ it('sets renderWhitespace to `all`', () => {
+ vm.$store.state.renderWhitespaceInCode = true;
+
+ expect(vm.editorOptions.renderWhitespace).toEqual('all');
+ });
+
+ it('sets renderWhitespace to `none`', () => {
+ vm.$store.state.renderWhitespaceInCode = false;
+
+ expect(vm.editorOptions.renderWhitespace).toEqual('none');
+ });
+
+ it('renders an ide container', () => {
+ expect(vm.shouldHideEditor).toBeFalsy();
+ expect(vm.showEditor).toBe(true);
+ expect(findEditor()).not.toHaveCss({ display: 'none' });
+ });
+
+ it('renders only an edit tab', done => {
+ Vue.nextTick(() => {
+ const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li');
+
+ expect(tabs.length).toBe(1);
+ expect(tabs[0].textContent.trim()).toBe('Edit');
+
+ done();
+ });
+ });
+
+ describe('when file is markdown', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onPost(/(.*)\/preview_markdown/).reply(200, {
+ body: '<p>testing 123</p>',
+ });
+
+ Vue.set(vm, 'file', {
+ ...vm.file,
+ projectId: 'namespace/project',
+ path: 'sample.md',
+ content: 'testing 123',
+ });
+
+ vm.$store.state.entries[vm.file.path] = vm.file;
+
+ return vm.$nextTick();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('renders an Edit and a Preview Tab', done => {
+ Vue.nextTick(() => {
+ const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li');
+
+ expect(tabs.length).toBe(2);
+ expect(tabs[0].textContent.trim()).toBe('Edit');
+ expect(tabs[1].textContent.trim()).toBe('Preview Markdown');
+
+ done();
+ });
+ });
+
+ it('renders markdown for tempFile', done => {
+ vm.file.tempFile = true;
+
+ vm.$nextTick()
+ .then(() => {
+ vm.$el.querySelectorAll('.ide-mode-tabs .nav-links a')[1].click();
+ })
+ .then(waitForPromises)
+ .then(() => {
+ expect(vm.$el.querySelector('.preview-container').innerHTML).toContain(
+ '<p>testing 123</p>',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('when not in edit mode', () => {
+ beforeEach(async () => {
+ await vm.$nextTick();
+
+ vm.$store.state.currentActivityView = leftSidebarViews.review.name;
+
+ return vm.$nextTick();
+ });
+
+ it('shows no tabs', () => {
+ expect(vm.$el.querySelectorAll('.ide-mode-tabs .nav-links a')).toHaveLength(0);
+ });
+ });
+ });
+
+ 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();
+ });
+ });
+
+ describe('createEditorInstance', () => {
+ it('calls createInstance when viewer is editor', done => {
+ jest.spyOn(vm.editor, 'createInstance').mockImplementation();
+
+ vm.createEditorInstance();
+
+ vm.$nextTick(() => {
+ expect(vm.editor.createInstance).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('calls createDiffInstance when viewer is diff', done => {
+ vm.$store.state.viewer = 'diff';
+
+ jest.spyOn(vm.editor, 'createDiffInstance').mockImplementation();
+
+ vm.createEditorInstance();
+
+ vm.$nextTick(() => {
+ expect(vm.editor.createDiffInstance).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('calls createDiffInstance when viewer is a merge request diff', done => {
+ vm.$store.state.viewer = 'mrdiff';
+
+ jest.spyOn(vm.editor, 'createDiffInstance').mockImplementation();
+
+ vm.createEditorInstance();
+
+ vm.$nextTick(() => {
+ expect(vm.editor.createDiffInstance).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+
+ describe('setupEditor', () => {
+ it('creates new model', () => {
+ jest.spyOn(vm.editor, 'createModel');
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, null);
+ expect(vm.model).not.toBeNull();
+ });
+
+ it('attaches model to editor', () => {
+ jest.spyOn(vm.editor, 'attachModel');
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.attachModel).toHaveBeenCalledWith(vm.model);
+ });
+
+ it('attaches model to merge request editor', () => {
+ vm.$store.state.viewer = 'mrdiff';
+ vm.file.mrChange = true;
+ jest.spyOn(vm.editor, 'attachMergeRequestModel').mockImplementation();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.attachMergeRequestModel).toHaveBeenCalledWith(vm.model);
+ });
+
+ it('does not attach model to merge request editor when not a MR change', () => {
+ vm.$store.state.viewer = 'mrdiff';
+ vm.file.mrChange = false;
+ jest.spyOn(vm.editor, 'attachMergeRequestModel').mockImplementation();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.attachMergeRequestModel).not.toHaveBeenCalledWith(vm.model);
+ });
+
+ it('adds callback methods', () => {
+ jest.spyOn(vm.editor, 'onPositionChange');
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.onPositionChange).toHaveBeenCalled();
+ expect(vm.model.events.size).toBe(2);
+ });
+
+ it('updates state with the value of the model', () => {
+ vm.model.setValue('testing 1234\n');
+
+ vm.setupEditor();
+
+ expect(vm.file.content).toBe('testing 1234\n');
+ });
+
+ it('sets head model as staged file', () => {
+ jest.spyOn(vm.editor, 'createModel');
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.$store.state.stagedFiles.push({ ...vm.file, key: 'staged' });
+ vm.file.staged = true;
+ vm.file.key = `unstaged-${vm.file.key}`;
+
+ vm.setupEditor();
+
+ expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, vm.$store.state.stagedFiles[0]);
+ });
+ });
+
+ describe('editor updateDimensions', () => {
+ beforeEach(() => {
+ jest.spyOn(vm.editor, 'updateDimensions');
+ jest.spyOn(vm.editor, 'updateDiffView').mockImplementation();
+ });
+
+ it('calls updateDimensions when panelResizing is false', done => {
+ vm.$store.state.panelResizing = true;
+
+ vm.$nextTick()
+ .then(() => {
+ vm.$store.state.panelResizing = false;
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.editor.updateDimensions).toHaveBeenCalled();
+ expect(vm.editor.updateDiffView).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not call updateDimensions when panelResizing is true', done => {
+ vm.$store.state.panelResizing = true;
+
+ vm.$nextTick(() => {
+ expect(vm.editor.updateDimensions).not.toHaveBeenCalled();
+ expect(vm.editor.updateDiffView).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('calls updateDimensions when rightPane is opened', done => {
+ vm.$store.state.rightPane.isOpen = true;
+
+ vm.$nextTick(() => {
+ expect(vm.editor.updateDimensions).toHaveBeenCalled();
+ expect(vm.editor.updateDiffView).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+
+ describe('show tabs', () => {
+ it('shows tabs in edit mode', () => {
+ expect(vm.$el.querySelector('.nav-links')).not.toBe(null);
+ });
+
+ it('hides tabs in review mode', done => {
+ vm.$store.state.currentActivityView = leftSidebarViews.review.name;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.nav-links')).toBe(null);
+
+ done();
+ });
+ });
+
+ it('hides tabs in commit mode', done => {
+ vm.$store.state.currentActivityView = leftSidebarViews.commit.name;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.nav-links')).toBe(null);
+
+ done();
+ });
+ });
+ });
+
+ describe('when files view mode is preview', () => {
+ beforeEach(done => {
+ jest.spyOn(vm.editor, 'updateDimensions').mockImplementation();
+ vm.file.viewMode = FILE_VIEW_MODE_PREVIEW;
+ vm.$nextTick(done);
+ });
+
+ it('should hide editor', () => {
+ expect(vm.showEditor).toBe(false);
+ expect(findEditor()).toHaveCss({ display: 'none' });
+ });
+
+ describe('when file view mode changes to editor', () => {
+ it('should update dimensions', () => {
+ vm.file.viewMode = FILE_VIEW_MODE_EDITOR;
+
+ return vm.$nextTick().then(() => {
+ expect(vm.editor.updateDimensions).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+
+ describe('initEditor', () => {
+ beforeEach(() => {
+ vm.file.tempFile = false;
+ jest.spyOn(vm.editor, 'createInstance').mockImplementation();
+ jest.spyOn(vm, 'shouldHideEditor', 'get').mockReturnValue(true);
+ });
+
+ it('does not fetch file information for temp entries', done => {
+ vm.file.tempFile = true;
+
+ vm.initEditor();
+ vm.$nextTick()
+ .then(() => {
+ expect(mockActions.getFileData).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('is being initialised for files without content even if shouldHideEditor is `true`', done => {
+ vm.file.content = '';
+ vm.file.raw = '';
+
+ vm.initEditor();
+ vm.$nextTick()
+ .then(() => {
+ expect(mockActions.getFileData).toHaveBeenCalled();
+ expect(mockActions.getRawFileData).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not initialize editor for files already with content', done => {
+ vm.file.content = 'foo';
+
+ vm.initEditor();
+ vm.$nextTick()
+ .then(() => {
+ expect(mockActions.getFileData).not.toHaveBeenCalled();
+ expect(mockActions.getRawFileData).not.toHaveBeenCalled();
+ expect(vm.editor.createInstance).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('updates on file changes', () => {
+ beforeEach(() => {
+ jest.spyOn(vm, 'initEditor').mockImplementation();
+ });
+
+ it('calls removePendingTab when old file is pending', done => {
+ jest.spyOn(vm, 'shouldHideEditor', 'get').mockReturnValue(true);
+ jest.spyOn(vm, 'removePendingTab').mockImplementation();
+
+ vm.file.pending = true;
+
+ vm.$nextTick()
+ .then(() => {
+ vm.file = file('testing');
+ vm.file.content = 'foo'; // need to prevent full cycle of initEditor
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.removePendingTab).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not call initEditor if the file did not change', done => {
+ Vue.set(vm, 'file', vm.file);
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.initEditor).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls initEditor when file key is changed', done => {
+ expect(vm.initEditor).not.toHaveBeenCalled();
+
+ Vue.set(vm, 'file', {
+ ...vm.file,
+ key: 'new',
+ });
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.initEditor).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('onPaste', () => {
+ const setFileName = name => {
+ Vue.set(vm, 'file', {
+ ...vm.file,
+ content: 'hello world\n',
+ name,
+ path: `foo/${name}`,
+ key: 'new',
+ });
+
+ vm.$store.state.entries[vm.file.path] = vm.file;
+ };
+
+ const pasteImage = () => {
+ window.dispatchEvent(
+ Object.assign(new Event('paste'), {
+ clipboardData: {
+ files: [new File(['foo'], 'foo.png', { type: 'image/png' })],
+ },
+ }),
+ );
+ };
+
+ const watchState = watched =>
+ new Promise(resolve => {
+ const unwatch = vm.$store.watch(watched, () => {
+ unwatch();
+ resolve();
+ });
+ });
+
+ beforeEach(() => {
+ setFileName('bar.md');
+
+ vm.$store.state.trees['gitlab-org/gitlab'] = { tree: [] };
+ vm.$store.state.currentProjectId = 'gitlab-org';
+ vm.$store.state.currentBranchId = 'gitlab';
+
+ // create a new model each time, otherwise tests conflict with each other
+ // because of same model being used in multiple tests
+ Editor.editorInstance.modelManager.dispose();
+ vm.setupEditor();
+
+ return waitForPromises().then(() => {
+ // set cursor to line 2, column 1
+ vm.editor.instance.setSelection(new Range(2, 1, 2, 1));
+ vm.editor.instance.focus();
+ });
+ });
+
+ it('adds an image entry to the same folder for a pasted image in a markdown file', () => {
+ pasteImage();
+
+ return waitForPromises().then(() => {
+ expect(vm.$store.state.entries['foo/foo.png']).toMatchObject({
+ path: 'foo/foo.png',
+ type: 'blob',
+ content: 'Zm9v',
+ binary: true,
+ rawPath: 'data:image/png;base64,Zm9v',
+ });
+ });
+ });
+
+ it("adds a markdown image tag to the file's contents", () => {
+ pasteImage();
+
+ // Pasting an image does a lot of things like using the FileReader API,
+ // so, waitForPromises isn't very reliable (and causes a flaky spec)
+ // Read more about state.watch: https://vuex.vuejs.org/api/#watch
+ return watchState(s => s.entries['foo/bar.md'].content).then(() => {
+ expect(vm.file.content).toBe('hello world\n![foo.png](./foo.png)');
+ });
+ });
+
+ it("does not add file to state or set markdown image syntax if the file isn't markdown", () => {
+ setFileName('myfile.txt');
+ pasteImage();
+
+ return waitForPromises().then(() => {
+ expect(vm.$store.state.entries['foo/foo.png']).toBeUndefined();
+ expect(vm.file.content).toBe('hello world\n');
+ });
+ });
+ });
+ });
+
+ describe('fetchEditorconfigRules', () => {
+ beforeEach(() => {
+ exampleConfigs.forEach(({ path, content }) => {
+ store.state.entries[path] = { ...file(), path, content };
+ });
+ });
+
+ it.each(exampleFiles)(
+ 'does not fetch content from remote for .editorconfig files present locally (case %#)',
+ ({ path, monacoRules }) => {
+ createOpenFile(path);
+ createComponent();
+
+ return waitForEditorSetup().then(() => {
+ expect(vm.rules).toEqual(monacoRules);
+ expect(vm.model.options).toMatchObject(monacoRules);
+ expect(mockActions.getFileData).not.toHaveBeenCalled();
+ expect(mockActions.getRawFileData).not.toHaveBeenCalled();
+ });
+ },
+ );
+
+ it('fetches content from remote for .editorconfig files not available locally', () => {
+ exampleConfigs.forEach(({ path }) => {
+ delete store.state.entries[path].content;
+ delete store.state.entries[path].raw;
+ });
+
+ // Include a "test" directory which does not exist in store. This one should be skipped.
+ createOpenFile('foo/bar/baz/test/my_spec.js');
+ createComponent();
+
+ return waitForEditorSetup().then(() => {
+ expect(mockActions.getFileData.mock.calls.map(([, args]) => args)).toEqual([
+ { makeFileActive: false, path: 'foo/bar/baz/.editorconfig' },
+ { makeFileActive: false, path: 'foo/bar/.editorconfig' },
+ { makeFileActive: false, path: 'foo/.editorconfig' },
+ { makeFileActive: false, path: '.editorconfig' },
+ ]);
+ expect(mockActions.getRawFileData.mock.calls.map(([, args]) => args)).toEqual([
+ { path: 'foo/bar/baz/.editorconfig' },
+ { path: 'foo/bar/.editorconfig' },
+ { path: 'foo/.editorconfig' },
+ { path: '.editorconfig' },
+ ]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js
index 82ea73ffbb1..5a591d3dcd0 100644
--- a/spec/frontend/ide/components/repo_tab_spec.js
+++ b/spec/frontend/ide/components/repo_tab_spec.js
@@ -1,11 +1,13 @@
import Vue from 'vue';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import repoTab from '~/ide/components/repo_tab.vue';
-import router from '~/ide/ide_router';
-import { file, resetStore } from '../helpers';
+import { createRouter } from '~/ide/ide_router';
+import { file } from '../helpers';
describe('RepoTab', () => {
let vm;
+ let store;
+ let router;
function createComponent(propsData) {
const RepoTab = Vue.extend(repoTab);
@@ -17,13 +19,13 @@ describe('RepoTab', () => {
}
beforeEach(() => {
+ store = createStore();
+ router = createRouter(store);
jest.spyOn(router, 'push').mockImplementation(() => {});
});
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
it('renders a close link and a name link', () => {
diff --git a/spec/frontend/ide/components/repo_tabs_spec.js b/spec/frontend/ide/components/repo_tabs_spec.js
index 583f71e6121..df5b01770f5 100644
--- a/spec/frontend/ide/components/repo_tabs_spec.js
+++ b/spec/frontend/ide/components/repo_tabs_spec.js
@@ -16,9 +16,7 @@ describe('RepoTabs', () => {
vm = createComponent(RepoTabs, {
files: openedFiles,
viewer: 'editor',
- hasChanges: false,
activeFile: file('activeFile'),
- hasMergeRequest: false,
});
openedFiles[0].active = true;
diff --git a/spec/frontend/ide/components/resizable_panel_spec.js b/spec/frontend/ide/components/resizable_panel_spec.js
new file mode 100644
index 00000000000..7368de0cee7
--- /dev/null
+++ b/spec/frontend/ide/components/resizable_panel_spec.js
@@ -0,0 +1,114 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import ResizablePanel from '~/ide/components/resizable_panel.vue';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
+import { SIDE_LEFT, SIDE_RIGHT } from '~/ide/constants';
+
+const TEST_WIDTH = 500;
+const TEST_MIN_WIDTH = 400;
+
+describe('~/ide/components/resizable_panel', () => {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ let wrapper;
+ let store;
+
+ beforeEach(() => {
+ store = new Vuex.Store({});
+ jest.spyOn(store, 'dispatch').mockImplementation();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(ResizablePanel, {
+ propsData: {
+ initialWidth: TEST_WIDTH,
+ minSize: TEST_MIN_WIDTH,
+ side: SIDE_LEFT,
+ ...props,
+ },
+ store,
+ localVue,
+ });
+ };
+ const findResizer = () => wrapper.find(PanelResizer);
+ const findInlineStyle = () => wrapper.element.style.cssText;
+ const createInlineStyle = width => `width: ${width}px;`;
+
+ describe.each`
+ props | showResizer | resizerSide | expectedStyle
+ ${{ resizable: true, side: SIDE_LEFT }} | ${true} | ${SIDE_RIGHT} | ${createInlineStyle(TEST_WIDTH)}
+ ${{ resizable: true, side: SIDE_RIGHT }} | ${true} | ${SIDE_LEFT} | ${createInlineStyle(TEST_WIDTH)}
+ ${{ resizable: false, side: SIDE_LEFT }} | ${false} | ${SIDE_RIGHT} | ${''}
+ `('with props $props', ({ props, showResizer, resizerSide, expectedStyle }) => {
+ beforeEach(() => {
+ createComponent(props);
+ });
+
+ it(`show resizer is ${showResizer}`, () => {
+ const expectedDisplay = showResizer ? '' : 'none';
+ const resizer = findResizer();
+
+ expect(resizer.exists()).toBe(true);
+ expect(resizer.element.style.display).toBe(expectedDisplay);
+ });
+
+ it(`resizer side is '${resizerSide}'`, () => {
+ const resizer = findResizer();
+
+ expect(resizer.props('side')).toBe(resizerSide);
+ });
+
+ it(`has style '${expectedStyle}'`, () => {
+ expect(findInlineStyle()).toBe(expectedStyle);
+ });
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not dispatch anything', () => {
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+
+ it.each`
+ event | dispatchArgs
+ ${'resize-start'} | ${['setResizingStatus', true]}
+ ${'resize-end'} | ${['setResizingStatus', false]}
+ `('when resizer emits $event, dispatch $dispatchArgs', ({ event, dispatchArgs }) => {
+ const resizer = findResizer();
+
+ resizer.vm.$emit(event);
+
+ expect(store.dispatch).toHaveBeenCalledWith(...dispatchArgs);
+ });
+
+ it('renders resizer', () => {
+ const resizer = findResizer();
+
+ expect(resizer.props()).toMatchObject({
+ maxSize: window.innerWidth / 2,
+ minSize: TEST_MIN_WIDTH,
+ startSize: TEST_WIDTH,
+ });
+ });
+
+ it('when resizer emits update:size, changes inline width', () => {
+ const newSize = TEST_WIDTH - 100;
+ const resizer = findResizer();
+
+ resizer.vm.$emit('update:size', newSize);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findInlineStyle()).toBe(createInlineStyle(newSize));
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/terminal/empty_state_spec.js b/spec/frontend/ide/components/terminal/empty_state_spec.js
new file mode 100644
index 00000000000..a3f2089608d
--- /dev/null
+++ b/spec/frontend/ide/components/terminal/empty_state_spec.js
@@ -0,0 +1,107 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { TEST_HOST } from 'spec/test_constants';
+import TerminalEmptyState from '~/ide/components/terminal/empty_state.vue';
+
+const TEST_HELP_PATH = `${TEST_HOST}/help/test`;
+const TEST_PATH = `${TEST_HOST}/home.png`;
+const TEST_HTML_MESSAGE = 'lorem <strong>ipsum</strong>';
+
+describe('IDE TerminalEmptyState', () => {
+ let wrapper;
+
+ const factory = (options = {}) => {
+ wrapper = shallowMount(TerminalEmptyState, {
+ ...options,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('does not show illustration, if no path specified', () => {
+ factory();
+
+ expect(wrapper.find('.svg-content').exists()).toBe(false);
+ });
+
+ it('shows illustration with path', () => {
+ factory({
+ propsData: {
+ illustrationPath: TEST_PATH,
+ },
+ });
+
+ const img = wrapper.find('.svg-content img');
+
+ expect(img.exists()).toBe(true);
+ expect(img.attributes('src')).toEqual(TEST_PATH);
+ });
+
+ it('when loading, shows loading icon', () => {
+ factory({
+ propsData: {
+ isLoading: true,
+ },
+ });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('when not loading, does not show loading icon', () => {
+ factory({
+ propsData: {
+ isLoading: false,
+ },
+ });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ });
+
+ describe('when valid', () => {
+ let button;
+
+ beforeEach(() => {
+ factory({
+ propsData: {
+ isLoading: false,
+ isValid: true,
+ helpPath: TEST_HELP_PATH,
+ },
+ });
+
+ button = wrapper.find('button');
+ });
+
+ it('shows button', () => {
+ expect(button.text()).toEqual('Start Web Terminal');
+ expect(button.attributes('disabled')).toBeFalsy();
+ });
+
+ it('emits start when button is clicked', () => {
+ expect(wrapper.emitted().start).toBeFalsy();
+
+ button.trigger('click');
+
+ expect(wrapper.emitted().start).toHaveLength(1);
+ });
+
+ it('shows help path link', () => {
+ expect(wrapper.find('a').attributes('href')).toEqual(TEST_HELP_PATH);
+ });
+ });
+
+ it('when not valid, shows disabled button and message', () => {
+ factory({
+ propsData: {
+ isLoading: false,
+ isValid: false,
+ message: TEST_HTML_MESSAGE,
+ },
+ });
+
+ expect(wrapper.find('button').attributes('disabled')).not.toBe(null);
+ expect(wrapper.find('.bs-callout').element.innerHTML).toEqual(TEST_HTML_MESSAGE);
+ });
+});
diff --git a/spec/frontend/ide/components/terminal/session_spec.js b/spec/frontend/ide/components/terminal/session_spec.js
new file mode 100644
index 00000000000..2399446ed15
--- /dev/null
+++ b/spec/frontend/ide/components/terminal/session_spec.js
@@ -0,0 +1,96 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import TerminalSession from '~/ide/components/terminal/session.vue';
+import Terminal from '~/ide/components/terminal/terminal.vue';
+import {
+ STARTING,
+ PENDING,
+ RUNNING,
+ STOPPING,
+ STOPPED,
+} from '~/ide/stores/modules/terminal/constants';
+
+const TEST_TERMINAL_PATH = 'terminal/path';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('IDE TerminalSession', () => {
+ let wrapper;
+ let actions;
+ let state;
+
+ const factory = (options = {}) => {
+ const store = new Vuex.Store({
+ modules: {
+ terminal: {
+ namespaced: true,
+ actions,
+ state,
+ },
+ },
+ });
+
+ wrapper = shallowMount(TerminalSession, {
+ localVue,
+ store,
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ state = {
+ session: { status: RUNNING, terminalPath: TEST_TERMINAL_PATH },
+ };
+ actions = {
+ restartSession: jest.fn(),
+ stopSession: jest.fn(),
+ };
+ });
+
+ it('is empty if session is falsey', () => {
+ state.session = null;
+ factory();
+
+ expect(wrapper.isEmpty()).toBe(true);
+ });
+
+ it('shows terminal', () => {
+ factory();
+
+ expect(wrapper.find(Terminal).props()).toEqual({
+ terminalPath: TEST_TERMINAL_PATH,
+ status: RUNNING,
+ });
+ });
+
+ [STARTING, PENDING, RUNNING].forEach(status => {
+ it(`show stop button when status is ${status}`, () => {
+ state.session = { status };
+ factory();
+
+ const button = wrapper.find('button');
+ button.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(button.text()).toEqual('Stop Terminal');
+ expect(actions.stopSession).toHaveBeenCalled();
+ });
+ });
+ });
+
+ [STOPPING, STOPPED].forEach(status => {
+ it(`show stop button when status is ${status}`, () => {
+ state.session = { status };
+ factory();
+
+ const button = wrapper.find('button');
+ button.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(button.text()).toEqual('Restart Terminal');
+ expect(actions.restartSession).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/terminal/terminal_controls_spec.js b/spec/frontend/ide/components/terminal/terminal_controls_spec.js
new file mode 100644
index 00000000000..6c2871abb46
--- /dev/null
+++ b/spec/frontend/ide/components/terminal/terminal_controls_spec.js
@@ -0,0 +1,65 @@
+import { shallowMount } from '@vue/test-utils';
+import TerminalControls from '~/ide/components/terminal/terminal_controls.vue';
+import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue';
+
+describe('IDE TerminalControls', () => {
+ let wrapper;
+ let buttons;
+
+ const factory = (options = {}) => {
+ wrapper = shallowMount(TerminalControls, {
+ ...options,
+ });
+
+ buttons = wrapper.findAll(ScrollButton);
+ };
+
+ it('shows an up and down scroll button', () => {
+ factory();
+
+ expect(buttons.wrappers.map(x => x.props())).toEqual([
+ expect.objectContaining({ direction: 'up', disabled: true }),
+ expect.objectContaining({ direction: 'down', disabled: true }),
+ ]);
+ });
+
+ it('enables up button with prop', () => {
+ factory({ propsData: { canScrollUp: true } });
+
+ expect(buttons.at(0).props()).toEqual(
+ expect.objectContaining({ direction: 'up', disabled: false }),
+ );
+ });
+
+ it('enables down button with prop', () => {
+ factory({ propsData: { canScrollDown: true } });
+
+ expect(buttons.at(1).props()).toEqual(
+ expect.objectContaining({ direction: 'down', disabled: false }),
+ );
+ });
+
+ it('emits "scroll-up" when click up button', () => {
+ factory({ propsData: { canScrollUp: true } });
+
+ expect(wrapper.emittedByOrder()).toEqual([]);
+
+ buttons.at(0).vm.$emit('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emittedByOrder()).toEqual([{ name: 'scroll-up', args: [] }]);
+ });
+ });
+
+ it('emits "scroll-down" when click down button', () => {
+ factory({ propsData: { canScrollDown: true } });
+
+ expect(wrapper.emittedByOrder()).toEqual([]);
+
+ buttons.at(1).vm.$emit('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emittedByOrder()).toEqual([{ name: 'scroll-down', args: [] }]);
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/terminal/terminal_spec.js b/spec/frontend/ide/components/terminal/terminal_spec.js
new file mode 100644
index 00000000000..3095288bb28
--- /dev/null
+++ b/spec/frontend/ide/components/terminal/terminal_spec.js
@@ -0,0 +1,225 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import Terminal from '~/ide/components/terminal/terminal.vue';
+import TerminalControls from '~/ide/components/terminal/terminal_controls.vue';
+import {
+ STARTING,
+ PENDING,
+ RUNNING,
+ STOPPING,
+ STOPPED,
+} from '~/ide/stores/modules/terminal/constants';
+import GLTerminal from '~/terminal/terminal';
+
+const TEST_TERMINAL_PATH = 'terminal/path';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+jest.mock('~/terminal/terminal', () =>
+ jest.fn().mockImplementation(() => ({
+ dispose: jest.fn(),
+ disable: jest.fn(),
+ addScrollListener: jest.fn(),
+ scrollToTop: jest.fn(),
+ scrollToBottom: jest.fn(),
+ })),
+);
+
+describe('IDE Terminal', () => {
+ let wrapper;
+ let state;
+
+ const factory = propsData => {
+ const store = new Vuex.Store({
+ state,
+ mutations: {
+ set(prevState, newState) {
+ Object.assign(prevState, newState);
+ },
+ },
+ });
+
+ wrapper = shallowMount(localVue.extend(Terminal), {
+ propsData: {
+ status: RUNNING,
+ terminalPath: TEST_TERMINAL_PATH,
+ ...propsData,
+ },
+ localVue,
+ store,
+ });
+ };
+
+ beforeEach(() => {
+ state = {
+ panelResizing: false,
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('loading text', () => {
+ [STARTING, PENDING].forEach(status => {
+ it(`shows when starting (${status})`, () => {
+ factory({ status });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.find('.top-bar').text()).toBe('Starting...');
+ });
+ });
+
+ it(`shows when stopping`, () => {
+ factory({ status: STOPPING });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.find('.top-bar').text()).toBe('Stopping...');
+ });
+
+ [RUNNING, STOPPED].forEach(status => {
+ it('hides when not loading', () => {
+ factory({ status });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find('.top-bar').text()).toBe('');
+ });
+ });
+ });
+
+ describe('refs.terminal', () => {
+ it('has terminal path in data', () => {
+ factory();
+
+ expect(wrapper.vm.$refs.terminal.dataset.projectPath).toBe(TEST_TERMINAL_PATH);
+ });
+ });
+
+ describe('terminal controls', () => {
+ beforeEach(() => {
+ factory();
+ wrapper.vm.createTerminal();
+
+ return localVue.nextTick();
+ });
+
+ it('is visible if terminal is created', () => {
+ expect(wrapper.find(TerminalControls).exists()).toBe(true);
+ });
+
+ it('scrolls glterminal on scroll-up', () => {
+ wrapper.find(TerminalControls).vm.$emit('scroll-up');
+
+ expect(wrapper.vm.glterminal.scrollToTop).toHaveBeenCalled();
+ });
+
+ it('scrolls glterminal on scroll-down', () => {
+ wrapper.find(TerminalControls).vm.$emit('scroll-down');
+
+ expect(wrapper.vm.glterminal.scrollToBottom).toHaveBeenCalled();
+ });
+
+ it('has props set', () => {
+ expect(wrapper.find(TerminalControls).props()).toEqual({
+ canScrollUp: false,
+ canScrollDown: false,
+ });
+
+ wrapper.setData({ canScrollUp: true, canScrollDown: true });
+
+ return localVue.nextTick().then(() => {
+ expect(wrapper.find(TerminalControls).props()).toEqual({
+ canScrollUp: true,
+ canScrollDown: true,
+ });
+ });
+ });
+ });
+
+ describe('refresh', () => {
+ let createTerminal;
+ let stopTerminal;
+
+ beforeEach(() => {
+ createTerminal = jest.fn().mockName('createTerminal');
+ stopTerminal = jest.fn().mockName('stopTerminal');
+ });
+
+ it('creates the terminal if running', () => {
+ factory({ status: RUNNING, terminalPath: TEST_TERMINAL_PATH });
+
+ wrapper.setMethods({ createTerminal });
+ wrapper.vm.refresh();
+
+ expect(createTerminal).toHaveBeenCalled();
+ });
+
+ it('stops the terminal if stopping', () => {
+ factory({ status: STOPPING });
+
+ wrapper.setMethods({ stopTerminal });
+ wrapper.vm.refresh();
+
+ expect(stopTerminal).toHaveBeenCalled();
+ });
+ });
+
+ describe('createTerminal', () => {
+ beforeEach(() => {
+ factory();
+ wrapper.vm.createTerminal();
+ });
+
+ it('creates the terminal', () => {
+ expect(GLTerminal).toHaveBeenCalledWith(wrapper.vm.$refs.terminal);
+ expect(wrapper.vm.glterminal).toBeTruthy();
+ });
+
+ describe('scroll listener', () => {
+ it('has been called', () => {
+ expect(wrapper.vm.glterminal.addScrollListener).toHaveBeenCalled();
+ });
+
+ it('updates scroll data when called', () => {
+ expect(wrapper.vm.canScrollUp).toBe(false);
+ expect(wrapper.vm.canScrollDown).toBe(false);
+
+ const listener = wrapper.vm.glterminal.addScrollListener.mock.calls[0][0];
+ listener({ canScrollUp: true, canScrollDown: true });
+
+ expect(wrapper.vm.canScrollUp).toBe(true);
+ expect(wrapper.vm.canScrollDown).toBe(true);
+ });
+ });
+ });
+
+ describe('destroyTerminal', () => {
+ it('calls dispose', () => {
+ factory();
+ wrapper.vm.createTerminal();
+ const disposeSpy = wrapper.vm.glterminal.dispose;
+
+ expect(disposeSpy).not.toHaveBeenCalled();
+
+ wrapper.vm.destroyTerminal();
+
+ expect(disposeSpy).toHaveBeenCalled();
+ expect(wrapper.vm.glterminal).toBe(null);
+ });
+ });
+
+ describe('stopTerminal', () => {
+ it('calls disable', () => {
+ factory();
+ wrapper.vm.createTerminal();
+
+ expect(wrapper.vm.glterminal.disable).not.toHaveBeenCalled();
+
+ wrapper.vm.stopTerminal();
+
+ expect(wrapper.vm.glterminal.disable).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/terminal/view_spec.js b/spec/frontend/ide/components/terminal/view_spec.js
new file mode 100644
index 00000000000..eff200550da
--- /dev/null
+++ b/spec/frontend/ide/components/terminal/view_spec.js
@@ -0,0 +1,91 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { TEST_HOST } from 'spec/test_constants';
+import TerminalEmptyState from '~/ide/components/terminal/empty_state.vue';
+import TerminalView from '~/ide/components/terminal/view.vue';
+import TerminalSession from '~/ide/components/terminal/session.vue';
+
+const TEST_HELP_PATH = `${TEST_HOST}/help`;
+const TEST_SVG_PATH = `${TEST_HOST}/illustration.svg`;
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('IDE TerminalView', () => {
+ let state;
+ let actions;
+ let getters;
+ let wrapper;
+
+ const factory = () => {
+ const store = new Vuex.Store({
+ modules: {
+ terminal: {
+ namespaced: true,
+ state,
+ actions,
+ getters,
+ },
+ },
+ });
+
+ wrapper = shallowMount(TerminalView, { localVue, store });
+ };
+
+ beforeEach(() => {
+ state = {
+ isShowSplash: true,
+ paths: {
+ webTerminalHelpPath: TEST_HELP_PATH,
+ webTerminalSvgPath: TEST_SVG_PATH,
+ },
+ };
+
+ actions = {
+ hideSplash: jest.fn().mockName('hideSplash'),
+ startSession: jest.fn().mockName('startSession'),
+ };
+
+ getters = {
+ allCheck: () => ({
+ isLoading: false,
+ isValid: false,
+ message: 'bad',
+ }),
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders empty state', () => {
+ factory();
+
+ expect(wrapper.find(TerminalEmptyState).props()).toEqual({
+ helpPath: TEST_HELP_PATH,
+ illustrationPath: TEST_SVG_PATH,
+ ...getters.allCheck(),
+ });
+ });
+
+ it('hides splash and starts, when started', () => {
+ factory();
+
+ expect(actions.startSession).not.toHaveBeenCalled();
+ expect(actions.hideSplash).not.toHaveBeenCalled();
+
+ wrapper.find(TerminalEmptyState).vm.$emit('start');
+
+ expect(actions.startSession).toHaveBeenCalled();
+ expect(actions.hideSplash).toHaveBeenCalled();
+ });
+
+ it('shows Web Terminal when started', () => {
+ state.isShowSplash = false;
+ factory();
+
+ expect(wrapper.find(TerminalEmptyState).exists()).toBe(false);
+ expect(wrapper.find(TerminalSession).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
new file mode 100644
index 00000000000..afdecb7bbbd
--- /dev/null
+++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
@@ -0,0 +1,47 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import TerminalSyncStatus from '~/ide/components/terminal_sync/terminal_sync_status.vue';
+import TerminalSyncStatusSafe from '~/ide/components/terminal_sync/terminal_sync_status_safe.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('ide/components/terminal_sync/terminal_sync_status_safe', () => {
+ let store;
+ let wrapper;
+
+ const createComponent = () => {
+ store = new Vuex.Store({
+ state: {},
+ });
+
+ wrapper = shallowMount(TerminalSyncStatusSafe, {
+ localVue,
+ store,
+ });
+ };
+
+ beforeEach(createComponent);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('with terminal sync module in store', () => {
+ beforeEach(() => {
+ store.registerModule('terminalSync', {
+ state: {},
+ });
+ });
+
+ it('renders terminal sync status', () => {
+ expect(wrapper.find(TerminalSyncStatus).exists()).toBe(true);
+ });
+ });
+
+ describe('without terminal sync module', () => {
+ it('does not render terminal sync status', () => {
+ expect(wrapper.find(TerminalSyncStatus).exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
new file mode 100644
index 00000000000..16a76fae1dd
--- /dev/null
+++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
@@ -0,0 +1,99 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import TerminalSyncStatus from '~/ide/components/terminal_sync/terminal_sync_status.vue';
+import {
+ MSG_TERMINAL_SYNC_CONNECTING,
+ MSG_TERMINAL_SYNC_UPLOADING,
+ MSG_TERMINAL_SYNC_RUNNING,
+} from '~/ide/stores/modules/terminal_sync/messages';
+import Icon from '~/vue_shared/components/icon.vue';
+
+const TEST_MESSAGE = 'lorem ipsum dolar sit';
+const START_LOADING = 'START_LOADING';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('ide/components/terminal_sync/terminal_sync_status', () => {
+ let moduleState;
+ let store;
+ let wrapper;
+
+ const createComponent = () => {
+ store = new Vuex.Store({
+ modules: {
+ terminalSync: {
+ namespaced: true,
+ state: moduleState,
+ mutations: {
+ [START_LOADING]: state => {
+ state.isLoading = true;
+ },
+ },
+ },
+ },
+ });
+
+ wrapper = shallowMount(TerminalSyncStatus, {
+ localVue,
+ store,
+ });
+ };
+
+ beforeEach(() => {
+ moduleState = {
+ isLoading: false,
+ isStarted: false,
+ isError: false,
+ message: '',
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when doing nothing', () => {
+ it('shows nothing', () => {
+ createComponent();
+
+ expect(wrapper.isEmpty()).toBe(true);
+ });
+ });
+
+ describe.each`
+ description | state | statusMessage | icon
+ ${'when loading'} | ${{ isLoading: true }} | ${MSG_TERMINAL_SYNC_CONNECTING} | ${''}
+ ${'when loading and started'} | ${{ isLoading: true, isStarted: true }} | ${MSG_TERMINAL_SYNC_UPLOADING} | ${''}
+ ${'when error'} | ${{ isError: true, message: TEST_MESSAGE }} | ${TEST_MESSAGE} | ${'warning'}
+ ${'when started'} | ${{ isStarted: true }} | ${MSG_TERMINAL_SYNC_RUNNING} | ${'mobile-issue-close'}
+ `('$description', ({ state, statusMessage, icon }) => {
+ beforeEach(() => {
+ Object.assign(moduleState, state);
+ createComponent();
+ });
+
+ it('shows message', () => {
+ expect(wrapper.attributes('title')).toContain(statusMessage);
+ });
+
+ if (!icon) {
+ it('does not render icon', () => {
+ expect(wrapper.find(Icon).exists()).toBe(false);
+ });
+
+ it('renders loading icon', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ } else {
+ it('renders icon', () => {
+ expect(wrapper.find(Icon).props('name')).toEqual(icon);
+ });
+
+ it('does not render loading icon', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ });
+ }
+ });
+});