From fc1df8c8307fc5022f9e8aae04164c089d8fdf2e Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 17 Mar 2020 09:09:20 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../gitlab/import_export/invalid_json/project.json | 3 + spec/frontend/__mocks__/mousetrap/index.js | 6 + spec/frontend/diffs/components/app_spec.js | 723 +++++++++++++++++++++ spec/frontend/diffs/create_diffs_store.js | 15 + .../pipelines/__snapshots__/list_spec.js.snap | 1 + spec/frontend/lib/utils/icon_utils_spec.js | 72 +- .../frontend/mocks/ce/diffs/workers/tree_worker.js | 8 + spec/javascripts/diffs/components/app_spec.js | 718 -------------------- spec/javascripts/diffs/create_diffs_store.js | 16 +- .../import_export/json/legacy_reader_spec.rb | 149 +++++ .../import_export/project/tree_loader_spec.rb | 49 -- .../import_export/project/tree_restorer_spec.rb | 3 +- .../import_export/relation_tree_restorer_spec.rb | 27 +- spec/services/system_note_service_spec.rb | 10 + .../system_notes/issuables_service_spec.rb | 12 + 15 files changed, 994 insertions(+), 818 deletions(-) create mode 100644 spec/fixtures/lib/gitlab/import_export/invalid_json/project.json create mode 100644 spec/frontend/__mocks__/mousetrap/index.js create mode 100644 spec/frontend/diffs/components/app_spec.js create mode 100644 spec/frontend/diffs/create_diffs_store.js create mode 100644 spec/frontend/mocks/ce/diffs/workers/tree_worker.js delete mode 100644 spec/javascripts/diffs/components/app_spec.js create mode 100644 spec/lib/gitlab/import_export/json/legacy_reader_spec.rb delete mode 100644 spec/lib/gitlab/import_export/project/tree_loader_spec.rb (limited to 'spec') diff --git a/spec/fixtures/lib/gitlab/import_export/invalid_json/project.json b/spec/fixtures/lib/gitlab/import_export/invalid_json/project.json new file mode 100644 index 00000000000..83cb34eea91 --- /dev/null +++ b/spec/fixtures/lib/gitlab/import_export/invalid_json/project.json @@ -0,0 +1,3 @@ +{ + "invalid" json +} diff --git a/spec/frontend/__mocks__/mousetrap/index.js b/spec/frontend/__mocks__/mousetrap/index.js new file mode 100644 index 00000000000..63c92fa9a09 --- /dev/null +++ b/spec/frontend/__mocks__/mousetrap/index.js @@ -0,0 +1,6 @@ +/* global Mousetrap */ +// `mousetrap` uses amd which webpack understands but Jest does not +// Thankfully it also writes to a global export so we can es6-ify it +import 'mousetrap'; + +export default Mousetrap; diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js new file mode 100644 index 00000000000..15f91871437 --- /dev/null +++ b/spec/frontend/diffs/components/app_spec.js @@ -0,0 +1,723 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; +import { TEST_HOST } from 'spec/test_constants'; +import Mousetrap from 'mousetrap'; +import App from '~/diffs/components/app.vue'; +import NoChanges from '~/diffs/components/no_changes.vue'; +import DiffFile from '~/diffs/components/diff_file.vue'; +import CompareVersions from '~/diffs/components/compare_versions.vue'; +import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue'; +import CommitWidget from '~/diffs/components/commit_widget.vue'; +import TreeList from '~/diffs/components/tree_list.vue'; +import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants'; +import createDiffsStore from '../create_diffs_store'; +import axios from '~/lib/utils/axios_utils'; +import diffsMockData from '../mock_data/merge_request_diffs'; + +const mergeRequestDiff = { version_index: 1 }; +const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`; + +describe('diffs/components/app', () => { + const oldMrTabs = window.mrTabs; + let store; + let wrapper; + let mock; + + function createComponent(props = {}, extendStore = () => {}) { + const localVue = createLocalVue(); + + localVue.use(Vuex); + + store = createDiffsStore(); + store.state.diffs.isLoading = false; + + extendStore(store); + + wrapper = shallowMount(localVue.extend(App), { + localVue, + propsData: { + endpoint: TEST_ENDPOINT, + endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`, + endpointBatch: `${TEST_HOST}/diff/endpointBatch`, + projectPath: 'namespace/project', + currentUser: {}, + changesEmptyStateIllustration: '', + dismissEndpoint: '', + showSuggestPopover: true, + ...props, + }, + store, + methods: { + isLatestVersion() { + return true; + }, + }, + }); + } + + function getOppositeViewType(currentViewType) { + return currentViewType === INLINE_DIFF_VIEW_TYPE + ? PARALLEL_DIFF_VIEW_TYPE + : INLINE_DIFF_VIEW_TYPE; + } + + beforeEach(() => { + // setup globals (needed for component to mount :/) + window.mrTabs = { + resetViewContainer: jest.fn(), + }; + window.mrTabs.expandViewContainer = jest.fn(); + mock = new MockAdapter(axios); + mock.onGet(TEST_ENDPOINT).reply(200, {}); + }); + + afterEach(() => { + // reset globals + window.mrTabs = oldMrTabs; + + // reset component + wrapper.destroy(); + + mock.restore(); + }); + + describe('fetch diff methods', () => { + beforeEach(done => { + const fetchResolver = () => { + store.state.diffs.retrievingBatches = false; + store.state.notes.discussions = 'test'; + return Promise.resolve({ real_size: 100 }); + }; + jest.spyOn(window, 'requestIdleCallback').mockImplementation(fn => fn()); + createComponent(); + jest.spyOn(wrapper.vm, 'fetchDiffFiles').mockImplementation(fetchResolver); + jest.spyOn(wrapper.vm, 'fetchDiffFilesMeta').mockImplementation(fetchResolver); + jest.spyOn(wrapper.vm, 'fetchDiffFilesBatch').mockImplementation(fetchResolver); + jest.spyOn(wrapper.vm, 'setDiscussions').mockImplementation(() => {}); + jest.spyOn(wrapper.vm, 'startRenderDiffsQueue').mockImplementation(() => {}); + jest.spyOn(wrapper.vm, 'unwatchDiscussions').mockImplementation(() => {}); + jest.spyOn(wrapper.vm, 'unwatchRetrievingBatches').mockImplementation(() => {}); + store.state.diffs.retrievingBatches = true; + store.state.diffs.diffFiles = []; + wrapper.vm.$nextTick(done); + }); + + describe('when the diff view type changes and it should load a single diff view style', () => { + const noLinesDiff = { + highlighted_diff_lines: [], + parallel_diff_lines: [], + }; + const parallelLinesDiff = { + highlighted_diff_lines: [], + parallel_diff_lines: ['line'], + }; + const inlineLinesDiff = { + highlighted_diff_lines: ['line'], + parallel_diff_lines: [], + }; + const fullDiff = { + highlighted_diff_lines: ['line'], + parallel_diff_lines: ['line'], + }; + + function expectFetchToOccur({ + vueInstance, + done = () => {}, + batch = false, + existingFiles = 1, + } = {}) { + vueInstance.$nextTick(() => { + expect(vueInstance.diffFiles.length).toEqual(existingFiles); + + if (!batch) { + expect(vueInstance.fetchDiffFiles).toHaveBeenCalled(); + expect(vueInstance.fetchDiffFilesBatch).not.toHaveBeenCalled(); + } else { + expect(vueInstance.fetchDiffFiles).not.toHaveBeenCalled(); + expect(vueInstance.fetchDiffFilesBatch).toHaveBeenCalled(); + } + + done(); + }); + } + + beforeEach(() => { + wrapper.vm.glFeatures.singleMrDiffView = true; + }); + + it('fetches diffs if it has none', done => { + wrapper.vm.isLatestVersion = () => false; + + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, batch: false, existingFiles: 0, done }); + }); + + it('fetches diffs if it has both view styles, but no lines in either', done => { + wrapper.vm.isLatestVersion = () => false; + + store.state.diffs.diffFiles.push(noLinesDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, done }); + }); + + it('fetches diffs if it only has inline view style', done => { + wrapper.vm.isLatestVersion = () => false; + + store.state.diffs.diffFiles.push(inlineLinesDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, done }); + }); + + it('fetches diffs if it only has parallel view style', done => { + wrapper.vm.isLatestVersion = () => false; + + store.state.diffs.diffFiles.push(parallelLinesDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, done }); + }); + + it('fetches batch diffs if it has none', done => { + wrapper.vm.glFeatures.diffsBatchLoad = true; + + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, existingFiles: 0, done }); + }); + + it('fetches batch diffs if it has both view styles, but no lines in either', done => { + wrapper.vm.glFeatures.diffsBatchLoad = true; + + store.state.diffs.diffFiles.push(noLinesDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done }); + }); + + it('fetches batch diffs if it only has inline view style', done => { + wrapper.vm.glFeatures.diffsBatchLoad = true; + + store.state.diffs.diffFiles.push(inlineLinesDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done }); + }); + + it('fetches batch diffs if it only has parallel view style', done => { + wrapper.vm.glFeatures.diffsBatchLoad = true; + + store.state.diffs.diffFiles.push(parallelLinesDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done }); + }); + + it('does not fetch diffs if it has already fetched both styles of diff', () => { + wrapper.vm.glFeatures.diffsBatchLoad = false; + + store.state.diffs.diffFiles.push(fullDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expect(wrapper.vm.diffFiles.length).toEqual(1); + expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled(); + }); + + it('does not fetch batch diffs if it has already fetched both styles of diff', () => { + wrapper.vm.glFeatures.diffsBatchLoad = true; + + store.state.diffs.diffFiles.push(fullDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expect(wrapper.vm.diffFiles.length).toEqual(1); + expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled(); + }); + }); + + it('calls fetchDiffFiles if diffsBatchLoad is not enabled', done => { + expect(wrapper.vm.diffFilesLength).toEqual(0); + wrapper.vm.glFeatures.diffsBatchLoad = false; + wrapper.vm.fetchData(false); + + expect(wrapper.vm.fetchDiffFiles).toHaveBeenCalled(); + setImmediate(() => { + expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesMeta).not.toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled(); + expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled(); + expect(wrapper.vm.diffFilesLength).toEqual(100); + expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled(); + + done(); + }); + }); + + it('calls batch methods if diffsBatchLoad is enabled, and not latest version', done => { + expect(wrapper.vm.diffFilesLength).toEqual(0); + wrapper.vm.glFeatures.diffsBatchLoad = true; + wrapper.vm.isLatestVersion = () => false; + wrapper.vm.fetchData(false); + + expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled(); + setImmediate(() => { + expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled(); + expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled(); + expect(wrapper.vm.diffFilesLength).toEqual(100); + expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled(); + done(); + }); + }); + + it('calls batch methods if diffsBatchLoad is enabled, and latest version', done => { + expect(wrapper.vm.diffFilesLength).toEqual(0); + wrapper.vm.glFeatures.diffsBatchLoad = true; + wrapper.vm.fetchData(false); + + expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled(); + setImmediate(() => { + expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled(); + expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled(); + expect(wrapper.vm.diffFilesLength).toEqual(100); + expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled(); + done(); + }); + }); + }); + + it('adds container-limiting classes when showFileTree is false with inline diffs', () => { + createComponent({}, ({ state }) => { + state.diffs.showTreeList = false; + state.diffs.isParallelView = false; + }); + + expect(wrapper.contains('.container-limited.limit-container-width')).toBe(true); + }); + + it('does not add container-limiting classes when showFileTree is false with inline diffs', () => { + createComponent({}, ({ state }) => { + state.diffs.showTreeList = true; + state.diffs.isParallelView = false; + }); + + expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false); + }); + + it('does not add container-limiting classes when isFluidLayout', () => { + createComponent({ isFluidLayout: true }, ({ state }) => { + state.diffs.isParallelView = false; + }); + + expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false); + }); + + it('displays loading icon on loading', () => { + createComponent({}, ({ state }) => { + state.diffs.isLoading = true; + }); + + expect(wrapper.contains(GlLoadingIcon)).toBe(true); + }); + + it('displays loading icon on batch loading', () => { + createComponent({}, ({ state }) => { + state.diffs.isBatchLoading = true; + }); + + expect(wrapper.contains(GlLoadingIcon)).toBe(true); + }); + + it('displays diffs container when not loading', () => { + createComponent(); + + expect(wrapper.contains(GlLoadingIcon)).toBe(false); + expect(wrapper.contains('#diffs')).toBe(true); + }); + + it('does not show commit info', () => { + createComponent(); + + expect(wrapper.contains('.blob-commit-info')).toBe(false); + }); + + describe('row highlighting', () => { + beforeEach(() => { + window.location.hash = 'ABC_123'; + }); + + it('sets highlighted row if hash exists in location object', done => { + createComponent({ + shouldShow: true, + }); + + // Component uses $nextTick so we wait until that has finished + setImmediate(() => { + expect(store.state.diffs.highlightedRow).toBe('ABC_123'); + + done(); + }); + }); + + it('marks current diff file based on currently highlighted row', () => { + createComponent({ + shouldShow: true, + }); + + // Component uses $nextTick so we wait until that has finished + return wrapper.vm.$nextTick().then(() => { + expect(store.state.diffs.currentDiffFileId).toBe('ABC'); + }); + }); + }); + + describe('resizable', () => { + afterEach(() => { + localStorage.removeItem('mr_tree_list_width'); + }); + + it('sets initial width when no localStorage has been set', () => { + createComponent(); + + expect(wrapper.vm.treeWidth).toEqual(320); + }); + + it('sets initial width to localStorage size', () => { + localStorage.setItem('mr_tree_list_width', '200'); + + createComponent(); + + expect(wrapper.vm.treeWidth).toEqual(200); + }); + + it('sets width of tree list', () => { + createComponent(); + + expect(wrapper.find('.js-diff-tree-list').element.style.width).toEqual('320px'); + }); + }); + + it('marks current diff file based on currently highlighted row', done => { + createComponent({ + shouldShow: true, + }); + + // Component uses $nextTick so we wait until that has finished + setImmediate(() => { + expect(store.state.diffs.currentDiffFileId).toBe('ABC'); + + done(); + }); + }); + + describe('empty state', () => { + it('renders empty state when no diff files exist', () => { + createComponent(); + + expect(wrapper.contains(NoChanges)).toBe(true); + }); + + it('does not render empty state when diff files exist', () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ + id: 1, + }); + }); + + expect(wrapper.contains(NoChanges)).toBe(false); + expect(wrapper.findAll(DiffFile).length).toBe(1); + }); + + it('does not render empty state when versions match', () => { + createComponent({}, ({ state }) => { + state.diffs.startVersion = mergeRequestDiff; + state.diffs.mergeRequestDiff = mergeRequestDiff; + }); + + expect(wrapper.contains(NoChanges)).toBe(false); + }); + }); + + describe('keyboard shortcut navigation', () => { + const mappings = { + '[': -1, + k: -1, + ']': +1, + j: +1, + }; + let spy; + + describe('visible app', () => { + beforeEach(() => { + spy = jest.fn(); + + createComponent({ + shouldShow: true, + }); + wrapper.setMethods({ + jumpToFile: spy, + }); + }); + + it.each(Object.keys(mappings))( + 'calls `jumpToFile()` with correct parameter whenever pre-defined %s is pressed', + key => { + return wrapper.vm.$nextTick().then(() => { + expect(spy).not.toHaveBeenCalled(); + + Mousetrap.trigger(key); + + expect(spy).toHaveBeenCalledWith(mappings[key]); + }); + }, + ); + + it('does not call `jumpToFile()` when unknown key is pressed', done => { + wrapper.vm + .$nextTick() + .then(() => { + Mousetrap.trigger('d'); + + expect(spy).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('hideen app', () => { + beforeEach(() => { + spy = jest.fn(); + + createComponent({ + shouldShow: false, + }); + wrapper.setMethods({ + jumpToFile: spy, + }); + }); + + it('stops calling `jumpToFile()` when application is hidden', done => { + wrapper.vm + .$nextTick() + .then(() => { + Object.keys(mappings).forEach(key => { + Mousetrap.trigger(key); + + expect(spy).not.toHaveBeenCalled(); + }); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('jumpToFile', () => { + let spy; + + beforeEach(() => { + spy = jest.fn(); + + createComponent({}, () => { + store.state.diffs.diffFiles = [ + { file_hash: '111', file_path: '111.js' }, + { file_hash: '222', file_path: '222.js' }, + { file_hash: '333', file_path: '333.js' }, + ]; + }); + + wrapper.setMethods({ + scrollToFile: spy, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('jumps to next and previous files in the list', done => { + wrapper.vm + .$nextTick() + .then(() => { + wrapper.vm.jumpToFile(+1); + + expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['222.js']); + store.state.diffs.currentDiffFileId = '222'; + wrapper.vm.jumpToFile(+1); + + expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['333.js']); + store.state.diffs.currentDiffFileId = '333'; + wrapper.vm.jumpToFile(-1); + + expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['222.js']); + }) + .then(done) + .catch(done.fail); + }); + + it('does not jump to previous file from the first one', done => { + wrapper.vm + .$nextTick() + .then(() => { + store.state.diffs.currentDiffFileId = '333'; + + expect(wrapper.vm.currentDiffIndex).toEqual(2); + + wrapper.vm.jumpToFile(+1); + + expect(wrapper.vm.currentDiffIndex).toEqual(2); + expect(spy).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('does not jump to next file from the last one', done => { + wrapper.vm + .$nextTick() + .then(() => { + expect(wrapper.vm.currentDiffIndex).toEqual(0); + + wrapper.vm.jumpToFile(-1); + + expect(wrapper.vm.currentDiffIndex).toEqual(0); + expect(spy).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('diffs', () => { + it('should render compare versions component', () => { + createComponent({}, ({ state }) => { + state.diffs.mergeRequestDiffs = diffsMockData; + state.diffs.targetBranchName = 'target-branch'; + state.diffs.mergeRequestDiff = mergeRequestDiff; + }); + + expect(wrapper.contains(CompareVersions)).toBe(true); + expect(wrapper.find(CompareVersions).props()).toEqual( + expect.objectContaining({ + targetBranch: { + branchName: 'target-branch', + versionIndex: -1, + path: '', + }, + mergeRequestDiffs: diffsMockData, + mergeRequestDiff, + }), + ); + }); + + it('should render hidden files warning if render overflow warning is present', () => { + createComponent({}, ({ state }) => { + state.diffs.renderOverflowWarning = true; + state.diffs.realSize = '5'; + state.diffs.plainDiffPath = 'plain diff path'; + state.diffs.emailPatchPath = 'email patch path'; + state.diffs.size = 1; + }); + + expect(wrapper.contains(HiddenFilesWarning)).toBe(true); + expect(wrapper.find(HiddenFilesWarning).props()).toEqual( + expect.objectContaining({ + total: '5', + plainDiffPath: 'plain diff path', + emailPatchPath: 'email patch path', + visible: 1, + }), + ); + }); + + it('should display commit widget if store has a commit', () => { + createComponent({}, () => { + store.state.diffs.commit = { + author: 'John Doe', + }; + }); + + expect(wrapper.contains(CommitWidget)).toBe(true); + }); + + it('should display diff file if there are diff files', () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + }); + + expect(wrapper.contains(DiffFile)).toBe(true); + }); + + it('should render tree list', () => { + createComponent(); + + expect(wrapper.find(TreeList).exists()).toBe(true); + }); + }); + + describe('hideTreeListIfJustOneFile', () => { + let toggleShowTreeList; + + beforeEach(() => { + toggleShowTreeList = jest.fn(); + }); + + afterEach(() => { + localStorage.removeItem('mr_tree_show'); + }); + + it('calls toggleShowTreeList when only 1 file', () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + }); + + wrapper.setMethods({ + toggleShowTreeList, + }); + + wrapper.vm.hideTreeListIfJustOneFile(); + + expect(toggleShowTreeList).toHaveBeenCalledWith(false); + }); + + it('does not call toggleShowTreeList when more than 1 file', () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + state.diffs.diffFiles.push({ sha: '124' }); + }); + + wrapper.setMethods({ + toggleShowTreeList, + }); + + wrapper.vm.hideTreeListIfJustOneFile(); + + expect(toggleShowTreeList).not.toHaveBeenCalled(); + }); + + it('does not call toggleShowTreeList when localStorage is set', () => { + localStorage.setItem('mr_tree_show', 'true'); + + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + }); + + wrapper.setMethods({ + toggleShowTreeList, + }); + + wrapper.vm.hideTreeListIfJustOneFile(); + + expect(toggleShowTreeList).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/diffs/create_diffs_store.js b/spec/frontend/diffs/create_diffs_store.js new file mode 100644 index 00000000000..aacde99964c --- /dev/null +++ b/spec/frontend/diffs/create_diffs_store.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import diffsModule from '~/diffs/store/modules'; +import notesModule from '~/notes/stores/modules'; + +Vue.use(Vuex); + +export default function createDiffsStore() { + return new Vuex.Store({ + modules: { + diffs: diffsModule(), + notes: notesModule(), + }, + }); +} 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 177cd4559ca..efa58a4a47b 100644 --- a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap +++ b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap @@ -8,6 +8,7 @@ exports[`IDE pipelines list when loaded renders empty state when no latestPipeli diff --git a/spec/frontend/lib/utils/icon_utils_spec.js b/spec/frontend/lib/utils/icon_utils_spec.js index 816d634ad15..f798dc6744d 100644 --- a/spec/frontend/lib/utils/icon_utils_spec.js +++ b/spec/frontend/lib/utils/icon_utils_spec.js @@ -1,10 +1,14 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; -import * as iconUtils from '~/lib/utils/icon_utils'; +import { clearSvgIconPathContentCache, getSvgIconPathContent } from '~/lib/utils/icon_utils'; describe('Icon utils', () => { describe('getSvgIconPathContent', () => { let spriteIcons; + let axiosMock; + const mockName = 'mockIconName'; + const mockPath = 'mockPath'; + const mockIcons = ``; beforeAll(() => { spriteIcons = gon.sprite_icons; @@ -15,45 +19,63 @@ describe('Icon utils', () => { gon.sprite_icons = spriteIcons; }); - let axiosMock; - let mockEndpoint; - const mockName = 'mockIconName'; - const mockPath = 'mockPath'; - const getIcon = () => iconUtils.getSvgIconPathContent(mockName); - beforeEach(() => { axiosMock = new MockAdapter(axios); - mockEndpoint = axiosMock.onGet(gon.sprite_icons); }); afterEach(() => { axiosMock.restore(); + clearSvgIconPathContentCache(); }); - it('extracts svg icon path content from sprite icons', () => { - mockEndpoint.replyOnce( - 200, - ``, - ); - - return getIcon().then(path => { - expect(path).toBe(mockPath); + describe('when the icons can be loaded', () => { + beforeEach(() => { + axiosMock.onGet(gon.sprite_icons).reply(200, mockIcons); }); - }); - it('returns null if icon path content does not exist', () => { - mockEndpoint.replyOnce(200, ``); + it('extracts svg icon path content from sprite icons', () => { + return getSvgIconPathContent(mockName).then(path => { + expect(path).toBe(mockPath); + }); + }); - return getIcon().then(path => { - expect(path).toBe(null); + it('returns null if icon path content does not exist', () => { + return getSvgIconPathContent('missing-icon').then(path => { + expect(path).toBe(null); + }); }); }); - it('returns null if an http error occurs', () => { - mockEndpoint.replyOnce(500); + describe('when the icons cannot be loaded on the first 2 tries', () => { + beforeEach(() => { + axiosMock + .onGet(gon.sprite_icons) + .replyOnce(500) + .onGet(gon.sprite_icons) + .replyOnce(500) + .onGet(gon.sprite_icons) + .reply(200, mockIcons); + }); + + it('returns null', () => { + return getSvgIconPathContent(mockName).then(path => { + expect(path).toBe(null); + }); + }); - return getIcon().then(path => { - expect(path).toBe(null); + it('extracts svg icon path content, after 2 attempts', () => { + return getSvgIconPathContent(mockName) + .then(path1 => { + expect(path1).toBe(null); + return getSvgIconPathContent(mockName); + }) + .then(path2 => { + expect(path2).toBe(null); + return getSvgIconPathContent(mockName); + }) + .then(path3 => { + expect(path3).toBe(mockPath); + }); }); }); }); diff --git a/spec/frontend/mocks/ce/diffs/workers/tree_worker.js b/spec/frontend/mocks/ce/diffs/workers/tree_worker.js new file mode 100644 index 00000000000..a33ddbbfe63 --- /dev/null +++ b/spec/frontend/mocks/ce/diffs/workers/tree_worker.js @@ -0,0 +1,8 @@ +/* eslint-disable class-methods-use-this */ +export default class TreeWorkerMock { + addEventListener() {} + + terminate() {} + + postMessage() {} +} diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js deleted file mode 100644 index 5f97182489e..00000000000 --- a/spec/javascripts/diffs/components/app_spec.js +++ /dev/null @@ -1,718 +0,0 @@ -import Vuex from 'vuex'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { GlLoadingIcon } from '@gitlab/ui'; -import { TEST_HOST } from 'spec/test_constants'; -import Mousetrap from 'mousetrap'; -import App from '~/diffs/components/app.vue'; -import NoChanges from '~/diffs/components/no_changes.vue'; -import DiffFile from '~/diffs/components/diff_file.vue'; -import CompareVersions from '~/diffs/components/compare_versions.vue'; -import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue'; -import CommitWidget from '~/diffs/components/commit_widget.vue'; -import TreeList from '~/diffs/components/tree_list.vue'; -import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants'; -import createDiffsStore from '../create_diffs_store'; -import diffsMockData from '../mock_data/merge_request_diffs'; - -const mergeRequestDiff = { version_index: 1 }; - -describe('diffs/components/app', () => { - const oldMrTabs = window.mrTabs; - let store; - let wrapper; - - function createComponent(props = {}, extendStore = () => {}) { - const localVue = createLocalVue(); - - localVue.use(Vuex); - - store = createDiffsStore(); - store.state.diffs.isLoading = false; - - extendStore(store); - - wrapper = shallowMount(localVue.extend(App), { - localVue, - propsData: { - endpoint: `${TEST_HOST}/diff/endpoint`, - endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`, - endpointBatch: `${TEST_HOST}/diff/endpointBatch`, - projectPath: 'namespace/project', - currentUser: {}, - changesEmptyStateIllustration: '', - dismissEndpoint: '', - showSuggestPopover: true, - ...props, - }, - store, - methods: { - isLatestVersion() { - return true; - }, - }, - }); - } - - function getOppositeViewType(currentViewType) { - return currentViewType === INLINE_DIFF_VIEW_TYPE - ? PARALLEL_DIFF_VIEW_TYPE - : INLINE_DIFF_VIEW_TYPE; - } - - beforeEach(() => { - // setup globals (needed for component to mount :/) - window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']); - window.mrTabs.expandViewContainer = jasmine.createSpy(); - }); - - afterEach(() => { - // reset globals - window.mrTabs = oldMrTabs; - - // reset component - wrapper.destroy(); - }); - - describe('fetch diff methods', () => { - beforeEach(done => { - const fetchResolver = () => { - store.state.diffs.retrievingBatches = false; - store.state.notes.discussions = 'test'; - return Promise.resolve({ real_size: 100 }); - }; - spyOn(window, 'requestIdleCallback').and.callFake(fn => fn()); - createComponent(); - spyOn(wrapper.vm, 'fetchDiffFiles').and.callFake(fetchResolver); - spyOn(wrapper.vm, 'fetchDiffFilesMeta').and.callFake(fetchResolver); - spyOn(wrapper.vm, 'fetchDiffFilesBatch').and.callFake(fetchResolver); - spyOn(wrapper.vm, 'setDiscussions'); - spyOn(wrapper.vm, 'startRenderDiffsQueue'); - spyOn(wrapper.vm, 'unwatchDiscussions'); - spyOn(wrapper.vm, 'unwatchRetrievingBatches'); - store.state.diffs.retrievingBatches = true; - store.state.diffs.diffFiles = []; - wrapper.vm.$nextTick(done); - }); - - describe('when the diff view type changes and it should load a single diff view style', () => { - const noLinesDiff = { - highlighted_diff_lines: [], - parallel_diff_lines: [], - }; - const parallelLinesDiff = { - highlighted_diff_lines: [], - parallel_diff_lines: ['line'], - }; - const inlineLinesDiff = { - highlighted_diff_lines: ['line'], - parallel_diff_lines: [], - }; - const fullDiff = { - highlighted_diff_lines: ['line'], - parallel_diff_lines: ['line'], - }; - - function expectFetchToOccur({ - vueInstance, - done = () => {}, - batch = false, - existingFiles = 1, - } = {}) { - vueInstance.$nextTick(() => { - expect(vueInstance.diffFiles.length).toEqual(existingFiles); - - if (!batch) { - expect(vueInstance.fetchDiffFiles).toHaveBeenCalled(); - expect(vueInstance.fetchDiffFilesBatch).not.toHaveBeenCalled(); - } else { - expect(vueInstance.fetchDiffFiles).not.toHaveBeenCalled(); - expect(vueInstance.fetchDiffFilesBatch).toHaveBeenCalled(); - } - - done(); - }); - } - - beforeEach(() => { - wrapper.vm.glFeatures.singleMrDiffView = true; - }); - - it('fetches diffs if it has none', done => { - wrapper.vm.isLatestVersion = () => false; - - store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); - - expectFetchToOccur({ vueInstance: wrapper.vm, batch: false, existingFiles: 0, done }); - }); - - it('fetches diffs if it has both view styles, but no lines in either', done => { - wrapper.vm.isLatestVersion = () => false; - - store.state.diffs.diffFiles.push(noLinesDiff); - store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); - - expectFetchToOccur({ vueInstance: wrapper.vm, done }); - }); - - it('fetches diffs if it only has inline view style', done => { - wrapper.vm.isLatestVersion = () => false; - - store.state.diffs.diffFiles.push(inlineLinesDiff); - store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); - - expectFetchToOccur({ vueInstance: wrapper.vm, done }); - }); - - it('fetches diffs if it only has parallel view style', done => { - wrapper.vm.isLatestVersion = () => false; - - store.state.diffs.diffFiles.push(parallelLinesDiff); - store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); - - expectFetchToOccur({ vueInstance: wrapper.vm, done }); - }); - - it('fetches batch diffs if it has none', done => { - wrapper.vm.glFeatures.diffsBatchLoad = true; - - store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); - - expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, existingFiles: 0, done }); - }); - - it('fetches batch diffs if it has both view styles, but no lines in either', done => { - wrapper.vm.glFeatures.diffsBatchLoad = true; - - store.state.diffs.diffFiles.push(noLinesDiff); - store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); - - expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done }); - }); - - it('fetches batch diffs if it only has inline view style', done => { - wrapper.vm.glFeatures.diffsBatchLoad = true; - - store.state.diffs.diffFiles.push(inlineLinesDiff); - store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); - - expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done }); - }); - - it('fetches batch diffs if it only has parallel view style', done => { - wrapper.vm.glFeatures.diffsBatchLoad = true; - - store.state.diffs.diffFiles.push(parallelLinesDiff); - store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); - - expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done }); - }); - - it('does not fetch diffs if it has already fetched both styles of diff', () => { - wrapper.vm.glFeatures.diffsBatchLoad = false; - - store.state.diffs.diffFiles.push(fullDiff); - store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); - - expect(wrapper.vm.diffFiles.length).toEqual(1); - expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled(); - expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled(); - }); - - it('does not fetch batch diffs if it has already fetched both styles of diff', () => { - wrapper.vm.glFeatures.diffsBatchLoad = true; - - store.state.diffs.diffFiles.push(fullDiff); - store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); - - expect(wrapper.vm.diffFiles.length).toEqual(1); - expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled(); - expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled(); - }); - }); - - it('calls fetchDiffFiles if diffsBatchLoad is not enabled', done => { - expect(wrapper.vm.diffFilesLength).toEqual(0); - wrapper.vm.glFeatures.diffsBatchLoad = false; - wrapper.vm.fetchData(false); - - expect(wrapper.vm.fetchDiffFiles).toHaveBeenCalled(); - setTimeout(() => { - expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled(); - expect(wrapper.vm.fetchDiffFilesMeta).not.toHaveBeenCalled(); - expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled(); - expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled(); - expect(wrapper.vm.diffFilesLength).toEqual(100); - expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled(); - - done(); - }); - }); - - it('calls batch methods if diffsBatchLoad is enabled, and not latest version', done => { - expect(wrapper.vm.diffFilesLength).toEqual(0); - wrapper.vm.glFeatures.diffsBatchLoad = true; - wrapper.vm.isLatestVersion = () => false; - wrapper.vm.fetchData(false); - - expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled(); - setTimeout(() => { - expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled(); - expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled(); - expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled(); - expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled(); - expect(wrapper.vm.diffFilesLength).toEqual(100); - expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled(); - done(); - }); - }); - - it('calls batch methods if diffsBatchLoad is enabled, and latest version', done => { - expect(wrapper.vm.diffFilesLength).toEqual(0); - wrapper.vm.glFeatures.diffsBatchLoad = true; - wrapper.vm.fetchData(false); - - expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled(); - setTimeout(() => { - expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled(); - expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled(); - expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled(); - expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled(); - expect(wrapper.vm.diffFilesLength).toEqual(100); - expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled(); - done(); - }); - }); - }); - - it('adds container-limiting classes when showFileTree is false with inline diffs', () => { - createComponent({}, ({ state }) => { - state.diffs.showTreeList = false; - state.diffs.isParallelView = false; - }); - - expect(wrapper.contains('.container-limited.limit-container-width')).toBe(true); - }); - - it('does not add container-limiting classes when showFileTree is false with inline diffs', () => { - createComponent({}, ({ state }) => { - state.diffs.showTreeList = true; - state.diffs.isParallelView = false; - }); - - expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false); - }); - - it('does not add container-limiting classes when isFluidLayout', () => { - createComponent({ isFluidLayout: true }, ({ state }) => { - state.diffs.isParallelView = false; - }); - - expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false); - }); - - it('displays loading icon on loading', () => { - createComponent({}, ({ state }) => { - state.diffs.isLoading = true; - }); - - expect(wrapper.contains(GlLoadingIcon)).toBe(true); - }); - - it('displays loading icon on batch loading', () => { - createComponent({}, ({ state }) => { - state.diffs.isBatchLoading = true; - }); - - expect(wrapper.contains(GlLoadingIcon)).toBe(true); - }); - - it('displays diffs container when not loading', () => { - createComponent(); - - expect(wrapper.contains(GlLoadingIcon)).toBe(false); - expect(wrapper.contains('#diffs')).toBe(true); - }); - - it('does not show commit info', () => { - createComponent(); - - expect(wrapper.contains('.blob-commit-info')).toBe(false); - }); - - describe('row highlighting', () => { - beforeEach(() => { - window.location.hash = 'ABC_123'; - }); - - it('sets highlighted row if hash exists in location object', done => { - createComponent({ - shouldShow: true, - }); - - // Component uses $nextTick so we wait until that has finished - setTimeout(() => { - expect(store.state.diffs.highlightedRow).toBe('ABC_123'); - - done(); - }); - }); - - it('marks current diff file based on currently highlighted row', done => { - createComponent({ - shouldShow: true, - }); - - // Component uses $nextTick so we wait until that has finished - setTimeout(() => { - expect(store.state.diffs.currentDiffFileId).toBe('ABC'); - - done(); - }); - }); - }); - - describe('resizable', () => { - afterEach(() => { - localStorage.removeItem('mr_tree_list_width'); - }); - - it('sets initial width when no localStorage has been set', () => { - createComponent(); - - expect(wrapper.vm.treeWidth).toEqual(320); - }); - - it('sets initial width to localStorage size', () => { - localStorage.setItem('mr_tree_list_width', '200'); - - createComponent(); - - expect(wrapper.vm.treeWidth).toEqual(200); - }); - - it('sets width of tree list', () => { - createComponent(); - - expect(wrapper.find('.js-diff-tree-list').element.style.width).toEqual('320px'); - }); - }); - - it('marks current diff file based on currently highlighted row', done => { - createComponent({ - shouldShow: true, - }); - - // Component uses $nextTick so we wait until that has finished - setTimeout(() => { - expect(store.state.diffs.currentDiffFileId).toBe('ABC'); - - done(); - }); - }); - - describe('empty state', () => { - it('renders empty state when no diff files exist', () => { - createComponent(); - - expect(wrapper.contains(NoChanges)).toBe(true); - }); - - it('does not render empty state when diff files exist', () => { - createComponent({}, ({ state }) => { - state.diffs.diffFiles.push({ - id: 1, - }); - }); - - expect(wrapper.contains(NoChanges)).toBe(false); - expect(wrapper.findAll(DiffFile).length).toBe(1); - }); - - it('does not render empty state when versions match', () => { - createComponent({}, ({ state }) => { - state.diffs.startVersion = mergeRequestDiff; - state.diffs.mergeRequestDiff = mergeRequestDiff; - }); - - expect(wrapper.contains(NoChanges)).toBe(false); - }); - }); - - describe('keyboard shortcut navigation', () => { - const mappings = { - '[': -1, - k: -1, - ']': +1, - j: +1, - }; - let spy; - - describe('visible app', () => { - beforeEach(() => { - spy = jasmine.createSpy('spy'); - - createComponent({ - shouldShow: true, - }); - wrapper.setMethods({ - jumpToFile: spy, - }); - }); - - it('calls `jumpToFile()` with correct parameter whenever pre-defined key is pressed', done => { - wrapper.vm - .$nextTick() - .then(() => { - Object.keys(mappings).forEach(function(key) { - Mousetrap.trigger(key); - - expect(spy.calls.mostRecent().args).toEqual([mappings[key]]); - }); - - expect(spy.calls.count()).toEqual(Object.keys(mappings).length); - }) - .then(done) - .catch(done.fail); - }); - - it('does not call `jumpToFile()` when unknown key is pressed', done => { - wrapper.vm - .$nextTick() - .then(() => { - Mousetrap.trigger('d'); - - expect(spy).not.toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('hideen app', () => { - beforeEach(() => { - spy = jasmine.createSpy('spy'); - - createComponent({ - shouldShow: false, - }); - wrapper.setMethods({ - jumpToFile: spy, - }); - }); - - it('stops calling `jumpToFile()` when application is hidden', done => { - wrapper.vm - .$nextTick() - .then(() => { - Object.keys(mappings).forEach(function(key) { - Mousetrap.trigger(key); - - expect(spy).not.toHaveBeenCalled(); - }); - }) - .then(done) - .catch(done.fail); - }); - }); - }); - - describe('jumpToFile', () => { - let spy; - - beforeEach(() => { - spy = jasmine.createSpy(); - - createComponent({}, () => { - store.state.diffs.diffFiles = [ - { file_hash: '111', file_path: '111.js' }, - { file_hash: '222', file_path: '222.js' }, - { file_hash: '333', file_path: '333.js' }, - ]; - }); - - wrapper.setMethods({ - scrollToFile: spy, - }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('jumps to next and previous files in the list', done => { - wrapper.vm - .$nextTick() - .then(() => { - wrapper.vm.jumpToFile(+1); - - expect(spy.calls.mostRecent().args).toEqual(['222.js']); - store.state.diffs.currentDiffFileId = '222'; - wrapper.vm.jumpToFile(+1); - - expect(spy.calls.mostRecent().args).toEqual(['333.js']); - store.state.diffs.currentDiffFileId = '333'; - wrapper.vm.jumpToFile(-1); - - expect(spy.calls.mostRecent().args).toEqual(['222.js']); - }) - .then(done) - .catch(done.fail); - }); - - it('does not jump to previous file from the first one', done => { - wrapper.vm - .$nextTick() - .then(() => { - store.state.diffs.currentDiffFileId = '333'; - - expect(wrapper.vm.currentDiffIndex).toEqual(2); - - wrapper.vm.jumpToFile(+1); - - expect(wrapper.vm.currentDiffIndex).toEqual(2); - expect(spy).not.toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('does not jump to next file from the last one', done => { - wrapper.vm - .$nextTick() - .then(() => { - expect(wrapper.vm.currentDiffIndex).toEqual(0); - - wrapper.vm.jumpToFile(-1); - - expect(wrapper.vm.currentDiffIndex).toEqual(0); - expect(spy).not.toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('diffs', () => { - it('should render compare versions component', () => { - createComponent({}, ({ state }) => { - state.diffs.mergeRequestDiffs = diffsMockData; - state.diffs.targetBranchName = 'target-branch'; - state.diffs.mergeRequestDiff = mergeRequestDiff; - }); - - expect(wrapper.contains(CompareVersions)).toBe(true); - expect(wrapper.find(CompareVersions).props()).toEqual( - jasmine.objectContaining({ - targetBranch: { - branchName: 'target-branch', - versionIndex: -1, - path: '', - }, - mergeRequestDiffs: diffsMockData, - mergeRequestDiff, - }), - ); - }); - - it('should render hidden files warning if render overflow warning is present', () => { - createComponent({}, ({ state }) => { - state.diffs.renderOverflowWarning = true; - state.diffs.realSize = '5'; - state.diffs.plainDiffPath = 'plain diff path'; - state.diffs.emailPatchPath = 'email patch path'; - state.diffs.size = 1; - }); - - expect(wrapper.contains(HiddenFilesWarning)).toBe(true); - expect(wrapper.find(HiddenFilesWarning).props()).toEqual( - jasmine.objectContaining({ - total: '5', - plainDiffPath: 'plain diff path', - emailPatchPath: 'email patch path', - visible: 1, - }), - ); - }); - - it('should display commit widget if store has a commit', () => { - createComponent({}, () => { - store.state.diffs.commit = { - author: 'John Doe', - }; - }); - - expect(wrapper.contains(CommitWidget)).toBe(true); - }); - - it('should display diff file if there are diff files', () => { - createComponent({}, ({ state }) => { - state.diffs.diffFiles.push({ sha: '123' }); - }); - - expect(wrapper.contains(DiffFile)).toBe(true); - }); - - it('should render tree list', () => { - createComponent(); - - expect(wrapper.find(TreeList).exists()).toBe(true); - }); - }); - - describe('hideTreeListIfJustOneFile', () => { - let toggleShowTreeList; - - beforeEach(() => { - toggleShowTreeList = jasmine.createSpy('toggleShowTreeList'); - }); - - afterEach(() => { - localStorage.removeItem('mr_tree_show'); - }); - - it('calls toggleShowTreeList when only 1 file', () => { - createComponent({}, ({ state }) => { - state.diffs.diffFiles.push({ sha: '123' }); - }); - - wrapper.setMethods({ - toggleShowTreeList, - }); - - wrapper.vm.hideTreeListIfJustOneFile(); - - expect(toggleShowTreeList).toHaveBeenCalledWith(false); - }); - - it('does not call toggleShowTreeList when more than 1 file', () => { - createComponent({}, ({ state }) => { - state.diffs.diffFiles.push({ sha: '123' }); - state.diffs.diffFiles.push({ sha: '124' }); - }); - - wrapper.setMethods({ - toggleShowTreeList, - }); - - wrapper.vm.hideTreeListIfJustOneFile(); - - expect(toggleShowTreeList).not.toHaveBeenCalled(); - }); - - it('does not call toggleShowTreeList when localStorage is set', () => { - localStorage.setItem('mr_tree_show', 'true'); - - createComponent({}, ({ state }) => { - state.diffs.diffFiles.push({ sha: '123' }); - }); - - wrapper.setMethods({ - toggleShowTreeList, - }); - - wrapper.vm.hideTreeListIfJustOneFile(); - - expect(toggleShowTreeList).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/javascripts/diffs/create_diffs_store.js b/spec/javascripts/diffs/create_diffs_store.js index aacde99964c..cfefd4238b8 100644 --- a/spec/javascripts/diffs/create_diffs_store.js +++ b/spec/javascripts/diffs/create_diffs_store.js @@ -1,15 +1 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; -import diffsModule from '~/diffs/store/modules'; -import notesModule from '~/notes/stores/modules'; - -Vue.use(Vuex); - -export default function createDiffsStore() { - return new Vuex.Store({ - modules: { - diffs: diffsModule(), - notes: notesModule(), - }, - }); -} +export { default } from '../../frontend/diffs/create_diffs_store'; diff --git a/spec/lib/gitlab/import_export/json/legacy_reader_spec.rb b/spec/lib/gitlab/import_export/json/legacy_reader_spec.rb new file mode 100644 index 00000000000..0009a5f81de --- /dev/null +++ b/spec/lib/gitlab/import_export/json/legacy_reader_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::ImportExport::JSON::LegacyReader::User do + let(:relation_names) { [] } + let(:legacy_reader) { described_class.new(tree_hash, relation_names) } + + describe '#valid?' do + subject { legacy_reader.valid? } + + context 'tree_hash not present' do + let(:tree_hash) { nil } + + it { is_expected.to be false } + end + + context 'tree_hash presents' do + let(:tree_hash) { { "issues": [] } } + + it { is_expected.to be true } + end + end +end + +describe Gitlab::ImportExport::JSON::LegacyReader::File do + let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/light/project.json' } + let(:project_tree) { JSON.parse(File.read(fixture)) } + let(:relation_names) { [] } + let(:legacy_reader) { described_class.new(path, relation_names) } + + describe '#valid?' do + subject { legacy_reader.valid? } + + context 'given valid path' do + let(:path) { fixture } + + it { is_expected.to be true } + end + + context 'given invalid path' do + let(:path) { 'spec/non-existing-folder/do-not-create-this-file.json' } + + it { is_expected.to be false } + end + end + + describe '#root_attributes' do + let(:path) { fixture } + + subject { legacy_reader.root_attributes(excluded_attributes) } + + context 'No excluded attributes' do + let(:excluded_attributes) { [] } + let(:relation_names) { [] } + + it 'returns the whole tree from parsed JSON' do + expect(subject).to eq(project_tree) + end + end + + context 'Some attributes are excluded' do + let(:excluded_attributes) { %w[milestones labels issues services snippets] } + let(:relation_names) { %w[import_type archived] } + + it 'returns hash without excluded attributes and relations' do + expect(subject).not_to include('milestones', 'labels', 'issues', 'services', 'snippets', 'import_type', 'archived') + end + end + end + + describe '#consume_relation' do + let(:path) { fixture } + let(:key) { 'description' } + + context 'block not given' do + it 'returns value of the key' do + expect(legacy_reader).to receive(:relations).and_return({ key => 'test value' }) + expect(legacy_reader.consume_relation(key)).to eq('test value') + end + end + + context 'key has been consumed' do + before do + legacy_reader.consume_relation(key) + end + + it 'does not yield' do + expect do |blk| + legacy_reader.consume_relation(key, &blk) + end.not_to yield_control + end + end + + context 'value is nil' do + before do + expect(legacy_reader).to receive(:relations).and_return({ key => nil }) + end + + it 'does not yield' do + expect do |blk| + legacy_reader.consume_relation(key, &blk) + end.not_to yield_control + end + end + + context 'value is not array' do + before do + expect(legacy_reader).to receive(:relations).and_return({ key => 'value' }) + end + + it 'yield the value with index 0' do + expect do |blk| + legacy_reader.consume_relation(key, &blk) + end.to yield_with_args('value', 0) + end + end + + context 'value is an array' do + before do + expect(legacy_reader).to receive(:relations).and_return({ key => %w[item1 item2 item3] }) + end + + it 'yield each array element with index' do + expect do |blk| + legacy_reader.consume_relation(key, &blk) + end.to yield_successive_args(['item1', 0], ['item2', 1], ['item3', 2]) + end + end + end + + describe '#tree_hash' do + let(:path) { fixture } + + subject { legacy_reader.send(:tree_hash) } + + it 'parses the JSON into the expected tree' do + expect(subject).to eq(project_tree) + end + + context 'invalid JSON' do + let(:path) { 'spec/fixtures/lib/gitlab/import_export/invalid_json/project.json' } + + it 'raise Exception' do + expect { subject }.to raise_exception(Gitlab::ImportExport::Error, 'Incorrect JSON format') + end + end + end +end diff --git a/spec/lib/gitlab/import_export/project/tree_loader_spec.rb b/spec/lib/gitlab/import_export/project/tree_loader_spec.rb deleted file mode 100644 index e683eefa7c0..00000000000 --- a/spec/lib/gitlab/import_export/project/tree_loader_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Gitlab::ImportExport::Project::TreeLoader do - let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/with_duplicates.json' } - let(:project_tree) { JSON.parse(File.read(fixture)) } - - context 'without de-duplicating entries' do - let(:parsed_tree) do - subject.load(fixture) - end - - it 'parses the JSON into the expected tree' do - expect(parsed_tree).to eq(project_tree) - end - - it 'does not de-duplicate entries' do - expect(parsed_tree['duped_hash_with_id']).not_to be(parsed_tree['array'][0]['duped_hash_with_id']) - end - end - - context 'with de-duplicating entries' do - let(:parsed_tree) do - subject.load(fixture, dedup_entries: true) - end - - it 'parses the JSON into the expected tree' do - expect(parsed_tree).to eq(project_tree) - end - - it 'de-duplicates equal values' do - expect(parsed_tree['duped_hash_with_id']).to be(parsed_tree['array'][0]['duped_hash_with_id']) - expect(parsed_tree['duped_hash_with_id']).to be(parsed_tree['nested']['duped_hash_with_id']) - expect(parsed_tree['duped_array']).to be(parsed_tree['array'][1]['duped_array']) - expect(parsed_tree['duped_array']).to be(parsed_tree['nested']['duped_array']) - end - - it 'does not de-duplicate hashes without IDs' do - expect(parsed_tree['duped_hash_no_id']).to eq(parsed_tree['array'][2]['duped_hash_no_id']) - expect(parsed_tree['duped_hash_no_id']).not_to be(parsed_tree['array'][2]['duped_hash_no_id']) - end - - it 'keeps single entries intact' do - expect(parsed_tree['simple']).to eq(42) - expect(parsed_tree['nested']['array']).to eq(["don't touch"]) - end - end -end diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index 9c2b202d5bb..e38ef75d085 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -783,7 +783,8 @@ describe Gitlab::ImportExport::Project::TreeRestorer do end before do - expect(restorer).to receive(:read_tree_hash) { tree_hash } + allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:valid?).and_return(true) + allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:tree_hash) { tree_hash } end context 'no group visibility' do diff --git a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb index 80901feb893..578418998c0 100644 --- a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # This spec is a lightweight version of: -# * project_tree_restorer_spec.rb +# * project/tree_restorer_spec.rb # # In depth testing is being done in the above specs. # This spec tests that restore project works @@ -25,7 +25,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do described_class.new( user: user, shared: shared, - tree_hash: tree_hash, + relation_reader: relation_reader, importable: importable, object_builder: object_builder, members_mapper: members_mapper, @@ -36,14 +36,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do subject { relation_tree_restorer.restore } - context 'when restoring a project' do - let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' } - let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') } - let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder } - let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory } - let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) } - let(:tree_hash) { importable_hash } - + shared_examples 'import project successfully' do it 'restores project tree' do expect(subject).to eq(true) end @@ -66,4 +59,18 @@ describe Gitlab::ImportExport::RelationTreeRestorer do end end end + + context 'when restoring a project' do + let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' } + let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') } + let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder } + let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory } + let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) } + + context 'using legacy reader' do + let(:relation_reader) { Gitlab::ImportExport::JSON::LegacyReader::File.new(path, reader.project_relation_names) } + + it_behaves_like 'import project successfully' + end + end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 3df620d1fea..5b87ec022ae 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -625,4 +625,14 @@ describe SystemNoteService do described_class.discussion_lock(issuable, double) end end + + describe '.auto_resolve_prometheus_alert' do + it 'calls IssuableService' do + expect_next_instance_of(::SystemNotes::IssuablesService) do |service| + expect(service).to receive(:auto_resolve_prometheus_alert) + end + + described_class.auto_resolve_prometheus_alert(noteable, project, author) + end + end end diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb index c0aaa65971a..477f9eae39e 100644 --- a/spec/services/system_notes/issuables_service_spec.rb +++ b/spec/services/system_notes/issuables_service_spec.rb @@ -654,4 +654,16 @@ describe ::SystemNotes::IssuablesService do .to eq('resolved the corresponding error and closed the issue.') end end + + describe '#auto_resolve_prometheus_alert' do + subject { service.auto_resolve_prometheus_alert } + + it_behaves_like 'a system note' do + let(:action) { 'closed' } + end + + it 'creates the expected system note' do + expect(subject.note).to eq('automatically closed this issue because the alert resolved.') + end + end end -- cgit v1.2.3