diff options
author | Lin Jen-Shin <godfat@godfat.org> | 2018-06-27 16:15:06 +0800 |
---|---|---|
committer | Lin Jen-Shin <godfat@godfat.org> | 2018-06-27 16:15:06 +0800 |
commit | 849f9995d97c85d88b36a40ee563f7dd51fdc3f1 (patch) | |
tree | 3523089d253b001f7e3b029266399e497407441b /spec/javascripts/ide/components | |
parent | ef6b3e0271d226462bed5f899f3964cf5652978c (diff) | |
parent | 87f7597a4fb7852fc81f830158cdfd5fdec8fac4 (diff) | |
download | gitlab-ce-849f9995d97c85d88b36a40ee563f7dd51fdc3f1.tar.gz |
Merge remote-tracking branch 'upstream/master' into 14995-custom_wiki_sidebar
* upstream/master: (4180 commits)
Enable frozen string literals for app/workers/*.rb
Resolve "Search dropdown hides & shows when typing"
Revert merge request widget button max height
Update CHANGELOG.md for 11.0.2
Update external link icon in header user dropdown
Added Diff Viewer to new VUE based MR page
Fixed eslint failure in IDE spec helpers
Use refs instead of querySelector.
Show file in tree on WebIDE open
Resolve "Remove unused bootstrap component CSS"
Resolve "Explain what Groups are in the New Group page"
[QA] Make sure we wait for the deploy key list to load
Update _scopes_form.html.haml to remove duplicate information
Use the branch instead of the tag to install
port the EE changes
Add index on deployable_type/id for deployments
Add a helper to rename a column using a background migration
Fix performance bottleneck when rendering large wiki pages
Port Namespace#root_ancestor to CE
Remove duplicate spec
...
Diffstat (limited to 'spec/javascripts/ide/components')
45 files changed, 2783 insertions, 434 deletions
diff --git a/spec/javascripts/ide/components/activity_bar_spec.js b/spec/javascripts/ide/components/activity_bar_spec.js new file mode 100644 index 00000000000..946c7e8e9c8 --- /dev/null +++ b/spec/javascripts/ide/components/activity_bar_spec.js @@ -0,0 +1,92 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import { activityBarViews } from '~/ide/constants'; +import ActivityBar from '~/ide/components/activity_bar.vue'; +import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { resetStore } from '../helpers'; + +describe('IDE activity bar', () => { + const Component = Vue.extend(ActivityBar); + let vm; + + beforeEach(() => { + Vue.set(store.state.projects, 'abcproject', { + web_url: 'testing', + }); + Vue.set(store.state, 'currentProjectId', 'abcproject'); + + vm = createComponentWithStore(Component, store); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + describe('goBackUrl', () => { + it('renders the Go Back link with the referrer when present', () => { + const fakeReferrer = '/example/README.md'; + spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer); + + vm.$mount(); + + expect(vm.goBackUrl).toEqual(fakeReferrer); + }); + + it('renders the Go Back link with the project url when referrer is not present', () => { + const fakeReferrer = ''; + spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer); + + vm.$mount(); + + expect(vm.goBackUrl).toEqual('testing'); + }); + }); + + describe('updateActivityBarView', () => { + beforeEach(() => { + spyOn(vm, 'updateActivityBarView'); + + vm.$mount(); + }); + + it('calls updateActivityBarView with edit value on click', () => { + vm.$el.querySelector('.js-ide-edit-mode').click(); + + expect(vm.updateActivityBarView).toHaveBeenCalledWith(activityBarViews.edit); + }); + + it('calls updateActivityBarView with commit value on click', () => { + vm.$el.querySelector('.js-ide-commit-mode').click(); + + expect(vm.updateActivityBarView).toHaveBeenCalledWith(activityBarViews.commit); + }); + + it('calls updateActivityBarView with review value on click', () => { + vm.$el.querySelector('.js-ide-review-mode').click(); + + expect(vm.updateActivityBarView).toHaveBeenCalledWith(activityBarViews.review); + }); + }); + + describe('active item', () => { + beforeEach(() => { + vm.$mount(); + }); + + it('sets edit item active', () => { + expect(vm.$el.querySelector('.js-ide-edit-mode').classList).toContain('active'); + }); + + it('sets commit item active', done => { + vm.$store.state.currentActivityView = activityBarViews.commit; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active'); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/changed_file_icon_spec.js b/spec/javascripts/ide/components/changed_file_icon_spec.js index 987aea7befc..541864e912e 100644 --- a/spec/javascripts/ide/components/changed_file_icon_spec.js +++ b/spec/javascripts/ide/components/changed_file_icon_spec.js @@ -11,6 +11,7 @@ describe('IDE changed file icon', () => { vm = createComponent(component, { file: { tempFile: false, + changed: true, }, }); }); @@ -20,7 +21,7 @@ describe('IDE changed file icon', () => { }); describe('changedIcon', () => { - it('equals file-modified when not a temp file', () => { + it('equals file-modified when not a temp file and has changes', () => { expect(vm.changedIcon).toBe('file-modified'); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js index 144e78d14b5..27f10caccb1 100644 --- a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js @@ -3,6 +3,7 @@ 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'; +import { projectData } from 'spec/ide/mock_data'; describe('IDE commit sidebar actions', () => { let vm; @@ -13,6 +14,8 @@ describe('IDE commit sidebar actions', () => { vm = createComponentWithStore(Component, store); vm.$store.state.currentBranchId = 'master'; + vm.$store.state.currentProjectId = 'abcproject'; + Vue.set(vm.$store.state.projects, 'abcproject', { ...projectData }); vm.$mount(); @@ -32,4 +35,15 @@ describe('IDE commit sidebar actions', () => { it('renders current branch text', () => { expect(vm.$el.textContent).toContain('Commit to master branch'); }); + + it('hides merge request option when project merge requests are disabled', done => { + vm.$store.state.projects.abcproject.merge_requests_enabled = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2); + expect(vm.$el.textContent).not.toContain('Create a new branch and merge request'); + + done(); + }); + }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js new file mode 100644 index 00000000000..16d0b354a30 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import emptyState from '~/ide/components/commit_sidebar/empty_state.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { resetStore } from '../../helpers'; + +describe('IDE commit panel empty state', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(emptyState); + + Vue.set(store.state, 'noChangesStateSvgPath', 'no-changes'); + + vm = createComponentWithStore(Component, store); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders no changes text when last commit message is empty', () => { + expect(vm.$el.textContent).toContain('No changes'); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/form_spec.js b/spec/javascripts/ide/components/commit_sidebar/form_spec.js new file mode 100644 index 00000000000..8b47a365582 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/form_spec.js @@ -0,0 +1,149 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import CommitForm from '~/ide/components/commit_sidebar/form.vue'; +import { activityBarViews } from '~/ide/constants'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; +import { projectData } from 'spec/ide/mock_data'; +import { resetStore } from '../../helpers'; + +describe('IDE commit form', () => { + const Component = Vue.extend(CommitForm); + let vm; + + beforeEach(() => { + spyOnProperty(window, 'innerHeight').and.returnValue(800); + + store.state.changedFiles.push('test'); + store.state.currentProjectId = 'abcproject'; + Vue.set(store.state.projects, 'abcproject', { ...projectData }); + + vm = createComponentWithStore(Component, store).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('enables button when has changes', () => { + expect(vm.$el.querySelector('[disabled]')).toBe(null); + }); + + describe('compact', () => { + it('renders commit button in compact mode', () => { + expect(vm.$el.querySelector('.btn-primary')).not.toBeNull(); + expect(vm.$el.querySelector('.btn-primary').textContent).toContain('Commit'); + }); + + it('does not render form', () => { + expect(vm.$el.querySelector('form')).toBeNull(); + }); + + it('renders overview text', done => { + vm.$store.state.stagedFiles.push('test'); + + vm.$nextTick(() => { + expect(vm.$el.querySelector('p').textContent).toContain('1 unstaged and 1 staged changes'); + done(); + }); + }); + + it('shows form when clicking commit button', done => { + vm.$el.querySelector('.btn-primary').click(); + + vm.$nextTick(() => { + expect(vm.$el.querySelector('form')).not.toBeNull(); + + done(); + }); + }); + + it('toggles activity bar vie when clicking commit button', done => { + vm.$el.querySelector('.btn-primary').click(); + + vm.$nextTick(() => { + expect(store.state.currentActivityView).toBe(activityBarViews.commit); + + done(); + }); + }); + }); + + describe('full', () => { + beforeEach(done => { + vm.isCompact = false; + + vm.$nextTick(done); + }); + + 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); + }); + + it('updating currentActivityView not to commit view sets compact mode', done => { + store.state.currentActivityView = 'a'; + + vm.$nextTick(() => { + expect(vm.isCompact).toBe(true); + + done(); + }); + }); + + describe('discard draft button', () => { + it('hidden when commitMessage is empty', () => { + expect(vm.$el.querySelector('.btn-default').textContent).toContain('Collapse'); + }); + + it('resets commitMessage when clicking discard button', done => { + vm.$store.state.commit.commitMessage = 'testing commit message'; + + getSetTimeoutPromise() + .then(() => { + vm.$el.querySelector('.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'); + vm.$store.state.stagedFiles.push('test'); + }); + + it('calls commitChanges', done => { + vm.$store.state.commit.commitMessage = 'testing commit message'; + + getSetTimeoutPromise() + .then(() => { + vm.$el.querySelector('.btn-success').click(); + }) + .then(Vue.nextTick) + .then(() => { + expect(vm.commitChanges).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js index 5b402886b55..9af3c15a4e3 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js @@ -3,6 +3,7 @@ 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'; +import { removeWhitespace } from '../../../helpers/vue_component_helper'; describe('Multi-file editor commit sidebar list collapsed', () => { let vm; @@ -10,10 +11,17 @@ describe('Multi-file editor commit sidebar list collapsed', () => { 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 = createComponentWithStore(Component, store, { + files: [ + { + ...file('file1'), + tempFile: true, + }, + file('file2'), + ], + iconName: 'staged', + title: 'Staged', + }); vm.$mount(); }); @@ -23,6 +31,42 @@ describe('Multi-file editor commit sidebar list collapsed', () => { }); it('renders added & modified files count', () => { - expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1'); + expect(removeWhitespace(vm.$el.textContent).trim()).toBe('1 1'); + }); + + describe('addedFilesLength', () => { + it('returns an length of temp files', () => { + expect(vm.addedFilesLength).toBe(1); + }); + }); + + describe('modifiedFilesLength', () => { + it('returns an length of modified files', () => { + expect(vm.modifiedFilesLength).toBe(1); + }); + }); + + describe('addedFilesIconClass', () => { + it('includes multi-file-addition when addedFiles is not empty', () => { + expect(vm.addedFilesIconClass).toContain('multi-file-addition'); + }); + + it('excludes multi-file-addition when addedFiles is empty', () => { + vm.files = []; + + expect(vm.addedFilesIconClass).not.toContain('multi-file-addition'); + }); + }); + + describe('modifiedFilesClass', () => { + it('includes multi-file-modified when addedFiles is not empty', () => { + expect(vm.modifiedFilesClass).toContain('multi-file-modified'); + }); + + it('excludes multi-file-modified when addedFiles is empty', () => { + vm.files = []; + + expect(vm.modifiedFilesClass).not.toContain('multi-file-modified'); + }); }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js index 15b66952d99..bf96170f703 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js @@ -1,8 +1,9 @@ import Vue from 'vue'; +import store from '~/ide/stores'; 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'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { file, resetStore } from '../../helpers'; describe('Multi-file editor commit sidebar list item', () => { let vm; @@ -13,48 +14,55 @@ describe('Multi-file editor commit sidebar list item', () => { f = file('test-file'); - vm = mountComponent(Component, { + store.state.entries[f.path] = f; + + vm = createComponentWithStore(Component, store, { file: f, - }); + actionComponent: 'stage-button', + activeFileKey: `staged-${f.key}`, + }).$mount(); }); afterEach(() => { vm.$destroy(); + + resetStore(store); }); it('renders file path', () => { - expect( - vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim(), - ).toBe(f.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('renders actionn button', () => { + expect(vm.$el.querySelector('.multi-file-discard-btn')).not.toBeNull(); }); - it('opens a closed file in the editor when clicking the file path', () => { - spyOn(vm, 'openFileInEditor').and.callThrough(); - spyOn(vm, 'updateViewer'); + it('opens a closed file in the editor when clicking the file path', done => { + spyOn(vm, 'openPendingTab').and.callThrough(); spyOn(router, 'push'); vm.$el.querySelector('.multi-file-commit-list-path').click(); - expect(vm.openFileInEditor).toHaveBeenCalled(); - expect(router.push).toHaveBeenCalled(); + setTimeout(() => { + expect(vm.openPendingTab).toHaveBeenCalled(); + expect(router.push).toHaveBeenCalled(); + + done(); + }); }); - it('calls updateViewer with diff when clicking file', () => { + it('calls updateViewer with diff when clicking file', done => { spyOn(vm, 'openFileInEditor').and.callThrough(); - spyOn(vm, 'updateViewer'); + spyOn(vm, 'updateViewer').and.callThrough(); spyOn(router, 'push'); vm.$el.querySelector('.multi-file-commit-list-path').click(); - expect(vm.updateViewer).toHaveBeenCalledWith('diff'); + setTimeout(() => { + expect(vm.updateViewer).toHaveBeenCalledWith('diff'); + + done(); + }); }); describe('computed', () => { @@ -82,4 +90,20 @@ describe('Multi-file editor commit sidebar list item', () => { }); }); }); + + describe('is active', () => { + it('does not add active class when dont keys match', () => { + expect(vm.$el.querySelector('.is-active')).toBe(null); + }); + + it('adds active class when keys match', done => { + vm.keyPrefix = 'staged'; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.is-active')).not.toBe(null); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js index a62c0a28340..b786be55019 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js @@ -2,7 +2,7 @@ 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'; +import { file, resetStore } from '../../helpers'; describe('Multi-file editor commit sidebar list', () => { let vm; @@ -13,6 +13,13 @@ describe('Multi-file editor commit sidebar list', () => { vm = createComponentWithStore(Component, store, { title: 'Staged', fileList: [], + iconName: 'staged', + action: 'stageAllChanges', + actionBtnText: 'stage all', + actionBtnIcon: 'history', + itemActionComponent: 'stage-button', + activeFileKey: 'staged-testing', + keyPrefix: 'staged', }); vm.$store.state.rightPanelCollapsed = false; @@ -22,6 +29,8 @@ describe('Multi-file editor commit sidebar list', () => { afterEach(() => { vm.$destroy(); + + resetStore(vm.$store); }); describe('with a list of files', () => { @@ -34,20 +43,13 @@ describe('Multi-file editor commit sidebar list', () => { }); it('renders list', () => { - expect(vm.$el.querySelectorAll('li').length).toBe(1); + expect(vm.$el.querySelectorAll('.multi-file-commit-list > 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(); + describe('empty files array', () => { + it('renders no changes text when empty', () => { + expect(vm.$el.textContent).toContain('No changes'); }); }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js b/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js new file mode 100644 index 00000000000..d62d58101d6 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js @@ -0,0 +1,174 @@ +import Vue from 'vue'; +import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue'; +import createComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('IDE commit message field', () => { + const Component = Vue.extend(CommitMessageField); + let vm; + + beforeEach(() => { + setFixtures('<div id="app"></div>'); + + vm = createComponent( + Component, + { + text: '', + }, + '#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', () => { + spyOn(vm, '$emit'); + + 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(0px, -50px, 0px)', + ); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js index 4e8243439f3..21bfe4be52f 100644 --- a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js @@ -69,19 +69,6 @@ describe('IDE commit sidebar radio group', () => { }); }); - 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(); diff --git a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js new file mode 100644 index 00000000000..a5b906da8a1 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js @@ -0,0 +1,46 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import stageButton from '~/ide/components/commit_sidebar/stage_button.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { file, resetStore } from '../../helpers'; + +describe('IDE stage file button', () => { + let vm; + let f; + + beforeEach(() => { + const Component = Vue.extend(stageButton); + f = file(); + + vm = createComponentWithStore(Component, store, { + path: f.path, + }); + + spyOn(vm, 'stageChange'); + spyOn(vm, 'discardFileChanges'); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders button to discard & stage', () => { + expect(vm.$el.querySelectorAll('.btn').length).toBe(2); + }); + + it('calls store with stage button', () => { + vm.$el.querySelectorAll('.btn')[0].click(); + + expect(vm.stageChange).toHaveBeenCalledWith(f.path); + }); + + it('calls store with discard button', () => { + vm.$el.querySelector('.dropdown-menu button').click(); + + expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/success_message_spec.js b/spec/javascripts/ide/components/commit_sidebar/success_message_spec.js new file mode 100644 index 00000000000..e1a432b81be --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/success_message_spec.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import successMessage from '~/ide/components/commit_sidebar/success_message.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { resetStore } from '../../helpers'; + +describe('IDE commit panel successful commit state', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(successMessage); + + vm = createComponentWithStore(Component, store, { + committedStateSvgPath: 'committed-state', + }); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders last commit message when it exists', done => { + vm.$store.state.lastCommitMsg = 'testing commit message'; + + Vue.nextTick(() => { + expect(vm.$el.textContent).toContain('testing commit message'); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js new file mode 100644 index 00000000000..917bbb9fb46 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import unstageButton from '~/ide/components/commit_sidebar/unstage_button.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { file, resetStore } from '../../helpers'; + +describe('IDE unstage file button', () => { + let vm; + let f; + + beforeEach(() => { + const Component = Vue.extend(unstageButton); + f = file(); + + vm = createComponentWithStore(Component, store, { + path: f.path, + }); + + spyOn(vm, 'unstageChange'); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders button to unstage', () => { + expect(vm.$el.querySelectorAll('.btn').length).toBe(1); + }); + + it('calls store with unnstage button', () => { + vm.$el.querySelector('.btn').click(); + + expect(vm.unstageChange).toHaveBeenCalledWith(f.path); + }); +}); diff --git a/spec/javascripts/ide/components/external_link_spec.js b/spec/javascripts/ide/components/external_link_spec.js new file mode 100644 index 00000000000..b3d94c041fa --- /dev/null +++ b/spec/javascripts/ide/components/external_link_spec.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import externalLink from '~/ide/components/external_link.vue'; +import createVueComponent from '../../helpers/vue_mount_component_helper'; +import { file } from '../helpers'; + +describe('ExternalLink', () => { + const activeFile = file(); + let vm; + + function createComponent() { + const ExternalLink = Vue.extend(externalLink); + + activeFile.permalink = 'test'; + + return createVueComponent(ExternalLink, { + file: activeFile, + }); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders the external link with the correct href', done => { + activeFile.binary = true; + vm = createComponent(); + + vm.$nextTick(() => { + const openLink = vm.$el.querySelector('a'); + + expect(openLink.href).toMatch(`/${activeFile.permalink}`); + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/file_finder/index_spec.js b/spec/javascripts/ide/components/file_finder/index_spec.js new file mode 100644 index 00000000000..4f208e946d2 --- /dev/null +++ b/spec/javascripts/ide/components/file_finder/index_spec.js @@ -0,0 +1,308 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import FindFileComponent from '~/ide/components/file_finder/index.vue'; +import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; +import router from '~/ide/ide_router'; +import { file, resetStore } from '../../helpers'; +import { mountComponentWithStore } from '../../../helpers/vue_mount_component_helper'; + +describe('IDE File finder item spec', () => { + const Component = Vue.extend(FindFileComponent); + let vm; + + beforeEach(done => { + setFixtures('<div id="app"></div>'); + + vm = mountComponentWithStore(Component, { + store, + el: '#app', + props: { + index: 0, + }, + }); + + setTimeout(done); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + describe('with entries', () => { + beforeEach(done => { + Vue.set(vm.$store.state.entries, 'folder', { + ...file('folder'), + path: 'folder', + type: 'folder', + }); + + Vue.set(vm.$store.state.entries, 'index.js', { + ...file('index.js'), + path: 'index.js', + type: 'blob', + url: '/index.jsurl', + }); + + Vue.set(vm.$store.state.entries, 'component.js', { + ...file('component.js'), + path: 'component.js', + type: 'blob', + }); + + setTimeout(done); + }); + + it('renders list of blobs', () => { + expect(vm.$el.textContent).toContain('index.js'); + expect(vm.$el.textContent).toContain('component.js'); + expect(vm.$el.textContent).not.toContain('folder'); + }); + + it('filters entries', done => { + vm.searchText = 'index'; + + vm.$nextTick(() => { + expect(vm.$el.textContent).toContain('index.js'); + expect(vm.$el.textContent).not.toContain('component.js'); + + done(); + }); + }); + + it('shows clear button when searchText is not empty', done => { + vm.searchText = 'index'; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dropdown-input-clear').classList).toContain('show'); + expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden'); + + done(); + }); + }); + + it('clear button resets searchText', done => { + vm.searchText = 'index'; + + vm + .$nextTick() + .then(() => { + vm.$el.querySelector('.dropdown-input-clear').click(); + }) + .then(vm.$nextTick) + .then(() => { + expect(vm.searchText).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + + it('clear button focues search input', done => { + spyOn(vm.$refs.searchInput, 'focus'); + vm.searchText = 'index'; + + vm + .$nextTick() + .then(() => { + vm.$el.querySelector('.dropdown-input-clear').click(); + }) + .then(vm.$nextTick) + .then(() => { + expect(vm.$refs.searchInput.focus).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + describe('listShowCount', () => { + it('returns 1 when no filtered entries exist', done => { + vm.searchText = 'testing 123'; + + vm.$nextTick(() => { + expect(vm.listShowCount).toBe(1); + + done(); + }); + }); + + it('returns entries length when not filtered', () => { + expect(vm.listShowCount).toBe(2); + }); + }); + + describe('listHeight', () => { + it('returns 55 when entries exist', () => { + expect(vm.listHeight).toBe(55); + }); + + it('returns 33 when entries dont exist', done => { + vm.searchText = 'testing 123'; + + vm.$nextTick(() => { + expect(vm.listHeight).toBe(33); + + done(); + }); + }); + }); + + describe('filteredBlobsLength', () => { + it('returns length of filtered blobs', done => { + vm.searchText = 'index'; + + vm.$nextTick(() => { + expect(vm.filteredBlobsLength).toBe(1); + + done(); + }); + }); + }); + + describe('watches', () => { + describe('searchText', () => { + it('resets focusedIndex when updated', done => { + vm.focusedIndex = 1; + vm.searchText = 'test'; + + vm.$nextTick(() => { + expect(vm.focusedIndex).toBe(0); + + done(); + }); + }); + }); + + describe('fileFindVisible', () => { + it('returns searchText when false', done => { + vm.searchText = 'test'; + vm.$store.state.fileFindVisible = true; + + vm + .$nextTick() + .then(() => { + vm.$store.state.fileFindVisible = false; + }) + .then(vm.$nextTick) + .then(() => { + expect(vm.searchText).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('openFile', () => { + beforeEach(() => { + spyOn(router, 'push'); + spyOn(vm, 'toggleFileFinder'); + }); + + it('closes file finder', () => { + vm.openFile(vm.$store.state.entries['index.js']); + + expect(vm.toggleFileFinder).toHaveBeenCalled(); + }); + + it('pushes to router', () => { + vm.openFile(vm.$store.state.entries['index.js']); + + expect(router.push).toHaveBeenCalledWith('/project/index.jsurl'); + }); + }); + + describe('onKeyup', () => { + it('opens file on enter key', done => { + const event = new CustomEvent('keyup'); + event.keyCode = ENTER_KEY_CODE; + + spyOn(vm, 'openFile'); + + vm.$refs.searchInput.dispatchEvent(event); + + vm.$nextTick(() => { + expect(vm.openFile).toHaveBeenCalledWith(vm.$store.state.entries['index.js']); + + done(); + }); + }); + + it('closes file finder on esc key', done => { + const event = new CustomEvent('keyup'); + event.keyCode = ESC_KEY_CODE; + + spyOn(vm, 'toggleFileFinder'); + + vm.$refs.searchInput.dispatchEvent(event); + + vm.$nextTick(() => { + expect(vm.toggleFileFinder).toHaveBeenCalled(); + + done(); + }); + }); + }); + + describe('onKeyDown', () => { + let el; + + beforeEach(() => { + el = vm.$refs.searchInput; + }); + + describe('up key', () => { + const event = new CustomEvent('keydown'); + event.keyCode = UP_KEY_CODE; + + it('resets to last index when at top', () => { + el.dispatchEvent(event); + + expect(vm.focusedIndex).toBe(1); + }); + + it('minus 1 from focusedIndex', () => { + vm.focusedIndex = 1; + + el.dispatchEvent(event); + + expect(vm.focusedIndex).toBe(0); + }); + }); + + describe('down key', () => { + const event = new CustomEvent('keydown'); + event.keyCode = DOWN_KEY_CODE; + + it('resets to first index when at bottom', () => { + vm.focusedIndex = 1; + el.dispatchEvent(event); + + expect(vm.focusedIndex).toBe(0); + }); + + it('adds 1 to focusedIndex', () => { + el.dispatchEvent(event); + + expect(vm.focusedIndex).toBe(1); + }); + }); + }); + }); + + describe('without entries', () => { + it('renders loading text when loading', done => { + store.state.loading = true; + + vm.$nextTick(() => { + expect(vm.$el.textContent).toContain('Loading...'); + + done(); + }); + }); + + it('renders no files text', () => { + expect(vm.$el.textContent).toContain('No files found.'); + }); + }); +}); diff --git a/spec/javascripts/ide/components/file_finder/item_spec.js b/spec/javascripts/ide/components/file_finder/item_spec.js new file mode 100644 index 00000000000..0f1116c6912 --- /dev/null +++ b/spec/javascripts/ide/components/file_finder/item_spec.js @@ -0,0 +1,140 @@ +import Vue from 'vue'; +import ItemComponent from '~/ide/components/file_finder/item.vue'; +import { file } from '../../helpers'; +import createComponent from '../../../helpers/vue_mount_component_helper'; + +describe('IDE File finder item spec', () => { + const Component = Vue.extend(ItemComponent); + let vm; + let localFile; + + beforeEach(() => { + localFile = { + ...file(), + name: 'test file', + path: 'test/file', + }; + + vm = createComponent(Component, { + file: localFile, + focused: true, + searchText: '', + index: 0, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders file name & path', () => { + expect(vm.$el.textContent).toContain('test file'); + expect(vm.$el.textContent).toContain('test/file'); + }); + + describe('focused', () => { + it('adds is-focused class', () => { + expect(vm.$el.classList).toContain('is-focused'); + }); + + it('does not have is-focused class when not focused', done => { + vm.focused = false; + + vm.$nextTick(() => { + expect(vm.$el.classList).not.toContain('is-focused'); + + done(); + }); + }); + }); + + describe('changed file icon', () => { + it('does not render when not a changed or temp file', () => { + expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null); + }); + + it('renders when a changed file', done => { + vm.file.changed = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); + + done(); + }); + }); + + it('renders when a temp file', done => { + vm.file.tempFile = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); + + done(); + }); + }); + }); + + it('emits event when clicked', () => { + spyOn(vm, '$emit'); + + vm.$el.click(); + + expect(vm.$emit).toHaveBeenCalledWith('click', vm.file); + }); + + describe('path', () => { + let el; + + beforeEach(done => { + vm.searchText = 'file'; + + el = vm.$el.querySelector('.diff-changed-file-path'); + + vm.$nextTick(done); + }); + + it('highlights text', () => { + expect(el.querySelectorAll('.highlighted').length).toBe(4); + }); + + it('adds ellipsis to long text', done => { + vm.file.path = new Array(70) + .fill() + .map((_, i) => `${i}-`) + .join(''); + + vm.$nextTick(() => { + expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`); + done(); + }); + }); + }); + + describe('name', () => { + let el; + + beforeEach(done => { + vm.searchText = 'file'; + + el = vm.$el.querySelector('.diff-changed-file-name'); + + vm.$nextTick(done); + }); + + it('highlights text', () => { + expect(el.querySelectorAll('.highlighted').length).toBe(4); + }); + + it('does not add ellipsis to long text', done => { + vm.file.name = new Array(70) + .fill() + .map((_, i) => `${i}-`) + .join(''); + + vm.$nextTick(() => { + expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_context_bar_spec.js b/spec/javascripts/ide/components/ide_context_bar_spec.js deleted file mode 100644 index e17b051f137..00000000000 --- a/spec/javascripts/ide/components/ide_context_bar_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index 9f6cb459f3b..00000000000 --- a/spec/javascripts/ide/components/ide_external_links_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -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_project_tree_spec.js b/spec/javascripts/ide/components/ide_project_tree_spec.js deleted file mode 100644 index 657682cb39c..00000000000 --- a/spec/javascripts/ide/components/ide_project_tree_spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import Vue from 'vue'; -import ProjectTree from '~/ide/components/ide_project_tree.vue'; -import createComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('IDE project tree', () => { - const Component = Vue.extend(ProjectTree); - let vm; - - beforeEach(() => { - vm = createComponent(Component, { - project: { - id: 1, - name: 'test', - web_url: gl.TEST_HOST, - avatar_url: '', - branches: [], - }, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders identicon when projct has no avatar', () => { - expect(vm.$el.querySelector('.identicon')).not.toBeNull(); - }); - - it('renders avatar image if project has avatar', done => { - vm.project.avatar_url = gl.TEST_HOST; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.identicon')).toBeNull(); - expect(vm.$el.querySelector('img.avatar')).not.toBeNull(); - - done(); - }); - }); -}); diff --git a/spec/javascripts/ide/components/ide_repo_tree_spec.js b/spec/javascripts/ide/components/ide_repo_tree_spec.js deleted file mode 100644 index e0fbc90ca61..00000000000 --- a/spec/javascripts/ide/components/ide_repo_tree_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -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_review_spec.js b/spec/javascripts/ide/components/ide_review_spec.js new file mode 100644 index 00000000000..b9ee22b7c1a --- /dev/null +++ b/spec/javascripts/ide/components/ide_review_spec.js @@ -0,0 +1,69 @@ +import Vue from 'vue'; +import IdeReview from '~/ide/components/ide_review.vue'; +import store from '~/ide/stores'; +import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { trimText } from '../../helpers/vue_component_helper'; +import { resetStore, file } from '../helpers'; +import { projectData } from '../mock_data'; + +describe('IDE review mode', () => { + const Component = Vue.extend(IdeReview); + let vm; + + beforeEach(() => { + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'master'; + store.state.projects.abcproject = Object.assign({}, projectData); + Vue.set(store.state.trees, 'abcproject/master', { + tree: [file('fileName')], + loading: false, + }); + + vm = createComponentWithStore(Component, store).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders list of files', () => { + expect(vm.$el.textContent).toContain('fileName'); + }); + + describe('merge request', () => { + beforeEach(done => { + store.state.currentMergeRequestId = '1'; + store.state.projects.abcproject.mergeRequests['1'] = { + iid: 123, + web_url: 'testing123', + }; + + vm.$nextTick(done); + }); + + it('renders edit dropdown', () => { + expect(vm.$el.querySelector('.btn')).not.toBe(null); + }); + + it('renders merge request link & IID', () => { + const link = vm.$el.querySelector('.ide-review-sub-header'); + + expect(link.querySelector('a').getAttribute('href')).toBe('testing123'); + expect(trimText(link.textContent)).toBe('Merge request (!123)'); + }); + + it('changes text to latest changes when viewer is not mrdiff', done => { + store.state.viewer = 'diff'; + + vm.$nextTick(() => { + expect(trimText(vm.$el.querySelector('.ide-review-sub-header').textContent)).toBe( + 'Latest changes', + ); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_side_bar_spec.js b/spec/javascripts/ide/components/ide_side_bar_spec.js index 699dae1ce2f..20ee20bc1d7 100644 --- a/spec/javascripts/ide/components/ide_side_bar_spec.js +++ b/spec/javascripts/ide/components/ide_side_bar_spec.js @@ -1,8 +1,10 @@ import Vue from 'vue'; import store from '~/ide/stores'; import ideSidebar from '~/ide/components/ide_side_bar.vue'; +import { activityBarViews } from '~/ide/constants'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from '../helpers'; +import { projectData } from '../mock_data'; describe('IdeSidebar', () => { let vm; @@ -10,6 +12,9 @@ describe('IdeSidebar', () => { beforeEach(() => { const Component = Vue.extend(ideSidebar); + store.state.currentProjectId = 'abcproject'; + store.state.projects.abcproject = projectData; + vm = createComponentWithStore(Component, store).$mount(); }); @@ -20,23 +25,33 @@ describe('IdeSidebar', () => { }); it('renders a sidebar', () => { - expect( - vm.$el.querySelector('.multi-file-commit-panel-inner'), - ).not.toBeNull(); + 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); + expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull(); + expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3); done(); }); }); + + describe('activityBarComponent', () => { + it('renders tree component', () => { + expect(vm.$el.querySelector('.ide-file-list')).not.toBeNull(); + }); + + it('renders commit component', done => { + vm.$store.state.currentActivityView = activityBarViews.commit; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.multi-file-commit-panel-section')).not.toBeNull(); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js index 5bd890094cc..045a60e56a0 100644 --- a/spec/javascripts/ide/components/ide_spec.js +++ b/spec/javascripts/ide/components/ide_spec.js @@ -1,8 +1,10 @@ import Vue from 'vue'; +import Mousetrap from 'mousetrap'; 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'; +import { projectData } from '../mock_data'; describe('ide component', () => { let vm; @@ -10,6 +12,10 @@ describe('ide component', () => { beforeEach(() => { const Component = Vue.extend(ide); + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'master'; + store.state.projects.abcproject = Object.assign({}, projectData); + vm = createComponentWithStore(Component, store, { emptyStateSvgPath: 'svg', noChangesStateSvgPath: 'svg', @@ -23,11 +29,11 @@ describe('ide component', () => { resetStore(vm.$store); }); - it('does not render panel right when no files open', () => { + it('does not render right right when no files open', () => { expect(vm.$el.querySelector('.panel-right')).toBeNull(); }); - it('renders panel right when files are open', done => { + it('renders right panel when files are open', done => { vm.$store.state.trees['abcproject/mybranch'] = { tree: [file()], }; @@ -38,4 +44,74 @@ describe('ide component', () => { done(); }); }); + + describe('file finder', () => { + beforeEach(done => { + spyOn(vm, 'toggleFileFinder'); + + vm.$store.state.fileFindVisible = true; + + vm.$nextTick(done); + }); + + it('calls toggleFileFinder on `t` key press', done => { + Mousetrap.trigger('t'); + + vm + .$nextTick() + .then(() => { + expect(vm.toggleFileFinder).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('calls toggleFileFinder on `command+p` key press', done => { + Mousetrap.trigger('command+p'); + + vm + .$nextTick() + .then(() => { + expect(vm.toggleFileFinder).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('calls toggleFileFinder on `ctrl+p` key press', done => { + Mousetrap.trigger('ctrl+p'); + + vm + .$nextTick() + .then(() => { + expect(vm.toggleFileFinder).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('always allows `command+p` to trigger toggleFileFinder', () => { + expect( + vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'), + ).toBe(false); + }); + + it('always allows `ctrl+p` to trigger toggleFileFinder', () => { + expect( + vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'), + ).toBe(false); + }); + + it('onlys handles `t` when focused in input-field', () => { + expect( + vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'), + ).toBe(true); + }); + + it('stops callback in monaco editor', () => { + setFixtures('<div class="inputarea"></div>'); + + expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true); + }); + }); }); diff --git a/spec/javascripts/ide/components/ide_status_bar_spec.js b/spec/javascripts/ide/components/ide_status_bar_spec.js new file mode 100644 index 00000000000..770dca9cb0f --- /dev/null +++ b/spec/javascripts/ide/components/ide_status_bar_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import ideStatusBar from '~/ide/components/ide_status_bar.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { resetStore } from '../helpers'; +import { projectData } from '../mock_data'; + +describe('ideStatusBar', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(ideStatusBar); + + store.state.currentProjectId = 'abcproject'; + store.state.projects.abcproject = projectData; + + vm = createComponentWithStore(Component, store).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders the statusbar', () => { + expect(vm.$el.className).toBe('ide-status-bar'); + }); + + describe('mounted', () => { + it('triggers a setInterval', () => { + expect(vm.intervalId).not.toBe(null); + }); + }); + + describe('commitAgeUpdate', () => { + beforeEach(function() { + jasmine.clock().install(); + spyOn(vm, 'commitAgeUpdate').and.callFake(() => {}); + vm.startTimer(); + }); + + afterEach(function() { + jasmine.clock().uninstall(); + }); + + it('gets called every second', () => { + expect(vm.commitAgeUpdate).not.toHaveBeenCalled(); + + jasmine.clock().tick(1100); + expect(vm.commitAgeUpdate.calls.count()).toEqual(1); + + jasmine.clock().tick(1000); + expect(vm.commitAgeUpdate.calls.count()).toEqual(2); + }); + }); + + describe('getCommitPath', () => { + it('returns the path to the commit details', () => { + expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de'); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_tree_list_spec.js b/spec/javascripts/ide/components/ide_tree_list_spec.js new file mode 100644 index 00000000000..4ecbdb8a55e --- /dev/null +++ b/spec/javascripts/ide/components/ide_tree_list_spec.js @@ -0,0 +1,54 @@ +import Vue from 'vue'; +import IdeTreeList from '~/ide/components/ide_tree_list.vue'; +import store from '~/ide/stores'; +import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { resetStore, file } from '../helpers'; +import { projectData } from '../mock_data'; + +describe('IDE tree list', () => { + const Component = Vue.extend(IdeTreeList); + let vm; + + beforeEach(() => { + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'master'; + store.state.projects.abcproject = Object.assign({}, projectData); + Vue.set(store.state.trees, 'abcproject/master', { + tree: [file('fileName')], + loading: false, + }); + + vm = createComponentWithStore(Component, store, { + viewerType: 'edit', + }); + + spyOn(vm, 'updateViewer').and.callThrough(); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('updates viewer on mount', () => { + expect(vm.updateViewer).toHaveBeenCalledWith('edit'); + }); + + it('renders loading indicator', done => { + store.state.trees['abcproject/master'].loading = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull(); + expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3); + + done(); + }); + }); + + it('renders list of files', () => { + expect(vm.$el.textContent).toContain('fileName'); + }); +}); diff --git a/spec/javascripts/ide/components/ide_tree_spec.js b/spec/javascripts/ide/components/ide_tree_spec.js new file mode 100644 index 00000000000..97a0a2432f1 --- /dev/null +++ b/spec/javascripts/ide/components/ide_tree_spec.js @@ -0,0 +1,34 @@ +import Vue from 'vue'; +import IdeTree from '~/ide/components/ide_tree.vue'; +import store from '~/ide/stores'; +import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { resetStore, file } from '../helpers'; +import { projectData } from '../mock_data'; + +describe('IdeRepoTree', () => { + let vm; + + beforeEach(() => { + const IdeRepoTree = Vue.extend(IdeTree); + + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'master'; + store.state.projects.abcproject = Object.assign({}, projectData); + Vue.set(store.state.trees, 'abcproject/master', { + tree: [file('fileName')], + loading: false, + }); + + vm = createComponentWithStore(IdeRepoTree, store).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders list of files', () => { + expect(vm.$el.textContent).toContain('fileName'); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/detail/description_spec.js b/spec/javascripts/ide/components/jobs/detail/description_spec.js new file mode 100644 index 00000000000..9b715a41499 --- /dev/null +++ b/spec/javascripts/ide/components/jobs/detail/description_spec.js @@ -0,0 +1,28 @@ +import Vue from 'vue'; +import Description from '~/ide/components/jobs/detail/description.vue'; +import mountComponent from '../../../../helpers/vue_mount_component_helper'; +import { jobs } from '../../../mock_data'; + +describe('IDE job description', () => { + const Component = Vue.extend(Description); + let vm; + + beforeEach(() => { + vm = mountComponent(Component, { + job: jobs[0], + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders job details', () => { + expect(vm.$el.textContent).toContain('#1'); + expect(vm.$el.textContent).toContain('test'); + }); + + it('renders CI icon', () => { + expect(vm.$el.querySelector('.ci-status-icon .ic-status_passed_borderless')).not.toBe(null); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js b/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js new file mode 100644 index 00000000000..fff382a107f --- /dev/null +++ b/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js @@ -0,0 +1,59 @@ +import Vue from 'vue'; +import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue'; +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +describe('IDE job log scroll button', () => { + const Component = Vue.extend(ScrollButton); + let vm; + + beforeEach(() => { + vm = mountComponent(Component, { + direction: 'up', + disabled: false, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('iconName', () => { + ['up', 'down'].forEach(direction => { + it(`returns icon name for ${direction}`, () => { + vm.direction = direction; + + expect(vm.iconName).toBe(`scroll_${direction}`); + }); + }); + }); + + describe('tooltipTitle', () => { + it('returns title for up', () => { + expect(vm.tooltipTitle).toBe('Scroll to top'); + }); + + it('returns title for down', () => { + vm.direction = 'down'; + + expect(vm.tooltipTitle).toBe('Scroll to bottom'); + }); + }); + + it('emits click event on click', () => { + spyOn(vm, '$emit'); + + vm.$el.querySelector('.btn-scroll').click(); + + expect(vm.$emit).toHaveBeenCalledWith('click'); + }); + + it('disables button when disabled is true', done => { + vm.disabled = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.btn-scroll').hasAttribute('disabled')).toBe(true); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/detail_spec.js b/spec/javascripts/ide/components/jobs/detail_spec.js new file mode 100644 index 00000000000..8f8d4b9709e --- /dev/null +++ b/spec/javascripts/ide/components/jobs/detail_spec.js @@ -0,0 +1,185 @@ +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'; + +describe('IDE jobs detail view', () => { + const Component = Vue.extend(JobDetail); + let vm; + + beforeEach(() => { + const store = createStore(); + + store.state.pipelines.detailJob = { + ...jobs[0], + isLoading: true, + output: 'testing', + rawPath: `${gl.TEST_HOST}/raw`, + }; + + vm = createComponentWithStore(Component, store); + + spyOn(vm, 'fetchJobTrace').and.returnValue(Promise.resolve()); + + vm = vm.$mount(); + + spyOn(vm.$refs.buildTrace, 'scrollTo'); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('calls fetchJobTrace on mount', () => { + expect(vm.fetchJobTrace).toHaveBeenCalled(); + }); + + it('scrolls to bottom on mount', done => { + setTimeout(() => { + expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalled(); + + done(); + }); + }); + + 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', () => { + spyOn(vm, 'setDetailJob'); + + vm.$el.querySelector('.btn').click(); + + expect(vm.setDetailJob).toHaveBeenCalledWith(null); + }); + + it('renders raw path link', () => { + expect(vm.$el.querySelector('.controllers-buttons').getAttribute('href')).toBe( + `${gl.TEST_HOST}/raw`, + ); + }); + + describe('scroll buttons', () => { + it('triggers scrollDown when clicking down button', done => { + spyOn(vm, 'scrollDown'); + + vm.$el.querySelectorAll('.btn-scroll')[1].click(); + + vm.$nextTick(() => { + expect(vm.scrollDown).toHaveBeenCalled(); + + done(); + }); + }); + + it('triggers scrollUp when clicking up button', done => { + spyOn(vm, 'scrollUp'); + + vm.scrollPos = 1; + + vm + .$nextTick() + .then(() => vm.$el.querySelector('.btn-scroll').click()) + .then(() => vm.$nextTick()) + .then(() => { + expect(vm.scrollUp).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('scrollDown', () => { + it('scrolls build trace to bottom', () => { + spyOnProperty(vm.$refs.buildTrace, 'scrollHeight').and.returnValue(1000); + + vm.scrollDown(); + + expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 1000); + }); + }); + + describe('scrollUp', () => { + it('scrolls build trace to top', () => { + vm.scrollUp(); + + expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 0); + }); + }); + + describe('scrollBuildLog', () => { + beforeEach(() => { + spyOnProperty(vm.$refs.buildTrace, 'offsetHeight').and.returnValue(100); + spyOnProperty(vm.$refs.buildTrace, 'scrollHeight').and.returnValue(200); + }); + + it('sets scrollPos to bottom when at the bottom', done => { + spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(100); + + vm.scrollBuildLog(); + + setTimeout(() => { + expect(vm.scrollPos).toBe(1); + + done(); + }); + }); + + it('sets scrollPos to top when at the top', done => { + spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(0); + vm.scrollPos = 1; + + vm.scrollBuildLog(); + + setTimeout(() => { + expect(vm.scrollPos).toBe(0); + + done(); + }); + }); + + it('resets scrollPos when not at top or bottom', done => { + spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(10); + + vm.scrollBuildLog(); + + setTimeout(() => { + expect(vm.scrollPos).toBe(''); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/item_spec.js b/spec/javascripts/ide/components/jobs/item_spec.js new file mode 100644 index 00000000000..79e07f00e7b --- /dev/null +++ b/spec/javascripts/ide/components/jobs/item_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import JobItem from '~/ide/components/jobs/item.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; +import { jobs } from '../../mock_data'; + +describe('IDE jobs item', () => { + const Component = Vue.extend(JobItem); + const job = jobs[0]; + let vm; + + beforeEach(() => { + vm = mountComponent(Component, { + job, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders job details', () => { + expect(vm.$el.textContent).toContain(job.name); + expect(vm.$el.textContent).toContain(`#${job.id}`); + }); + + it('renders CI icon', () => { + expect(vm.$el.querySelector('.ic-status_passed_borderless')).not.toBe(null); + }); + + it('does not render view logs button if not started', done => { + vm.job.started = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.btn')).toBe(null); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/list_spec.js b/spec/javascripts/ide/components/jobs/list_spec.js new file mode 100644 index 00000000000..b24853c56fa --- /dev/null +++ b/spec/javascripts/ide/components/jobs/list_spec.js @@ -0,0 +1,67 @@ +import Vue from 'vue'; +import StageList from '~/ide/components/jobs/list.vue'; +import { createStore } from '~/ide/stores'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { stages, jobs } from '../../mock_data'; + +describe('IDE stages list', () => { + const Component = Vue.extend(StageList); + let vm; + + beforeEach(() => { + const store = createStore(); + + vm = createComponentWithStore(Component, store, { + stages: stages.map((mappedState, i) => ({ + ...mappedState, + id: i, + dropdownPath: mappedState.dropdown_path, + jobs: [...jobs], + isLoading: false, + isCollapsed: false, + })), + loading: false, + }); + + spyOn(vm, 'fetchJobs'); + spyOn(vm, 'toggleStageCollapsed'); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders list of stages', () => { + expect(vm.$el.querySelectorAll('.card').length).toBe(2); + }); + + it('renders loading icon when no stages & is loading', done => { + vm.stages = []; + vm.loading = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + + done(); + }); + }); + + it('calls toggleStageCollapsed when clicking stage header', done => { + vm.$el.querySelector('.card-header').click(); + + vm.$nextTick(() => { + expect(vm.toggleStageCollapsed).toHaveBeenCalledWith(0); + + done(); + }); + }); + + it('calls fetchJobs when stage is mounted', () => { + expect(vm.fetchJobs.calls.count()).toBe(stages.length); + + expect(vm.fetchJobs.calls.argsFor(0)).toEqual([vm.stages[0]]); + expect(vm.fetchJobs.calls.argsFor(1)).toEqual([vm.stages[1]]); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/stage_spec.js b/spec/javascripts/ide/components/jobs/stage_spec.js new file mode 100644 index 00000000000..fc3831f2d05 --- /dev/null +++ b/spec/javascripts/ide/components/jobs/stage_spec.js @@ -0,0 +1,95 @@ +import Vue from 'vue'; +import Stage from '~/ide/components/jobs/stage.vue'; +import { stages, jobs } from '../../mock_data'; + +describe('IDE pipeline stage', () => { + const Component = Vue.extend(Stage); + let vm; + let stage; + + beforeEach(() => { + stage = { + ...stages[0], + id: 0, + dropdownPath: stages[0].dropdown_path, + jobs: [...jobs], + isLoading: false, + isCollapsed: false, + }; + + vm = new Component({ + propsData: { stage }, + }); + + spyOn(vm, '$emit'); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('emits fetch event when mounted', () => { + expect(vm.$emit).toHaveBeenCalledWith('fetch', vm.stage); + }); + + it('renders stages details', () => { + expect(vm.$el.textContent).toContain(vm.stage.name); + }); + + it('renders CI icon', () => { + expect(vm.$el.querySelector('.ic-status_failed')).not.toBe(null); + }); + + describe('collapsed', () => { + it('emits event when clicking header', done => { + vm.$el.querySelector('.card-header').click(); + + vm.$nextTick(() => { + expect(vm.$emit).toHaveBeenCalledWith('toggleCollapsed', vm.stage.id); + + done(); + }); + }); + + it('toggles collapse status when collapsed', done => { + vm.stage.isCollapsed = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.card-body').style.display).toBe('none'); + + done(); + }); + }); + + it('sets border bottom class when collapsed', done => { + vm.stage.isCollapsed = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.card-header').classList).toContain('border-bottom-0'); + + done(); + }); + }); + }); + + it('renders jobs count', () => { + expect(vm.$el.querySelector('.badge').textContent).toContain('4'); + }); + + it('renders loading icon when no jobs and isLoading is true', done => { + vm.stage.isLoading = true; + vm.stage.jobs = []; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + + done(); + }); + }); + + it('renders list of jobs', () => { + expect(vm.$el.querySelectorAll('.ide-job-item').length).toBe(4); + }); +}); diff --git a/spec/javascripts/ide/components/merge_requests/dropdown_spec.js b/spec/javascripts/ide/components/merge_requests/dropdown_spec.js new file mode 100644 index 00000000000..74884c9a362 --- /dev/null +++ b/spec/javascripts/ide/components/merge_requests/dropdown_spec.js @@ -0,0 +1,47 @@ +import Vue from 'vue'; +import { createStore } from '~/ide/stores'; +import Dropdown from '~/ide/components/merge_requests/dropdown.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { mergeRequests } from '../../mock_data'; + +describe('IDE merge requests dropdown', () => { + const Component = Vue.extend(Dropdown); + let vm; + + beforeEach(() => { + const store = createStore(); + + vm = createComponentWithStore(Component, store, { show: false }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('does not render tabs when show is false', () => { + expect(vm.$el.querySelector('.nav-links')).toBe(null); + }); + + describe('when show is true', () => { + beforeEach(done => { + vm.show = true; + vm.$store.state.mergeRequests.assigned.mergeRequests.push(mergeRequests[0]); + + vm.$nextTick(done); + }); + + it('renders tabs', () => { + expect(vm.$el.querySelector('.nav-links')).not.toBe(null); + }); + + it('renders count for assigned & created data', () => { + expect(vm.$el.querySelector('.nav-links a').textContent).toContain('Created by me'); + expect(vm.$el.querySelector('.nav-links a .badge').textContent).toContain('0'); + + expect(vm.$el.querySelectorAll('.nav-links a')[1].textContent).toContain('Assigned to me'); + expect( + vm.$el.querySelectorAll('.nav-links a')[1].querySelector('.badge').textContent, + ).toContain('1'); + }); + }); +}); diff --git a/spec/javascripts/ide/components/merge_requests/item_spec.js b/spec/javascripts/ide/components/merge_requests/item_spec.js new file mode 100644 index 00000000000..51c4cddef2f --- /dev/null +++ b/spec/javascripts/ide/components/merge_requests/item_spec.js @@ -0,0 +1,61 @@ +import Vue from 'vue'; +import Item from '~/ide/components/merge_requests/item.vue'; +import mountCompontent from '../../../helpers/vue_mount_component_helper'; + +describe('IDE merge request item', () => { + const Component = Vue.extend(Item); + let vm; + + beforeEach(() => { + vm = mountCompontent(Component, { + item: { + iid: 1, + projectPathWithNamespace: 'gitlab-org/gitlab-ce', + title: 'Merge request title', + }, + currentId: '1', + currentProjectId: 'gitlab-org/gitlab-ce', + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders merge requests data', () => { + expect(vm.$el.textContent).toContain('Merge request title'); + expect(vm.$el.textContent).toContain('gitlab-org/gitlab-ce!1'); + }); + + it('renders icon if ID matches currentId', () => { + expect(vm.$el.querySelector('.ic-mobile-issue-close')).not.toBe(null); + }); + + 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); + + done(); + }); + }); + + 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); + + done(); + }); + }); + + it('emits click event on click', () => { + spyOn(vm, '$emit'); + + vm.$el.click(); + + expect(vm.$emit).toHaveBeenCalledWith('click', vm.item); + }); +}); diff --git a/spec/javascripts/ide/components/merge_requests/list_spec.js b/spec/javascripts/ide/components/merge_requests/list_spec.js new file mode 100644 index 00000000000..f4b393778dc --- /dev/null +++ b/spec/javascripts/ide/components/merge_requests/list_spec.js @@ -0,0 +1,126 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import List from '~/ide/components/merge_requests/list.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { mergeRequests } from '../../mock_data'; +import { resetStore } from '../../helpers'; + +describe('IDE merge requests list', () => { + const Component = Vue.extend(List); + let vm; + + beforeEach(() => { + vm = createComponentWithStore(Component, store, { + type: 'created', + emptyText: 'empty text', + }); + + spyOn(vm, 'fetchMergeRequests'); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('calls fetch on mounted', () => { + expect(vm.fetchMergeRequests).toHaveBeenCalledWith({ + type: 'created', + search: '', + }); + }); + + it('renders loading icon', done => { + vm.$store.state.mergeRequests.created.isLoading = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + + done(); + }); + }); + + it('renders empty text when no merge requests exist', () => { + expect(vm.$el.textContent).toContain('empty text'); + }); + + it('renders no search results text when search is not empty', done => { + vm.search = 'testing'; + + vm.$nextTick(() => { + expect(vm.$el.textContent).toContain('No merge requests found'); + + done(); + }); + }); + + describe('with merge requests', () => { + beforeEach(done => { + vm.$store.state.mergeRequests.created.mergeRequests.push({ + ...mergeRequests[0], + projectPathWithNamespace: 'gitlab-org/gitlab-ce', + }); + + vm.$nextTick(done); + }); + + it('renders list', () => { + expect(vm.$el.querySelectorAll('li').length).toBe(1); + expect(vm.$el.querySelector('li').textContent).toContain(mergeRequests[0].title); + }); + + it('calls openMergeRequest when clicking merge request', done => { + spyOn(vm, 'openMergeRequest'); + vm.$el.querySelector('li button').click(); + + vm.$nextTick(() => { + expect(vm.openMergeRequest).toHaveBeenCalledWith({ + projectPath: 'gitlab-org/gitlab-ce', + id: 1, + }); + + done(); + }); + }); + }); + + describe('focusSearch', () => { + it('focuses search input when loading is false', done => { + spyOn(vm.$refs.searchInput, 'focus'); + + vm.$store.state.mergeRequests.created.isLoading = false; + vm.focusSearch(); + + vm.$nextTick(() => { + expect(vm.$refs.searchInput.focus).toHaveBeenCalled(); + + done(); + }); + }); + }); + + describe('searchMergeRequests', () => { + beforeEach(() => { + spyOn(vm, 'loadMergeRequests'); + + jasmine.clock().install(); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + }); + + it('calls loadMergeRequests on input in search field', () => { + const event = new Event('input'); + + vm.$el.querySelector('input').dispatchEvent(event); + + jasmine.clock().tick(300); + + expect(vm.loadMergeRequests).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js index e08abe7d849..7b637f37eba 100644 --- a/spec/javascripts/ide/components/new_dropdown/index_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js @@ -32,12 +32,8 @@ describe('new dropdown component', () => { 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', - ); + expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('Upload file'); + expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory'); }); describe('createNewItem', () => { @@ -81,4 +77,18 @@ describe('new dropdown component', () => { .catch(done.fail); }); }); + + describe('dropdownOpen', () => { + it('scrolls dropdown into view', done => { + spyOn(vm.$refs.dropdownMenu, 'scrollIntoView'); + + vm.dropdownOpen = true; + + setTimeout(() => { + expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalled(); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js index a6e1e5a0d35..f362ed4db65 100644 --- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js @@ -25,25 +25,17 @@ describe('new file modal component', () => { it(`sets modal title as ${type}`, () => { const title = type === 'tree' ? 'directory' : 'file'; - expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe( - `Create new ${title}`, - ); + 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}`, - ); + 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`, - ); + expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe('Name'); }); describe('createEntryInStore', () => { diff --git a/spec/javascripts/ide/components/pipelines/list_spec.js b/spec/javascripts/ide/components/pipelines/list_spec.js new file mode 100644 index 00000000000..68487733cb9 --- /dev/null +++ b/spec/javascripts/ide/components/pipelines/list_spec.js @@ -0,0 +1,120 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { createStore } from '~/ide/stores'; +import List from '~/ide/components/pipelines/list.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { pipelines, projectData, stages, jobs } from '../../mock_data'; + +describe('IDE pipelines list', () => { + const Component = Vue.extend(List); + let vm; + let mock; + + beforeEach(done => { + const store = createStore(); + + mock = new MockAdapter(axios); + + store.state.currentProjectId = 'abc/def'; + store.state.currentBranchId = 'master'; + store.state.projects['abc/def'] = { + ...projectData, + path_with_namespace: 'abc/def', + branches: { + master: { commit: { id: '123' } }, + }, + }; + store.state.links = { ciHelpPagePath: gl.TEST_HOST }; + store.state.pipelinesEmptyStateSvgPath = gl.TEST_HOST; + store.state.pipelines.stages = stages.map((mappedState, i) => ({ + ...mappedState, + id: i, + dropdownPath: mappedState.dropdown_path, + jobs: [...jobs], + isLoading: false, + isCollapsed: false, + })); + + mock + .onGet('/abc/def/commit/123/pipelines') + .replyOnce(200, { pipelines: [...pipelines] }, { 'poll-interval': '-1' }); + + vm = createComponentWithStore(Component, store).$mount(); + + setTimeout(done); + }); + + afterEach(done => { + vm.$destroy(); + mock.restore(); + + vm.$store + .dispatch('pipelines/stopPipelinePolling') + .then(() => vm.$store.dispatch('pipelines/clearEtagPoll')) + .then(done) + .catch(done.fail); + }); + + it('renders pipeline data', () => { + expect(vm.$el.textContent).toContain('#1'); + }); + + it('renders CI icon', () => { + expect(vm.$el.querySelector('.ci-status-icon-failed')).not.toBe(null); + }); + + it('renders list of jobs', () => { + expect(vm.$el.querySelectorAll('.tab-pane:first-child .ide-job-item').length).toBe( + jobs.length * stages.length, + ); + }); + + it('renders list of failed jobs on failed jobs tab', done => { + vm.$el.querySelectorAll('.tab-links a')[1].click(); + + vm.$nextTick(() => { + expect(vm.$el.querySelectorAll('.tab-pane.active .ide-job-item').length).toBe(2); + + done(); + }); + }); + + describe('YAML error', () => { + it('renders YAML error', done => { + vm.$store.state.pipelines.latestPipeline.yamlError = 'test yaml error'; + + vm.$nextTick(() => { + expect(vm.$el.textContent).toContain('Found errors in your .gitlab-ci.yml:'); + expect(vm.$el.textContent).toContain('test yaml error'); + + done(); + }); + }); + }); + + describe('empty state', () => { + it('renders pipelines empty state', done => { + vm.$store.state.pipelines.latestPipeline = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.empty-state')).not.toBe(null); + + done(); + }); + }); + }); + + describe('loading state', () => { + it('renders loading state when there is no latest pipeline', done => { + vm.$store.state.pipelines.latestPipeline = null; + vm.$store.state.pipelines.isLoadingPipeline = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js index 113ade269e9..6bf309fb4bf 100644 --- a/spec/javascripts/ide/components/repo_commit_section_spec.js +++ b/spec/javascripts/ide/components/repo_commit_section_spec.js @@ -1,9 +1,9 @@ import Vue from 'vue'; import store from '~/ide/stores'; import service from '~/ide/services'; +import router from '~/ide/ide_router'; 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', () => { @@ -12,10 +12,10 @@ describe('RepoCommitSection', () => { function createComponent() { const Component = Vue.extend(repoCommitSection); - vm = createComponentWithStore(Component, store, { - noChangesStateSvgPath: 'svg', - committedStateSvgPath: 'commitsvg', - }); + store.state.noChangesStateSvgPath = 'svg'; + store.state.committedStateSvgPath = 'commitsvg'; + + vm = createComponentWithStore(Component, store); vm.$store.state.currentProjectId = 'abcproject'; vm.$store.state.currentBranchId = 'master'; @@ -28,22 +28,46 @@ describe('RepoCommitSection', () => { }, }; + const files = [file('file1'), file('file2')].map(f => + Object.assign(f, { + type: 'blob', + }), + ); + vm.$store.state.rightPanelCollapsed = false; vm.$store.state.currentBranch = 'master'; - vm.$store.state.changedFiles = [file('file1'), file('file2')]; + vm.$store.state.changedFiles = [...files]; vm.$store.state.changedFiles.forEach(f => Object.assign(f, { changed: true, + content: 'changedFile testing', + }), + ); + + vm.$store.state.stagedFiles = [{ ...files[0] }, { ...files[1] }]; + vm.$store.state.stagedFiles.forEach(f => + Object.assign(f, { + changed: true, content: 'testing', }), ); - return vm.$mount(); + vm.$store.state.changedFiles.forEach(f => { + vm.$store.state.entries[f.path] = f; + }); + + return vm; } beforeEach(done => { + spyOn(router, 'push'); + vm = createComponent(); + spyOn(vm, 'openPendingTab').and.callThrough(); + + vm.$mount(); + spyOn(service, 'getTreeData').and.returnValue( Promise.resolve({ headers: { @@ -75,99 +99,98 @@ describe('RepoCommitSection', () => { resetStore(vm.$store); const Component = Vue.extend(repoCommitSection); - vm = createComponentWithStore(Component, store, { - noChangesStateSvgPath: 'nochangessvg', - committedStateSvgPath: 'svg', - }).$mount(); + store.state.noChangesStateSvgPath = 'nochangessvg'; + store.state.committedStateSvgPath = 'svg'; - expect( - vm.$el.querySelector('.js-empty-state').textContent.trim(), - ).toContain('No changes'); - expect( - vm.$el.querySelector('.js-empty-state img').getAttribute('src'), - ).toBe('nochangessvg'); + vm.$destroy(); + vm = createComponentWithStore(Component, store).$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'); + const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list > li')]; + const allFiles = vm.$store.state.changedFiles.concat(vm.$store.state.stagedFiles); - expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull(); - expect(changedFileElements.length).toEqual(2); + expect(changedFileElements.length).toEqual(4); changedFileElements.forEach((changedFile, i) => { - expect(changedFile.textContent.trim()).toContain( - vm.$store.state.changedFiles[i].path, - ); + expect(changedFile.textContent.trim()).toContain(allFiles[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() + it('adds changed files into staged files', done => { + vm.$el.querySelector('.multi-file-discard-btn .btn').click(); + vm + .$nextTick() + .then(() => vm.$el.querySelector('.multi-file-discard-btn .btn').click()) + .then(vm.$nextTick) .then(() => { - expect(vm.$store.state.commit.commitMessage).toBe( - 'testing commit message', + expect(vm.$el.querySelector('.ide-commit-list-container').textContent).toContain( + 'No changes', ); }) .then(done) .catch(done.fail); }); - describe('discard draft button', () => { - it('hidden when commitMessage is empty', () => { + it('stages a single file', done => { + vm.$el.querySelector('.multi-file-discard-btn .btn').click(); + + Vue.nextTick(() => { expect( - vm.$el.querySelector('.multi-file-commit-form .btn-default'), - ).toBeNull(); + vm.$el + .querySelector('.ide-commit-list-container') + .querySelectorAll('.multi-file-commit-list > li').length, + ).toBe(1); + + done(); }); + }); + + it('discards a single file', done => { + vm.$el.querySelector('.multi-file-discard-btn .dropdown-menu button').click(); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1'); + expect( + vm.$el + .querySelector('.ide-commit-list-container') + .querySelectorAll('.multi-file-commit-list > li').length, + ).toBe(1); + + done(); + }); + }); + + it('unstages a single file', done => { + vm.$el + .querySelectorAll('.multi-file-discard-btn')[2] + .querySelector('.btn') + .click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelectorAll('.ide-commit-list-container')[1].querySelectorAll('li').length, + ).toBe(1); - 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); + done(); }); }); - describe('when submitting', () => { - beforeEach(() => { - spyOn(vm, 'commitChanges'); + describe('mounted', () => { + it('opens last opened file', () => { + expect(store.state.openFiles.length).toBe(1); + expect(store.state.openFiles[0].pending).toBe(true); }); - 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); + it('calls openPendingTab', () => { + expect(vm.openPendingTab).toHaveBeenCalledWith({ + file: vm.lastOpenedFile, + keyPrefix: 'unstaged', + }); }); }); }); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index ae657e8c881..2256deb7dac 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -1,9 +1,12 @@ import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; 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 { activityBarViews } from '~/ide/constants'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import setTimeoutPromise from '../../helpers/set_timeout_promise_helper'; import { file, resetStore } from '../helpers'; describe('RepoEditor', () => { @@ -19,16 +22,12 @@ describe('RepoEditor', () => { 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; + Vue.set(vm.$store.state.entries, f.path, f); vm.$mount(); - monacoLoader(['vs/editor/editor.main'], () => { - setTimeout(done, 0); - }); + Vue.nextTick(() => setTimeout(done)); }); afterEach(() => { @@ -36,7 +35,7 @@ describe('RepoEditor', () => { resetStore(vm.$store); - Editor.editorInstance.modelManager.dispose(); + Editor.editorInstance.dispose(); }); it('renders an ide container', done => { @@ -47,6 +46,95 @@ describe('RepoEditor', () => { }); }); + 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', () => { + beforeEach(done => { + vm.file.previewMode = { + id: 'markdown', + previewTitle: 'Preview Markdown', + }; + + vm.$nextTick(done); + }); + + 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(); + }); + }); + }); + + describe('when file is markdown and viewer mode is review', () => { + let mock; + + beforeEach(done => { + mock = new MockAdapter(axios); + + vm.file.projectId = 'namespace/project'; + vm.file.previewMode = { + id: 'markdown', + previewTitle: 'Preview Markdown', + }; + vm.file.content = 'testing 123'; + vm.$store.state.viewer = 'diff'; + + mock.onPost(/(.*)\/preview_markdown/).reply(200, { + body: '<p>testing 123</p>', + }); + + vm.$nextTick(done); + }); + + 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('Review'); + expect(tabs[1].textContent.trim()).toBe('Preview Markdown'); + + done(); + }); + }); + + it('renders markdown for tempFile', done => { + vm.file.tempFile = true; + vm.file.path = `${vm.file.path}.md`; + vm.$store.state.entries[vm.file.path] = vm.file; + + vm + .$nextTick() + .then(() => { + vm.$el.querySelectorAll('.ide-mode-tabs .nav-links a')[1].click(); + }) + .then(setTimeoutPromise) + .then(() => { + expect(vm.$el.querySelector('.preview-container').innerHTML).toContain( + '<p>testing 123</p>', + ); + }) + .then(done) + .catch(done.fail); + }); + }); + describe('when open file is binary and not raw', () => { beforeEach(done => { vm.file.binary = true; @@ -57,10 +145,6 @@ describe('RepoEditor', () => { it('does not render the IDE', () => { expect(vm.shouldHideEditor).toBeTruthy(); }); - - it('shows activeFile html', () => { - expect(vm.$el.textContent).toContain('testing'); - }); }); describe('createEditorInstance', () => { @@ -89,6 +173,20 @@ describe('RepoEditor', () => { done(); }); }); + + it('calls createDiffInstance when viewer is a merge request diff', done => { + vm.$store.state.viewer = 'mrdiff'; + + spyOn(vm.editor, 'createDiffInstance'); + + vm.createEditorInstance(); + + vm.$nextTick(() => { + expect(vm.editor.createDiffInstance).toHaveBeenCalled(); + + done(); + }); + }); }); describe('setupEditor', () => { @@ -99,7 +197,7 @@ describe('RepoEditor', () => { vm.setupEditor(); - expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file); + expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, null); expect(vm.model).not.toBeNull(); }); @@ -113,6 +211,30 @@ describe('RepoEditor', () => { expect(vm.editor.attachModel).toHaveBeenCalledWith(vm.model); }); + it('attaches model to merge request editor', () => { + vm.$store.state.viewer = 'mrdiff'; + vm.file.mrChange = true; + spyOn(vm.editor, 'attachMergeRequestModel'); + + 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; + spyOn(vm.editor, 'attachMergeRequestModel'); + + Editor.editorInstance.modelManager.dispose(); + + vm.setupEditor(); + + expect(vm.editor.attachMergeRequestModel).not.toHaveBeenCalledWith(vm.model); + }); + it('adds callback methods', () => { spyOn(vm.editor, 'onPositionChange').and.callThrough(); @@ -121,7 +243,7 @@ describe('RepoEditor', () => { vm.setupEditor(); expect(vm.editor.onPositionChange).toHaveBeenCalled(); - expect(vm.model.events.size).toBe(1); + expect(vm.model.events.size).toBe(2); }); it('updates state when model content changed', done => { @@ -133,5 +255,122 @@ describe('RepoEditor', () => { done(); }); }); + + it('sets head model as staged file', () => { + spyOn(vm.editor, 'createModel').and.callThrough(); + + 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(() => { + spyOn(vm.editor, 'updateDimensions').and.callThrough(); + spyOn(vm.editor, 'updateDiffView'); + }); + + it('calls updateDimensions when rightPanelCollapsed is changed', done => { + vm.$store.state.rightPanelCollapsed = true; + + vm.$nextTick(() => { + expect(vm.editor.updateDimensions).toHaveBeenCalled(); + expect(vm.editor.updateDiffView).toHaveBeenCalled(); + + done(); + }); + }); + + 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 updated', done => { + vm.$store.state.rightPane = 'testing'; + + 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 = activityBarViews.review; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.nav-links')).toBe(null); + + done(); + }); + }); + + it('hides tabs in commit mode', done => { + vm.$store.state.currentActivityView = activityBarViews.commit; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.nav-links')).toBe(null); + + done(); + }); + }); + }); + + it('calls removePendingTab when old file is pending', done => { + spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true); + spyOn(vm, 'removePendingTab'); + + vm.file.pending = true; + + vm + .$nextTick() + .then(() => { + vm.file = file('testing'); + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.removePendingTab).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); }); }); diff --git a/spec/javascripts/ide/components/repo_file_buttons_spec.js b/spec/javascripts/ide/components/repo_file_buttons_spec.js deleted file mode 100644 index c86bdb132b4..00000000000 --- a/spec/javascripts/ide/components/repo_file_buttons_spec.js +++ /dev/null @@ -1,47 +0,0 @@ -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 index ff391cb4351..156233653ab 100644 --- a/spec/javascripts/ide/components/repo_file_spec.js +++ b/spec/javascripts/ide/components/repo_file_spec.js @@ -48,6 +48,70 @@ describe('RepoFile', () => { }); }); + describe('folder', () => { + it('renders changes count inside folder', () => { + const f = { + ...file('folder'), + path: 'testing', + type: 'tree', + branchId: 'master', + projectId: 'project', + }; + + store.state.changedFiles.push({ + ...file('fileName'), + path: 'testing/fileName', + }); + + createComponent({ + file: f, + level: 0, + }); + + const treeChangesEl = vm.$el.querySelector('.ide-tree-changes'); + + expect(treeChangesEl).not.toBeNull(); + expect(treeChangesEl.textContent).toContain('1'); + }); + + it('renders action dropdown', done => { + createComponent({ + file: { + ...file('t4'), + type: 'tree', + branchId: 'master', + projectId: 'project', + }, + level: 0, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.ide-new-btn')).not.toBeNull(); + + done(); + }); + }); + + it('disables action dropdown', done => { + createComponent({ + file: { + ...file('t4'), + type: 'tree', + branchId: 'master', + projectId: 'project', + }, + level: 0, + disableActionDropdown: true, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.ide-new-btn')).toBeNull(); + + done(); + }); + }); + }); + describe('locked file', () => { let f; @@ -72,8 +136,7 @@ describe('RepoFile', () => { it('renders a tooltip', () => { expect( - vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset - .originalTitle, + 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 index 8f9644216bc..7c20b8302f9 100644 --- a/spec/javascripts/ide/components/repo_loading_file_spec.js +++ b/spec/javascripts/ide/components/repo_loading_file_spec.js @@ -27,7 +27,7 @@ describe('RepoLoadingFile', () => { const lines = [...container.querySelectorAll(':scope > div')]; expect(container).toBeTruthy(); - expect(lines.length).toEqual(6); + expect(lines.length).toEqual(3); assertLines(lines); }); } diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js index ddb5204e3a7..fc0695a4263 100644 --- a/spec/javascripts/ide/components/repo_tab_spec.js +++ b/spec/javascripts/ide/components/repo_tab_spec.js @@ -38,6 +38,26 @@ describe('RepoTab', () => { expect(name.textContent.trim()).toEqual(vm.tab.name); }); + it('does not call openPendingTab when tab is active', done => { + vm = createComponent({ + tab: { + ...file(), + pending: true, + active: true, + }, + }); + + spyOn(vm, 'openPendingTab'); + + vm.$el.click(); + + vm.$nextTick(() => { + expect(vm.openPendingTab).not.toHaveBeenCalled(); + + done(); + }); + }); + it('fires clickFile when the link is clicked', () => { vm = createComponent({ tab: file(), @@ -59,7 +79,7 @@ describe('RepoTab', () => { vm.$el.querySelector('.multi-file-tab-close').click(); - expect(vm.closeFile).toHaveBeenCalledWith(vm.tab.path); + expect(vm.closeFile).toHaveBeenCalledWith(vm.tab); }); it('changes icon on hover', done => { @@ -112,9 +132,9 @@ describe('RepoTab', () => { }); it('renders a tooltip', () => { - expect( - vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle, - ).toContain('Locked by testuser'); + expect(vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle).toContain( + 'Locked by testuser', + ); }); }); diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js index ceb0416aff8..583f71e6121 100644 --- a/spec/javascripts/ide/components/repo_tabs_spec.js +++ b/spec/javascripts/ide/components/repo_tabs_spec.js @@ -17,6 +17,8 @@ describe('RepoTabs', () => { files: openedFiles, viewer: 'editor', hasChanges: false, + activeFile: file('activeFile'), + hasMergeRequest: false, }); openedFiles[0].active = true; @@ -24,58 +26,10 @@ describe('RepoTabs', () => { 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); + expect(tabs[0].parentNode.classList.contains('active')).toEqual(true); + expect(tabs[1].parentNode.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); - }); - }); }); |