diff options
author | Nathan Friend <nfriend@gitlab.com> | 2019-03-12 19:35:55 +0300 |
---|---|---|
committer | mfluharty <mfluharty@gitlab.com> | 2019-04-03 08:58:01 +0300 |
commit | 06b88af04657be961a4da97a586706fb99eb6a27 (patch) | |
tree | e574c1a39cdc6ca75a734c8256604baa698aa447 /spec/javascripts/vue_shared | |
parent | 645303c7e71d554c3ee1a338730d8b779c47acc1 (diff) |
Add reusable project_selector component
This commit adds a resuable UI component that allows a user to search
for a project name, shows the search results, and allows the user to
select one or more projects. This component communicates with its
parent using props and events.
This component was originally created for use in the EE-specific
"Operations Dashboard" page, but it is applicable for CE use cases as
well, and so was added as a CE shared component.
In addition, some logic was extracted from the frequent_items_list_item
component into shared filters to avoid logic duplication.
Diffstat (limited to 'spec/javascripts/vue_shared')
-rw-r--r-- | spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js | 104 | ||||
-rw-r--r-- | spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js | 152 |
2 files changed, 256 insertions, 0 deletions
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js new file mode 100644 index 00000000000..8dbdfe97f8f --- /dev/null +++ b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js @@ -0,0 +1,104 @@ +import _ from 'underscore'; +import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { trimText } from 'spec/helpers/vue_component_helper'; + +const localVue = createLocalVue(); + +describe('ProjectListItem component', () => { + let wrapper; + let vm; + loadJSONFixtures('projects.json'); + const project = getJSONFixture('projects.json')[0]; + + beforeEach(() => { + wrapper = shallowMount(localVue.extend(ProjectListItem), { + propsData: { + project, + selected: false, + }, + sync: false, + localVue, + }); + + ({ vm } = wrapper); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('does not render a check mark icon if selected === false', () => { + expect(vm.$el.querySelector('.js-selected-icon.js-unselected')).toBeTruthy(); + }); + + it('renders a check mark icon if selected === true', done => { + wrapper.setProps({ selected: true }); + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.js-selected-icon.js-selected')).toBeTruthy(); + done(); + }); + }); + + it(`emits a "clicked" event when clicked`, () => { + spyOn(vm, '$emit'); + vm.onClick(); + + expect(vm.$emit).toHaveBeenCalledWith('click'); + }); + + it(`renders the project avatar`, () => { + expect(vm.$el.querySelector('.js-project-avatar')).toBeTruthy(); + }); + + it(`renders a simple namespace name with a trailing slash`, done => { + project.name_with_namespace = 'a / b'; + wrapper.setProps({ project: _.clone(project) }); + + vm.$nextTick(() => { + const renderedNamespace = trimText(vm.$el.querySelector('.js-project-namespace').textContent); + + expect(renderedNamespace).toBe('a /'); + done(); + }); + }); + + it(`renders a properly truncated namespace with a trailing slash`, done => { + project.name_with_namespace = 'a / b / c / d / e / f'; + wrapper.setProps({ project: _.clone(project) }); + + vm.$nextTick(() => { + const renderedNamespace = trimText(vm.$el.querySelector('.js-project-namespace').textContent); + + expect(renderedNamespace).toBe('a / ... / e /'); + done(); + }); + }); + + it(`renders the project name`, done => { + project.name = 'my-test-project'; + wrapper.setProps({ project: _.clone(project) }); + + vm.$nextTick(() => { + const renderedName = trimText(vm.$el.querySelector('.js-project-name').innerHTML); + + expect(renderedName).toBe('my-test-project'); + done(); + }); + }); + + it(`renders the project name with highlighting in the case of a search query match`, done => { + project.name = 'my-test-project'; + wrapper.setProps({ project: _.clone(project), matcher: 'pro' }); + + vm.$nextTick(() => { + const renderedName = trimText(vm.$el.querySelector('.js-project-name').innerHTML); + + const expected = 'my-test-<b>p</b><b>r</b><b>o</b>ject'; + + expect(renderedName).toBe(expected); + done(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js new file mode 100644 index 00000000000..88c1dff76a1 --- /dev/null +++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js @@ -0,0 +1,152 @@ +import Vue from 'vue'; +import _ from 'underscore'; +import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue'; +import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; +import { shallowMount } from '@vue/test-utils'; +import { trimText } from 'spec/helpers/vue_component_helper'; + +describe('ProjectSelector component', () => { + let wrapper; + let vm; + loadJSONFixtures('projects.json'); + const allProjects = getJSONFixture('projects.json'); + const searchResults = allProjects.slice(0, 5); + let selected = []; + selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8)); + + beforeEach(() => { + jasmine.clock().install(); + + wrapper = shallowMount(Vue.extend(ProjectSelector), { + propsData: { + projectSearchResults: searchResults, + selectedProjects: selected, + showNoResultsMessage: false, + showMinimumSearchQueryMessage: false, + showLoadingIndicator: false, + showSearchErrorMessage: false, + }, + attachToDocument: true, + }); + + ({ vm } = wrapper); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + vm.$destroy(); + }); + + it('renders the search results', () => { + expect(vm.$el.querySelectorAll('.js-project-list-item').length).toBe(5); + }); + + it(`triggers a (debounced) search when the search input value changes`, done => { + spyOn(vm, '$emit'); + const query = 'my test query!'; + const searchInput = vm.$el.querySelector('.js-project-selector-input'); + searchInput.value = query; + searchInput.dispatchEvent(new Event('input')); + + vm.$nextTick(() => { + expect(vm.$emit).not.toHaveBeenCalledWith(); + jasmine.clock().tick(501); + + expect(vm.$emit).toHaveBeenCalledWith('searched', query); + done(); + }); + }); + + it(`debounces the search input`, done => { + spyOn(vm, '$emit'); + const searchInput = vm.$el.querySelector('.js-project-selector-input'); + + const updateSearchQuery = (count = 0) => { + if (count === 10) { + jasmine.clock().tick(101); + + expect(vm.$emit).toHaveBeenCalledTimes(1); + expect(vm.$emit).toHaveBeenCalledWith('searched', `search query #9`); + done(); + } else { + searchInput.value = `search query #${count}`; + searchInput.dispatchEvent(new Event('input')); + + vm.$nextTick(() => { + jasmine.clock().tick(400); + updateSearchQuery(count + 1); + }); + } + }; + + updateSearchQuery(); + }); + + it(`includes a placeholder in the search box`, () => { + expect(vm.$el.querySelector('.js-project-selector-input').placeholder).toBe( + 'Search your projects', + ); + }); + + it(`triggers a "projectClicked" event when a project is clicked`, () => { + spyOn(vm, '$emit'); + wrapper.find(ProjectListItem).vm.$emit('click', _.first(searchResults)); + + expect(vm.$emit).toHaveBeenCalledWith('projectClicked', _.first(searchResults)); + }); + + it(`shows a "no results" message if showNoResultsMessage === true`, done => { + wrapper.setProps({ showNoResultsMessage: true }); + + vm.$nextTick(() => { + const noResultsEl = vm.$el.querySelector('.js-no-results-message'); + + expect(noResultsEl).toBeTruthy(); + + expect(trimText(noResultsEl.textContent)).toEqual('Sorry, no projects matched your search'); + + done(); + }); + }); + + it(`shows a "minimum seach query" message if showMinimumSearchQueryMessage === true`, done => { + wrapper.setProps({ showMinimumSearchQueryMessage: true }); + + vm.$nextTick(() => { + const minimumSearchEl = vm.$el.querySelector('.js-minimum-search-query-message'); + + expect(minimumSearchEl).toBeTruthy(); + + expect(trimText(minimumSearchEl.textContent)).toEqual( + 'Enter at least three characters to search', + ); + + done(); + }); + }); + + it(`shows a error message if showSearchErrorMessage === true`, done => { + wrapper.setProps({ showSearchErrorMessage: true }); + + vm.$nextTick(() => { + const errorMessageEl = vm.$el.querySelector('.js-search-error-message'); + + expect(errorMessageEl).toBeTruthy(); + + expect(trimText(errorMessageEl.textContent)).toEqual( + 'Something went wrong, unable to search projects', + ); + + done(); + }); + }); + + it(`focuses the input element when the focusSearchInput() method is called`, () => { + const input = vm.$el.querySelector('.js-project-selector-input'); + + expect(document.activeElement).not.toBe(input); + vm.focusSearchInput(); + + expect(document.activeElement).toBe(input); + }); +}); |