diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 18:40:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 18:40:28 +0300 |
commit | b595cb0c1dec83de5bdee18284abe86614bed33b (patch) | |
tree | 8c3d4540f193c5ff98019352f554e921b3a41a72 /spec/frontend/ide | |
parent | 2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff) |
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'spec/frontend/ide')
13 files changed, 478 insertions, 285 deletions
diff --git a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js index 4f81c0aa5d3..7c48c0e6f95 100644 --- a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js @@ -1,29 +1,21 @@ -import Vue from 'vue'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import emptyState from '~/ide/components/commit_sidebar/empty_state.vue'; +import { shallowMount } from '@vue/test-utils'; +import EmptyState from '~/ide/components/commit_sidebar/empty_state.vue'; import { createStore } from '~/ide/stores'; -describe('IDE commit panel empty state', () => { - let vm; - let store; +describe('IDE commit panel EmptyState component', () => { + let wrapper; beforeEach(() => { - store = createStore(); - - const Component = Vue.extend(emptyState); - - Vue.set(store.state, 'noChangesStateSvgPath', 'no-changes'); - - vm = createComponentWithStore(Component, store); - - vm.$mount(); + const store = createStore(); + store.state.noChangesStateSvgPath = 'no-changes'; + wrapper = shallowMount(EmptyState, { store }); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); it('renders no changes text when last commit message is empty', () => { - expect(vm.$el.textContent).toContain('No changes'); + expect(wrapper.find('h4').text()).toBe('No changes'); }); }); diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js index 1d42512c9ee..81c81fc0a9f 100644 --- a/spec/frontend/ide/components/commit_sidebar/list_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js @@ -1,51 +1,47 @@ -import Vue, { nextTick } from 'vue'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import commitSidebarList from '~/ide/components/commit_sidebar/list.vue'; -import { createStore } from '~/ide/stores'; +import { shallowMount } from '@vue/test-utils'; +import CommitSidebarList from '~/ide/components/commit_sidebar/list.vue'; +import ListItem from '~/ide/components/commit_sidebar/list_item.vue'; import { file } from '../../helpers'; describe('Multi-file editor commit sidebar list', () => { - let store; - let vm; - - beforeEach(() => { - store = createStore(); - - const Component = Vue.extend(commitSidebarList); - - vm = createComponentWithStore(Component, store, { - title: 'Staged', - fileList: [], - action: 'stageAllChanges', - actionBtnText: 'stage all', - actionBtnIcon: 'history', - activeFileKey: 'staged-testing', - keyPrefix: 'staged', + let wrapper; + + const mountComponent = ({ fileList }) => + shallowMount(CommitSidebarList, { + propsData: { + title: 'Staged', + fileList, + action: 'stageAllChanges', + actionBtnText: 'stage all', + actionBtnIcon: 'history', + activeFileKey: 'staged-testing', + keyPrefix: 'staged', + }, }); - vm.$mount(); - }); - afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('with a list of files', () => { beforeEach(async () => { const f = file('file name'); f.changed = true; - vm.fileList.push(f); - await nextTick(); + wrapper = mountComponent({ fileList: [f] }); }); it('renders list', () => { - expect(vm.$el.querySelectorAll('.multi-file-commit-list > li').length).toBe(1); + expect(wrapper.findAllComponents(ListItem)).toHaveLength(1); }); }); - describe('empty files array', () => { - it('renders no changes text when empty', () => { - expect(vm.$el.textContent).toContain('No changes'); + describe('with empty files array', () => { + beforeEach(() => { + wrapper = mountComponent({ fileList: [] }); + }); + + it('renders no changes text ', () => { + expect(wrapper.text()).toContain('No changes'); }); }); }); diff --git a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js index 52e35bdbb73..63d51953915 100644 --- a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js @@ -1,32 +1,22 @@ -import Vue, { nextTick } from 'vue'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import successMessage from '~/ide/components/commit_sidebar/success_message.vue'; +import { shallowMount } from '@vue/test-utils'; +import SuccessMessage from '~/ide/components/commit_sidebar/success_message.vue'; import { createStore } from '~/ide/stores'; describe('IDE commit panel successful commit state', () => { - let vm; - let store; + let wrapper; beforeEach(() => { - store = createStore(); - - const Component = Vue.extend(successMessage); - - vm = createComponentWithStore(Component, store, { - committedStateSvgPath: 'committed-state', - }); - - vm.$mount(); + const store = createStore(); + store.state.committedStateSvgPath = 'committed-state'; + store.state.lastCommitMsg = 'testing commit message'; + wrapper = shallowMount(SuccessMessage, { store }); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); - it('renders last commit message when it exists', async () => { - vm.$store.state.lastCommitMsg = 'testing commit message'; - - await nextTick(); - expect(vm.$el.textContent).toContain('testing commit message'); + it('renders last commit message when it exists', () => { + expect(wrapper.text()).toContain('testing commit message'); }); }); diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js index 37b42001a80..9172c69b10e 100644 --- a/spec/frontend/ide/components/ide_spec.js +++ b/spec/frontend/ide/components/ide_spec.js @@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; import waitForPromises from 'helpers/wait_for_promises'; +import { stubPerformanceWebAPI } from 'helpers/performance'; import CannotPushCodeAlert from '~/ide/components/cannot_push_code_alert.vue'; import ErrorMessage from '~/ide/components/error_message.vue'; import Ide from '~/ide/components/ide.vue'; @@ -40,6 +41,8 @@ describe('WebIDE', () => { const findAlert = () => wrapper.findComponent(CannotPushCodeAlert); beforeEach(() => { + stubPerformanceWebAPI(); + store = createStore(); }); diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js index a85c52f5e86..0f61aa80e53 100644 --- a/spec/frontend/ide/components/ide_tree_list_spec.js +++ b/spec/frontend/ide/components/ide_tree_list_spec.js @@ -1,82 +1,72 @@ -import Vue, { nextTick } from 'vue'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { GlSkeletonLoader } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import IdeTreeList from '~/ide/components/ide_tree_list.vue'; import { createStore } from '~/ide/stores'; +import FileTree from '~/vue_shared/components/file_tree.vue'; import { 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; - let store; +describe('IdeTreeList component', () => { + let wrapper; - const bootstrapWithTree = (tree = normalBranchTree) => { + const mountComponent = ({ tree, loading = false } = {}) => { + const store = createStore(); store.state.currentProjectId = 'abcproject'; store.state.currentBranchId = 'main'; store.state.projects.abcproject = { ...projectData }; - Vue.set(store.state.trees, 'abcproject/main', { - tree, - loading: false, - }); + Vue.set(store.state.trees, 'abcproject/main', { tree, loading }); - vm = createComponentWithStore(Component, store, { - viewerType: 'edit', + wrapper = shallowMount(IdeTreeList, { + propsData: { + viewerType: 'edit', + }, + store, }); }; - beforeEach(() => { - store = createStore(); - }); - afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('normal branch', () => { - beforeEach(() => { - bootstrapWithTree(); - - jest.spyOn(vm, '$emit').mockImplementation(() => {}); - - vm.$mount(); - }); + const tree = [file('fileName')]; it('emits tree-ready event', () => { - expect(vm.$emit).toHaveBeenCalledTimes(1); - expect(vm.$emit).toHaveBeenCalledWith('tree-ready'); + mountComponent({ tree }); + + expect(wrapper.emitted('tree-ready')).toEqual([[]]); }); - it('renders loading indicator', async () => { - store.state.trees['abcproject/main'].loading = true; + it('renders loading indicator', () => { + mountComponent({ tree, loading: true }); - await nextTick(); - expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull(); - expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3); + expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(3); }); it('renders list of files', () => { - expect(vm.$el.textContent).toContain('fileName'); + mountComponent({ tree }); + + expect(wrapper.findAllComponents(FileTree)).toHaveLength(1); + expect(wrapper.findComponent(FileTree).props('file')).toEqual(tree[0]); }); }); describe('empty-branch state', () => { beforeEach(() => { - bootstrapWithTree(emptyBranchTree); - - jest.spyOn(vm, '$emit').mockImplementation(() => {}); + mountComponent({ tree: [] }); + }); - vm.$mount(); + it('emits tree-ready event', () => { + expect(wrapper.emitted('tree-ready')).toEqual([[]]); }); - it('still emits tree-ready event', () => { - expect(vm.$emit).toHaveBeenCalledWith('tree-ready'); + it('does not render files', () => { + expect(wrapper.findAllComponents(FileTree)).toHaveLength(0); }); - it('does not load files if the branch is empty', () => { - expect(vm.$el.textContent).not.toContain('fileName'); - expect(vm.$el.textContent).toContain('No files'); + it('renders empty state text', () => { + expect(wrapper.text()).toBe('No files'); }); }); }); diff --git a/spec/frontend/ide/components/nav_dropdown_button_spec.js b/spec/frontend/ide/components/nav_dropdown_button_spec.js index 1c14685df68..8eebcdd9e08 100644 --- a/spec/frontend/ide/components/nav_dropdown_button_spec.js +++ b/spec/frontend/ide/components/nav_dropdown_button_spec.js @@ -1,81 +1,74 @@ -import Vue, { nextTick } from 'vue'; import { trimText } from 'helpers/text_helper'; -import { mountComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue'; import { createStore } from '~/ide/stores'; +import { __ } from '~/locale'; -describe('NavDropdown', () => { +describe('NavDropdownButton component', () => { const TEST_BRANCH_ID = 'lorem-ipsum-dolar'; const TEST_MR_ID = '12345'; - let store; - let vm; - - beforeEach(() => { - store = createStore(); - }); + let wrapper; afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); - const createComponent = (props = {}) => { - vm = mountComponentWithStore(Vue.extend(NavDropdownButton), { props, store }); - vm.$mount(); + const createComponent = ({ props = {}, state = {} } = {}) => { + const store = createStore(); + store.replaceState(state); + wrapper = mountExtended(NavDropdownButton, { propsData: props, store }); }; - const findIcon = (name) => vm.$el.querySelector(`[data-testid="${name}-icon"]`); - const findMRIcon = () => findIcon('merge-request'); - const findBranchIcon = () => findIcon('branch'); + const findMRIcon = () => wrapper.findByLabelText(__('Merge request')); + const findBranchIcon = () => wrapper.findByLabelText(__('Current Branch')); describe('normal', () => { - beforeEach(() => { + it('renders empty placeholders, if state is falsey', () => { createComponent(); - }); - it('renders empty placeholders, if state is falsey', () => { - expect(trimText(vm.$el.textContent)).toEqual('- -'); + expect(trimText(wrapper.text())).toBe('- -'); }); - it('renders branch name, if state has currentBranchId', async () => { - vm.$store.state.currentBranchId = TEST_BRANCH_ID; + it('renders branch name, if state has currentBranchId', () => { + createComponent({ state: { currentBranchId: TEST_BRANCH_ID } }); - await nextTick(); - expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`); + expect(trimText(wrapper.text())).toBe(`${TEST_BRANCH_ID} -`); }); - it('renders mr id, if state has currentMergeRequestId', async () => { - vm.$store.state.currentMergeRequestId = TEST_MR_ID; + it('renders mr id, if state has currentMergeRequestId', () => { + createComponent({ state: { currentMergeRequestId: TEST_MR_ID } }); - await nextTick(); - expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`); + expect(trimText(wrapper.text())).toBe(`- !${TEST_MR_ID}`); }); - it('renders branch and mr, if state has both', async () => { - vm.$store.state.currentBranchId = TEST_BRANCH_ID; - vm.$store.state.currentMergeRequestId = TEST_MR_ID; + it('renders branch and mr, if state has both', () => { + createComponent({ + state: { currentBranchId: TEST_BRANCH_ID, currentMergeRequestId: TEST_MR_ID }, + }); - await nextTick(); - expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`); + expect(trimText(wrapper.text())).toBe(`${TEST_BRANCH_ID} !${TEST_MR_ID}`); }); it('shows icons', () => { - expect(findBranchIcon()).toBeTruthy(); - expect(findMRIcon()).toBeTruthy(); + createComponent(); + + expect(findBranchIcon().exists()).toBe(true); + expect(findMRIcon().exists()).toBe(true); }); }); - describe('with showMergeRequests false', () => { + describe('when showMergeRequests=false', () => { beforeEach(() => { - createComponent({ showMergeRequests: false }); + createComponent({ props: { showMergeRequests: false } }); }); it('shows single empty placeholder, if state is falsey', () => { - expect(trimText(vm.$el.textContent)).toEqual('-'); + expect(trimText(wrapper.text())).toBe('-'); }); it('shows only branch icon', () => { - expect(findBranchIcon()).toBeTruthy(); - expect(findMRIcon()).toBe(null); + expect(findBranchIcon().exists()).toBe(true); + expect(findMRIcon().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js index e8635444801..68cc08d2ebc 100644 --- a/spec/frontend/ide/components/new_dropdown/modal_spec.js +++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js @@ -1,209 +1,419 @@ -import Vue, { nextTick } from 'vue'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { GlButton, GlModal } from '@gitlab/ui'; +import { nextTick } from 'vue'; import createFlash from '~/flash'; -import modal from '~/ide/components/new_dropdown/modal.vue'; +import Modal from '~/ide/components/new_dropdown/modal.vue'; import { createStore } from '~/ide/stores'; +import { stubComponent } from 'helpers/stub_component'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { createEntriesFromPaths } from '../../helpers'; jest.mock('~/flash'); +const NEW_NAME = 'babar'; + describe('new file modal component', () => { - const Component = Vue.extend(modal); - let vm; + const showModal = jest.fn(); + const toggleModal = jest.fn(); + + let store; + let wrapper; + + const findForm = () => wrapper.findByTestId('file-name-form'); + const findGlModal = () => wrapper.findComponent(GlModal); + const findInput = () => wrapper.findByTestId('file-name-field'); + const findTemplateButtons = () => wrapper.findAllComponents(GlButton); + const findTemplateButtonsModel = () => + findTemplateButtons().wrappers.map((x) => ({ + text: x.text(), + variant: x.props('variant'), + category: x.props('category'), + })); + + const open = (type, path) => { + // TODO: This component can not be passed props + // We have to interact with the open() method? + wrapper.vm.open(type, path); + }; + const triggerSubmitForm = () => { + findForm().trigger('submit'); + }; + const triggerSubmitModal = () => { + findGlModal().vm.$emit('primary'); + }; + const triggerCancel = () => { + findGlModal().vm.$emit('cancel'); + }; + + const mountComponent = () => { + const GlModalStub = stubComponent(GlModal); + jest.spyOn(GlModalStub.methods, 'show').mockImplementation(showModal); + jest.spyOn(GlModalStub.methods, 'toggle').mockImplementation(toggleModal); + + wrapper = shallowMountExtended(Modal, { + store, + stubs: { + GlModal: GlModalStub, + }, + // We need to attach to document for "focus" to work + attachTo: document.body, + }); + }; + + beforeEach(() => { + store = createStore(); + + Object.assign( + store.state.entries, + createEntriesFromPaths([ + 'README.md', + 'src', + 'src/deleted.js', + 'src/parent_dir', + 'src/parent_dir/foo.js', + ]), + ); + Object.assign(store.state.entries['src/deleted.js'], { deleted: true }); + + jest.spyOn(store, 'dispatch').mockImplementation(); + }); afterEach(() => { - vm.$destroy(); + store = null; + wrapper.destroy(); + document.body.innerHTML = ''; }); - 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 }) => { + describe('default', () => { beforeEach(async () => { - const store = createStore(); - - vm = createComponentWithStore(Component, store).$mount(); - vm.open(entryType); - vm.name = 'testing'; + mountComponent(); + // Not necessarily needed, but used to ensure that nothing extra is happening after the tick await nextTick(); }); - afterEach(() => { - vm.close(); + it('renders modal', () => { + expect(findGlModal().props()).toMatchObject({ + actionCancel: { + attributes: [{ variant: 'default' }], + text: 'Cancel', + }, + actionPrimary: { + attributes: [{ variant: 'confirm' }], + text: 'Create file', + }, + actionSecondary: null, + size: 'lg', + modalId: 'ide-new-entry', + title: 'Create new file', + }); }); - it(`sets modal title as ${entryType}`, () => { - expect(document.querySelector('.modal-title').textContent.trim()).toBe(modalTitle); + it('renders name label', () => { + expect(wrapper.find('label').text()).toBe('Name'); }); - it(`sets button label as ${entryType}`, () => { - expect(document.querySelector('.btn-confirm').textContent.trim()).toBe(btnTitle); + it('renders template buttons', () => { + const actual = findTemplateButtonsModel(); + + expect(actual.length).toBeGreaterThan(0); + expect(actual).toEqual( + store.getters['fileTemplates/templateTypes'].map((template) => ({ + category: 'secondary', + text: template.name, + variant: 'dashed', + })), + ); }); - it(`sets form label as ${entryType}`, () => { - expect(document.querySelector('.label-bold').textContent.trim()).toBe('Name'); + // These negative ".not.toHaveBeenCalled" assertions complement the positive "toHaveBeenCalled" + // assertions that show up later in this spec. Without these, we're not guaranteed the "act" + // actually caused the change in behavior. + it('does not dispatch actions by default', () => { + expect(store.dispatch).not.toHaveBeenCalled(); }); - it(`shows file templates: ${showsFileTemplates}`, () => { - const templateFilesEl = document.querySelector('.file-templates'); - expect(Boolean(templateFilesEl)).toBe(showsFileTemplates); + it('does not trigger modal by default', () => { + expect(showModal).not.toHaveBeenCalled(); + expect(toggleModal).not.toHaveBeenCalled(); }); - }); - 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('does not focus input by default', () => { + expect(document.activeElement).toBe(document.body); }); + }); - it.each` - entryType | modalTitle | btnTitle - ${'tree'} | ${'Rename folder'} | ${'Rename folder'} - ${'blob'} | ${'Rename file'} | ${'Rename file'} - `( - 'renders title and button for renaming $entryType', - async ({ entryType, modalTitle, btnTitle }) => { - vm.$store.state.entries['test-path'].type = entryType; - vm.open('rename', 'test-path'); + describe.each` + entryType | path | modalTitle | btnTitle | showsFileTemplates | inputValue | inputPlaceholder + ${'tree'} | ${''} | ${'Create new directory'} | ${'Create directory'} | ${false} | ${''} | ${'dir/'} + ${'blob'} | ${''} | ${'Create new file'} | ${'Create file'} | ${true} | ${''} | ${'dir/file_name'} + ${'blob'} | ${'foo/bar'} | ${'Create new file'} | ${'Create file'} | ${true} | ${'foo/bar/'} | ${'dir/file_name'} + `( + 'when opened as $entryType with path "$path"', + ({ + entryType, + path, + modalTitle, + btnTitle, + showsFileTemplates, + inputValue, + inputPlaceholder, + }) => { + beforeEach(async () => { + mountComponent(); + + open(entryType, path); await nextTick(); - expect(document.querySelector('.modal-title').textContent.trim()).toBe(modalTitle); - expect(document.querySelector('.btn-confirm').textContent.trim()).toBe(btnTitle); - }, - ); + }); - describe('entryName', () => { - it('returns entries name', () => { - vm.open('rename', 'test-path'); + it('sets modal props', () => { + expect(findGlModal().props()).toMatchObject({ + title: modalTitle, + actionPrimary: { + attributes: [{ variant: 'confirm' }], + text: btnTitle, + }, + }); + }); - expect(vm.entryName).toBe('test-path'); + it('sets input attributes', () => { + expect(findInput().element.value).toBe(inputValue); + expect(findInput().attributes('placeholder')).toBe(inputPlaceholder); }); - it('does not reset entryName to its old value if empty', () => { - vm.entryName = 'hello'; - vm.entryName = ''; + it(`shows file templates: ${showsFileTemplates}`, () => { + const actual = findTemplateButtonsModel().length > 0; - expect(vm.entryName).toBe(''); + expect(actual).toBe(showsFileTemplates); + }); + + it('shows modal', () => { + expect(showModal).toHaveBeenCalled(); }); - }); - describe('open', () => { - it('sets entryName to path provided if modalType is rename', () => { - vm.open('rename', 'test-path'); + it('focus on input', () => { + expect(document.activeElement).toBe(findInput().element); + }); + + it('resets when canceled', async () => { + triggerCancel(); + + await nextTick(); - expect(vm.entryName).toBe('test-path'); + // Resets input value + expect(findInput().element.value).toBe(''); + // Resets to blob mode + expect(findGlModal().props('title')).toBe('Create new file'); }); + }, + ); + + describe.each` + modalType | name | expectedName + ${'blob'} | ${'foo/bar.js'} | ${'foo/bar.js'} + ${'blob'} | ${'foo /bar.js'} | ${'foo/bar.js'} + ${'tree'} | ${'foo/dir'} | ${'foo/dir'} + ${'tree'} | ${'foo /dir'} | ${'foo/dir'} + `('when submitting as $modalType with "$name"', ({ modalType, name, expectedName }) => { + describe('when using the modal primary button', () => { + beforeEach(async () => { + mountComponent(); + + open(modalType, ''); + await nextTick(); - it("appends '/' to the path if modalType isn't rename", () => { - vm.open('blob', 'test-path'); + findInput().setValue(name); + triggerSubmitModal(); + }); - expect(vm.entryName).toBe('test-path/'); + it('triggers createTempEntry action', () => { + expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', { + name: expectedName, + type: modalType, + }); }); + }); + + describe('when triggering form submit (pressing enter)', () => { + beforeEach(async () => { + mountComponent(); + + open(modalType, ''); + await nextTick(); - it('leaves entryName blank if no path is provided', () => { - vm.open('blob'); + findInput().setValue(name); + triggerSubmitForm(); + }); - expect(vm.entryName).toBe(''); + it('triggers createTempEntry action', () => { + expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', { + name: expectedName, + type: modalType, + }); }); }); }); - describe('createFromTemplate', () => { - let store; + describe('when creating from template type', () => { + beforeEach(async () => { + mountComponent(); - beforeEach(() => { - store = createStore(); - store.state.entries = { - 'test-path/test': { - name: 'test', - deleted: false, - }, - }; + open('blob', 'some_dir'); - vm = createComponentWithStore(Component, store).$mount(); - vm.open('blob'); + await nextTick(); - jest.spyOn(vm, 'createTempEntry').mockImplementation(); + // Set input, then trigger button + findInput().setValue('some_dir/foo.js'); + findTemplateButtons().at(1).vm.$emit('click'); }); - it.each` - entryName | newFilePath - ${''} | ${'.gitignore'} - ${'README.md'} | ${'.gitignore'} - ${'test-path/test/'} | ${'test-path/test/.gitignore'} - ${'test-path/test'} | ${'test-path/.gitignore'} - ${'test-path/test/abc.md'} | ${'test-path/test/.gitignore'} - `( - 'creates a new file with the given template name in appropriate directory for path: $path', - ({ entryName, newFilePath }) => { - vm.entryName = entryName; + it('triggers createTempEntry action', () => { + const { name: expectedName } = store.getters['fileTemplates/templateTypes'][1]; - vm.createFromTemplate({ name: '.gitignore' }); + expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', { + name: `some_dir/${expectedName}`, + type: 'blob', + }); + }); - expect(vm.createTempEntry).toHaveBeenCalledWith({ - name: newFilePath, - type: 'blob', - }); - }, - ); + it('toggles modal', () => { + expect(toggleModal).toHaveBeenCalled(); + }); }); - describe('submitForm', () => { - let store; + describe.each` + origPath | title | inputValue | inputSelectionStart + ${'src/parent_dir'} | ${'Rename folder'} | ${'src/parent_dir'} | ${'src/'.length} + ${'README.md'} | ${'Rename file'} | ${'README.md'} | ${0} + `('when renaming for $origPath', ({ origPath, title, inputValue, inputSelectionStart }) => { + beforeEach(async () => { + mountComponent(); + + open('rename', origPath); + + await nextTick(); + }); - beforeEach(() => { - store = createStore(); - store.state.entries = { - 'test-path/test': { - name: 'test', - deleted: false, + it('sets modal props for renaming', () => { + expect(findGlModal().props()).toMatchObject({ + title, + actionPrimary: { + attributes: [{ variant: 'confirm' }], + text: title, }, - }; + }); + }); + + it('sets input value', () => { + expect(findInput().element.value).toBe(inputValue); + }); - vm = createComponentWithStore(Component, store).$mount(); + it(`does not show file templates`, () => { + expect(findTemplateButtonsModel()).toHaveLength(0); }); - it('throws an error when target entry exists', () => { - vm.open('rename', 'test-path/test'); + it('shows modal when renaming', () => { + expect(showModal).toHaveBeenCalled(); + }); - expect(createFlash).not.toHaveBeenCalled(); + it('focus on input when renaming', () => { + expect(document.activeElement).toBe(findInput().element); + }); + + it('selects name part of the input', () => { + expect(findInput().element.selectionStart).toBe(inputSelectionStart); + expect(findInput().element.selectionEnd).toBe(origPath.length); + }); + + describe('when renames is submitted successfully', () => { + describe('when using the modal primary button', () => { + beforeEach(() => { + findInput().setValue(NEW_NAME); + triggerSubmitModal(); + }); + + it('dispatches renameEntry event', () => { + expect(store.dispatch).toHaveBeenCalledWith('renameEntry', { + path: origPath, + parentPath: '', + name: NEW_NAME, + }); + }); + + it('does not trigger flash', () => { + expect(createFlash).not.toHaveBeenCalled(); + }); + }); - vm.submitForm(); + describe('when triggering form submit (pressing enter)', () => { + beforeEach(() => { + findInput().setValue(NEW_NAME); + triggerSubmitForm(); + }); + it('dispatches renameEntry event', () => { + expect(store.dispatch).toHaveBeenCalledWith('renameEntry', { + path: origPath, + parentPath: '', + name: NEW_NAME, + }); + }); + + it('does not trigger flash', () => { + expect(createFlash).not.toHaveBeenCalled(); + }); + }); + }); + }); + + describe('when renaming and file already exists', () => { + beforeEach(async () => { + mountComponent(); + + open('rename', 'src/parent_dir'); + + await nextTick(); + + // Set to something that already exists! + findInput().setValue('src'); + triggerSubmitModal(); + }); + + it('creates flash', () => { expect(createFlash).toHaveBeenCalledWith({ - message: 'The name "test-path/test" is already taken in this directory.', + message: 'The name "src" is already taken in this directory.', fadeTransition: false, addBodyClass: true, }); }); - it('does not throw error when target entry does not exist', () => { - jest.spyOn(vm, 'renameEntry').mockImplementation(); + it('does not dispatch event', () => { + expect(store.dispatch).not.toHaveBeenCalled(); + }); + }); - vm.open('rename', 'test-path/test'); - vm.entryName = 'test-path/test2'; - vm.submitForm(); + describe('when renaming and file has been deleted', () => { + beforeEach(async () => { + mountComponent(); - expect(createFlash).not.toHaveBeenCalled(); - }); + open('rename', 'src/parent_dir/foo.js'); - it('removes leading/trailing found in the new name', () => { - vm.open('rename', 'test-path/test'); + await nextTick(); - vm.entryName = 'test-path /test'; + findInput().setValue('src/deleted.js'); + triggerSubmitModal(); + }); - vm.submitForm(); + it('does not create flash', () => { + expect(createFlash).not.toHaveBeenCalled(); + }); - expect(vm.entryName).toBe('test-path/test'); + it('dispatches event', () => { + expect(store.dispatch).toHaveBeenCalledWith('renameEntry', { + path: 'src/parent_dir/foo.js', + name: 'deleted.js', + parentPath: 'src', + }); }); }); }); diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index b44651481e9..7a0bcda1b7a 100644 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -1,3 +1,4 @@ +import { GlTab } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import { editor as monacoEditor, Range } from 'monaco-editor'; import Vue, { nextTick } from 'vue'; @@ -5,6 +6,7 @@ import Vuex from 'vuex'; import { shallowMount } from '@vue/test-utils'; import '~/behaviors/markdown/render_gfm'; import waitForPromises from 'helpers/wait_for_promises'; +import { stubPerformanceWebAPI } from 'helpers/performance'; import { exampleConfigs, exampleFiles } from 'jest/ide/lib/editorconfig/mock_data'; import { EDITOR_CODE_INSTANCE_FN, EDITOR_DIFF_INSTANCE_FN } from '~/editor/constants'; import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext'; @@ -125,10 +127,12 @@ describe('RepoEditor', () => { }; const findEditor = () => wrapper.find('[data-testid="editor-container"]'); - const findTabs = () => wrapper.findAll('.ide-mode-tabs .nav-links li'); + const findTabs = () => wrapper.findAllComponents(GlTab); const findPreviewTab = () => wrapper.find('[data-testid="preview-tab"]'); beforeEach(() => { + stubPerformanceWebAPI(); + createInstanceSpy = jest.spyOn(SourceEditor.prototype, EDITOR_CODE_INSTANCE_FN); createDiffInstanceSpy = jest.spyOn(SourceEditor.prototype, EDITOR_DIFF_INSTANCE_FN); createModelSpy = jest.spyOn(monacoEditor, 'createModel'); @@ -201,12 +205,12 @@ describe('RepoEditor', () => { const tabs = findTabs(); expect(tabs).toHaveLength(2); - expect(tabs.at(0).text()).toBe('Edit'); - expect(tabs.at(1).text()).toBe('Preview Markdown'); + expect(tabs.at(0).element.dataset.testid).toBe('edit-tab'); + expect(tabs.at(1).element.dataset.testid).toBe('preview-tab'); }); it('renders markdown for tempFile', async () => { - findPreviewTab().trigger('click'); + findPreviewTab().vm.$emit('click'); await waitForPromises(); expect(wrapper.find(ContentViewer).html()).toContain(dummyFile.text.content); }); diff --git a/spec/frontend/ide/ide_router_spec.js b/spec/frontend/ide/ide_router_spec.js index cd10812f8ea..adbdba1b11e 100644 --- a/spec/frontend/ide/ide_router_spec.js +++ b/spec/frontend/ide/ide_router_spec.js @@ -1,4 +1,5 @@ import waitForPromises from 'helpers/wait_for_promises'; +import { stubPerformanceWebAPI } from 'helpers/performance'; import { createRouter } from '~/ide/ide_router'; import { createStore } from '~/ide/stores'; @@ -12,6 +13,8 @@ describe('IDE router', () => { let router; beforeEach(() => { + stubPerformanceWebAPI(); + window.history.replaceState({}, '', '/'); store = createStore(); router = createRouter(store, DEFAULT_BRANCH); diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js index 45d1beea3f8..6c1dee1e5ca 100644 --- a/spec/frontend/ide/stores/actions/file_spec.js +++ b/spec/frontend/ide/stores/actions/file_spec.js @@ -7,6 +7,7 @@ import { createStore } from '~/ide/stores'; import * as actions from '~/ide/stores/actions/file'; import * as types from '~/ide/stores/mutation_types'; import axios from '~/lib/utils/axios_utils'; +import { stubPerformanceWebAPI } from 'helpers/performance'; import { file, createTriggerRenameAction, createTriggerUpdatePayload } from '../../helpers'; const ORIGINAL_CONTENT = 'original content'; @@ -19,6 +20,8 @@ describe('IDE store file actions', () => { let router; beforeEach(() => { + stubPerformanceWebAPI(); + mock = new MockAdapter(axios); originalGon = window.gon; window.gon = { diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js index 5592e2664c4..abc3ba5b0a2 100644 --- a/spec/frontend/ide/stores/actions/merge_request_spec.js +++ b/spec/frontend/ide/stores/actions/merge_request_spec.js @@ -1,5 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import { range } from 'lodash'; +import { stubPerformanceWebAPI } from 'helpers/performance'; import { TEST_HOST } from 'helpers/test_constants'; import testAction from 'helpers/vuex_action_helper'; import createFlash from '~/flash'; @@ -35,6 +36,8 @@ describe('IDE store merge request actions', () => { let mock; beforeEach(() => { + stubPerformanceWebAPI(); + store = createStore(); mock = new MockAdapter(axios); diff --git a/spec/frontend/ide/stores/actions/tree_spec.js b/spec/frontend/ide/stores/actions/tree_spec.js index fc44cbb21ae..d43393875eb 100644 --- a/spec/frontend/ide/stores/actions/tree_spec.js +++ b/spec/frontend/ide/stores/actions/tree_spec.js @@ -1,4 +1,5 @@ import MockAdapter from 'axios-mock-adapter'; +import { stubPerformanceWebAPI } from 'helpers/performance'; import { TEST_HOST } from 'helpers/test_constants'; import testAction from 'helpers/vuex_action_helper'; import { createRouter } from '~/ide/ide_router'; @@ -24,6 +25,8 @@ describe('Multi-file store tree actions', () => { }; beforeEach(() => { + stubPerformanceWebAPI(); + store = createStore(); router = createRouter(store); jest.spyOn(router, 'push').mockImplementation(); diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js index 3889c4f11c3..f6d54491d77 100644 --- a/spec/frontend/ide/stores/actions_spec.js +++ b/spec/frontend/ide/stores/actions_spec.js @@ -1,4 +1,5 @@ import MockAdapter from 'axios-mock-adapter'; +import { stubPerformanceWebAPI } from 'helpers/performance'; import testAction from 'helpers/vuex_action_helper'; import eventHub from '~/ide/eventhub'; import { createRouter } from '~/ide/ide_router'; @@ -34,6 +35,8 @@ describe('Multi-file store actions', () => { let router; beforeEach(() => { + stubPerformanceWebAPI(); + store = createStore(); router = createRouter(store); |