diff options
Diffstat (limited to 'spec/frontend/vue_shared/components/project_selector')
-rw-r--r-- | spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js | 109 | ||||
-rw-r--r-- | spec/frontend/vue_shared/components/project_selector/project_selector_spec.js | 112 |
2 files changed, 221 insertions, 0 deletions
diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js new file mode 100644 index 00000000000..eb1d9e93634 --- /dev/null +++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js @@ -0,0 +1,109 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { trimText } from 'helpers/text_helper'; +import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; + +const localVue = createLocalVue(); + +describe('ProjectListItem component', () => { + const Component = localVue.extend(ProjectListItem); + let wrapper; + let vm; + let options; + + const project = getJSONFixture('static/projects.json')[0]; + + beforeEach(() => { + options = { + propsData: { + project, + selected: false, + }, + localVue, + }; + }); + + afterEach(() => { + wrapper.vm.$destroy(); + }); + + it('does not render a check mark icon if selected === false', () => { + wrapper = shallowMount(Component, options); + + expect(wrapper.contains('.js-selected-icon.js-unselected')).toBe(true); + }); + + it('renders a check mark icon if selected === true', () => { + options.propsData.selected = true; + + wrapper = shallowMount(Component, options); + + expect(wrapper.contains('.js-selected-icon.js-selected')).toBe(true); + }); + + it(`emits a "clicked" event when clicked`, () => { + wrapper = shallowMount(Component, options); + ({ vm } = wrapper); + + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + wrapper.vm.onClick(); + + expect(wrapper.vm.$emit).toHaveBeenCalledWith('click'); + }); + + it(`renders the project avatar`, () => { + wrapper = shallowMount(Component, options); + + expect(wrapper.contains('.js-project-avatar')).toBe(true); + }); + + it(`renders a simple namespace name with a trailing slash`, () => { + options.propsData.project.name_with_namespace = 'a / b'; + + wrapper = shallowMount(Component, options); + const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text()); + + expect(renderedNamespace).toBe('a /'); + }); + + it(`renders a properly truncated namespace with a trailing slash`, () => { + options.propsData.project.name_with_namespace = 'a / b / c / d / e / f'; + + wrapper = shallowMount(Component, options); + const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text()); + + expect(renderedNamespace).toBe('a / ... / e /'); + }); + + it(`renders the project name`, () => { + options.propsData.project.name = 'my-test-project'; + + wrapper = shallowMount(Component, options); + const renderedName = trimText(wrapper.find('.js-project-name').text()); + + expect(renderedName).toBe('my-test-project'); + }); + + it(`renders the project name with highlighting in the case of a search query match`, () => { + options.propsData.project.name = 'my-test-project'; + options.propsData.matcher = 'pro'; + + wrapper = shallowMount(Component, options); + const renderedName = trimText(wrapper.find('.js-project-name').html()); + const expected = 'my-test-<b>p</b><b>r</b><b>o</b>ject'; + + expect(renderedName).toContain(expected); + }); + + it('prevents search query and project name XSS', () => { + const alertSpy = jest.spyOn(window, 'alert'); + options.propsData.project.name = "my-xss-pro<script>alert('XSS');</script>ject"; + options.propsData.matcher = "pro<script>alert('XSS');</script>"; + + wrapper = shallowMount(Component, options); + const renderedName = trimText(wrapper.find('.js-project-name').html()); + const expected = 'my-xss-project'; + + expect(renderedName).toContain(expected); + expect(alertSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js new file mode 100644 index 00000000000..29bced394dc --- /dev/null +++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js @@ -0,0 +1,112 @@ +import Vue from 'vue'; +import { head } from 'lodash'; + +import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui'; +import { mount, createLocalVue } from '@vue/test-utils'; +import { trimText } from 'helpers/text_helper'; +import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; +import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue'; + +const localVue = createLocalVue(); + +describe('ProjectSelector component', () => { + let wrapper; + let vm; + const allProjects = getJSONFixture('static/projects.json'); + const searchResults = allProjects.slice(0, 5); + let selected = []; + selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8)); + + const findSearchInput = () => wrapper.find(GlSearchBoxByType).find('input'); + + beforeEach(() => { + wrapper = mount(Vue.extend(ProjectSelector), { + localVue, + propsData: { + projectSearchResults: searchResults, + selectedProjects: selected, + showNoResultsMessage: false, + showMinimumSearchQueryMessage: false, + showLoadingIndicator: false, + showSearchErrorMessage: false, + }, + attachToDocument: true, + }); + + ({ vm } = wrapper); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders the search results', () => { + expect(wrapper.findAll('.js-project-list-item').length).toBe(5); + }); + + it(`triggers a search when the search input value changes`, () => { + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + const query = 'my test query!'; + const searchInput = findSearchInput(); + + searchInput.setValue(query); + searchInput.trigger('input'); + + expect(vm.$emit).toHaveBeenCalledWith('searched', query); + }); + + it(`includes a placeholder in the search box`, () => { + const searchInput = findSearchInput(); + + expect(searchInput.attributes('placeholder')).toBe('Search your projects'); + }); + + it(`triggers a "bottomReached" event when user has scrolled to the bottom of the list`, () => { + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + wrapper.find(GlInfiniteScroll).vm.$emit('bottomReached'); + + expect(vm.$emit).toHaveBeenCalledWith('bottomReached'); + }); + + it(`triggers a "projectClicked" event when a project is clicked`, () => { + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + wrapper.find(ProjectListItem).vm.$emit('click', head(searchResults)); + + expect(vm.$emit).toHaveBeenCalledWith('projectClicked', head(searchResults)); + }); + + it(`shows a "no results" message if showNoResultsMessage === true`, () => { + wrapper.setProps({ showNoResultsMessage: true }); + + return vm.$nextTick().then(() => { + const noResultsEl = wrapper.find('.js-no-results-message'); + + expect(noResultsEl.exists()).toBe(true); + expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search'); + }); + }); + + it(`shows a "minimum search query" message if showMinimumSearchQueryMessage === true`, () => { + wrapper.setProps({ showMinimumSearchQueryMessage: true }); + + return vm.$nextTick().then(() => { + const minimumSearchEl = wrapper.find('.js-minimum-search-query-message'); + + expect(minimumSearchEl.exists()).toBe(true); + expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search'); + }); + }); + + it(`shows a error message if showSearchErrorMessage === true`, () => { + wrapper.setProps({ showSearchErrorMessage: true }); + + return vm.$nextTick().then(() => { + const errorMessageEl = wrapper.find('.js-search-error-message'); + + expect(errorMessageEl.exists()).toBe(true); + expect(trimText(errorMessageEl.text())).toEqual( + 'Something went wrong, unable to search projects', + ); + }); + }); +}); |