From f64a639bcfa1fc2bc89ca7db268f594306edfd7c Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 16 Mar 2021 18:18:33 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-10-stable-ee --- .../ide/components/commit_sidebar/form_spec.js | 5 +- spec/frontend/ide/components/ide_spec.js | 3 +- .../pipelines/__snapshots__/list_spec.js.snap | 1 - .../frontend/ide/components/pipelines/list_spec.js | 1 - spec/frontend/ide/components/repo_editor_spec.js | 1106 ++++++++++---------- spec/frontend/ide/components/repo_tab_spec.js | 19 +- spec/frontend/ide/services/index_spec.js | 4 +- spec/frontend/ide/stores/getters_spec.js | 70 +- 8 files changed, 628 insertions(+), 581 deletions(-) (limited to 'spec/frontend/ide') diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js index 2b567816ce8..083a2a73b24 100644 --- a/spec/frontend/ide/components/commit_sidebar/form_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js @@ -14,6 +14,7 @@ import { createBranchChangedCommitError, branchAlreadyExistsCommitError, } from '~/ide/lib/errors'; +import { MSG_CANNOT_PUSH_CODE_SHORT } from '~/ide/messages'; import { createStore } from '~/ide/stores'; import { COMMIT_TO_NEW_BRANCH } from '~/ide/stores/modules/commit/constants'; @@ -84,8 +85,8 @@ describe('IDE commit form', () => { ${'when there are no changes'} | ${[]} | ${{ pushCode: true }} | ${goToEditView} | ${findBeginCommitButtonData} | ${true} | ${''} ${'when there are changes'} | ${['test']} | ${{ pushCode: true }} | ${goToEditView} | ${findBeginCommitButtonData} | ${false} | ${''} ${'when there are changes'} | ${['test']} | ${{ pushCode: true }} | ${goToCommitView} | ${findCommitButtonData} | ${false} | ${''} - ${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToEditView} | ${findBeginCommitButtonData} | ${true} | ${CommitForm.MSG_CANNOT_PUSH_CODE} - ${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToCommitView} | ${findCommitButtonData} | ${true} | ${CommitForm.MSG_CANNOT_PUSH_CODE} + ${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToEditView} | ${findBeginCommitButtonData} | ${true} | ${MSG_CANNOT_PUSH_CODE_SHORT} + ${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToCommitView} | ${findCommitButtonData} | ${true} | ${MSG_CANNOT_PUSH_CODE_SHORT} `('$desc', ({ stagedFiles, userPermissions, viewFn, buttonFn, disabled, tooltip }) => { beforeEach(async () => { store.state.stagedFiles = stagedFiles; diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js index c9d19c18d03..bd251f78654 100644 --- a/spec/frontend/ide/components/ide_spec.js +++ b/spec/frontend/ide/components/ide_spec.js @@ -4,6 +4,7 @@ import Vuex from 'vuex'; import waitForPromises from 'helpers/wait_for_promises'; import ErrorMessage from '~/ide/components/error_message.vue'; import Ide from '~/ide/components/ide.vue'; +import { MSG_CANNOT_PUSH_CODE } from '~/ide/messages'; import { createStore } from '~/ide/stores'; import { file } from '../helpers'; import { projectData } from '../mock_data'; @@ -158,7 +159,7 @@ describe('WebIDE', () => { expect(findAlert().props()).toMatchObject({ dismissible: false, }); - expect(findAlert().text()).toBe(Ide.MSG_CANNOT_PUSH_CODE); + expect(findAlert().text()).toBe(MSG_CANNOT_PUSH_CODE); }); it.each` diff --git a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap index efa58a4a47b..194a619c4aa 100644 --- a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap +++ b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap @@ -10,7 +10,6 @@ exports[`IDE pipelines list when loaded renders empty state when no latestPipeli cansetci="true" class="mb-auto mt-auto" emptystatesvgpath="http://test.host" - helppagepath="http://test.host" /> `; diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js index 58d8c0629fb..a917f4c0230 100644 --- a/spec/frontend/ide/components/pipelines/list_spec.js +++ b/spec/frontend/ide/components/pipelines/list_spec.js @@ -19,7 +19,6 @@ describe('IDE pipelines list', () => { let wrapper; const defaultState = { - links: { ciHelpPagePath: TEST_HOST }, pipelinesEmptyStateSvgPath: TEST_HOST, }; const defaultPipelinesState = { diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index 1985feb1615..a3b327343e5 100644 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -1,11 +1,15 @@ +import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; -import { Range } from 'monaco-editor'; +import { editor as monacoEditor, Range } from 'monaco-editor'; import Vue from 'vue'; import Vuex from 'vuex'; import '~/behaviors/markdown/render_gfm'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import waitForPromises from 'helpers/wait_for_promises'; import waitUsingRealTimer from 'helpers/wait_using_real_timer'; +import { exampleConfigs, exampleFiles } from 'jest/ide/lib/editorconfig/mock_data'; +import { EDITOR_CODE_INSTANCE_FN, EDITOR_DIFF_INSTANCE_FN } from '~/editor/constants'; +import EditorLite from '~/editor/editor_lite'; +import { EditorWebIdeExtension } from '~/editor/extensions/editor_lite_webide_ext'; import RepoEditor from '~/ide/components/repo_editor.vue'; import { leftSidebarViews, @@ -13,733 +17,723 @@ import { FILE_VIEW_MODE_PREVIEW, viewerTypes, } from '~/ide/constants'; -import Editor from '~/ide/lib/editor'; +import ModelManager from '~/ide/lib/common/model_manager'; import service from '~/ide/services'; import { createStoreOptions } from '~/ide/stores'; import axios from '~/lib/utils/axios_utils'; +import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import { file } from '../helpers'; -import { exampleConfigs, exampleFiles } from '../lib/editorconfig/mock_data'; + +const defaultFileProps = { + ...file('file.txt'), + content: 'hello world', + active: true, + tempFile: true, +}; +const createActiveFile = (props) => { + return { + ...defaultFileProps, + ...props, + }; +}; + +const dummyFile = { + markdown: (() => + createActiveFile({ + projectId: 'namespace/project', + path: 'sample.md', + name: 'sample.md', + }))(), + binary: (() => + createActiveFile({ + name: 'file.dat', + content: '🐱', // non-ascii binary content, + }))(), + empty: (() => + createActiveFile({ + tempFile: false, + content: '', + raw: '', + }))(), +}; + +const prepareStore = (state, activeFile) => { + const localState = { + openFiles: [activeFile], + projects: { + 'gitlab-org/gitlab': { + branches: { + master: { + name: 'master', + commit: { + id: 'abcdefgh', + }, + }, + }, + }, + }, + currentProjectId: 'gitlab-org/gitlab', + currentBranchId: 'master', + entries: { + [activeFile.path]: activeFile, + }, + }; + const storeOptions = createStoreOptions(); + return new Vuex.Store({ + ...createStoreOptions(), + state: { + ...storeOptions.state, + ...localState, + ...state, + }, + }); +}; describe('RepoEditor', () => { + let wrapper; let vm; - let store; + let createInstanceSpy; + let createDiffInstanceSpy; + let createModelSpy; const waitForEditorSetup = () => new Promise((resolve) => { vm.$once('editorSetup', resolve); }); - const createComponent = () => { - if (vm) { - throw new Error('vm already exists'); - } - vm = createComponentWithStore(Vue.extend(RepoEditor), store, { - file: store.state.openFiles[0], + const createComponent = async ({ state = {}, activeFile = defaultFileProps } = {}) => { + const store = prepareStore(state, activeFile); + wrapper = shallowMount(RepoEditor, { + store, + propsData: { + file: store.state.openFiles[0], + }, + mocks: { + ContentViewer, + }, }); - + await waitForPromises(); + vm = wrapper.vm; jest.spyOn(vm, 'getFileData').mockResolvedValue(); jest.spyOn(vm, 'getRawFileData').mockResolvedValue(); - - vm.$mount(); }; - const createOpenFile = (path) => { - const origFile = store.state.openFiles[0]; - const newFile = { ...origFile, path, key: path, name: 'myfile.txt', content: 'hello world' }; - - store.state.entries[path] = newFile; - - store.state.openFiles = [newFile]; - }; + const findEditor = () => wrapper.find('[data-testid="editor-container"]'); + const findTabs = () => wrapper.findAll('.ide-mode-tabs .nav-links li'); + const findPreviewTab = () => wrapper.find('[data-testid="preview-tab"]'); beforeEach(() => { - const f = { - ...file('file.txt'), - content: 'hello world', - }; - - const storeOptions = createStoreOptions(); - store = new Vuex.Store(storeOptions); - - f.active = true; - f.tempFile = true; - - store.state.openFiles.push(f); - store.state.projects = { - 'gitlab-org/gitlab': { - branches: { - master: { - name: 'master', - commit: { - id: 'abcdefgh', - }, - }, - }, - }, - }; - store.state.currentProjectId = 'gitlab-org/gitlab'; - store.state.currentBranchId = 'master'; - - Vue.set(store.state.entries, f.path, f); + createInstanceSpy = jest.spyOn(EditorLite.prototype, EDITOR_CODE_INSTANCE_FN); + createDiffInstanceSpy = jest.spyOn(EditorLite.prototype, EDITOR_DIFF_INSTANCE_FN); + createModelSpy = jest.spyOn(monacoEditor, 'createModel'); + jest.spyOn(service, 'getFileData').mockResolvedValue(); + jest.spyOn(service, 'getRawFileData').mockResolvedValue(); }); afterEach(() => { - vm.$destroy(); - vm = null; - - Editor.editorInstance.dispose(); + jest.clearAllMocks(); + // create a new model each time, otherwise tests conflict with each other + // because of same model being used in multiple tests + // eslint-disable-next-line no-undef + monaco.editor.getModels().forEach((model) => model.dispose()); + wrapper.destroy(); + wrapper = null; }); - const findEditor = () => vm.$el.querySelector('.multi-file-editor-holder'); - const changeViewMode = (viewMode) => - store.dispatch('editor/updateFileEditor', { path: vm.file.path, data: { viewMode } }); - describe('default', () => { - beforeEach(() => { - createComponent(); - - return waitForEditorSetup(); + it.each` + boolVal | textVal + ${true} | ${'all'} + ${false} | ${'none'} + `('sets renderWhitespace to "$textVal"', async ({ boolVal, textVal } = {}) => { + await createComponent({ + state: { + renderWhitespaceInCode: boolVal, + }, + }); + expect(vm.editorOptions.renderWhitespace).toEqual(textVal); }); - it('sets renderWhitespace to `all`', () => { - vm.$store.state.renderWhitespaceInCode = true; - - expect(vm.editorOptions.renderWhitespace).toEqual('all'); + it('renders an ide container', async () => { + await createComponent(); + expect(findEditor().isVisible()).toBe(true); }); - it('sets renderWhitespace to `none`', () => { - vm.$store.state.renderWhitespaceInCode = false; + it('renders only an edit tab', async () => { + await createComponent(); + const tabs = findTabs(); - expect(vm.editorOptions.renderWhitespace).toEqual('none'); + expect(tabs).toHaveLength(1); + expect(tabs.at(0).text()).toBe('Edit'); }); + }); - it('renders an ide container', () => { - expect(vm.shouldHideEditor).toBeFalsy(); - expect(vm.showEditor).toBe(true); - expect(findEditor()).not.toHaveCss({ display: 'none' }); - }); + describe('when file is markdown', () => { + let mock; + let activeFile; - it('renders only an edit tab', (done) => { - Vue.nextTick(() => { - const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li'); + beforeEach(() => { + activeFile = dummyFile.markdown; - expect(tabs.length).toBe(1); - expect(tabs[0].textContent.trim()).toBe('Edit'); + mock = new MockAdapter(axios); - done(); + mock.onPost(/(.*)\/preview_markdown/).reply(200, { + body: `

${defaultFileProps.content}

`, }); }); - describe('when file is markdown', () => { - let mock; - - beforeEach(() => { - mock = new MockAdapter(axios); - - mock.onPost(/(.*)\/preview_markdown/).reply(200, { - body: '

testing 123

', - }); - - Vue.set(vm, 'file', { - ...vm.file, - projectId: 'namespace/project', - path: 'sample.md', - name: 'sample.md', - content: 'testing 123', - }); - - vm.$store.state.entries[vm.file.path] = vm.file; + afterEach(() => { + mock.restore(); + }); - return vm.$nextTick(); - }); + it('renders an Edit and a Preview Tab', async () => { + await createComponent({ activeFile }); + const tabs = findTabs(); - afterEach(() => { - mock.restore(); - }); + expect(tabs).toHaveLength(2); + expect(tabs.at(0).text()).toBe('Edit'); + expect(tabs.at(1).text()).toBe('Preview Markdown'); + }); - it('renders an Edit and a Preview Tab', (done) => { - Vue.nextTick(() => { - const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li'); + it('renders markdown for tempFile', async () => { + // by default files created in the spec are temp: no need for explicitly sending the param + await createComponent({ activeFile }); - expect(tabs.length).toBe(2); - expect(tabs[0].textContent.trim()).toBe('Edit'); - expect(tabs[1].textContent.trim()).toBe('Preview Markdown'); + findPreviewTab().trigger('click'); + await waitForPromises(); + expect(wrapper.find(ContentViewer).html()).toContain(defaultFileProps.content); + }); - done(); - }); + it('shows no tabs when not in Edit mode', async () => { + await createComponent({ + state: { + currentActivityView: leftSidebarViews.review.name, + }, + activeFile, }); + expect(findTabs()).toHaveLength(0); + }); + }); - it('renders markdown for tempFile', (done) => { - vm.file.tempFile = true; - - vm.$nextTick() - .then(() => { - vm.$el.querySelectorAll('.ide-mode-tabs .nav-links a')[1].click(); - }) - .then(waitForPromises) - .then(() => { - expect(vm.$el.querySelector('.preview-container').innerHTML).toContain( - '

testing 123

', - ); - }) - .then(done) - .catch(done.fail); - }); + describe('when file is binary and not raw', () => { + beforeEach(async () => { + const activeFile = dummyFile.binary; + await createComponent({ activeFile }); + }); - describe('when not in edit mode', () => { - beforeEach(async () => { - await vm.$nextTick(); + it('does not render the IDE', () => { + expect(findEditor().isVisible()).toBe(false); + }); - vm.$store.state.currentActivityView = leftSidebarViews.review.name; + it('does not create an instance', () => { + expect(createInstanceSpy).not.toHaveBeenCalled(); + expect(createDiffInstanceSpy).not.toHaveBeenCalled(); + }); + }); - return vm.$nextTick(); + describe('createEditorInstance', () => { + it.each` + viewer | diffInstance + ${viewerTypes.edit} | ${undefined} + ${viewerTypes.diff} | ${true} + ${viewerTypes.mr} | ${true} + `( + 'creates instance of correct type when viewer is $viewer', + async ({ viewer, diffInstance }) => { + await createComponent({ + state: { viewer }, }); + const isDiff = () => { + return diffInstance ? { isDiff: true } : {}; + }; + expect(createInstanceSpy).toHaveBeenCalledWith(expect.objectContaining(isDiff())); + expect(createDiffInstanceSpy).toHaveBeenCalledTimes((diffInstance && 1) || 0); + }, + ); - it('shows no tabs', () => { - expect(vm.$el.querySelectorAll('.ide-mode-tabs .nav-links a')).toHaveLength(0); + it('installs the WebIDE extension', async () => { + const extensionSpy = jest.spyOn(EditorLite, 'instanceApplyExtension'); + await createComponent(); + expect(extensionSpy).toHaveBeenCalled(); + Reflect.ownKeys(EditorWebIdeExtension.prototype) + .filter((fn) => fn !== 'constructor') + .forEach((fn) => { + expect(vm.editor[fn]).toBe(EditorWebIdeExtension.prototype[fn]); }); - }); }); + }); - describe('when open file is binary and not raw', () => { - beforeEach((done) => { - vm.file.name = 'file.dat'; - vm.file.content = '🐱'; // non-ascii binary content - jest.spyOn(vm.editor, 'createInstance').mockImplementation(); - jest.spyOn(vm.editor, 'createDiffInstance').mockImplementation(); - - vm.$nextTick(done); - }); - - it('does not render the IDE', () => { - expect(vm.shouldHideEditor).toBeTruthy(); - }); - - it('does not call createInstance', async () => { - // Mirror the act's in the `createEditorInstance` - vm.createEditorInstance(); - - await vm.$nextTick(); + describe('setupEditor', () => { + beforeEach(async () => { + await createComponent(); + }); - expect(vm.editor.createInstance).not.toHaveBeenCalled(); - expect(vm.editor.createDiffInstance).not.toHaveBeenCalled(); - }); + it('creates new model on load', () => { + // We always create two models per file to be able to build a diff of changes + expect(createModelSpy).toHaveBeenCalledTimes(2); + // The model with the most recent changes is the last one + const [content] = createModelSpy.mock.calls[1]; + expect(content).toBe(defaultFileProps.content); }); - describe('createEditorInstance', () => { - it('calls createInstance when viewer is editor', (done) => { - jest.spyOn(vm.editor, 'createInstance').mockImplementation(); + it('does not create a new model on subsequent calls to setupEditor and re-uses the already-existing model', () => { + const existingModel = vm.model; + createModelSpy.mockClear(); - vm.createEditorInstance(); + vm.setupEditor(); - vm.$nextTick(() => { - expect(vm.editor.createInstance).toHaveBeenCalled(); + expect(createModelSpy).not.toHaveBeenCalled(); + expect(vm.model).toBe(existingModel); + }); - done(); - }); - }); + it('adds callback methods', () => { + jest.spyOn(vm.editor, 'onPositionChange'); + jest.spyOn(vm.model, 'onChange'); + jest.spyOn(vm.model, 'updateOptions'); - it('calls createDiffInstance when viewer is diff', (done) => { - vm.$store.state.viewer = 'diff'; + vm.setupEditor(); - jest.spyOn(vm.editor, 'createDiffInstance').mockImplementation(); + expect(vm.editor.onPositionChange).toHaveBeenCalledTimes(1); + expect(vm.model.onChange).toHaveBeenCalledTimes(1); + expect(vm.model.updateOptions).toHaveBeenCalledWith(vm.rules); + }); - vm.createEditorInstance(); + it('updates state with the value of the model', () => { + const newContent = 'As Gregor Samsa\n awoke one morning\n'; + vm.model.setValue(newContent); - vm.$nextTick(() => { - expect(vm.editor.createDiffInstance).toHaveBeenCalled(); + vm.setupEditor(); - done(); - }); - }); + expect(vm.file.content).toBe(newContent); + }); - it('calls createDiffInstance when viewer is a merge request diff', (done) => { - vm.$store.state.viewer = 'mrdiff'; + it('sets head model as staged file', () => { + vm.modelManager.dispose(); + const addModelSpy = jest.spyOn(ModelManager.prototype, 'addModel'); - jest.spyOn(vm.editor, 'createDiffInstance').mockImplementation(); + vm.$store.state.stagedFiles.push({ ...vm.file, key: 'staged' }); + vm.file.staged = true; + vm.file.key = `unstaged-${vm.file.key}`; - vm.createEditorInstance(); + vm.setupEditor(); - vm.$nextTick(() => { - expect(vm.editor.createDiffInstance).toHaveBeenCalled(); + expect(addModelSpy).toHaveBeenCalledWith(vm.file, vm.$store.state.stagedFiles[0]); + }); + }); - done(); - }); - }); + describe('editor updateDimensions', () => { + let updateDimensionsSpy; + let updateDiffViewSpy; + beforeEach(async () => { + await createComponent(); + updateDimensionsSpy = jest.spyOn(vm.editor, 'updateDimensions'); + updateDiffViewSpy = jest.spyOn(vm.editor, 'updateDiffView').mockImplementation(); }); - describe('setupEditor', () => { - it('creates new model', () => { - jest.spyOn(vm.editor, 'createModel'); + it('calls updateDimensions only when panelResizing is false', async () => { + expect(updateDimensionsSpy).not.toHaveBeenCalled(); + expect(updateDiffViewSpy).not.toHaveBeenCalled(); + expect(vm.$store.state.panelResizing).toBe(false); // default value - Editor.editorInstance.modelManager.dispose(); + vm.$store.state.panelResizing = true; + await vm.$nextTick(); - vm.setupEditor(); + expect(updateDimensionsSpy).not.toHaveBeenCalled(); + expect(updateDiffViewSpy).not.toHaveBeenCalled(); - expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, null); - expect(vm.model).not.toBeNull(); - }); + vm.$store.state.panelResizing = false; + await vm.$nextTick(); - it('attaches model to editor', () => { - jest.spyOn(vm.editor, 'attachModel'); + expect(updateDimensionsSpy).toHaveBeenCalledTimes(1); + expect(updateDiffViewSpy).toHaveBeenCalledTimes(1); - Editor.editorInstance.modelManager.dispose(); + vm.$store.state.panelResizing = true; + await vm.$nextTick(); - vm.setupEditor(); + expect(updateDimensionsSpy).toHaveBeenCalledTimes(1); + expect(updateDiffViewSpy).toHaveBeenCalledTimes(1); + }); - expect(vm.editor.attachModel).toHaveBeenCalledWith(vm.model); - }); + it('calls updateDimensions when rightPane is toggled', async () => { + expect(updateDimensionsSpy).not.toHaveBeenCalled(); + expect(updateDiffViewSpy).not.toHaveBeenCalled(); + expect(vm.$store.state.rightPane.isOpen).toBe(false); // default value - it('attaches model to merge request editor', () => { - vm.$store.state.viewer = 'mrdiff'; - vm.file.mrChange = true; - jest.spyOn(vm.editor, 'attachMergeRequestModel').mockImplementation(); + vm.$store.state.rightPane.isOpen = true; + await vm.$nextTick(); - Editor.editorInstance.modelManager.dispose(); + expect(updateDimensionsSpy).toHaveBeenCalledTimes(1); + expect(updateDiffViewSpy).toHaveBeenCalledTimes(1); - vm.setupEditor(); + vm.$store.state.rightPane.isOpen = false; + await vm.$nextTick(); - expect(vm.editor.attachMergeRequestModel).toHaveBeenCalledWith(vm.model); - }); + expect(updateDimensionsSpy).toHaveBeenCalledTimes(2); + expect(updateDiffViewSpy).toHaveBeenCalledTimes(2); + }); + }); - it('does not attach model to merge request editor when not a MR change', () => { - vm.$store.state.viewer = 'mrdiff'; - vm.file.mrChange = false; - jest.spyOn(vm.editor, 'attachMergeRequestModel').mockImplementation(); + describe('editor tabs', () => { + beforeEach(async () => { + await createComponent(); + }); - Editor.editorInstance.modelManager.dispose(); + it.each` + mode | isVisible + ${'edit'} | ${true} + ${'review'} | ${false} + ${'commit'} | ${false} + `('tabs in $mode are $isVisible', async ({ mode, isVisible } = {}) => { + vm.$store.state.currentActivityView = leftSidebarViews[mode].name; - vm.setupEditor(); + await vm.$nextTick(); + expect(wrapper.find('.nav-links').exists()).toBe(isVisible); + }); + }); - expect(vm.editor.attachMergeRequestModel).not.toHaveBeenCalledWith(vm.model); + describe('files in preview mode', () => { + let updateDimensionsSpy; + const changeViewMode = (viewMode) => + vm.$store.dispatch('editor/updateFileEditor', { + path: vm.file.path, + data: { viewMode }, }); - it('adds callback methods', () => { - jest.spyOn(vm.editor, 'onPositionChange'); - - Editor.editorInstance.modelManager.dispose(); - - vm.setupEditor(); - - expect(vm.editor.onPositionChange).toHaveBeenCalled(); - expect(vm.model.events.size).toBe(2); + beforeEach(async () => { + await createComponent({ + activeFile: dummyFile.markdown, }); - it('updates state with the value of the model', () => { - vm.model.setValue('testing 1234\n'); - - vm.setupEditor(); - - expect(vm.file.content).toBe('testing 1234\n'); - }); + updateDimensionsSpy = jest.spyOn(vm.editor, 'updateDimensions'); - it('sets head model as staged file', () => { - jest.spyOn(vm.editor, 'createModel'); + changeViewMode(FILE_VIEW_MODE_PREVIEW); + await vm.$nextTick(); + }); - Editor.editorInstance.modelManager.dispose(); + it('do not show the editor', () => { + expect(vm.showEditor).toBe(false); + expect(findEditor().isVisible()).toBe(false); + }); - vm.$store.state.stagedFiles.push({ ...vm.file, key: 'staged' }); - vm.file.staged = true; - vm.file.key = `unstaged-${vm.file.key}`; + it('updates dimensions when switching view back to edit', async () => { + expect(updateDimensionsSpy).not.toHaveBeenCalled(); - vm.setupEditor(); + changeViewMode(FILE_VIEW_MODE_EDITOR); + await vm.$nextTick(); - expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, vm.$store.state.stagedFiles[0]); - }); + expect(updateDimensionsSpy).toHaveBeenCalled(); }); + }); - describe('editor updateDimensions', () => { - beforeEach(() => { - jest.spyOn(vm.editor, 'updateDimensions'); - jest.spyOn(vm.editor, 'updateDiffView').mockImplementation(); - }); - - it('calls updateDimensions when panelResizing is false', (done) => { - vm.$store.state.panelResizing = true; - - vm.$nextTick() - .then(() => { - vm.$store.state.panelResizing = false; - }) - .then(vm.$nextTick) - .then(() => { - expect(vm.editor.updateDimensions).toHaveBeenCalled(); - expect(vm.editor.updateDiffView).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('does not call updateDimensions when panelResizing is true', (done) => { - vm.$store.state.panelResizing = true; + describe('initEditor', () => { + const hideEditorAndRunFn = async () => { + jest.clearAllMocks(); + jest.spyOn(vm, 'shouldHideEditor', 'get').mockReturnValue(true); - vm.$nextTick(() => { - expect(vm.editor.updateDimensions).not.toHaveBeenCalled(); - expect(vm.editor.updateDiffView).not.toHaveBeenCalled(); + vm.initEditor(); + await vm.$nextTick(); + }; - done(); - }); + it('does not fetch file information for temp entries', async () => { + await createComponent({ + activeFile: createActiveFile(), }); - it('calls updateDimensions when rightPane is opened', (done) => { - vm.$store.state.rightPane.isOpen = true; - - vm.$nextTick(() => { - expect(vm.editor.updateDimensions).toHaveBeenCalled(); - expect(vm.editor.updateDiffView).toHaveBeenCalled(); - - done(); - }); - }); + expect(vm.getFileData).not.toHaveBeenCalled(); }); - describe('show tabs', () => { - it('shows tabs in edit mode', () => { - expect(vm.$el.querySelector('.nav-links')).not.toBe(null); + it('is being initialised for files without content even if shouldHideEditor is `true`', async () => { + await createComponent({ + activeFile: dummyFile.empty, }); - it('hides tabs in review mode', (done) => { - vm.$store.state.currentActivityView = leftSidebarViews.review.name; + await hideEditorAndRunFn(); - vm.$nextTick(() => { - expect(vm.$el.querySelector('.nav-links')).toBe(null); + expect(vm.getFileData).toHaveBeenCalled(); + expect(vm.getRawFileData).toHaveBeenCalled(); + }); - done(); - }); + it('does not initialize editor for files already with content when shouldHideEditor is `true`', async () => { + await createComponent({ + activeFile: createActiveFile(), }); - it('hides tabs in commit mode', (done) => { - vm.$store.state.currentActivityView = leftSidebarViews.commit.name; + await hideEditorAndRunFn(); - vm.$nextTick(() => { - expect(vm.$el.querySelector('.nav-links')).toBe(null); + expect(vm.getFileData).not.toHaveBeenCalled(); + expect(vm.getRawFileData).not.toHaveBeenCalled(); + expect(createInstanceSpy).not.toHaveBeenCalled(); + }); + }); - done(); - }); + describe('updates on file changes', () => { + beforeEach(async () => { + await createComponent({ + activeFile: createActiveFile({ + content: 'foo', // need to prevent full cycle of initEditor + }), }); + jest.spyOn(vm, 'initEditor').mockImplementation(); }); - describe('when files view mode is preview', () => { - beforeEach((done) => { - jest.spyOn(vm.editor, 'updateDimensions').mockImplementation(); - changeViewMode(FILE_VIEW_MODE_PREVIEW); - vm.file.name = 'myfile.md'; - vm.file.content = 'hello world'; + it('calls removePendingTab when old file is pending', async () => { + jest.spyOn(vm, 'shouldHideEditor', 'get').mockReturnValue(true); + jest.spyOn(vm, 'removePendingTab').mockImplementation(); - vm.$nextTick(done); - }); + const origFile = vm.file; + vm.file.pending = true; + await vm.$nextTick(); - it('should hide editor', () => { - expect(vm.showEditor).toBe(false); - expect(findEditor()).toHaveCss({ display: 'none' }); + wrapper.setProps({ + file: file('testing'), }); + vm.file.content = 'foo'; // need to prevent full cycle of initEditor + await vm.$nextTick(); - describe('when file view mode changes to editor', () => { - it('should update dimensions', () => { - changeViewMode(FILE_VIEW_MODE_EDITOR); - - return vm.$nextTick().then(() => { - expect(vm.editor.updateDimensions).toHaveBeenCalled(); - }); - }); - }); + expect(vm.removePendingTab).toHaveBeenCalledWith(origFile); }); - describe('initEditor', () => { - beforeEach(() => { - vm.file.tempFile = false; - jest.spyOn(vm.editor, 'createInstance').mockImplementation(); - jest.spyOn(vm, 'shouldHideEditor', 'get').mockReturnValue(true); - }); + it('does not call initEditor if the file did not change', async () => { + Vue.set(vm, 'file', vm.file); + await vm.$nextTick(); - it('does not fetch file information for temp entries', (done) => { - vm.file.tempFile = true; - - vm.initEditor(); - vm.$nextTick() - .then(() => { - expect(vm.getFileData).not.toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('is being initialised for files without content even if shouldHideEditor is `true`', (done) => { - vm.file.content = ''; - vm.file.raw = ''; + expect(vm.initEditor).not.toHaveBeenCalled(); + }); - vm.initEditor(); + it('calls initEditor when file key is changed', async () => { + expect(vm.initEditor).not.toHaveBeenCalled(); - vm.$nextTick() - .then(() => { - expect(vm.getFileData).toHaveBeenCalled(); - expect(vm.getRawFileData).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + wrapper.setProps({ + file: { + ...vm.file, + key: 'new', + }, }); + await vm.$nextTick(); - it('does not initialize editor for files already with content', (done) => { - vm.file.content = 'foo'; - - vm.initEditor(); - vm.$nextTick() - .then(() => { - expect(vm.getFileData).not.toHaveBeenCalled(); - expect(vm.getRawFileData).not.toHaveBeenCalled(); - expect(vm.editor.createInstance).not.toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); + expect(vm.initEditor).toHaveBeenCalled(); + }); + }); + + describe('populates editor with the fetched content', () => { + const createRemoteFile = (name) => ({ + ...file(name), + tmpFile: false, }); - describe('updates on file changes', () => { - beforeEach(() => { - jest.spyOn(vm, 'initEditor').mockImplementation(); - }); + beforeEach(async () => { + await createComponent(); + vm.getRawFileData.mockRestore(); + }); - it('calls removePendingTab when old file is pending', (done) => { - jest.spyOn(vm, 'shouldHideEditor', 'get').mockReturnValue(true); - jest.spyOn(vm, 'removePendingTab').mockImplementation(); + it('after switching viewer from edit to diff', async () => { + const f = createRemoteFile('newFile'); + Vue.set(vm.$store.state.entries, f.path, f); - vm.file.pending = true; + jest.spyOn(service, 'getRawFileData').mockImplementation(async () => { + expect(vm.file.loading).toBe(true); - vm.$nextTick() - .then(() => { - vm.file = file('testing'); - vm.file.content = 'foo'; // need to prevent full cycle of initEditor + // switching from edit to diff mode usually triggers editor initialization + vm.$store.state.viewer = viewerTypes.diff; - return vm.$nextTick(); - }) - .then(() => { - expect(vm.removePendingTab).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + // we delay returning the file to make sure editor doesn't initialize before we fetch file content + await waitUsingRealTimer(30); + return 'rawFileData123\n'; }); - it('does not call initEditor if the file did not change', (done) => { - Vue.set(vm, 'file', vm.file); - - vm.$nextTick() - .then(() => { - expect(vm.initEditor).not.toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + wrapper.setProps({ + file: f, }); - it('calls initEditor when file key is changed', (done) => { - expect(vm.initEditor).not.toHaveBeenCalled(); + await waitForEditorSetup(); + expect(vm.model.getModel().getValue()).toBe('rawFileData123\n'); + }); - Vue.set(vm, 'file', { - ...vm.file, - key: 'new', + it('after opening multiple files at the same time', async () => { + const fileA = createRemoteFile('fileA'); + const aContent = 'fileA-rawContent\n'; + const bContent = 'fileB-rawContent\n'; + const fileB = createRemoteFile('fileB'); + Vue.set(vm.$store.state.entries, fileA.path, fileA); + Vue.set(vm.$store.state.entries, fileB.path, fileB); + + jest + .spyOn(service, 'getRawFileData') + .mockImplementation(async () => { + // opening fileB while the content of fileA is still being fetched + wrapper.setProps({ + file: fileB, + }); + return aContent; + }) + .mockImplementationOnce(async () => { + // we delay returning fileB content to make sure the editor doesn't initialize prematurely + await waitUsingRealTimer(30); + return bContent; }); - vm.$nextTick() - .then(() => { - expect(vm.initEditor).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + wrapper.setProps({ + file: fileA, }); - }); - describe('populates editor with the fetched content', () => { - beforeEach(() => { - vm.getRawFileData.mockRestore(); - }); + await waitForEditorSetup(); + expect(vm.model.getModel().getValue()).toBe(bContent); + }); + }); - const createRemoteFile = (name) => ({ - ...file(name), - tmpFile: false, + describe('onPaste', () => { + const setFileName = (name) => + createActiveFile({ + content: 'hello world\n', + name, + path: `foo/${name}`, + key: 'new', }); - it('after switching viewer from edit to diff', async () => { - jest.spyOn(service, 'getRawFileData').mockImplementation(async () => { - expect(vm.file.loading).toBe(true); - - // switching from edit to diff mode usually triggers editor initialization - store.state.viewer = viewerTypes.diff; + const pasteImage = () => { + window.dispatchEvent( + Object.assign(new Event('paste'), { + clipboardData: { + files: [new File(['foo'], 'foo.png', { type: 'image/png' })], + }, + }), + ); + }; - // we delay returning the file to make sure editor doesn't initialize before we fetch file content - await waitUsingRealTimer(30); - return 'rawFileData123\n'; + const watchState = (watched) => + new Promise((resolve) => { + const unwatch = vm.$store.watch(watched, () => { + unwatch(); + resolve(); }); - - const f = createRemoteFile('newFile'); - Vue.set(store.state.entries, f.path, f); - - vm.file = f; - - await waitForEditorSetup(); - expect(vm.model.getModel().getValue()).toBe('rawFileData123\n'); }); - it('after opening multiple files at the same time', async () => { - const fileA = createRemoteFile('fileA'); - const fileB = createRemoteFile('fileB'); - Vue.set(store.state.entries, fileA.path, fileA); - Vue.set(store.state.entries, fileB.path, fileB); - - jest - .spyOn(service, 'getRawFileData') - .mockImplementationOnce(async () => { - // opening fileB while the content of fileA is still being fetched - vm.file = fileB; - return 'fileA-rawContent\n'; - }) - .mockImplementationOnce(async () => { - // we delay returning fileB content to make sure the editor doesn't initialize prematurely - await waitUsingRealTimer(30); - return 'fileB-rawContent\n'; - }); + // Pasting an image does a lot of things like using the FileReader API, + // so, waitForPromises isn't very reliable (and causes a flaky spec) + // Read more about state.watch: https://vuex.vuejs.org/api/#watch + const waitForFileContentChange = () => watchState((s) => s.entries['foo/bar.md'].content); - vm.file = fileA; - - await waitForEditorSetup(); - expect(vm.model.getModel().getValue()).toBe('fileB-rawContent\n'); + beforeEach(async () => { + await createComponent({ + state: { + trees: { + 'gitlab-org/gitlab': { tree: [] }, + }, + currentProjectId: 'gitlab-org', + currentBranchId: 'gitlab', + }, + activeFile: setFileName('bar.md'), }); - }); - - describe('onPaste', () => { - const setFileName = (name) => { - Vue.set(vm, 'file', { - ...vm.file, - content: 'hello world\n', - name, - path: `foo/${name}`, - key: 'new', - }); - vm.$store.state.entries[vm.file.path] = vm.file; - }; + vm.setupEditor(); - const pasteImage = () => { - window.dispatchEvent( - Object.assign(new Event('paste'), { - clipboardData: { - files: [new File(['foo'], 'foo.png', { type: 'image/png' })], - }, - }), - ); - }; - - const watchState = (watched) => - new Promise((resolve) => { - const unwatch = vm.$store.watch(watched, () => { - unwatch(); - resolve(); - }); - }); + await waitForPromises(); + // set cursor to line 2, column 1 + vm.editor.setSelection(new Range(2, 1, 2, 1)); + vm.editor.focus(); - // Pasting an image does a lot of things like using the FileReader API, - // so, waitForPromises isn't very reliable (and causes a flaky spec) - // Read more about state.watch: https://vuex.vuejs.org/api/#watch - const waitForFileContentChange = () => watchState((s) => s.entries['foo/bar.md'].content); - - beforeEach(() => { - setFileName('bar.md'); - - vm.$store.state.trees['gitlab-org/gitlab'] = { tree: [] }; - vm.$store.state.currentProjectId = 'gitlab-org'; - vm.$store.state.currentBranchId = 'gitlab'; - - // create a new model each time, otherwise tests conflict with each other - // because of same model being used in multiple tests - Editor.editorInstance.modelManager.dispose(); - vm.setupEditor(); + jest.spyOn(vm.editor, 'hasTextFocus').mockReturnValue(true); + }); - return waitForPromises().then(() => { - // set cursor to line 2, column 1 - vm.editor.instance.setSelection(new Range(2, 1, 2, 1)); - vm.editor.instance.focus(); + it('adds an image entry to the same folder for a pasted image in a markdown file', async () => { + pasteImage(); - jest.spyOn(vm.editor.instance, 'hasTextFocus').mockReturnValue(true); - }); + await waitForFileContentChange(); + expect(vm.$store.state.entries['foo/foo.png']).toMatchObject({ + path: 'foo/foo.png', + type: 'blob', + content: 'Zm9v', + rawPath: 'data:image/png;base64,Zm9v', }); + }); - it('adds an image entry to the same folder for a pasted image in a markdown file', () => { - pasteImage(); - - return waitForFileContentChange().then(() => { - expect(vm.$store.state.entries['foo/foo.png']).toMatchObject({ - path: 'foo/foo.png', - type: 'blob', - content: 'Zm9v', - rawPath: 'data:image/png;base64,Zm9v', - }); - }); - }); + it("adds a markdown image tag to the file's contents", async () => { + pasteImage(); - it("adds a markdown image tag to the file's contents", () => { - pasteImage(); + await waitForFileContentChange(); + expect(vm.file.content).toBe('hello world\n![foo.png](./foo.png)'); + }); - return waitForFileContentChange().then(() => { - expect(vm.file.content).toBe('hello world\n![foo.png](./foo.png)'); - }); + it("does not add file to state or set markdown image syntax if the file isn't markdown", async () => { + wrapper.setProps({ + file: setFileName('myfile.txt'), }); + pasteImage(); - it("does not add file to state or set markdown image syntax if the file isn't markdown", () => { - setFileName('myfile.txt'); - pasteImage(); - - return waitForPromises().then(() => { - expect(vm.$store.state.entries['foo/foo.png']).toBeUndefined(); - expect(vm.file.content).toBe('hello world\n'); - }); - }); + await waitForPromises(); + expect(vm.$store.state.entries['foo/foo.png']).toBeUndefined(); + expect(vm.file.content).toBe('hello world\n'); }); }); describe('fetchEditorconfigRules', () => { - beforeEach(() => { - exampleConfigs.forEach(({ path, content }) => { - store.state.entries[path] = { ...file(), path, content }; - }); - }); - it.each(exampleFiles)( 'does not fetch content from remote for .editorconfig files present locally (case %#)', - ({ path, monacoRules }) => { - createOpenFile(path); - createComponent(); - - return waitForEditorSetup().then(() => { - expect(vm.rules).toEqual(monacoRules); - expect(vm.model.options).toMatchObject(monacoRules); - expect(vm.getFileData).not.toHaveBeenCalled(); - expect(vm.getRawFileData).not.toHaveBeenCalled(); + async ({ path, monacoRules }) => { + await createComponent({ + state: { + entries: (() => { + const res = {}; + exampleConfigs.forEach(({ path: configPath, content }) => { + res[configPath] = { ...file(), path: configPath, content }; + }); + return res; + })(), + }, + activeFile: createActiveFile({ + path, + key: path, + name: 'myfile.txt', + content: 'hello world', + }), }); + + expect(vm.rules).toEqual(monacoRules); + expect(vm.model.options).toMatchObject(monacoRules); + expect(vm.getFileData).not.toHaveBeenCalled(); + expect(vm.getRawFileData).not.toHaveBeenCalled(); }, ); - it('fetches content from remote for .editorconfig files not available locally', () => { - exampleConfigs.forEach(({ path }) => { - delete store.state.entries[path].content; - delete store.state.entries[path].raw; + it('fetches content from remote for .editorconfig files not available locally', async () => { + const activeFile = createActiveFile({ + path: 'foo/bar/baz/test/my_spec.js', + key: 'foo/bar/baz/test/my_spec.js', + name: 'myfile.txt', + content: 'hello world', + }); + + const expectations = [ + 'foo/bar/baz/.editorconfig', + 'foo/bar/.editorconfig', + 'foo/.editorconfig', + '.editorconfig', + ]; + + await createComponent({ + state: { + entries: (() => { + const res = { + [activeFile.path]: activeFile, + }; + exampleConfigs.forEach(({ path: configPath }) => { + const f = { ...file(), path: configPath }; + delete f.content; + delete f.raw; + res[configPath] = f; + }); + return res; + })(), + }, + activeFile, }); - // Include a "test" directory which does not exist in store. This one should be skipped. - createOpenFile('foo/bar/baz/test/my_spec.js'); - createComponent(); - - return waitForEditorSetup().then(() => { - expect(vm.getFileData.mock.calls.map(([args]) => args)).toEqual([ - { makeFileActive: false, path: 'foo/bar/baz/.editorconfig' }, - { makeFileActive: false, path: 'foo/bar/.editorconfig' }, - { makeFileActive: false, path: 'foo/.editorconfig' }, - { makeFileActive: false, path: '.editorconfig' }, - ]); - expect(vm.getRawFileData.mock.calls.map(([args]) => args)).toEqual([ - { path: 'foo/bar/baz/.editorconfig' }, - { path: 'foo/bar/.editorconfig' }, - { path: 'foo/.editorconfig' }, - { path: '.editorconfig' }, - ]); - }); + expect(service.getFileData.mock.calls.map(([args]) => args)).toEqual( + expectations.map((expectation) => expect.stringContaining(expectation)), + ); + expect(service.getRawFileData.mock.calls.map(([args]) => args)).toEqual( + expectations.map((expectation) => expect.objectContaining({ path: expectation })), + ); }); }); }); diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js index b39a488b034..95d52e8f7a9 100644 --- a/spec/frontend/ide/components/repo_tab_spec.js +++ b/spec/frontend/ide/components/repo_tab_spec.js @@ -1,5 +1,7 @@ +import { GlTab } from '@gitlab/ui'; import { mount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; +import { stubComponent } from 'helpers/stub_component'; import RepoTab from '~/ide/components/repo_tab.vue'; import { createRouter } from '~/ide/ide_router'; import { createStore } from '~/ide/stores'; @@ -8,16 +10,25 @@ import { file } from '../helpers'; const localVue = createLocalVue(); localVue.use(Vuex); +const GlTabStub = stubComponent(GlTab, { + template: '
  • ', +}); + describe('RepoTab', () => { let wrapper; let store; let router; + const findTab = () => wrapper.find(GlTabStub); + function createComponent(propsData) { wrapper = mount(RepoTab, { localVue, store, propsData, + stubs: { + GlTab: GlTabStub, + }, }); } @@ -55,7 +66,7 @@ describe('RepoTab', () => { jest.spyOn(wrapper.vm, 'openPendingTab').mockImplementation(() => {}); - await wrapper.trigger('click'); + await findTab().vm.$emit('click'); expect(wrapper.vm.openPendingTab).not.toHaveBeenCalled(); }); @@ -67,7 +78,7 @@ describe('RepoTab', () => { jest.spyOn(wrapper.vm, 'clickFile').mockImplementation(() => {}); - wrapper.trigger('click'); + findTab().vm.$emit('click'); expect(wrapper.vm.clickFile).toHaveBeenCalledWith(wrapper.vm.tab); }); @@ -91,11 +102,11 @@ describe('RepoTab', () => { tab, }); - await wrapper.trigger('mouseover'); + await findTab().vm.$emit('mouseover'); expect(wrapper.find('.file-modified').exists()).toBe(false); - await wrapper.trigger('mouseout'); + await findTab().vm.$emit('mouseout'); expect(wrapper.find('.file-modified').exists()).toBe(true); }); diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js index 678d58cba34..3503834e24b 100644 --- a/spec/frontend/ide/services/index_spec.js +++ b/spec/frontend/ide/services/index_spec.js @@ -1,7 +1,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql'; import Api from '~/api'; -import getUserPermissions from '~/ide/queries/getUserPermissions.query.graphql'; import services from '~/ide/services'; import { query } from '~/ide/services/gql'; import { escapeFileUrl } from '~/lib/utils/url_utility'; @@ -228,7 +228,7 @@ describe('IDE services', () => { expect(response).toEqual({ data: { ...projectData, ...gqlProjectData } }); expect(Api.project).toHaveBeenCalledWith(TEST_PROJECT_ID); expect(query).toHaveBeenCalledWith({ - query: getUserPermissions, + query: getIdeProject, variables: { projectPath: TEST_PROJECT_ID, }, diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js index 450f5592026..6b66c87e205 100644 --- a/spec/frontend/ide/stores/getters_spec.js +++ b/spec/frontend/ide/stores/getters_spec.js @@ -1,7 +1,17 @@ import { TEST_HOST } from 'helpers/test_constants'; +import { + DEFAULT_PERMISSIONS, + PERMISSION_PUSH_CODE, + PUSH_RULE_REJECT_UNSIGNED_COMMITS, +} from '~/ide/constants'; +import { + MSG_CANNOT_PUSH_CODE, + MSG_CANNOT_PUSH_CODE_SHORT, + MSG_CANNOT_PUSH_UNSIGNED, + MSG_CANNOT_PUSH_UNSIGNED_SHORT, +} from '~/ide/messages'; import { createStore } from '~/ide/stores'; import * as getters from '~/ide/stores/getters'; -import { DEFAULT_PERMISSIONS } from '../../../../app/assets/javascripts/ide/constants'; import { file } from '../helpers'; const TEST_PROJECT_ID = 'test_project'; @@ -385,22 +395,23 @@ describe('IDE store getters', () => { ); }); - describe('findProjectPermissions', () => { - it('returns false if project not found', () => { - expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toEqual( - DEFAULT_PERMISSIONS, - ); + describe.each` + getterName | projectField | defaultValue + ${'findProjectPermissions'} | ${'userPermissions'} | ${DEFAULT_PERMISSIONS} + ${'findPushRules'} | ${'pushRules'} | ${{}} + `('$getterName', ({ getterName, projectField, defaultValue }) => { + const callGetter = (...args) => localStore.getters[getterName](...args); + + it('returns default if project not found', () => { + expect(callGetter(TEST_PROJECT_ID)).toEqual(defaultValue); }); - it('finds permission in given project', () => { - const userPermissions = { - readMergeRequest: true, - createMergeRequestsIn: false, - }; + it('finds field in given project', () => { + const obj = { test: 'foo' }; - localState.projects[TEST_PROJECT_ID] = { userPermissions }; + localState.projects[TEST_PROJECT_ID] = { [projectField]: obj }; - expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toBe(userPermissions); + expect(callGetter(TEST_PROJECT_ID)).toBe(obj); }); }); @@ -408,7 +419,6 @@ describe('IDE store getters', () => { getterName | permissionKey ${'canReadMergeRequests'} | ${'readMergeRequest'} ${'canCreateMergeRequests'} | ${'createMergeRequestIn'} - ${'canPushCode'} | ${'pushCode'} `('$getterName', ({ getterName, permissionKey }) => { it.each([true, false])('finds permission for current project (%s)', (val) => { localState.projects[TEST_PROJECT_ID] = { @@ -422,6 +432,38 @@ describe('IDE store getters', () => { }); }); + describe('canPushCodeStatus', () => { + it.each` + pushCode | rejectUnsignedCommits | expected + ${true} | ${false} | ${{ isAllowed: true, message: '', messageShort: '' }} + ${false} | ${false} | ${{ isAllowed: false, message: MSG_CANNOT_PUSH_CODE, messageShort: MSG_CANNOT_PUSH_CODE_SHORT }} + ${false} | ${true} | ${{ isAllowed: false, message: MSG_CANNOT_PUSH_UNSIGNED, messageShort: MSG_CANNOT_PUSH_UNSIGNED_SHORT }} + `( + 'with pushCode="$pushCode" and rejectUnsignedCommits="$rejectUnsignedCommits"', + ({ pushCode, rejectUnsignedCommits, expected }) => { + localState.projects[TEST_PROJECT_ID] = { + pushRules: { + [PUSH_RULE_REJECT_UNSIGNED_COMMITS]: rejectUnsignedCommits, + }, + userPermissions: { + [PERMISSION_PUSH_CODE]: pushCode, + }, + }; + localState.currentProjectId = TEST_PROJECT_ID; + + expect(localStore.getters.canPushCodeStatus).toEqual(expected); + }, + ); + }); + + describe('canPushCode', () => { + it.each([true, false])('with canPushCodeStatus.isAllowed = $s', (isAllowed) => { + const canPushCodeStatus = { isAllowed }; + + expect(getters.canPushCode({}, { canPushCodeStatus })).toBe(isAllowed); + }); + }); + describe('entryExists', () => { beforeEach(() => { localState.entries = { -- cgit v1.2.3