Commit 8ea0444f authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab-ce master

parents 2e7f0316 e37abfbb
......@@ -9,7 +9,7 @@ See [the general developer security release guidelines](https://gitlab.com/gitla
<!-- Mention the issue(s) this MR is related to -->
## Author's checklist
## Developer checklist
- [ ] Link to the developer security workflow issue on `dev.gitlab.org`
- [ ] MR targets `master` or `security-X-Y` for backports
......@@ -20,7 +20,7 @@ See [the general developer security release guidelines](https://gitlab.com/gitla
- [ ] Add a link to an EE MR if required
- [ ] Assign to a reviewer
## Reviewers checklist
## Reviewer checklist
- [ ] Correct milestone is applied and the title is matching across all backports
- [ ] Assigned to `@gitlab-release-tools-bot` with passing CI pipelines
......
......@@ -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']),
......
......@@ -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) ? '&#8984;' : 'Ctrl'}+P`,
FileRowStats,
};
</script>
......@@ -55,21 +33,17 @@ export default {
<div class="append-bottom-8 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
<icon name="search" class="position-absolute tree-list-icon" />
<input
v-model="search"
:placeholder="s__('MergeRequest|Filter files')"
type="search"
class="form-control"
/>
<button
v-show="search"
:aria-label="__('Clear search')"
type="button"
class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0"
@click="clearSearch"
class="form-control text-left text-secondary"
@click="toggleFileFinder(true)"
>
<icon name="close" />
{{ s__('MergeRequest|Search files') }}
</button>
<span
class="position-absolute text-secondary diff-tree-search-shortcut"
v-html="$options.shortcutKeyCharacter"
></span>
</div>
</div>
<div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll">
......@@ -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;
}
</style>
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',
......
......@@ -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 () => {};
......@@ -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;
......
......@@ -29,4 +29,5 @@ export default () => ({
highlightedRow: null,
renderTreeList: true,
showWhitespace: true,
fileFinderVisible: false,
});
......@@ -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';
......@@ -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;
},
};
<script>
import Vue from 'vue';
import Mousetrap from 'mousetrap';
import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
import NewModal from './new_dropdown/modal.vue';
import IdeSidebar from './ide_side_bar.vue';
import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
import RightPane from './panes/right.vue';
import ErrorMessage from './error_message.vue';
import CommitEditorHeader from './commit_sidebar/editor_header.vue';
const originalStopCallback = Mousetrap.stopCallback;
export default {
components: {
NewModal,
......@@ -42,21 +39,18 @@ export default {
'emptyStateSvgPath',
'currentProjectId',
'errorMessage',
'loading',
]),
...mapGetters([
'activeFile',
'hasChanges',
'someUncommittedChanges',
'isCommitModeActive',
'allBlobs',
]),
...mapGetters(['activeFile', 'hasChanges', 'someUncommittedChanges', 'isCommitModeActive']),
},
mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e);
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
if (e.preventDefault) {
e.preventDefault();
}
this.toggleFileFinder(!this.fileFindVisible);
});
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
},
methods: {
...mapActions(['toggleFileFinder']),
......@@ -70,17 +64,8 @@ export default {
});
return returnValue;
},
mousetrapStopCallback(e, el, combo) {
if (
(combo === 't' && el.classList.contains('dropdown-input-field')) ||
el.classList.contains('inputarea')
) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
}
return originalStopCallback(e, el, combo);
openFile(file) {
this.$router.push(`/project${file.url}`);
},
},
};
......@@ -90,7 +75,14 @@ export default {
<article class="ide position-relative d-flex flex-column align-items-stretch">
<error-message v-if="errorMessage" :message="errorMessage" />
<div class="ide-view flex-grow d-flex">
<find-file v-show="fileFindVisible" />
<find-file
v-show="fileFindVisible"
:files="allBlobs"
:visible="fileFindVisible"
:loading="loading"
@toggle="toggleFileFinder"
@click="openFile"
/>
<ide-sidebar />
<div class="multi-file-edit-pane">
<template v-if="activeFile">
......
// Fuzzy file finder
export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
// Commit message textarea
......
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import Mousetrap from 'mousetrap';
import VirtualList from 'vue-virtual-scroll-list';
import Item from './item.vue';
import router from '../../ide_router';
import {
MAX_FILE_FINDER_RESULTS,
FILE_FINDER_ROW_HEIGHT,
FILE_FINDER_EMPTY_ROW_HEIGHT,
} from '../../constants';
import {
UP_KEY_CODE,
DOWN_KEY_CODE,
ENTER_KEY_CODE,
ESC_KEY_CODE,
} from '../../../lib/utils/keycodes';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
const originalStopCallback = Mousetrap.stopCallback;
export default {
components: {
Item,
VirtualList,
},
props: {
files: {
type: Array,
required: true,
},
visible: {
type: Boolean,
required: true,
},
loading: {
type: Boolean,
required: true,
},
showDiffStats: {
type: Boolean,
required: false,
default: false,
},
clearSearchOnClose: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
focusedIndex: 0,
focusedIndex: -1,
searchText: '',
mouseOver: false,
cancelMouseOver: false,
};
},
computed: {
...mapGetters(['allBlobs']),
...mapState(['fileFindVisible', 'loading']),
filteredBlobs() {
const searchText = this.searchText.trim();
if (searchText === '') {
return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
return this.files.slice(0, MAX_FILE_FINDER_RESULTS);
}
return fuzzaldrinPlus.filter(this.allBlobs, searchText, {
return fuzzaldrinPlus.filter(this.files, searchText, {
key: 'path',
maxResults: MAX_FILE_FINDER_RESULTS,
});
......@@ -58,10 +75,12 @@ export default {
},
},
watch: {
fileFindVisible() {
visible() {
this.$nextTick(() => {
if (!this.fileFindVisible) {
this.searchText = '';
if (!this.visible) {
if (this.clearSearchOnClose) {
this.searchText = '';
}
} else {
this.focusedIndex = 0;
......@@ -72,7 +91,11 @@ export default {
});
},
searchText() {
this.focusedIndex = 0;
this.focusedIndex = -1;
this.$nextTick(() => {
this.focusedIndex = 0;
});
},
focusedIndex() {
if (!this.mouseOver) {
......@@ -98,8 +121,25 @@ export default {
}
},
},
mounted() {
if (this.files.length) {
this.focusedIndex = 0;
}
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
if (e.preventDefault) {
e.preventDefault();
}
this.toggle(!this.visible);
});
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
},
methods: {
...mapActions(['toggleFileFinder']),
toggle(visible) {
this.$emit('toggle', visible);
},
clearSearchInput() {
this.searchText = '';
......@@ -139,15 +179,15 @@ export default {
this.openFile(this.filteredBlobs[this.focusedIndex]);
break;
case ESC_KEY_CODE:
this.toggleFileFinder(false);
this.toggle(false);
break;
default:
break;
}
},
openFile(file) {
this.toggleFileFinder(false);
router.push(`/project${file.url}`);
this.toggle(false);
this.$emit('click', file);
},
onMouseOver(index) {
if (!this.cancelMouseOver) {
......@@ -159,14 +199,26 @@ export default {
this.cancelMouseOver = false;
this.onMouseOver(index);
},
mousetrapStopCallback(e, el, combo) {
if (
(combo === 't' && el.classList.contains('dropdown-input-field')) ||
el.classList.contains('inputarea')
) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
}
return originalStopCallback(e, el, combo);
},
},
};
</script>
<template>
<div class="ide-file-finder-overlay" @mousedown.self="toggleFileFinder(false)">
<div class="dropdown-menu diff-file-changes ide-file-finder show">
<div class="dropdown-input">
<div class="file-finder-overlay" @mousedown.self="toggle(false)">
<div class="dropdown-menu diff-file-changes file-finder show">
<div :class="{ 'has-value': showClearInputButton }" class="dropdown-input">
<input
ref="searchInput"
v-model="searchText"
......@@ -186,9 +238,6 @@ export default {
></i>
<i
:aria-label="__('Clear search input')"
:class="{
show: showClearInputButton,
}"
role="button"
class="fa fa-times dropdown-input-clear"
@click="clearSearchInput"
......@@ -203,6 +252,7 @@ export default {
:search-text="searchText"
:focused="index === focusedIndex"
:index="index"
:show-diff-stats="showDiffStats"
class="disable-hover"
@click="openFile"
@mouseover="onMouseOver"
......@@ -225,3 +275,25 @@ export default {
</div>
</div>
</template>
<style scoped>
.file-finder-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 200;
}
.file-finder {
top: 10px;
left: 50%;
transform: translateX(-50%);
}
.diff-file-changes {
top: 50px;
max-height: 327px;
}
</style>
<script>
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '../../../vue_shared/components/file_icon.vue';
import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue';
......@@ -7,6 +8,7 @@ const MAX_PATH_LENGTH = 60;
export default {
components: {
Icon,
ChangedFileIcon,
FileIcon,
},
......@@ -27,6 +29,11 @@ export default {
type: Number,
required: true,
},
showDiffStats: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
pathWithEllipsis() {
......@@ -97,8 +104,23 @@ export default {
</span>
</span>
</span>
<span v-if="file.changed || file.tempFile" class="diff-changed-stats">
<changed-file-icon :file="file" />
<span v-if="file.changed || file.tempFile" v-once class="diff-changed-stats">
<span v-if="showDiffStats">
<span class="cgreen bold">
<icon name="file-addition" class="align-text-top" /> {{ file.addedLines }}
</span>
<span class="cred bold ml-1">
<icon name="file-deletion" class="align-text-top" /> {{ file.removedLines }}
</span>
</span>
<changed-file-icon v-else :file="file" />
</span>
</button>
</template>
<style scoped>
.highlighted {
color: #1f78d1;
font-weight: 600;
}
</style>
......@@ -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;
......
......@@ -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;
}
......@@ -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
......
---
title: Added fuzzy file finder to merge requests
merge_request:
author:
type: changed
......@@ -5800,10 +5800,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"
......
......@@ -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('');
});
});
});
......@@ -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: '/',
......
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('<div class="inputarea"></div>');
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);
......
import Vue from 'vue';
import store from '~/ide/stores';
import FindFileComponent from '~/ide/components/file_finder/index.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 router from '~/ide/ide_router';
import { file, resetStore } from '../../helpers';
import { mountComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { file } from 'spec/ide/helpers';
import timeoutPromise from 'spec/helpers/set_timeout_promise_helper';
describe('IDE File finder item spec', () => {
describe('File finder item spec', () => {
const Component = Vue.extend(FindFileComponent);
let vm;
beforeEach(done => {
setFixtures('<div id="app"></div>');
vm = mountComponentWithStore(Component, {
store,
el: '#app',
props: {
index: 0,
function createComponent(props) {
vm = new Component({
propsData: {
files: [],
visible: true,
loading: false,
...props,
},
});
setTimeout(done);
vm.$mount('#app');
}
beforeEach(() => {
setFixtures('<div id="app"></div>');
});
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',
createComponent({
files: [
{
...file('index.js'),
path: 'index.js',
type: 'blob',
url: '/index.jsurl',
},
{
...file('component.js'),
path: 'component.js',
type: 'blob',
},
],
});
setTimeout(done);
......@@ -56,13 +53,14 @@ describe('IDE File finder item spec', () => {
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';
vm.$nextTick(() => {
setTimeout(() => {
expect(vm.$el.textContent).toContain('index.js');
expect(vm.$el.textContent).not.toContain('component.js');
......@@ -73,8 +71,8 @@ describe('IDE File finder item spec', () => {
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');
setTimeout(() => {
expect(vm.$el.querySelector('.dropdown-input').classList).toContain('has-value');
expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden');
done();
......@@ -84,11 +82,11 @@ describe('IDE File finder item spec', () => {
it('clear button resets searchText', done => {
vm.searchText = 'index';
vm.$nextTick()
timeoutPromise()
.then(() => {
vm.$el.querySelector('.dropdown-input-clear').click();
})
.then(vm.$nextTick)
.then(timeoutPromise)
.then(() => {
expect(vm.searchText).toBe('');
})
......@@ -100,11 +98,11 @@ describe('IDE File finder item spec', () => {
spyOn(vm.$refs.searchInput, 'focus');
vm.searchText = 'index';
vm.$nextTick()
timeoutPromise()
.then(() => {
vm.$el.querySelector('.dropdown-input-clear').click();
})
.then(vm.$nextTick)
.then(timeoutPromise)
.then(() => {
expect(vm.$refs.searchInput.focus).toHaveBeenCalled();
})
......@@ -116,7 +114,7 @@ describe('IDE File finder item spec', () => {
it('returns 1 when no filtered entries exist', done => {
vm.searchText = 'testing 123';
vm.$nextTick(() => {
setTimeout(() => {
expect(vm.listShowCount).toBe(1);
done();
......@@ -136,7 +134,7 @@ describe('IDE File finder item spec', () => {
it('returns 33 when entries dont exist', done => {
vm.searchText = 'testing 123';
vm.$nextTick(() => {
setTimeout(() => {
expect(vm.listHeight).toBe(33);
done();
......@@ -148,7 +146,7 @@ describe('IDE File finder item spec', () => {
it('returns length of filtered blobs', done => {
vm.searchText = 'index';
vm.$nextTick(() => {
setTimeout(() => {
expect(vm.filteredBlobsLength).toBe(1);
done();
......@@ -162,7 +160,7 @@ describe('IDE File finder item spec', () => {
vm.focusedIndex = 1;
vm.searchText = 'test';
vm.$nextTick(() => {
setTimeout(() => {
expect(vm.focusedIndex).toBe(0);
done();
......@@ -170,16 +168,16 @@ describe('IDE File finder item spec', () => {
});
});
describe('fileFindVisible', () => {
describe('visible', () => {
it('returns searchText when false', done => {
vm.searchText = 'test';
vm.$store.state.fileFindVisible = true;
vm.visible = true;
vm.$nextTick()
timeoutPromise()
.then(() => {
vm.$store.state.fileFindVisible = false;
vm.visible = false;
})
.then(vm.$nextTick)
.then(timeoutPromise)
.then(() => {
expect(vm.searchText).toBe('');
})
......@@ -191,20 +189,19 @@ describe('IDE File finder item spec', () => {
describe('openFile', () => {
beforeEach(() => {
spyOn(router, 'push');
spyOn(vm, 'toggleFileFinder');
spyOn(vm, '$emit');
});
it('closes file finder', () => {
vm.openFile(vm.$store.state.entries['index.js']);
vm.openFile(vm.files[0]);
expect(vm.toggleFileFinder).toHaveBeenCalled();
expect(vm.$emit).toHaveBeenCalledWith('toggle', false);
});
it('pushes to router', () => {
vm.openFile(vm.$store.state.entries['index.js']);
vm.openFile(vm.files[0]);
expect(router.push).toHaveBeenCalledWith('/project/index.jsurl');
expect(vm.$emit).toHaveBeenCalledWith('click', vm.files[0]);
});
});
......@@ -217,8 +214,8 @@ describe('IDE File finder item spec', () => {
vm.$refs.searchInput.dispatchEvent(event);
vm.$nextTick(() => {
expect(vm.openFile).toHaveBeenCalledWith(vm.$store.state.entries['index.js']);
setTimeout(() => {
expect(vm.openFile).toHaveBeenCalledWith(vm.files[0]);
done();
});
......@@ -228,12 +225,12 @@ describe('IDE File finder item spec', () => {
const event = new CustomEvent('keyup');
event.keyCode = ESC_KEY_CODE;
spyOn(vm, 'toggleFileFinder');
spyOn(vm, '$emit');
vm.$refs.searchInput.dispatchEvent(event);
vm.$nextTick(() => {
expect(vm.toggleFileFinder).toHaveBeenCalled();
setTimeout(() => {
expect(vm.$emit).toHaveBeenCalledWith('toggle', false);
done();
});
......@@ -287,18 +284,85 @@ describe('IDE File finder item spec', () => {
});
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 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('<div class="inputarea"></div>');
expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true);
});
});
});
import Vue from 'vue';
import ItemComponent from '~/ide/components/file_finder/item.vue';
import { file } from '../../helpers';
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('IDE File finder item spec', () => {
describe('File finder item spec', () => {
const Component = Vue.extend(ItemComponent);
let vm;
let localFile;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment