diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue index 680f229d5e8641cd0e45aedcaa968799d81a30e4..5c892377f400f14e0ac9af190b0ad337e4848deb 100644 --- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue +++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue @@ -221,7 +221,12 @@ export default { </script> <template> - <div v-if="visible" class="file-finder-overlay" @mousedown.self="toggle(false)"> + <div + v-if="visible" + data-testid="overlay" + 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 @@ -231,6 +236,7 @@ export default { type="search" class="dropdown-input-field" autocomplete="off" + data-testid="search-input" @keydown="onKeydown($event)" @keyup="onKeyup($event)" /> @@ -241,6 +247,7 @@ export default { /> <gl-icon name="close" + data-testid="clear-search-input" class="dropdown-input-clear" role="button" :aria-label="__('Clear search input')" diff --git a/spec/frontend/vue_shared/components/file_finder/index_spec.js b/spec/frontend/vue_shared/components/file_finder/index_spec.js index d7569ed7b7658c5c50e2c339a8c3f45b54aae0be..9708d689245cdae61ba27dc67dd32fe9bc416df6 100644 --- a/spec/frontend/vue_shared/components/file_finder/index_spec.js +++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js @@ -1,244 +1,215 @@ +import { GlLoadingIcon } from '@gitlab/ui'; import Mousetrap from 'mousetrap'; -import Vue, { nextTick } from 'vue'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { nextTick } from 'vue'; +import VirtualList from 'vue-virtual-scroll-list'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import { file } from 'jest/ide/helpers'; -import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import FindFileComponent from '~/vue_shared/components/file_finder/index.vue'; +import FileFinderItem from '~/vue_shared/components/file_finder/item.vue'; +import { setHTMLFixture } from 'helpers/fixtures'; describe('File finder item spec', () => { - const Component = Vue.extend(FindFileComponent); - let vm; + let wrapper; + + const TEST_FILES = [ + { + ...file('index.js'), + path: 'index.js', + type: 'blob', + url: '/index.jsurl', + }, + { + ...file('component.js'), + path: 'component.js', + type: 'blob', + }, + ]; function createComponent(props) { - vm = new Component({ + wrapper = mountExtended(FindFileComponent, { + attachTo: document.body, propsData: { - files: [], + files: TEST_FILES, visible: true, loading: false, ...props, }, }); - - vm.$mount('#app'); } - beforeEach(() => { - setHTMLFixture('<div id="app"></div>'); - }); - - afterEach(() => { - resetHTMLFixture(); - }); - - afterEach(() => { - vm.$destroy(); - }); + const findAllFileFinderItems = () => wrapper.findAllComponents(FileFinderItem); + const findSearchInput = () => wrapper.findByTestId('search-input'); + const enterSearchText = (text) => findSearchInput().setValue(text); + const clearSearch = () => wrapper.findByTestId('clear-search-input').vm.$emit('click'); describe('with entries', () => { beforeEach(() => { createComponent({ - files: [ - { - ...file('index.js'), - path: 'index.js', - type: 'blob', - url: '/index.jsurl', - }, - { - ...file('component.js'), - path: 'component.js', - type: 'blob', - }, - ], + files: TEST_FILES, }); return nextTick(); }); 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'); + expect(wrapper.text()).toContain('index.js'); + expect(wrapper.text()).toContain('component.js'); + expect(wrapper.text()).not.toContain('folder'); }); it('filters entries', async () => { - vm.searchText = 'index'; - - await nextTick(); + await enterSearchText('index'); - expect(vm.$el.textContent).toContain('index.js'); - expect(vm.$el.textContent).not.toContain('component.js'); + expect(wrapper.text()).toContain('index.js'); + expect(wrapper.text()).not.toContain('component.js'); }); it('shows clear button when searchText is not empty', async () => { - vm.searchText = 'index'; - - await nextTick(); + await enterSearchText('index'); - expect(vm.$el.querySelector('.dropdown-input').classList).toContain('has-value'); - expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden'); + expect(wrapper.find('.dropdown-input').classes()).toContain('has-value'); + expect(wrapper.find('.dropdown-input-search').classes()).toContain('hidden'); }); - it('clear button resets searchText', () => { - vm.searchText = 'index'; + it('clear button resets searchText', async () => { + await enterSearchText('index'); + expect(findSearchInput().element.value).toBe('index'); - vm.clearSearchInput(); + await clearSearch(); - expect(vm.searchText).toBe(''); + expect(findSearchInput().element.value).toBe(''); }); it('clear button focuses search input', async () => { - jest.spyOn(vm.$refs.searchInput, 'focus').mockImplementation(() => {}); - vm.searchText = 'index'; + expect(findSearchInput().element).not.toBe(document.activeElement); - vm.clearSearchInput(); + await enterSearchText('index'); + await clearSearch(); - await nextTick(); - - expect(vm.$refs.searchInput.focus).toHaveBeenCalled(); + expect(findSearchInput().element).toBe(document.activeElement); }); describe('listShowCount', () => { - it('returns 1 when no filtered entries exist', () => { - vm.searchText = 'testing 123'; + it('returns 1 when no filtered entries exist', async () => { + await enterSearchText('testing 123'); - expect(vm.listShowCount).toBe(1); + expect(wrapper.findComponent(VirtualList).props('remain')).toBe(1); }); it('returns entries length when not filtered', () => { - expect(vm.listShowCount).toBe(2); + expect(wrapper.findComponent(VirtualList).props('remain')).toBe(2); }); }); - describe('filteredBlobsLength', () => { - it('returns length of filtered blobs', () => { - vm.searchText = 'index'; + describe('filtering', () => { + it('renders only items that match the filter', async () => { + await enterSearchText('index'); - expect(vm.filteredBlobsLength).toBe(1); + expect(findAllFileFinderItems()).toHaveLength(1); }); }); describe('DOM Performance', () => { it('renders less DOM nodes if not visible by utilizing v-if', async () => { - vm.visible = false; + createComponent({ visible: false }); await nextTick(); - expect(vm.$el).toBeInstanceOf(Comment); + expect(wrapper.findByTestId('overlay').exists()).toBe(false); }); }); describe('watches', () => { describe('searchText', () => { it('resets focusedIndex when updated', async () => { - vm.focusedIndex = 1; - vm.searchText = 'test'; - + await enterSearchText('index'); await nextTick(); - expect(vm.focusedIndex).toBe(0); + expect(findAllFileFinderItems().at(0).props('focused')).toBe(true); }); }); describe('visible', () => { it('resets searchText when changed to false', async () => { - vm.searchText = 'test'; - vm.visible = false; - - await nextTick(); + await enterSearchText('test'); + await wrapper.setProps({ visible: false }); + // need to set it back to true, so the component's content renders + await wrapper.setProps({ visible: true }); - expect(vm.searchText).toBe(''); + expect(findSearchInput().element.value).toBe(''); }); }); }); describe('openFile', () => { - beforeEach(() => { - jest.spyOn(vm, '$emit').mockImplementation(() => {}); - }); - it('closes file finder', () => { - vm.openFile(vm.files[0]); + expect(wrapper.emitted('toggle')).toBeUndefined(); - expect(vm.$emit).toHaveBeenCalledWith('toggle', false); + findSearchInput().trigger('keyup.enter'); + + expect(wrapper.emitted('toggle')).toHaveLength(1); }); it('pushes to router', () => { - vm.openFile(vm.files[0]); + expect(wrapper.emitted('click')).toBeUndefined(); + + findSearchInput().trigger('keyup.enter'); - expect(vm.$emit).toHaveBeenCalledWith('click', vm.files[0]); + expect(wrapper.emitted('click')).toHaveLength(1); }); }); describe('onKeyup', () => { it('opens file on enter key', async () => { - const event = new CustomEvent('keyup'); - event.keyCode = ENTER_KEY_CODE; + expect(wrapper.emitted('click')).toBeUndefined(); - jest.spyOn(vm, 'openFile').mockImplementation(() => {}); + await findSearchInput().trigger('keyup.enter'); - vm.$refs.searchInput.dispatchEvent(event); - - await nextTick(); - - expect(vm.openFile).toHaveBeenCalledWith(vm.files[0]); + expect(wrapper.emitted('click')[0][0]).toBe(TEST_FILES[0]); }); it('closes file finder on esc key', async () => { - const event = new CustomEvent('keyup'); - event.keyCode = ESC_KEY_CODE; - - jest.spyOn(vm, '$emit').mockImplementation(() => {}); - - vm.$refs.searchInput.dispatchEvent(event); + expect(wrapper.emitted('toggle')).toBeUndefined(); - await nextTick(); + await findSearchInput().trigger('keyup.esc'); - expect(vm.$emit).toHaveBeenCalledWith('toggle', false); + expect(wrapper.emitted('toggle')[0][0]).toBe(false); }); }); 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', async () => { + expect(findAllFileFinderItems().at(0).props('focused')).toBe(true); - it('resets to last index when at top', () => { - el.dispatchEvent(event); + await findSearchInput().trigger('keydown.up'); - expect(vm.focusedIndex).toBe(1); + expect(findAllFileFinderItems().at(-1).props('focused')).toBe(true); }); - it('minus 1 from focusedIndex', () => { - vm.focusedIndex = 1; - - el.dispatchEvent(event); + it('minus 1 from focusedIndex', async () => { + await findSearchInput().trigger('keydown.up'); + await findSearchInput().trigger('keydown.up'); - expect(vm.focusedIndex).toBe(0); + expect(findAllFileFinderItems().at(0).props('focused')).toBe(true); }); }); describe('down key', () => { - const event = new CustomEvent('keydown'); - event.keyCode = DOWN_KEY_CODE; + it('resets to first index when at bottom', async () => { + await findSearchInput().trigger('keydown.down'); + expect(findAllFileFinderItems().at(-1).props('focused')).toBe(true); - it('resets to first index when at bottom', () => { - vm.focusedIndex = 1; - el.dispatchEvent(event); - - expect(vm.focusedIndex).toBe(0); + await findSearchInput().trigger('keydown.down'); + expect(findAllFileFinderItems().at(0).props('focused')).toBe(true); }); - it('adds 1 to focusedIndex', () => { - el.dispatchEvent(event); + it('adds 1 to focusedIndex', async () => { + expect(findAllFileFinderItems().at(0).props('focused')).toBe(true); + + await findSearchInput().trigger('keydown.down'); - expect(vm.focusedIndex).toBe(1); + expect(findAllFileFinderItems().at(1).props('focused')).toBe(true); }); }); }); @@ -246,46 +217,45 @@ describe('File finder item spec', () => { describe('without entries', () => { it('renders loading text when loading', () => { - createComponent({ loading: true }); + createComponent({ loading: true, files: [] }); - expect(vm.$el.querySelector('.gl-spinner')).not.toBe(null); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders no files text', () => { - createComponent(); + createComponent({ files: [] }); - expect(vm.$el.textContent).toContain('No files found.'); + expect(wrapper.text()).toContain('No files found.'); }); }); describe('keyboard shortcuts', () => { beforeEach(async () => { createComponent(); - - jest.spyOn(vm, 'toggle').mockImplementation(() => {}); - await nextTick(); }); - it('calls toggle on `t` key press', async () => { + it('calls toggle on `t` key press', () => { + expect(wrapper.emitted('toggle')).toBeUndefined(); + Mousetrap.trigger('t'); - await nextTick(); - expect(vm.toggle).toHaveBeenCalled(); + expect(wrapper.emitted('toggle')).not.toBeUndefined(); }); - it('calls toggle on `mod+p` key press', async () => { + it('calls toggle on `mod+p` key press', () => { + expect(wrapper.emitted('toggle')).toBeUndefined(); + Mousetrap.trigger('mod+p'); - await nextTick(); - expect(vm.toggle).toHaveBeenCalled(); + expect(wrapper.emitted('toggle')).not.toBeUndefined(); }); it('always allows `mod+p` to trigger toggle', () => { expect( Mousetrap.prototype.stopCallback( null, - vm.$el.querySelector('.dropdown-input-field'), + wrapper.find('.dropdown-input-field').element, 'mod+p', ), ).toBe(false); @@ -293,7 +263,7 @@ describe('File finder item spec', () => { it('onlys handles `t` when focused in input-field', () => { expect( - Mousetrap.prototype.stopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'), + Mousetrap.prototype.stopCallback(null, wrapper.find('.dropdown-input-field').element, 't'), ).toBe(true); });