diff options
Diffstat (limited to 'spec/frontend/ide/components')
28 files changed, 2204 insertions, 85 deletions
diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js new file mode 100644 index 00000000000..8b3853d4535 --- /dev/null +++ b/spec/frontend/ide/components/activity_bar_spec.js @@ -0,0 +1,72 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import { leftSidebarViews } 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('updateActivityBarView', () => { + beforeEach(() => { + jest.spyOn(vm, 'updateActivityBarView').mockImplementation(() => {}); + + vm.$mount(); + }); + + it('calls updateActivityBarView with edit value on click', () => { + vm.$el.querySelector('.js-ide-edit-mode').click(); + + expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.edit.name); + }); + + it('calls updateActivityBarView with commit value on click', () => { + vm.$el.querySelector('.js-ide-commit-mode').click(); + + expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.commit.name); + }); + + it('calls updateActivityBarView with review value on click', () => { + vm.$el.querySelector('.js-ide-review-mode').click(); + + expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.review.name); + }); + }); + + 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 = leftSidebarViews.commit.name; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active'); + + done(); + }); + }); + }); +}); diff --git a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js index a25aba61516..ff780939026 100644 --- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js @@ -7,27 +7,32 @@ import { file } from '../../helpers'; const localVue = createLocalVue(); localVue.use(Vuex); +const TEST_FILE_PATH = 'test/file/path'; + describe('IDE commit editor header', () => { let wrapper; - let f; let store; - const findDiscardModal = () => wrapper.find({ ref: 'discardModal' }); - const findDiscardButton = () => wrapper.find({ ref: 'discardButton' }); - - beforeEach(() => { - f = file('file'); - store = createStore(); - + const createComponent = (fileProps = {}) => { wrapper = mount(EditorHeader, { store, localVue, propsData: { - activeFile: f, + activeFile: { + ...file(TEST_FILE_PATH), + staged: true, + ...fileProps, + }, }, }); + }; - jest.spyOn(wrapper.vm, 'discardChanges').mockImplementation(); + const findDiscardModal = () => wrapper.find({ ref: 'discardModal' }); + const findDiscardButton = () => wrapper.find({ ref: 'discardButton' }); + + beforeEach(() => { + store = createStore(); + jest.spyOn(store, 'dispatch').mockImplementation(); }); afterEach(() => { @@ -35,29 +40,38 @@ describe('IDE commit editor header', () => { wrapper = null; }); - it('renders button to discard', () => { - expect(wrapper.vm.$el.querySelectorAll('.btn')).toHaveLength(1); + it.each` + fileProps | shouldExist + ${{ staged: false, changed: false }} | ${false} + ${{ staged: true, changed: false }} | ${true} + ${{ staged: false, changed: true }} | ${true} + ${{ staged: true, changed: true }} | ${true} + `('with $fileProps, show discard button is $shouldExist', ({ fileProps, shouldExist }) => { + createComponent(fileProps); + + expect(findDiscardButton().exists()).toBe(shouldExist); }); describe('discard button', () => { - let modal; - beforeEach(() => { - modal = findDiscardModal(); + createComponent(); + const modal = findDiscardModal(); jest.spyOn(modal.vm, 'show'); findDiscardButton().trigger('click'); }); it('opens a dialog confirming discard', () => { - expect(modal.vm.show).toHaveBeenCalled(); + expect(findDiscardModal().vm.show).toHaveBeenCalled(); }); it('calls discardFileChanges if dialog result is confirmed', () => { - modal.vm.$emit('ok'); + expect(store.dispatch).not.toHaveBeenCalled(); + + findDiscardModal().vm.$emit('ok'); - expect(wrapper.vm.discardChanges).toHaveBeenCalledWith(f.path); + expect(store.dispatch).toHaveBeenCalledWith('discardFileChanges', TEST_FILE_PATH); }); }); }); diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js index dfde69ab2df..129180bb46e 100644 --- a/spec/frontend/ide/components/commit_sidebar/form_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js @@ -1,6 +1,5 @@ import Vue from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import waitForPromises from 'helpers/wait_for_promises'; import { projectData } from 'jest/ide/mock_data'; import store from '~/ide/stores'; import CommitForm from '~/ide/components/commit_sidebar/form.vue'; @@ -31,10 +30,10 @@ describe('IDE commit form', () => { }); describe('compact', () => { - beforeEach(done => { + beforeEach(() => { vm.isCompact = true; - vm.$nextTick(done); + return vm.$nextTick(); }); it('renders commit button in compact mode', () => { @@ -46,95 +45,84 @@ describe('IDE commit form', () => { expect(vm.$el.querySelector('form')).toBeNull(); }); - it('renders overview text', done => { + it('renders overview text', () => { vm.$store.state.stagedFiles.push('test'); - vm.$nextTick(() => { + return vm.$nextTick(() => { expect(vm.$el.querySelector('p').textContent).toContain('1 changed file'); - done(); }); }); - it('shows form when clicking commit button', done => { + it('shows form when clicking commit button', () => { vm.$el.querySelector('.btn-primary').click(); - vm.$nextTick(() => { + return vm.$nextTick(() => { expect(vm.$el.querySelector('form')).not.toBeNull(); - - done(); }); }); - it('toggles activity bar view when clicking commit button', done => { + it('toggles activity bar view when clicking commit button', () => { vm.$el.querySelector('.btn-primary').click(); - vm.$nextTick(() => { + return vm.$nextTick(() => { expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name); - - done(); }); }); - it('collapses if lastCommitMsg is set to empty and current view is not commit view', done => { + it('collapses if lastCommitMsg is set to empty and current view is not commit view', () => { store.state.lastCommitMsg = 'abc'; store.state.currentActivityView = leftSidebarViews.edit.name; - vm.$nextTick(() => { - // if commit message is set, form is uncollapsed - expect(vm.isCompact).toBe(false); + return vm + .$nextTick() + .then(() => { + // if commit message is set, form is uncollapsed + expect(vm.isCompact).toBe(false); - store.state.lastCommitMsg = ''; + store.state.lastCommitMsg = ''; - vm.$nextTick(() => { + return vm.$nextTick(); + }) + .then(() => { // collapsed when set to empty expect(vm.isCompact).toBe(true); - - done(); }); - }); }); }); describe('full', () => { - beforeEach(done => { + beforeEach(() => { vm.isCompact = false; - vm.$nextTick(done); + return vm.$nextTick(); }); - it('updates commitMessage in store on input', done => { + it('updates commitMessage in store on input', () => { const textarea = vm.$el.querySelector('textarea'); textarea.value = 'testing commit message'; textarea.dispatchEvent(new Event('input')); - waitForPromises() - .then(() => { - expect(vm.$store.state.commit.commitMessage).toBe('testing commit message'); - }) - .then(done) - .catch(done.fail); + return vm.$nextTick().then(() => { + expect(vm.$store.state.commit.commitMessage).toBe('testing commit message'); + }); }); - it('updating currentActivityView not to commit view sets compact mode', done => { + it('updating currentActivityView not to commit view sets compact mode', () => { store.state.currentActivityView = 'a'; - vm.$nextTick(() => { + return vm.$nextTick(() => { expect(vm.isCompact).toBe(true); - - done(); }); }); - it('always opens itself in full view current activity view is not commit view when clicking commit button', done => { + it('always opens itself in full view current activity view is not commit view when clicking commit button', () => { vm.$el.querySelector('.btn-primary').click(); - vm.$nextTick(() => { + return vm.$nextTick(() => { expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name); expect(vm.isCompact).toBe(false); - - done(); }); }); @@ -143,41 +131,54 @@ describe('IDE commit form', () => { expect(vm.$el.querySelector('.btn-default').textContent).toContain('Collapse'); }); - it('resets commitMessage when clicking discard button', done => { + it('resets commitMessage when clicking discard button', () => { vm.$store.state.commit.commitMessage = 'testing commit message'; - waitForPromises() + return vm + .$nextTick() .then(() => { vm.$el.querySelector('.btn-default').click(); }) - .then(Vue.nextTick) + .then(() => vm.$nextTick()) .then(() => { expect(vm.$store.state.commit.commitMessage).not.toBe('testing commit message'); - }) - .then(done) - .catch(done.fail); + }); }); }); describe('when submitting', () => { beforeEach(() => { - jest.spyOn(vm, 'commitChanges').mockImplementation(() => {}); + jest.spyOn(vm, 'commitChanges'); + vm.$store.state.stagedFiles.push('test'); + vm.$store.state.commit.commitMessage = 'testing commit message'; }); - it('calls commitChanges', done => { - vm.$store.state.commit.commitMessage = 'testing commit message'; + it('calls commitChanges', () => { + vm.commitChanges.mockResolvedValue({ success: true }); + + return vm.$nextTick().then(() => { + vm.$el.querySelector('.btn-success').click(); + + expect(vm.commitChanges).toHaveBeenCalled(); + }); + }); + + it('opens new branch modal if commitChanges throws an error', () => { + vm.commitChanges.mockRejectedValue({ success: false }); - waitForPromises() + jest.spyOn(vm.$refs.createBranchModal, 'show').mockImplementation(); + + return vm + .$nextTick() .then(() => { vm.$el.querySelector('.btn-success').click(); + + return vm.$nextTick(); }) - .then(Vue.nextTick) .then(() => { - expect(vm.commitChanges).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + expect(vm.$refs.createBranchModal.show).toHaveBeenCalled(); + }); }); }); }); diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js index ee209487665..2b5664ffc4e 100644 --- a/spec/frontend/ide/components/commit_sidebar/list_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js @@ -21,8 +21,6 @@ describe('Multi-file editor commit sidebar list', () => { keyPrefix: 'staged', }); - vm.$store.state.rightPanelCollapsed = false; - vm.$mount(); }); diff --git a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js new file mode 100644 index 00000000000..ac80ba58056 --- /dev/null +++ b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js @@ -0,0 +1,134 @@ +import Vue from 'vue'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { resetStore } from 'jest/ide/helpers'; +import store from '~/ide/stores'; +import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue'; + +describe('IDE commit sidebar radio group', () => { + let vm; + + beforeEach(done => { + const Component = Vue.extend(radioGroup); + + store.state.commit.commitAction = '2'; + + vm = createComponentWithStore(Component, store, { + value: '1', + label: 'test', + checked: true, + }); + + vm.$mount(); + + Vue.nextTick(done); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('uses label if present', () => { + expect(vm.$el.textContent).toContain('test'); + }); + + it('uses slot if label is not present', done => { + vm.$destroy(); + + vm = new Vue({ + components: { + radioGroup, + }, + store, + render: createElement => + createElement('radio-group', { props: { value: '1' } }, 'Testing slot'), + }); + + vm.$mount(); + + Vue.nextTick(() => { + expect(vm.$el.textContent).toContain('Testing slot'); + + done(); + }); + }); + + it('updates store when changing radio button', done => { + vm.$el.querySelector('input').dispatchEvent(new Event('change')); + + Vue.nextTick(() => { + expect(store.state.commit.commitAction).toBe('1'); + + done(); + }); + }); + + describe('with input', () => { + beforeEach(done => { + vm.$destroy(); + + const Component = Vue.extend(radioGroup); + + store.state.commit.commitAction = '1'; + store.state.commit.newBranchName = 'test-123'; + + vm = createComponentWithStore(Component, store, { + value: '1', + label: 'test', + checked: true, + showInput: true, + }); + + vm.$mount(); + + Vue.nextTick(done); + }); + + it('renders input box when commitAction matches value', () => { + expect(vm.$el.querySelector('.form-control')).not.toBeNull(); + }); + + it('hides input when commitAction doesnt match value', done => { + store.state.commit.commitAction = '2'; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.form-control')).toBeNull(); + done(); + }); + }); + + it('updates branch name in store on input', done => { + const input = vm.$el.querySelector('.form-control'); + input.value = 'testing-123'; + input.dispatchEvent(new Event('input')); + + Vue.nextTick(() => { + expect(store.state.commit.newBranchName).toBe('testing-123'); + + done(); + }); + }); + + it('renders newBranchName if present', () => { + const input = vm.$el.querySelector('.form-control'); + + expect(input.value).toBe('test-123'); + }); + }); + + describe('tooltipTitle', () => { + it('returns title when disabled', () => { + vm.title = 'test title'; + vm.disabled = true; + + expect(vm.tooltipTitle).toBe('test title'); + }); + + it('returns blank when not disabled', () => { + vm.title = 'test title'; + + expect(vm.tooltipTitle).not.toBe('test title'); + }); + }); +}); diff --git a/spec/frontend/ide/components/file_row_extra_spec.js b/spec/frontend/ide/components/file_row_extra_spec.js new file mode 100644 index 00000000000..e78bacadebb --- /dev/null +++ b/spec/frontend/ide/components/file_row_extra_spec.js @@ -0,0 +1,170 @@ +import Vue from 'vue'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { createStore } from '~/ide/stores'; +import FileRowExtra from '~/ide/components/file_row_extra.vue'; +import { file, resetStore } from '../helpers'; + +describe('IDE extra file row component', () => { + let Component; + let vm; + let unstagedFilesCount = 0; + let stagedFilesCount = 0; + let changesCount = 0; + + beforeAll(() => { + Component = Vue.extend(FileRowExtra); + }); + + beforeEach(() => { + vm = createComponentWithStore(Component, createStore(), { + file: { + ...file('test'), + }, + dropdownOpen: false, + }); + + jest.spyOn(vm, 'getUnstagedFilesCountForPath', 'get').mockReturnValue(() => unstagedFilesCount); + jest.spyOn(vm, 'getStagedFilesCountForPath', 'get').mockReturnValue(() => stagedFilesCount); + jest.spyOn(vm, 'getChangesInFolder', 'get').mockReturnValue(() => changesCount); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + resetStore(vm.$store); + + stagedFilesCount = 0; + unstagedFilesCount = 0; + changesCount = 0; + }); + + describe('folderChangesTooltip', () => { + it('returns undefined when changes count is 0', () => { + changesCount = 0; + + expect(vm.folderChangesTooltip).toBe(undefined); + }); + + [{ input: 1, output: '1 changed file' }, { input: 2, output: '2 changed files' }].forEach( + ({ input, output }) => { + it('returns changed files count if changes count is not 0', () => { + changesCount = input; + + expect(vm.folderChangesTooltip).toBe(output); + }); + }, + ); + }); + + describe('show tree changes count', () => { + it('does not show for blobs', () => { + vm.file.type = 'blob'; + + expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null); + }); + + it('does not show when changes count is 0', () => { + vm.file.type = 'tree'; + + expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null); + }); + + it('does not show when tree is open', done => { + vm.file.type = 'tree'; + vm.file.opened = true; + changesCount = 1; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null); + + done(); + }); + }); + + it('shows for trees with changes', done => { + vm.file.type = 'tree'; + vm.file.opened = false; + changesCount = 1; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ide-tree-changes')).not.toBe(null); + + done(); + }); + }); + }); + + describe('changes file icon', () => { + it('hides when file is not changed', () => { + expect(vm.$el.querySelector('.file-changed-icon')).toBe(null); + }); + + it('shows when file is changed', done => { + vm.file.changed = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null); + + done(); + }); + }); + + it('shows when file is staged', done => { + vm.file.staged = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null); + + done(); + }); + }); + + it('shows when file is a tempFile', done => { + vm.file.tempFile = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null); + + done(); + }); + }); + + it('shows when file is renamed', done => { + vm.file.prevPath = 'original-file'; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null); + + done(); + }); + }); + + it('hides when file is renamed', done => { + vm.file.prevPath = 'original-file'; + vm.file.type = 'tree'; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.file-changed-icon')).toBe(null); + + done(); + }); + }); + }); + + describe('merge request icon', () => { + it('hides when not a merge request change', () => { + expect(vm.$el.querySelector('.ic-git-merge')).toBe(null); + }); + + it('shows when a merge request change', done => { + vm.file.mrChange = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ic-git-merge')).not.toBe(null); + + done(); + }); + }); + }); +}); diff --git a/spec/frontend/ide/components/file_templates/bar_spec.js b/spec/frontend/ide/components/file_templates/bar_spec.js new file mode 100644 index 00000000000..21dbe18a223 --- /dev/null +++ b/spec/frontend/ide/components/file_templates/bar_spec.js @@ -0,0 +1,117 @@ +import Vue from 'vue'; +import { mountComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { createStore } from '~/ide/stores'; +import Bar from '~/ide/components/file_templates/bar.vue'; +import { resetStore, file } from '../../helpers'; + +describe('IDE file templates bar component', () => { + let Component; + let vm; + + beforeAll(() => { + Component = Vue.extend(Bar); + }); + + beforeEach(() => { + const store = createStore(); + + store.state.openFiles.push({ + ...file('file'), + opened: true, + active: true, + }); + + vm = mountComponentWithStore(Component, { store }); + }); + + afterEach(() => { + vm.$destroy(); + resetStore(vm.$store); + }); + + describe('template type dropdown', () => { + it('renders dropdown component', () => { + expect(vm.$el.querySelector('.dropdown').textContent).toContain('Choose a type'); + }); + + it('calls setSelectedTemplateType when clicking item', () => { + jest.spyOn(vm, 'setSelectedTemplateType').mockImplementation(); + + vm.$el.querySelector('.dropdown-content button').click(); + + expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({ + name: '.gitlab-ci.yml', + key: 'gitlab_ci_ymls', + }); + }); + }); + + describe('template dropdown', () => { + beforeEach(done => { + vm.$store.state.fileTemplates.templates = [ + { + name: 'test', + }, + ]; + vm.$store.state.fileTemplates.selectedTemplateType = { + name: '.gitlab-ci.yml', + key: 'gitlab_ci_ymls', + }; + + vm.$nextTick(done); + }); + + it('renders dropdown component', () => { + expect(vm.$el.querySelectorAll('.dropdown')[1].textContent).toContain('Choose a template'); + }); + + it('calls fetchTemplate on click', () => { + jest.spyOn(vm, 'fetchTemplate').mockImplementation(); + + vm.$el + .querySelectorAll('.dropdown-content')[1] + .querySelector('button') + .click(); + + expect(vm.fetchTemplate).toHaveBeenCalledWith({ + name: 'test', + }); + }); + }); + + it('shows undo button if updateSuccess is true', done => { + vm.$store.state.fileTemplates.updateSuccess = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.btn-default').style.display).not.toBe('none'); + + done(); + }); + }); + + it('calls undoFileTemplate when clicking undo button', () => { + jest.spyOn(vm, 'undoFileTemplate').mockImplementation(); + + vm.$el.querySelector('.btn-default').click(); + + expect(vm.undoFileTemplate).toHaveBeenCalled(); + }); + + it('calls setSelectedTemplateType if activeFile name matches a template', done => { + const fileName = '.gitlab-ci.yml'; + + jest.spyOn(vm, 'setSelectedTemplateType').mockImplementation(() => {}); + vm.$store.state.openFiles[0].name = fileName; + + vm.setInitialType(); + + vm.$nextTick(() => { + expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({ + name: fileName, + key: 'gitlab_ci_ymls', + }); + + done(); + }); + }); +}); diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js new file mode 100644 index 00000000000..b56957e1f6d --- /dev/null +++ b/spec/frontend/ide/components/ide_review_spec.js @@ -0,0 +1,73 @@ +import Vue from 'vue'; +import IdeReview from '~/ide/components/ide_review.vue'; +import { createStore } from '~/ide/stores'; +import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { trimText } from '../../helpers/text_helper'; +import { resetStore, file } from '../helpers'; +import { projectData } from '../mock_data'; + +describe('IDE review mode', () => { + const Component = Vue.extend(IdeReview); + let vm; + let store; + + beforeEach(() => { + store = createStore(); + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'master'; + store.state.projects.abcproject = { ...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(() => { + store.state.currentMergeRequestId = '1'; + store.state.projects.abcproject.mergeRequests['1'] = { + iid: 123, + web_url: 'testing123', + }; + + return vm.$nextTick(); + }); + + it('renders edit dropdown', () => { + expect(vm.$el.querySelector('.btn')).not.toBe(null); + }); + + it('renders merge request link & IID', () => { + store.state.viewer = 'mrdiff'; + + return vm.$nextTick(() => { + 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', () => { + store.state.viewer = 'diff'; + + return vm.$nextTick(() => { + expect(trimText(vm.$el.querySelector('.ide-review-sub-header').textContent)).toBe( + 'Latest changes', + ); + }); + }); + }); +}); diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js new file mode 100644 index 00000000000..65cad2e7eb0 --- /dev/null +++ b/spec/frontend/ide/components/ide_side_bar_spec.js @@ -0,0 +1,57 @@ +import Vue from 'vue'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import store from '~/ide/stores'; +import ideSidebar from '~/ide/components/ide_side_bar.vue'; +import { leftSidebarViews } from '~/ide/constants'; +import { resetStore } from '../helpers'; +import { projectData } from '../mock_data'; + +describe('IdeSidebar', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(ideSidebar); + + store.state.currentProjectId = 'abcproject'; + store.state.projects.abcproject = projectData; + + vm = createComponentWithStore(Component, store).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders a sidebar', () => { + expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull(); + }); + + it('renders loading icon component', done => { + vm.$store.state.loading = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull(); + expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3); + + done(); + }); + }); + + describe('activityBarComponent', () => { + it('renders tree component', () => { + expect(vm.$el.querySelector('.ide-file-list')).not.toBeNull(); + }); + + it('renders commit component', done => { + vm.$store.state.currentActivityView = leftSidebarViews.commit.name; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.multi-file-commit-panel-section')).not.toBeNull(); + + done(); + }); + }); + }); +}); diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js new file mode 100644 index 00000000000..78a280e6304 --- /dev/null +++ b/spec/frontend/ide/components/ide_spec.js @@ -0,0 +1,125 @@ +import Vue from 'vue'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import store from '~/ide/stores'; +import ide from '~/ide/components/ide.vue'; +import { file, resetStore } from '../helpers'; +import { projectData } from '../mock_data'; + +function bootstrap(projData) { + const Component = Vue.extend(ide); + + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'master'; + store.state.projects.abcproject = { ...projData }; + Vue.set(store.state.trees, 'abcproject/master', { + tree: [], + loading: false, + }); + + return createComponentWithStore(Component, store, { + emptyStateSvgPath: 'svg', + noChangesStateSvgPath: 'svg', + committedStateSvgPath: 'svg', + }); +} + +describe('ide component, empty repo', () => { + let vm; + + beforeEach(() => { + const emptyProjData = { ...projectData, empty_repo: true, branches: {} }; + vm = bootstrap(emptyProjData); + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders "New file" button in empty repo', done => { + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).not.toBeNull(); + done(); + }); + }); +}); + +describe('ide component, non-empty repo', () => { + let vm; + + beforeEach(() => { + vm = bootstrap(projectData); + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('shows error message when set', done => { + expect(vm.$el.querySelector('.gl-alert')).toBe(null); + + vm.$store.state.errorMessage = { + text: 'error', + }; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.gl-alert')).not.toBe(null); + + done(); + }); + }); + + describe('onBeforeUnload', () => { + it('returns undefined when no staged files or changed files', () => { + expect(vm.onBeforeUnload()).toBe(undefined); + }); + + it('returns warning text when their are changed files', () => { + vm.$store.state.changedFiles.push(file()); + + expect(vm.onBeforeUnload()).toBe('Are you sure you want to lose unsaved changes?'); + }); + + it('returns warning text when their are staged files', () => { + vm.$store.state.stagedFiles.push(file()); + + expect(vm.onBeforeUnload()).toBe('Are you sure you want to lose unsaved changes?'); + }); + + it('updates event object', () => { + const event = {}; + vm.$store.state.stagedFiles.push(file()); + + vm.onBeforeUnload(event); + + expect(event.returnValue).toBe('Are you sure you want to lose unsaved changes?'); + }); + }); + + describe('non-existent branch', () => { + it('does not render "New file" button for non-existent branch when repo is not empty', done => { + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull(); + done(); + }); + }); + }); + + describe('branch with files', () => { + beforeEach(() => { + store.state.trees['abcproject/master'].tree = [file()]; + }); + + it('does not render "New file" button', done => { + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull(); + done(); + }); + }); + }); +}); diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js new file mode 100644 index 00000000000..bc8144f544c --- /dev/null +++ b/spec/frontend/ide/components/ide_status_bar_spec.js @@ -0,0 +1,127 @@ +import Vue from 'vue'; +import _ from 'lodash'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { TEST_HOST } from '../../helpers/test_constants'; +import { createStore } from '~/ide/stores'; +import IdeStatusBar from '~/ide/components/ide_status_bar.vue'; +import { rightSidebarViews } from '~/ide/constants'; +import { projectData } from '../mock_data'; + +const TEST_PROJECT_ID = 'abcproject'; +const TEST_MERGE_REQUEST_ID = '9001'; +const TEST_MERGE_REQUEST_URL = `${TEST_HOST}merge-requests/${TEST_MERGE_REQUEST_ID}`; + +describe('ideStatusBar', () => { + let store; + let vm; + + const createComponent = () => { + vm = createComponentWithStore(Vue.extend(IdeStatusBar), store).$mount(); + }; + const findMRStatus = () => vm.$el.querySelector('.js-ide-status-mr'); + + beforeEach(() => { + store = createStore(); + store.state.currentProjectId = TEST_PROJECT_ID; + store.state.projects[TEST_PROJECT_ID] = _.clone(projectData); + store.state.currentBranchId = 'master'; + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('triggers a setInterval', () => { + expect(vm.intervalId).not.toBe(null); + }); + + it('renders the statusbar', () => { + expect(vm.$el.className).toBe('ide-status-bar'); + }); + + describe('commitAgeUpdate', () => { + beforeEach(() => { + jest.spyOn(vm, 'commitAgeUpdate').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('gets called every second', () => { + expect(vm.commitAgeUpdate).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1000); + + expect(vm.commitAgeUpdate.mock.calls.length).toEqual(1); + + jest.advanceTimersByTime(1000); + + expect(vm.commitAgeUpdate.mock.calls.length).toEqual(2); + }); + }); + + describe('getCommitPath', () => { + it('returns the path to the commit details', () => { + expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de'); + }); + }); + + describe('pipeline status', () => { + it('opens right sidebar on clicking icon', done => { + jest.spyOn(vm, 'openRightPane').mockImplementation(() => {}); + Vue.set(vm.$store.state.pipelines, 'latestPipeline', { + details: { + status: { + text: 'success', + details_path: 'test', + icon: 'status_success', + }, + }, + commit: { + author_gravatar_url: 'www', + }, + }); + + vm.$nextTick() + .then(() => { + vm.$el.querySelector('.ide-status-pipeline button').click(); + + expect(vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines); + }) + .then(done) + .catch(done.fail); + }); + }); + + it('does not show merge request status', () => { + expect(findMRStatus()).toBe(null); + }); + }); + + describe('with merge request in store', () => { + beforeEach(() => { + store.state.projects[TEST_PROJECT_ID].mergeRequests = { + [TEST_MERGE_REQUEST_ID]: { + web_url: TEST_MERGE_REQUEST_URL, + references: { + short: `!${TEST_MERGE_REQUEST_ID}`, + }, + }, + }; + store.state.currentMergeRequestId = TEST_MERGE_REQUEST_ID; + + createComponent(); + }); + + it('shows merge request status', () => { + expect(findMRStatus().textContent.trim()).toEqual(`Merge request !${TEST_MERGE_REQUEST_ID}`); + expect(findMRStatus().querySelector('a').href).toEqual(TEST_MERGE_REQUEST_URL); + }); + }); +}); diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js new file mode 100644 index 00000000000..30f11db3153 --- /dev/null +++ b/spec/frontend/ide/components/ide_tree_list_spec.js @@ -0,0 +1,77 @@ +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); + const normalBranchTree = [file('fileName')]; + const emptyBranchTree = []; + let vm; + + const bootstrapWithTree = (tree = normalBranchTree) => { + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'master'; + store.state.projects.abcproject = { ...projectData }; + Vue.set(store.state.trees, 'abcproject/master', { + tree, + loading: false, + }); + + vm = createComponentWithStore(Component, store, { + viewerType: 'edit', + }); + }; + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + describe('normal branch', () => { + beforeEach(() => { + bootstrapWithTree(); + + jest.spyOn(vm, 'updateViewer'); + + vm.$mount(); + }); + + 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'); + }); + }); + + describe('empty-branch state', () => { + beforeEach(() => { + bootstrapWithTree(emptyBranchTree); + + jest.spyOn(vm, 'updateViewer'); + + vm.$mount(); + }); + + it('does not load files if the branch is empty', () => { + expect(vm.$el.textContent).not.toContain('fileName'); + expect(vm.$el.textContent).toContain('No files'); + }); + }); +}); diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js new file mode 100644 index 00000000000..01f007f09c3 --- /dev/null +++ b/spec/frontend/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 = { ...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/frontend/ide/components/jobs/detail/description_spec.js b/spec/frontend/ide/components/jobs/detail/description_spec.js new file mode 100644 index 00000000000..babae00d2f7 --- /dev/null +++ b/spec/frontend/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_success_borderless')).not.toBe(null); + }); +}); diff --git a/spec/frontend/ide/components/jobs/item_spec.js b/spec/frontend/ide/components/jobs/item_spec.js new file mode 100644 index 00000000000..2f97d39e98e --- /dev/null +++ b/spec/frontend/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_success_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/frontend/ide/components/merge_requests/item_spec.js b/spec/frontend/ide/components/merge_requests/item_spec.js new file mode 100644 index 00000000000..6a2451ad263 --- /dev/null +++ b/spec/frontend/ide/components/merge_requests/item_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; +import router from '~/ide/ide_router'; +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 link with href', () => { + const expectedHref = router.resolve( + `/project/${vm.item.projectPathWithNamespace}/merge_requests/${vm.item.iid}`, + ).href; + + expect(vm.$el.tagName.toLowerCase()).toBe('a'); + expect(vm.$el).toHaveAttr('href', expectedHref); + }); + + 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(); + }); + }); +}); diff --git a/spec/frontend/ide/components/nav_dropdown_button_spec.js b/spec/frontend/ide/components/nav_dropdown_button_spec.js new file mode 100644 index 00000000000..2aa3992a6d8 --- /dev/null +++ b/spec/frontend/ide/components/nav_dropdown_button_spec.js @@ -0,0 +1,93 @@ +import Vue from 'vue'; +import { trimText } from 'helpers/text_helper'; +import { mountComponentWithStore } from 'helpers/vue_mount_component_helper'; +import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue'; +import { createStore } from '~/ide/stores'; + +describe('NavDropdown', () => { + const TEST_BRANCH_ID = 'lorem-ipsum-dolar'; + const TEST_MR_ID = '12345'; + let store; + let vm; + + beforeEach(() => { + store = createStore(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + const createComponent = (props = {}) => { + vm = mountComponentWithStore(Vue.extend(NavDropdownButton), { props, store }); + vm.$mount(); + }; + + const findIcon = name => vm.$el.querySelector(`.ic-${name}`); + const findMRIcon = () => findIcon('merge-request'); + const findBranchIcon = () => findIcon('branch'); + + describe('normal', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders empty placeholders, if state is falsey', () => { + expect(trimText(vm.$el.textContent)).toEqual('- -'); + }); + + it('renders branch name, if state has currentBranchId', done => { + vm.$store.state.currentBranchId = TEST_BRANCH_ID; + + vm.$nextTick() + .then(() => { + expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`); + }) + .then(done) + .catch(done.fail); + }); + + it('renders mr id, if state has currentMergeRequestId', done => { + vm.$store.state.currentMergeRequestId = TEST_MR_ID; + + vm.$nextTick() + .then(() => { + expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`); + }) + .then(done) + .catch(done.fail); + }); + + it('renders branch and mr, if state has both', done => { + vm.$store.state.currentBranchId = TEST_BRANCH_ID; + vm.$store.state.currentMergeRequestId = TEST_MR_ID; + + vm.$nextTick() + .then(() => { + expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`); + }) + .then(done) + .catch(done.fail); + }); + + it('shows icons', () => { + expect(findBranchIcon()).toBeTruthy(); + expect(findMRIcon()).toBeTruthy(); + }); + }); + + describe('with showMergeRequests false', () => { + beforeEach(() => { + createComponent({ showMergeRequests: false }); + }); + + it('shows single empty placeholder, if state is falsey', () => { + expect(trimText(vm.$el.textContent)).toEqual('-'); + }); + + it('shows only branch icon', () => { + expect(findBranchIcon()).toBeTruthy(); + expect(findMRIcon()).toBe(null); + }); + }); +}); diff --git a/spec/frontend/ide/components/nav_dropdown_spec.js b/spec/frontend/ide/components/nav_dropdown_spec.js new file mode 100644 index 00000000000..ce123d925c8 --- /dev/null +++ b/spec/frontend/ide/components/nav_dropdown_spec.js @@ -0,0 +1,102 @@ +import $ from 'jquery'; +import { mount } from '@vue/test-utils'; +import { createStore } from '~/ide/stores'; +import NavDropdown from '~/ide/components/nav_dropdown.vue'; +import { PERMISSION_READ_MR } from '~/ide/constants'; + +const TEST_PROJECT_ID = 'lorem-ipsum'; + +describe('IDE NavDropdown', () => { + let store; + let wrapper; + + beforeEach(() => { + store = createStore(); + Object.assign(store.state, { + currentProjectId: TEST_PROJECT_ID, + currentBranchId: 'master', + projects: { + [TEST_PROJECT_ID]: { + userPermissions: { + [PERMISSION_READ_MR]: true, + }, + branches: { + master: { id: 'master' }, + }, + }, + }, + }); + jest.spyOn(store, 'dispatch').mockImplementation(() => {}); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + const createComponent = () => { + wrapper = mount(NavDropdown, { + store, + }); + }; + + const findIcon = name => wrapper.find(`.ic-${name}`); + const findMRIcon = () => findIcon('merge-request'); + const findNavForm = () => wrapper.find('.ide-nav-form'); + const showDropdown = () => { + $(wrapper.vm.$el).trigger('show.bs.dropdown'); + }; + const hideDropdown = () => { + $(wrapper.vm.$el).trigger('hide.bs.dropdown'); + }; + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders nothing initially', () => { + expect(findNavForm().exists()).toBe(false); + }); + + it('renders nav form when show.bs.dropdown', done => { + showDropdown(); + + wrapper.vm + .$nextTick() + .then(() => { + expect(findNavForm().exists()).toBe(true); + }) + .then(done) + .catch(done.fail); + }); + + it('destroys nav form when closed', done => { + showDropdown(); + hideDropdown(); + + wrapper.vm + .$nextTick() + .then(() => { + expect(findNavForm().exists()).toBe(false); + }) + .then(done) + .catch(done.fail); + }); + + it('renders merge request icon', () => { + expect(findMRIcon().exists()).toBe(true); + }); + }); + + describe('when user cannot read merge requests', () => { + beforeEach(() => { + store.state.projects[TEST_PROJECT_ID].userPermissions = {}; + + createComponent(); + }); + + it('does not render merge requests', () => { + expect(findMRIcon().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/ide/components/new_dropdown/button_spec.js b/spec/frontend/ide/components/new_dropdown/button_spec.js new file mode 100644 index 00000000000..3c611b7de8f --- /dev/null +++ b/spec/frontend/ide/components/new_dropdown/button_spec.js @@ -0,0 +1,65 @@ +import Vue from 'vue'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import Button from '~/ide/components/new_dropdown/button.vue'; + +describe('IDE new entry dropdown button component', () => { + let Component; + let vm; + + beforeAll(() => { + Component = Vue.extend(Button); + }); + + beforeEach(() => { + vm = mountComponent(Component, { + label: 'Testing', + icon: 'doc-new', + }); + + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders button with label', () => { + expect(vm.$el.textContent).toContain('Testing'); + }); + + it('renders icon', () => { + expect(vm.$el.querySelector('.ic-doc-new')).not.toBe(null); + }); + + it('emits click event', () => { + vm.$el.click(); + + expect(vm.$emit).toHaveBeenCalledWith('click'); + }); + + it('hides label if showLabel is false', done => { + vm.showLabel = false; + + vm.$nextTick(() => { + expect(vm.$el.textContent).not.toContain('Testing'); + + done(); + }); + }); + + describe('tooltipTitle', () => { + it('returns empty string when showLabel is true', () => { + expect(vm.tooltipTitle).toBe(''); + }); + + it('returns label', done => { + vm.showLabel = false; + + vm.$nextTick(() => { + expect(vm.tooltipTitle).toBe('Testing'); + + done(); + }); + }); + }); +}); diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js new file mode 100644 index 00000000000..00781c16609 --- /dev/null +++ b/spec/frontend/ide/components/new_dropdown/index_spec.js @@ -0,0 +1,84 @@ +import Vue from 'vue'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import store from '~/ide/stores'; +import newDropdown from '~/ide/components/new_dropdown/index.vue'; +import { resetStore } from '../../helpers'; + +describe('new dropdown component', () => { + let vm; + + beforeEach(() => { + const component = Vue.extend(newDropdown); + + vm = createComponentWithStore(component, store, { + branch: 'master', + path: '', + mouseOver: false, + type: 'tree', + }); + + vm.$store.state.currentProjectId = 'abcproject'; + vm.$store.state.path = ''; + vm.$store.state.trees['abcproject/mybranch'] = { + tree: [], + }; + + vm.$mount(); + + jest.spyOn(vm.$refs.newModal, 'open').mockImplementation(() => {}); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders new file, upload and new directory links', () => { + const buttons = vm.$el.querySelectorAll('.dropdown-menu button'); + + expect(buttons[0].textContent.trim()).toBe('New file'); + expect(buttons[1].textContent.trim()).toBe('Upload file'); + expect(buttons[2].textContent.trim()).toBe('New directory'); + }); + + describe('createNewItem', () => { + it('opens modal for a blob when new file is clicked', () => { + vm.$el.querySelectorAll('.dropdown-menu button')[0].click(); + + expect(vm.$refs.newModal.open).toHaveBeenCalledWith('blob', ''); + }); + + it('opens modal for a tree when new directory is clicked', () => { + vm.$el.querySelectorAll('.dropdown-menu button')[2].click(); + + expect(vm.$refs.newModal.open).toHaveBeenCalledWith('tree', ''); + }); + }); + + describe('isOpen', () => { + it('scrolls dropdown into view', done => { + jest.spyOn(vm.$refs.dropdownMenu, 'scrollIntoView').mockImplementation(() => {}); + + vm.isOpen = true; + + setImmediate(() => { + expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({ + block: 'nearest', + }); + + done(); + }); + }); + }); + + describe('delete entry', () => { + it('calls delete action', () => { + jest.spyOn(vm, 'deleteEntry').mockImplementation(() => {}); + + vm.$el.querySelectorAll('.dropdown-menu button')[4].click(); + + expect(vm.deleteEntry).toHaveBeenCalledWith(''); + }); + }); +}); diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js new file mode 100644 index 00000000000..62a59a76bf4 --- /dev/null +++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js @@ -0,0 +1,175 @@ +import Vue from 'vue'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { createStore } from '~/ide/stores'; +import modal from '~/ide/components/new_dropdown/modal.vue'; +import createFlash from '~/flash'; + +jest.mock('~/flash'); + +describe('new file modal component', () => { + const Component = Vue.extend(modal); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe.each` + entryType | modalTitle | btnTitle | showsFileTemplates + ${'tree'} | ${'Create new directory'} | ${'Create directory'} | ${false} + ${'blob'} | ${'Create new file'} | ${'Create file'} | ${true} + `('$entryType', ({ entryType, modalTitle, btnTitle, showsFileTemplates }) => { + beforeEach(done => { + const store = createStore(); + + vm = createComponentWithStore(Component, store).$mount(); + vm.open(entryType); + vm.name = 'testing'; + + vm.$nextTick(done); + }); + + afterEach(() => { + vm.close(); + }); + + it(`sets modal title as ${entryType}`, () => { + expect(document.querySelector('.modal-title').textContent.trim()).toBe(modalTitle); + }); + + it(`sets button label as ${entryType}`, () => { + expect(document.querySelector('.btn-success').textContent.trim()).toBe(btnTitle); + }); + + it(`sets form label as ${entryType}`, () => { + expect(document.querySelector('.label-bold').textContent.trim()).toBe('Name'); + }); + + it(`shows file templates: ${showsFileTemplates}`, () => { + const templateFilesEl = document.querySelector('.file-templates'); + expect(Boolean(templateFilesEl)).toBe(showsFileTemplates); + }); + }); + + describe('rename entry', () => { + beforeEach(() => { + const store = createStore(); + store.state.entries = { + 'test-path': { + name: 'test', + type: 'blob', + path: 'test-path', + }, + }; + + vm = createComponentWithStore(Component, store).$mount(); + }); + + it.each` + entryType | modalTitle | btnTitle + ${'tree'} | ${'Rename folder'} | ${'Rename folder'} + ${'blob'} | ${'Rename file'} | ${'Rename file'} + `( + 'renders title and button for renaming $entryType', + ({ entryType, modalTitle, btnTitle }, done) => { + vm.$store.state.entries['test-path'].type = entryType; + vm.open('rename', 'test-path'); + + vm.$nextTick(() => { + expect(document.querySelector('.modal-title').textContent.trim()).toBe(modalTitle); + expect(document.querySelector('.btn-success').textContent.trim()).toBe(btnTitle); + + done(); + }); + }, + ); + + describe('entryName', () => { + it('returns entries name', () => { + vm.open('rename', 'test-path'); + + expect(vm.entryName).toBe('test-path'); + }); + + it('does not reset entryName to its old value if empty', () => { + vm.entryName = 'hello'; + vm.entryName = ''; + + expect(vm.entryName).toBe(''); + }); + }); + + describe('open', () => { + it('sets entryName to path provided if modalType is rename', () => { + vm.open('rename', 'test-path'); + + expect(vm.entryName).toBe('test-path'); + }); + + it("appends '/' to the path if modalType isn't rename", () => { + vm.open('blob', 'test-path'); + + expect(vm.entryName).toBe('test-path/'); + }); + + it('leaves entryName blank if no path is provided', () => { + vm.open('blob'); + + expect(vm.entryName).toBe(''); + }); + }); + }); + + describe('submitForm', () => { + let store; + + beforeEach(() => { + store = createStore(); + store.state.entries = { + 'test-path/test': { + name: 'test', + deleted: false, + }, + }; + + vm = createComponentWithStore(Component, store).$mount(); + }); + + it('throws an error when target entry exists', () => { + vm.open('rename', 'test-path/test'); + + expect(createFlash).not.toHaveBeenCalled(); + + vm.submitForm(); + + expect(createFlash).toHaveBeenCalledWith( + 'The name "test-path/test" is already taken in this directory.', + 'alert', + expect.anything(), + null, + false, + true, + ); + }); + + it('does not throw error when target entry does not exist', () => { + jest.spyOn(vm, 'renameEntry').mockImplementation(); + + vm.open('rename', 'test-path/test'); + vm.entryName = 'test-path/test2'; + vm.submitForm(); + + expect(createFlash).not.toHaveBeenCalled(); + }); + + it('removes leading/trailing found in the new name', () => { + vm.open('rename', 'test-path/test'); + + vm.entryName = 'test-path /test'; + + vm.submitForm(); + + expect(vm.entryName).toBe('test-path/test'); + }); + }); +}); diff --git a/spec/frontend/ide/components/new_dropdown/upload_spec.js b/spec/frontend/ide/components/new_dropdown/upload_spec.js new file mode 100644 index 00000000000..a418fdeb572 --- /dev/null +++ b/spec/frontend/ide/components/new_dropdown/upload_spec.js @@ -0,0 +1,112 @@ +import Vue from 'vue'; +import createComponent from 'helpers/vue_mount_component_helper'; +import upload from '~/ide/components/new_dropdown/upload.vue'; + +describe('new dropdown upload', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(upload); + + vm = createComponent(Component, { + path: '', + }); + + vm.entryName = 'testing'; + + jest.spyOn(vm, '$emit'); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('openFile', () => { + it('calls for each file', () => { + const files = ['test', 'test2', 'test3']; + + jest.spyOn(vm, 'readFile').mockImplementation(() => {}); + jest.spyOn(vm.$refs.fileUpload, 'files', 'get').mockReturnValue(files); + + vm.openFile(); + + expect(vm.readFile.mock.calls.length).toBe(3); + + files.forEach((file, i) => { + expect(vm.readFile.mock.calls[i]).toEqual([file]); + }); + }); + }); + + describe('readFile', () => { + beforeEach(() => { + jest.spyOn(FileReader.prototype, 'readAsDataURL').mockImplementation(() => {}); + }); + + it('calls readAsDataURL for all files', () => { + const file = { + type: 'images/png', + }; + + vm.readFile(file); + + expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file); + }); + }); + + describe('createFile', () => { + const textTarget = { + result: 'base64,cGxhaW4gdGV4dA==', + }; + const binaryTarget = { + result: 'base64,w4I=', + }; + const textFile = new File(['plain text'], 'textFile'); + + const binaryFile = { + name: 'binaryFile', + type: 'image/png', + }; + + beforeEach(() => { + jest.spyOn(FileReader.prototype, 'readAsText'); + }); + + it('calls readAsText and creates file in plain text (without encoding) if the file content is plain text', done => { + const waitForCreate = new Promise(resolve => vm.$on('create', resolve)); + + vm.createFile(textTarget, textFile); + + expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(textFile); + + waitForCreate + .then(() => { + expect(vm.$emit).toHaveBeenCalledWith('create', { + name: textFile.name, + type: 'blob', + content: 'plain text', + base64: false, + binary: false, + rawPath: '', + }); + }) + .then(done) + .catch(done.fail); + }); + + it('splits content on base64 if binary', () => { + vm.createFile(binaryTarget, binaryFile); + + expect(FileReader.prototype.readAsText).not.toHaveBeenCalledWith(textFile); + + expect(vm.$emit).toHaveBeenCalledWith('create', { + name: binaryFile.name, + type: 'blob', + content: binaryTarget.result.split('base64,')[1], + base64: true, + binary: true, + rawPath: binaryTarget.result, + }); + }); + }); +}); diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js index 11e672b6685..d909a5e478e 100644 --- a/spec/frontend/ide/components/pipelines/list_spec.js +++ b/spec/frontend/ide/components/pipelines/list_spec.js @@ -7,10 +7,15 @@ import JobsList from '~/ide/components/jobs/list.vue'; import Tab from '~/vue_shared/components/tabs/tab.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import { pipelines } from '../../../../javascripts/ide/mock_data'; +import IDEServices from '~/ide/services'; const localVue = createLocalVue(); localVue.use(Vuex); +jest.mock('~/ide/services', () => ({ + pingUsage: jest.fn(), +})); + describe('IDE pipelines list', () => { let wrapper; @@ -25,14 +30,18 @@ describe('IDE pipelines list', () => { }; const fetchLatestPipelineMock = jest.fn(); + const pingUsageMock = jest.fn(); const failedStagesGetterMock = jest.fn().mockReturnValue([]); + const fakeProjectPath = 'alpha/beta'; const createComponent = (state = {}) => { const { pipelines: pipelinesState, ...restOfState } = state; const { defaultPipelines, ...defaultRestOfState } = defaultState; const fakeStore = new Vuex.Store({ - getters: { currentProject: () => ({ web_url: 'some/url ' }) }, + getters: { + currentProject: () => ({ web_url: 'some/url ', path_with_namespace: fakeProjectPath }), + }, state: { ...defaultRestOfState, ...restOfState, @@ -46,6 +55,7 @@ describe('IDE pipelines list', () => { }, actions: { fetchLatestPipeline: fetchLatestPipelineMock, + pingUsage: pingUsageMock, }, getters: { jobsCount: () => 1, @@ -77,6 +87,11 @@ describe('IDE pipelines list', () => { expect(fetchLatestPipelineMock).toHaveBeenCalled(); }); + it('pings pipeline usage', () => { + createComponent(); + expect(IDEServices.pingUsage).toHaveBeenCalledWith(fakeProjectPath); + }); + describe('when loading', () => { let defaultPipelinesLoadingState; beforeAll(() => { diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js index 0cde6fb6060..7b2025f5e9f 100644 --- a/spec/frontend/ide/components/preview/clientside_spec.js +++ b/spec/frontend/ide/components/preview/clientside_spec.js @@ -70,14 +70,6 @@ describe('IDE clientside preview', () => { }); }; - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - afterEach(() => { wrapper.destroy(); }); diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js index 5ea03eb1593..237be018807 100644 --- a/spec/frontend/ide/components/repo_commit_section_spec.js +++ b/spec/frontend/ide/components/repo_commit_section_spec.js @@ -36,7 +36,6 @@ describe('RepoCommitSection', () => { }), ); - store.state.rightPanelCollapsed = false; store.state.currentBranch = 'master'; store.state.changedFiles = []; store.state.stagedFiles = [{ ...files[0] }, { ...files[1] }]; diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js new file mode 100644 index 00000000000..82ea73ffbb1 --- /dev/null +++ b/spec/frontend/ide/components/repo_tab_spec.js @@ -0,0 +1,185 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import repoTab from '~/ide/components/repo_tab.vue'; +import router from '~/ide/ide_router'; +import { file, resetStore } from '../helpers'; + +describe('RepoTab', () => { + let vm; + + function createComponent(propsData) { + const RepoTab = Vue.extend(repoTab); + + return new RepoTab({ + store, + propsData, + }).$mount(); + } + + beforeEach(() => { + jest.spyOn(router, 'push').mockImplementation(() => {}); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders a close link and a name link', () => { + vm = createComponent({ + tab: file(), + }); + vm.$store.state.openFiles.push(vm.tab); + const close = vm.$el.querySelector('.multi-file-tab-close'); + const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`); + + expect(close.innerHTML).toContain('#close'); + expect(name.textContent.trim()).toEqual(vm.tab.name); + }); + + it('does not call openPendingTab when tab is active', done => { + vm = createComponent({ + tab: { + ...file(), + pending: true, + active: true, + }, + }); + + jest.spyOn(vm, 'openPendingTab').mockImplementation(() => {}); + + vm.$el.click(); + + vm.$nextTick(() => { + expect(vm.openPendingTab).not.toHaveBeenCalled(); + + done(); + }); + }); + + it('fires clickFile when the link is clicked', () => { + vm = createComponent({ + tab: file(), + }); + + jest.spyOn(vm, 'clickFile').mockImplementation(() => {}); + + vm.$el.click(); + + expect(vm.clickFile).toHaveBeenCalledWith(vm.tab); + }); + + it('calls closeFile when clicking close button', () => { + vm = createComponent({ + tab: file(), + }); + + jest.spyOn(vm, 'closeFile').mockImplementation(() => {}); + + vm.$el.querySelector('.multi-file-tab-close').click(); + + expect(vm.closeFile).toHaveBeenCalledWith(vm.tab); + }); + + it('changes icon on hover', done => { + const tab = file(); + tab.changed = true; + vm = createComponent({ + tab, + }); + + vm.$el.dispatchEvent(new Event('mouseover')); + + Vue.nextTick() + .then(() => { + expect(vm.$el.querySelector('.file-modified')).toBeNull(); + + vm.$el.dispatchEvent(new Event('mouseout')); + }) + .then(Vue.nextTick) + .then(() => { + expect(vm.$el.querySelector('.file-modified')).not.toBeNull(); + + done(); + }) + .catch(done.fail); + }); + + describe('locked file', () => { + let f; + + beforeEach(() => { + f = file('locked file'); + f.file_lock = { + user: { + name: 'testuser', + updated_at: new Date(), + }, + }; + + vm = createComponent({ + tab: f, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders lock icon', () => { + expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull(); + }); + + it('renders a tooltip', () => { + expect(vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle).toContain( + 'Locked by testuser', + ); + }); + }); + + describe('methods', () => { + describe('closeTab', () => { + it('closes tab if file has changed', done => { + const tab = file(); + tab.changed = true; + tab.opened = true; + vm = createComponent({ + tab, + }); + vm.$store.state.openFiles.push(tab); + vm.$store.state.changedFiles.push(tab); + vm.$store.state.entries[tab.path] = tab; + vm.$store.dispatch('setFileActive', tab.path); + + vm.$el.querySelector('.multi-file-tab-close').click(); + + vm.$nextTick(() => { + expect(tab.opened).toBeFalsy(); + expect(vm.$store.state.changedFiles.length).toBe(1); + + done(); + }); + }); + + it('closes tab when clicking close btn', done => { + const tab = file('lose'); + tab.opened = true; + vm = createComponent({ + tab, + }); + vm.$store.state.openFiles.push(tab); + vm.$store.state.entries[tab.path] = tab; + vm.$store.dispatch('setFileActive', tab.path); + + vm.$el.querySelector('.multi-file-tab-close').click(); + + vm.$nextTick(() => { + expect(tab.opened).toBeFalsy(); + + done(); + }); + }); + }); + }); +}); diff --git a/spec/frontend/ide/components/repo_tabs_spec.js b/spec/frontend/ide/components/repo_tabs_spec.js new file mode 100644 index 00000000000..583f71e6121 --- /dev/null +++ b/spec/frontend/ide/components/repo_tabs_spec.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import repoTabs from '~/ide/components/repo_tabs.vue'; +import createComponent from '../../helpers/vue_mount_component_helper'; +import { file } from '../helpers'; + +describe('RepoTabs', () => { + const openedFiles = [file('open1'), file('open2')]; + const RepoTabs = Vue.extend(repoTabs); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a list of tabs', done => { + vm = createComponent(RepoTabs, { + files: openedFiles, + viewer: 'editor', + hasChanges: false, + activeFile: file('activeFile'), + hasMergeRequest: false, + }); + openedFiles[0].active = true; + + vm.$nextTick(() => { + const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')]; + + expect(tabs.length).toEqual(2); + expect(tabs[0].parentNode.classList.contains('active')).toEqual(true); + expect(tabs[1].parentNode.classList.contains('active')).toEqual(false); + + done(); + }); + }); +}); diff --git a/spec/frontend/ide/components/shared/tokened_input_spec.js b/spec/frontend/ide/components/shared/tokened_input_spec.js new file mode 100644 index 00000000000..e687216bd06 --- /dev/null +++ b/spec/frontend/ide/components/shared/tokened_input_spec.js @@ -0,0 +1,133 @@ +import Vue from 'vue'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import TokenedInput from '~/ide/components/shared/tokened_input.vue'; + +const TEST_PLACEHOLDER = 'Searching in test'; +const TEST_TOKENS = [ + { label: 'lorem', id: 1 }, + { label: 'ipsum', id: 2 }, + { label: 'dolar', id: 3 }, +]; +const TEST_VALUE = 'lorem'; + +function getTokenElements(vm) { + return Array.from(vm.$el.querySelectorAll('.filtered-search-token button')); +} + +function createBackspaceEvent() { + const e = new Event('keyup'); + e.keyCode = 8; + e.which = e.keyCode; + e.altKey = false; + e.ctrlKey = true; + e.shiftKey = false; + e.metaKey = false; + return e; +} + +describe('IDE shared/TokenedInput', () => { + const Component = Vue.extend(TokenedInput); + let vm; + + beforeEach(() => { + vm = mountComponent(Component, { + tokens: TEST_TOKENS, + placeholder: TEST_PLACEHOLDER, + value: TEST_VALUE, + }); + + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders tokens', () => { + const renderedTokens = getTokenElements(vm).map(x => x.textContent.trim()); + + expect(renderedTokens).toEqual(TEST_TOKENS.map(x => x.label)); + }); + + it('renders input', () => { + expect(vm.$refs.input).toBeTruthy(); + expect(vm.$refs.input).toHaveValue(TEST_VALUE); + }); + + it('renders placeholder, when tokens are empty', done => { + vm.tokens = []; + + vm.$nextTick() + .then(() => { + expect(vm.$refs.input).toHaveAttr('placeholder', TEST_PLACEHOLDER); + }) + .then(done) + .catch(done.fail); + }); + + it('triggers "removeToken" on token click', () => { + getTokenElements(vm)[0].click(); + + expect(vm.$emit).toHaveBeenCalledWith('removeToken', TEST_TOKENS[0]); + }); + + it('when input triggers backspace event, it calls "onBackspace"', () => { + jest.spyOn(vm, 'onBackspace').mockImplementation(() => {}); + + vm.$refs.input.dispatchEvent(createBackspaceEvent()); + vm.$refs.input.dispatchEvent(createBackspaceEvent()); + + expect(vm.onBackspace).toHaveBeenCalledTimes(2); + }); + + it('triggers "removeToken" on backspaces when value is empty', () => { + vm.value = ''; + + vm.onBackspace(); + + expect(vm.$emit).not.toHaveBeenCalled(); + expect(vm.backspaceCount).toEqual(1); + + vm.onBackspace(); + + expect(vm.$emit).toHaveBeenCalledWith('removeToken', TEST_TOKENS[TEST_TOKENS.length - 1]); + expect(vm.backspaceCount).toEqual(0); + }); + + it('does not trigger "removeToken" on backspaces when value is not empty', () => { + vm.onBackspace(); + vm.onBackspace(); + + expect(vm.backspaceCount).toEqual(0); + expect(vm.$emit).not.toHaveBeenCalled(); + }); + + it('does not trigger "removeToken" on backspaces when tokens are empty', () => { + vm.tokens = []; + + vm.onBackspace(); + vm.onBackspace(); + + expect(vm.backspaceCount).toEqual(0); + expect(vm.$emit).not.toHaveBeenCalled(); + }); + + it('triggers "focus" on input focus', () => { + vm.$refs.input.dispatchEvent(new Event('focus')); + + expect(vm.$emit).toHaveBeenCalledWith('focus'); + }); + + it('triggers "blur" on input blur', () => { + vm.$refs.input.dispatchEvent(new Event('blur')); + + expect(vm.$emit).toHaveBeenCalledWith('blur'); + }); + + it('triggers "input" with value on input change', () => { + vm.$refs.input.value = 'something-else'; + vm.$refs.input.dispatchEvent(new Event('input')); + + expect(vm.$emit).toHaveBeenCalledWith('input', 'something-else'); + }); +}); |