From 6e5461d67f52cacc2c9ba408c8f6fddb1e9e417d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 16 Jan 2019 15:17:10 +0000 Subject: Added fuzzy file finder to merge requests Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/53304 --- app/assets/javascripts/diffs/components/app.vue | 4 + .../javascripts/diffs/components/tree_list.vue | 57 ++-- app/assets/javascripts/diffs/index.js | 51 ++- app/assets/javascripts/diffs/store/actions.js | 4 + app/assets/javascripts/diffs/store/getters.js | 37 ++- .../javascripts/diffs/store/modules/diff_state.js | 1 + .../javascripts/diffs/store/mutation_types.js | 1 + app/assets/javascripts/diffs/store/mutations.js | 3 + .../ide/components/file_finder/index.vue | 227 ------------- .../ide/components/file_finder/item.vue | 104 ------ app/assets/javascripts/ide/components/ide.vue | 46 ++- app/assets/javascripts/ide/constants.js | 5 - .../vue_shared/components/file_finder/index.vue | 299 +++++++++++++++++ .../vue_shared/components/file_finder/item.vue | 126 +++++++ app/assets/stylesheets/page_bundles/ide.scss | 20 -- app/assets/stylesheets/pages/merge_requests.scss | 6 + app/views/projects/merge_requests/show.html.haml | 1 + changelogs/unreleased/diff-file-finder.yml | 5 + locale/gitlab.pot | 4 +- .../javascripts/diffs/components/tree_list_spec.js | 21 -- spec/javascripts/diffs/store/getters_spec.js | 6 +- .../ide/components/file_finder/index_spec.js | 304 ----------------- .../ide/components/file_finder/item_spec.js | 140 -------- spec/javascripts/ide/components/ide_spec.js | 68 ---- .../components/file_finder/index_spec.js | 368 +++++++++++++++++++++ .../vue_shared/components/file_finder/item_spec.js | 140 ++++++++ 26 files changed, 1074 insertions(+), 974 deletions(-) delete mode 100644 app/assets/javascripts/ide/components/file_finder/index.vue delete mode 100644 app/assets/javascripts/ide/components/file_finder/item.vue create mode 100644 app/assets/javascripts/vue_shared/components/file_finder/index.vue create mode 100644 app/assets/javascripts/vue_shared/components/file_finder/item.vue create mode 100644 changelogs/unreleased/diff-file-finder.yml delete mode 100644 spec/javascripts/ide/components/file_finder/index_spec.js delete mode 100644 spec/javascripts/ide/components/file_finder/item_spec.js create mode 100644 spec/javascripts/vue_shared/components/file_finder/index_spec.js create mode 100644 spec/javascripts/vue_shared/components/file_finder/item_spec.js diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index d4c1b07093d..f0ce2579ee7 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -129,6 +129,10 @@ export default { created() { this.adjustView(); eventHub.$once('fetchedNotesData', this.setDiscussions); + eventHub.$once('fetchDiffData', this.fetchData); + }, + beforeDestroy() { + eventHub.$off('fetchDiffData', this.fetchData); }, methods: { ...mapActions(['startTaskList']), diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index 0b3def3d29d..a0f09932593 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -13,39 +13,17 @@ export default { Icon, FileRow, }, - data() { - return { - search: '', - }; - }, computed: { ...mapState('diffs', ['tree', 'addedLines', 'removedLines', 'renderTreeList']), ...mapGetters('diffs', ['allBlobs', 'diffFilesLength']), filteredTreeList() { - const search = this.search.toLowerCase().trim(); - - if (search === '') return this.renderTreeList ? this.tree : this.allBlobs; - - return this.allBlobs.reduce((acc, folder) => { - const tree = folder.tree.filter(f => f.path.toLowerCase().indexOf(search) >= 0); - - if (tree.length) { - return acc.concat({ - ...folder, - tree, - }); - } - - return acc; - }, []); + return this.renderTreeList ? this.tree : this.allBlobs; }, }, methods: { - ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']), - clearSearch() { - this.search = ''; - }, + ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']), }, + shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '⌘' : 'Ctrl'}+P`, FileRowStats, }; @@ -55,21 +33,17 @@ export default {
@@ -104,4 +78,15 @@ export default { .tree-list-blobs .file-row-name { margin-left: 12px; } + +.diff-tree-search-shortcut { + top: 50%; + right: 10px; + transform: translateY(-50%); + pointer-events: none; +} + +.tree-list-icon { + pointer-events: none; +} diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js index 094e5cdea9c..63954d9d412 100644 --- a/app/assets/javascripts/diffs/index.js +++ b/app/assets/javascripts/diffs/index.js @@ -1,11 +1,60 @@ import Vue from 'vue'; -import { mapActions, mapState } from 'vuex'; +import { mapActions, mapState, mapGetters } from 'vuex'; import { parseBoolean } from '~/lib/utils/common_utils'; import { getParameterValues } from '~/lib/utils/url_utility'; +import FindFile from '~/vue_shared/components/file_finder/index.vue'; +import eventHub from '../notes/event_hub'; import diffsApp from './components/app.vue'; import { TREE_LIST_STORAGE_KEY } from './constants'; export default function initDiffsApp(store) { + const fileFinderEl = document.getElementById('js-diff-file-finder'); + + if (fileFinderEl) { + // eslint-disable-next-line no-new + new Vue({ + el: fileFinderEl, + store, + computed: { + ...mapState('diffs', ['fileFinderVisible', 'isLoading']), + ...mapGetters('diffs', ['flatBlobsList']), + }, + watch: { + fileFinderVisible(newVal, oldVal) { + if (newVal && !oldVal && !this.flatBlobsList.length) { + eventHub.$emit('fetchDiffData'); + } + }, + }, + methods: { + ...mapActions('diffs', ['toggleFileFinder', 'scrollToFile']), + openFile(file) { + window.mrTabs.tabShown('diffs'); + this.scrollToFile(file.path); + }, + }, + render(createElement) { + return createElement(FindFile, { + props: { + files: this.flatBlobsList, + visible: this.fileFinderVisible, + loading: this.isLoading, + showDiffStats: true, + clearSearchOnClose: false, + }, + on: { + toggle: this.toggleFileFinder, + click: this.openFile, + }, + class: ['diff-file-finder'], + style: { + display: this.fileFinderVisible ? '' : 'none', + }, + }); + }, + }); + } + return new Vue({ el: '#js-diffs-app', name: 'MergeRequestDiffs', diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 2c5019fb652..7fb66ce433b 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -296,5 +296,9 @@ export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = fals } }; +export const toggleFileFinder = ({ commit }, visible) => { + commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index 86c0c7190f9..0e1ad654a2b 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -74,24 +74,25 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) = export const getDiffFileByHash = state => fileHash => state.diffFiles.find(file => file.file_hash === fileHash); -export const allBlobs = state => - Object.values(state.treeEntries) - .filter(f => f.type === 'blob') - .reduce((acc, file) => { - const { parentPath } = file; - - if (parentPath && !acc.some(f => f.path === parentPath)) { - acc.push({ - path: parentPath, - isHeader: true, - tree: [], - }); - } - - acc.find(f => f.path === parentPath).tree.push(file); - - return acc; - }, []); +export const flatBlobsList = state => + Object.values(state.treeEntries).filter(f => f.type === 'blob'); + +export const allBlobs = (state, getters) => + getters.flatBlobsList.reduce((acc, file) => { + const { parentPath } = file; + + if (parentPath && !acc.some(f => f.path === parentPath)) { + acc.push({ + path: parentPath, + isHeader: true, + tree: [], + }); + } + + acc.find(f => f.path === parentPath).tree.push(file); + + return acc; + }, []); export const diffFilesLength = state => state.diffFiles.length; diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 05b4c552f6e..6ee33d9fc6d 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -29,4 +29,5 @@ export default () => ({ highlightedRow: null, renderTreeList: true, showWhitespace: true, + fileFinderVisible: false, }); diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index e760b4d1079..71ad108ce88 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -22,3 +22,4 @@ export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW'; export const SET_TREE_DATA = 'SET_TREE_DATA'; export const SET_RENDER_TREE_LIST = 'SET_RENDER_TREE_LIST'; export const SET_SHOW_WHITESPACE = 'SET_SHOW_WHITESPACE'; +export const TOGGLE_FILE_FINDER_VISIBLE = 'TOGGLE_FILE_FINDER_VISIBLE'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 4aeb393b29b..7bbafe66199 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -244,4 +244,7 @@ export default { [types.SET_SHOW_WHITESPACE](state, showWhitespace) { state.showWhitespace = showWhitespace; }, + [types.TOGGLE_FILE_FINDER_VISIBLE](state, visible) { + state.fileFinderVisible = visible; + }, }; diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue deleted file mode 100644 index 0b0cd7b75eb..00000000000 --- a/app/assets/javascripts/ide/components/file_finder/index.vue +++ /dev/null @@ -1,227 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue deleted file mode 100644 index 83e80d50aff..00000000000 --- a/app/assets/javascripts/ide/components/file_finder/item.vue +++ /dev/null @@ -1,104 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index caec8779cac..9894ebb0624 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,20 +1,17 @@ + + + + diff --git a/app/assets/javascripts/vue_shared/components/file_finder/item.vue b/app/assets/javascripts/vue_shared/components/file_finder/item.vue new file mode 100644 index 00000000000..73511879ff2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/file_finder/item.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 1f24b8dfa9e..2ac98b5d18f 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -816,26 +816,6 @@ $ide-commit-header-height: 48px; z-index: 1; } -.ide-file-finder-overlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 100; -} - -.ide-file-finder { - top: 10px; - left: 50%; - transform: translateX(-50%); - - .highlighted { - color: $blue-500; - font-weight: $gl-font-weight-bold; - } -} - .ide-commit-message-field { height: 200px; background-color: $white-light; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 53afb182b54..38a7e199c6a 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -986,3 +986,9 @@ width: $ci-action-icon-size-lg; } } + +.merge-request-details .file-finder-overlay.diff-file-finder { + position: fixed; + z-index: 99999; + background: $black-transparent; +} diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 0b720e5d542..5111c9fab8d 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -59,6 +59,7 @@ #js-vue-discussion-counter .tab-content#diff-notes-app + #js-diff-file-finder #notes.notes.tab-pane.voting_notes .row %section.col-md-12 diff --git a/changelogs/unreleased/diff-file-finder.yml b/changelogs/unreleased/diff-file-finder.yml new file mode 100644 index 00000000000..3160e9fc91b --- /dev/null +++ b/changelogs/unreleased/diff-file-finder.yml @@ -0,0 +1,5 @@ +--- +title: Added fuzzy file finder to merge requests +merge_request: +author: +type: changed diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6b5b4e93e70..24ca8744414 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4438,10 +4438,10 @@ msgstr "" msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}" msgstr "" -msgid "MergeRequest|Filter files" +msgid "MergeRequest|No files found" msgstr "" -msgid "MergeRequest|No files found" +msgid "MergeRequest|Search files" msgstr "" msgid "Merged" diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js index 08b0b4f9e45..c5ef48a81e9 100644 --- a/spec/javascripts/diffs/components/tree_list_spec.js +++ b/spec/javascripts/diffs/components/tree_list_spec.js @@ -83,17 +83,6 @@ describe('Diffs tree list component', () => { expect(vm.$el.querySelectorAll('.file-row')[1].textContent).toContain('app'); }); - it('filters tree list to blobs matching search', done => { - vm.search = 'app/index'; - - vm.$nextTick(() => { - expect(vm.$el.querySelectorAll('.file-row').length).toBe(1); - expect(vm.$el.querySelectorAll('.file-row')[0].textContent).toContain('index.js'); - - done(); - }); - }); - it('calls toggleTreeOpen when clicking folder', () => { spyOn(vm.$store, 'dispatch').and.stub(); @@ -130,14 +119,4 @@ describe('Diffs tree list component', () => { }); }); }); - - describe('clearSearch', () => { - it('resets search', () => { - vm.search = 'test'; - - vm.$el.querySelector('.tree-list-clear-icon').click(); - - expect(vm.search).toBe(''); - }); - }); }); diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js index 190ca1230ca..4f69dc92ab8 100644 --- a/spec/javascripts/diffs/store/getters_spec.js +++ b/spec/javascripts/diffs/store/getters_spec.js @@ -242,7 +242,11 @@ describe('Diffs Module Getters', () => { }, }; - expect(getters.allBlobs(localState)).toEqual([ + expect( + getters.allBlobs(localState, { + flatBlobsList: getters.flatBlobsList(localState), + }), + ).toEqual([ { isHeader: true, path: '/', diff --git a/spec/javascripts/ide/components/file_finder/index_spec.js b/spec/javascripts/ide/components/file_finder/index_spec.js deleted file mode 100644 index 15ef8c31f91..00000000000 --- a/spec/javascripts/ide/components/file_finder/index_spec.js +++ /dev/null @@ -1,304 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import FindFileComponent from '~/ide/components/file_finder/index.vue'; -import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; -import router from '~/ide/ide_router'; -import { file, resetStore } from '../../helpers'; -import { mountComponentWithStore } from '../../../helpers/vue_mount_component_helper'; - -describe('IDE File finder item spec', () => { - const Component = Vue.extend(FindFileComponent); - let vm; - - beforeEach(done => { - setFixtures('
'); - - vm = mountComponentWithStore(Component, { - store, - el: '#app', - props: { - index: 0, - }, - }); - - setTimeout(done); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - describe('with entries', () => { - beforeEach(done => { - Vue.set(vm.$store.state.entries, 'folder', { - ...file('folder'), - path: 'folder', - type: 'folder', - }); - - Vue.set(vm.$store.state.entries, 'index.js', { - ...file('index.js'), - path: 'index.js', - type: 'blob', - url: '/index.jsurl', - }); - - Vue.set(vm.$store.state.entries, 'component.js', { - ...file('component.js'), - path: 'component.js', - type: 'blob', - }); - - setTimeout(done); - }); - - it('renders list of blobs', () => { - expect(vm.$el.textContent).toContain('index.js'); - expect(vm.$el.textContent).not.toContain('folder'); - }); - - it('filters entries', done => { - vm.searchText = 'index'; - - vm.$nextTick(() => { - expect(vm.$el.textContent).toContain('index.js'); - expect(vm.$el.textContent).not.toContain('component.js'); - - done(); - }); - }); - - it('shows clear button when searchText is not empty', done => { - vm.searchText = 'index'; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dropdown-input-clear').classList).toContain('show'); - expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden'); - - done(); - }); - }); - - it('clear button resets searchText', done => { - vm.searchText = 'index'; - - vm.$nextTick() - .then(() => { - vm.$el.querySelector('.dropdown-input-clear').click(); - }) - .then(vm.$nextTick) - .then(() => { - expect(vm.searchText).toBe(''); - }) - .then(done) - .catch(done.fail); - }); - - it('clear button focues search input', done => { - spyOn(vm.$refs.searchInput, 'focus'); - vm.searchText = 'index'; - - vm.$nextTick() - .then(() => { - vm.$el.querySelector('.dropdown-input-clear').click(); - }) - .then(vm.$nextTick) - .then(() => { - expect(vm.$refs.searchInput.focus).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - describe('listShowCount', () => { - it('returns 1 when no filtered entries exist', done => { - vm.searchText = 'testing 123'; - - vm.$nextTick(() => { - expect(vm.listShowCount).toBe(1); - - done(); - }); - }); - - it('returns entries length when not filtered', () => { - expect(vm.listShowCount).toBe(2); - }); - }); - - describe('listHeight', () => { - it('returns 55 when entries exist', () => { - expect(vm.listHeight).toBe(55); - }); - - it('returns 33 when entries dont exist', done => { - vm.searchText = 'testing 123'; - - vm.$nextTick(() => { - expect(vm.listHeight).toBe(33); - - done(); - }); - }); - }); - - describe('filteredBlobsLength', () => { - it('returns length of filtered blobs', done => { - vm.searchText = 'index'; - - vm.$nextTick(() => { - expect(vm.filteredBlobsLength).toBe(1); - - done(); - }); - }); - }); - - describe('watches', () => { - describe('searchText', () => { - it('resets focusedIndex when updated', done => { - vm.focusedIndex = 1; - vm.searchText = 'test'; - - vm.$nextTick(() => { - expect(vm.focusedIndex).toBe(0); - - done(); - }); - }); - }); - - describe('fileFindVisible', () => { - it('returns searchText when false', done => { - vm.searchText = 'test'; - vm.$store.state.fileFindVisible = true; - - vm.$nextTick() - .then(() => { - vm.$store.state.fileFindVisible = false; - }) - .then(vm.$nextTick) - .then(() => { - expect(vm.searchText).toBe(''); - }) - .then(done) - .catch(done.fail); - }); - }); - }); - - describe('openFile', () => { - beforeEach(() => { - spyOn(router, 'push'); - spyOn(vm, 'toggleFileFinder'); - }); - - it('closes file finder', () => { - vm.openFile(vm.$store.state.entries['index.js']); - - expect(vm.toggleFileFinder).toHaveBeenCalled(); - }); - - it('pushes to router', () => { - vm.openFile(vm.$store.state.entries['index.js']); - - expect(router.push).toHaveBeenCalledWith('/project/index.jsurl'); - }); - }); - - describe('onKeyup', () => { - it('opens file on enter key', done => { - const event = new CustomEvent('keyup'); - event.keyCode = ENTER_KEY_CODE; - - spyOn(vm, 'openFile'); - - vm.$refs.searchInput.dispatchEvent(event); - - vm.$nextTick(() => { - expect(vm.openFile).toHaveBeenCalledWith(vm.$store.state.entries['index.js']); - - done(); - }); - }); - - it('closes file finder on esc key', done => { - const event = new CustomEvent('keyup'); - event.keyCode = ESC_KEY_CODE; - - spyOn(vm, 'toggleFileFinder'); - - vm.$refs.searchInput.dispatchEvent(event); - - vm.$nextTick(() => { - expect(vm.toggleFileFinder).toHaveBeenCalled(); - - done(); - }); - }); - }); - - describe('onKeyDown', () => { - let el; - - beforeEach(() => { - el = vm.$refs.searchInput; - }); - - describe('up key', () => { - const event = new CustomEvent('keydown'); - event.keyCode = UP_KEY_CODE; - - it('resets to last index when at top', () => { - el.dispatchEvent(event); - - expect(vm.focusedIndex).toBe(1); - }); - - it('minus 1 from focusedIndex', () => { - vm.focusedIndex = 1; - - el.dispatchEvent(event); - - expect(vm.focusedIndex).toBe(0); - }); - }); - - describe('down key', () => { - const event = new CustomEvent('keydown'); - event.keyCode = DOWN_KEY_CODE; - - it('resets to first index when at bottom', () => { - vm.focusedIndex = 1; - el.dispatchEvent(event); - - expect(vm.focusedIndex).toBe(0); - }); - - it('adds 1 to focusedIndex', () => { - el.dispatchEvent(event); - - expect(vm.focusedIndex).toBe(1); - }); - }); - }); - }); - - describe('without entries', () => { - it('renders loading text when loading', done => { - store.state.loading = true; - - vm.$nextTick(() => { - expect(vm.$el.textContent).toContain('Loading...'); - - done(); - }); - }); - - it('renders no files text', () => { - expect(vm.$el.textContent).toContain('No files found.'); - }); - }); -}); diff --git a/spec/javascripts/ide/components/file_finder/item_spec.js b/spec/javascripts/ide/components/file_finder/item_spec.js deleted file mode 100644 index 0f1116c6912..00000000000 --- a/spec/javascripts/ide/components/file_finder/item_spec.js +++ /dev/null @@ -1,140 +0,0 @@ -import Vue from 'vue'; -import ItemComponent from '~/ide/components/file_finder/item.vue'; -import { file } from '../../helpers'; -import createComponent from '../../../helpers/vue_mount_component_helper'; - -describe('IDE File finder item spec', () => { - const Component = Vue.extend(ItemComponent); - let vm; - let localFile; - - beforeEach(() => { - localFile = { - ...file(), - name: 'test file', - path: 'test/file', - }; - - vm = createComponent(Component, { - file: localFile, - focused: true, - searchText: '', - index: 0, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders file name & path', () => { - expect(vm.$el.textContent).toContain('test file'); - expect(vm.$el.textContent).toContain('test/file'); - }); - - describe('focused', () => { - it('adds is-focused class', () => { - expect(vm.$el.classList).toContain('is-focused'); - }); - - it('does not have is-focused class when not focused', done => { - vm.focused = false; - - vm.$nextTick(() => { - expect(vm.$el.classList).not.toContain('is-focused'); - - done(); - }); - }); - }); - - describe('changed file icon', () => { - it('does not render when not a changed or temp file', () => { - expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null); - }); - - it('renders when a changed file', done => { - vm.file.changed = true; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); - - done(); - }); - }); - - it('renders when a temp file', done => { - vm.file.tempFile = true; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); - - done(); - }); - }); - }); - - it('emits event when clicked', () => { - spyOn(vm, '$emit'); - - vm.$el.click(); - - expect(vm.$emit).toHaveBeenCalledWith('click', vm.file); - }); - - describe('path', () => { - let el; - - beforeEach(done => { - vm.searchText = 'file'; - - el = vm.$el.querySelector('.diff-changed-file-path'); - - vm.$nextTick(done); - }); - - it('highlights text', () => { - expect(el.querySelectorAll('.highlighted').length).toBe(4); - }); - - it('adds ellipsis to long text', done => { - vm.file.path = new Array(70) - .fill() - .map((_, i) => `${i}-`) - .join(''); - - vm.$nextTick(() => { - expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`); - done(); - }); - }); - }); - - describe('name', () => { - let el; - - beforeEach(done => { - vm.searchText = 'file'; - - el = vm.$el.querySelector('.diff-changed-file-name'); - - vm.$nextTick(done); - }); - - it('highlights text', () => { - expect(el.querySelectorAll('.highlighted').length).toBe(4); - }); - - it('does not add ellipsis to long text', done => { - vm.file.name = new Array(70) - .fill() - .map((_, i) => `${i}-`) - .join(''); - - vm.$nextTick(() => { - expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`); - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js index 55f40be0e4e..dc5790f6562 100644 --- a/spec/javascripts/ide/components/ide_spec.js +++ b/spec/javascripts/ide/components/ide_spec.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import Mousetrap from 'mousetrap'; import store from '~/ide/stores'; import ide from '~/ide/components/ide.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; @@ -72,73 +71,6 @@ describe('ide component', () => { }); }); - describe('file finder', () => { - beforeEach(done => { - spyOn(vm, 'toggleFileFinder'); - - vm.$store.state.fileFindVisible = true; - - vm.$nextTick(done); - }); - - it('calls toggleFileFinder on `t` key press', done => { - Mousetrap.trigger('t'); - - vm.$nextTick() - .then(() => { - expect(vm.toggleFileFinder).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('calls toggleFileFinder on `command+p` key press', done => { - Mousetrap.trigger('command+p'); - - vm.$nextTick() - .then(() => { - expect(vm.toggleFileFinder).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('calls toggleFileFinder on `ctrl+p` key press', done => { - Mousetrap.trigger('ctrl+p'); - - vm.$nextTick() - .then(() => { - expect(vm.toggleFileFinder).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('always allows `command+p` to trigger toggleFileFinder', () => { - expect( - vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'), - ).toBe(false); - }); - - it('always allows `ctrl+p` to trigger toggleFileFinder', () => { - expect( - vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'), - ).toBe(false); - }); - - it('onlys handles `t` when focused in input-field', () => { - expect( - vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'), - ).toBe(true); - }); - - it('stops callback in monaco editor', () => { - setFixtures('
'); - - expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true); - }); - }); - it('shows error message when set', done => { expect(vm.$el.querySelector('.flash-container')).toBe(null); diff --git a/spec/javascripts/vue_shared/components/file_finder/index_spec.js b/spec/javascripts/vue_shared/components/file_finder/index_spec.js new file mode 100644 index 00000000000..bae4741f652 --- /dev/null +++ b/spec/javascripts/vue_shared/components/file_finder/index_spec.js @@ -0,0 +1,368 @@ +import Vue from 'vue'; +import Mousetrap from 'mousetrap'; +import FindFileComponent from '~/vue_shared/components/file_finder/index.vue'; +import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; +import { file } from 'spec/ide/helpers'; +import timeoutPromise from 'spec/helpers/set_timeout_promise_helper'; + +describe('File finder item spec', () => { + const Component = Vue.extend(FindFileComponent); + let vm; + + function createComponent(props) { + vm = new Component({ + propsData: { + files: [], + visible: true, + loading: false, + ...props, + }, + }); + + vm.$mount('#app'); + } + + beforeEach(() => { + setFixtures('
'); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('with entries', () => { + beforeEach(done => { + createComponent({ + files: [ + { + ...file('index.js'), + path: 'index.js', + type: 'blob', + url: '/index.jsurl', + }, + { + ...file('component.js'), + path: 'component.js', + type: 'blob', + }, + ], + }); + + setTimeout(done); + }); + + it('renders list of blobs', () => { + expect(vm.$el.textContent).toContain('index.js'); + expect(vm.$el.textContent).toContain('component.js'); + expect(vm.$el.textContent).not.toContain('folder'); + }); + + it('filters entries', done => { + vm.searchText = 'index'; + + setTimeout(() => { + expect(vm.$el.textContent).toContain('index.js'); + expect(vm.$el.textContent).not.toContain('component.js'); + + done(); + }); + }); + + it('shows clear button when searchText is not empty', done => { + vm.searchText = 'index'; + + setTimeout(() => { + expect(vm.$el.querySelector('.dropdown-input').classList).toContain('has-value'); + expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden'); + + done(); + }); + }); + + it('clear button resets searchText', done => { + vm.searchText = 'index'; + + timeoutPromise() + .then(() => { + vm.$el.querySelector('.dropdown-input-clear').click(); + }) + .then(timeoutPromise) + .then(() => { + expect(vm.searchText).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + + it('clear button focues search input', done => { + spyOn(vm.$refs.searchInput, 'focus'); + vm.searchText = 'index'; + + timeoutPromise() + .then(() => { + vm.$el.querySelector('.dropdown-input-clear').click(); + }) + .then(timeoutPromise) + .then(() => { + expect(vm.$refs.searchInput.focus).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + describe('listShowCount', () => { + it('returns 1 when no filtered entries exist', done => { + vm.searchText = 'testing 123'; + + setTimeout(() => { + expect(vm.listShowCount).toBe(1); + + done(); + }); + }); + + it('returns entries length when not filtered', () => { + expect(vm.listShowCount).toBe(2); + }); + }); + + describe('listHeight', () => { + it('returns 55 when entries exist', () => { + expect(vm.listHeight).toBe(55); + }); + + it('returns 33 when entries dont exist', done => { + vm.searchText = 'testing 123'; + + setTimeout(() => { + expect(vm.listHeight).toBe(33); + + done(); + }); + }); + }); + + describe('filteredBlobsLength', () => { + it('returns length of filtered blobs', done => { + vm.searchText = 'index'; + + setTimeout(() => { + expect(vm.filteredBlobsLength).toBe(1); + + done(); + }); + }); + }); + + describe('watches', () => { + describe('searchText', () => { + it('resets focusedIndex when updated', done => { + vm.focusedIndex = 1; + vm.searchText = 'test'; + + setTimeout(() => { + expect(vm.focusedIndex).toBe(0); + + done(); + }); + }); + }); + + describe('visible', () => { + it('returns searchText when false', done => { + vm.searchText = 'test'; + vm.visible = true; + + timeoutPromise() + .then(() => { + vm.visible = false; + }) + .then(timeoutPromise) + .then(() => { + expect(vm.searchText).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('openFile', () => { + beforeEach(() => { + spyOn(vm, '$emit'); + }); + + it('closes file finder', () => { + vm.openFile(vm.files[0]); + + expect(vm.$emit).toHaveBeenCalledWith('toggle', false); + }); + + it('pushes to router', () => { + vm.openFile(vm.files[0]); + + expect(vm.$emit).toHaveBeenCalledWith('click', vm.files[0]); + }); + }); + + describe('onKeyup', () => { + it('opens file on enter key', done => { + const event = new CustomEvent('keyup'); + event.keyCode = ENTER_KEY_CODE; + + spyOn(vm, 'openFile'); + + vm.$refs.searchInput.dispatchEvent(event); + + setTimeout(() => { + expect(vm.openFile).toHaveBeenCalledWith(vm.files[0]); + + done(); + }); + }); + + it('closes file finder on esc key', done => { + const event = new CustomEvent('keyup'); + event.keyCode = ESC_KEY_CODE; + + spyOn(vm, '$emit'); + + vm.$refs.searchInput.dispatchEvent(event); + + setTimeout(() => { + expect(vm.$emit).toHaveBeenCalledWith('toggle', false); + + done(); + }); + }); + }); + + describe('onKeyDown', () => { + let el; + + beforeEach(() => { + el = vm.$refs.searchInput; + }); + + describe('up key', () => { + const event = new CustomEvent('keydown'); + event.keyCode = UP_KEY_CODE; + + it('resets to last index when at top', () => { + el.dispatchEvent(event); + + expect(vm.focusedIndex).toBe(1); + }); + + it('minus 1 from focusedIndex', () => { + vm.focusedIndex = 1; + + el.dispatchEvent(event); + + expect(vm.focusedIndex).toBe(0); + }); + }); + + describe('down key', () => { + const event = new CustomEvent('keydown'); + event.keyCode = DOWN_KEY_CODE; + + it('resets to first index when at bottom', () => { + vm.focusedIndex = 1; + el.dispatchEvent(event); + + expect(vm.focusedIndex).toBe(0); + }); + + it('adds 1 to focusedIndex', () => { + el.dispatchEvent(event); + + expect(vm.focusedIndex).toBe(1); + }); + }); + }); + }); + + describe('without entries', () => { + it('renders loading text when loading', () => { + createComponent({ + loading: true, + }); + + expect(vm.$el.textContent).toContain('Loading...'); + }); + + it('renders no files text', () => { + createComponent(); + + expect(vm.$el.textContent).toContain('No files found.'); + }); + }); + + describe('keyboard shortcuts', () => { + beforeEach(done => { + createComponent(); + + spyOn(vm, 'toggle'); + + vm.$nextTick(done); + }); + + it('calls toggle on `t` key press', done => { + Mousetrap.trigger('t'); + + vm.$nextTick() + .then(() => { + expect(vm.toggle).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('calls toggle on `command+p` key press', done => { + Mousetrap.trigger('command+p'); + + vm.$nextTick() + .then(() => { + expect(vm.toggle).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('calls toggle on `ctrl+p` key press', done => { + Mousetrap.trigger('ctrl+p'); + + vm.$nextTick() + .then(() => { + expect(vm.toggle).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('always allows `command+p` to trigger toggle', () => { + expect( + vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'), + ).toBe(false); + }); + + it('always allows `ctrl+p` to trigger toggle', () => { + expect( + vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'), + ).toBe(false); + }); + + it('onlys handles `t` when focused in input-field', () => { + expect( + vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'), + ).toBe(true); + }); + + it('stops callback in monaco editor', () => { + setFixtures('
'); + + expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/file_finder/item_spec.js b/spec/javascripts/vue_shared/components/file_finder/item_spec.js new file mode 100644 index 00000000000..c1511643a9d --- /dev/null +++ b/spec/javascripts/vue_shared/components/file_finder/item_spec.js @@ -0,0 +1,140 @@ +import Vue from 'vue'; +import ItemComponent from '~/vue_shared/components/file_finder/item.vue'; +import { file } from 'spec/ide/helpers'; +import createComponent from '../../../helpers/vue_mount_component_helper'; + +describe('File finder item spec', () => { + const Component = Vue.extend(ItemComponent); + let vm; + let localFile; + + beforeEach(() => { + localFile = { + ...file(), + name: 'test file', + path: 'test/file', + }; + + vm = createComponent(Component, { + file: localFile, + focused: true, + searchText: '', + index: 0, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders file name & path', () => { + expect(vm.$el.textContent).toContain('test file'); + expect(vm.$el.textContent).toContain('test/file'); + }); + + describe('focused', () => { + it('adds is-focused class', () => { + expect(vm.$el.classList).toContain('is-focused'); + }); + + it('does not have is-focused class when not focused', done => { + vm.focused = false; + + vm.$nextTick(() => { + expect(vm.$el.classList).not.toContain('is-focused'); + + done(); + }); + }); + }); + + describe('changed file icon', () => { + it('does not render when not a changed or temp file', () => { + expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null); + }); + + it('renders when a changed file', done => { + vm.file.changed = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); + + done(); + }); + }); + + it('renders when a temp file', done => { + vm.file.tempFile = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); + + done(); + }); + }); + }); + + it('emits event when clicked', () => { + spyOn(vm, '$emit'); + + vm.$el.click(); + + expect(vm.$emit).toHaveBeenCalledWith('click', vm.file); + }); + + describe('path', () => { + let el; + + beforeEach(done => { + vm.searchText = 'file'; + + el = vm.$el.querySelector('.diff-changed-file-path'); + + vm.$nextTick(done); + }); + + it('highlights text', () => { + expect(el.querySelectorAll('.highlighted').length).toBe(4); + }); + + it('adds ellipsis to long text', done => { + vm.file.path = new Array(70) + .fill() + .map((_, i) => `${i}-`) + .join(''); + + vm.$nextTick(() => { + expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`); + done(); + }); + }); + }); + + describe('name', () => { + let el; + + beforeEach(done => { + vm.searchText = 'file'; + + el = vm.$el.querySelector('.diff-changed-file-name'); + + vm.$nextTick(done); + }); + + it('highlights text', () => { + expect(el.querySelectorAll('.highlighted').length).toBe(4); + }); + + it('does not add ellipsis to long text', done => { + vm.file.name = new Array(70) + .fill() + .map((_, i) => `${i}-`) + .join(''); + + vm.$nextTick(() => { + expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`); + done(); + }); + }); + }); +}); -- cgit v1.2.1