diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-19 22:11:55 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-19 22:11:55 +0000 |
commit | 5a8431feceba47fd8e1804d9aa1b1730606b71d5 (patch) | |
tree | e5df8e0ceee60f4af8093f5c4c2f934b8abced05 /spec/frontend/vue_shared/components | |
parent | 4d477238500c347c6553d335d920bedfc5a46869 (diff) | |
download | gitlab-ce-5a8431feceba47fd8e1804d9aa1b1730606b71d5.tar.gz |
Add latest changes from gitlab-org/gitlab@12-5-stable-ee
Diffstat (limited to 'spec/frontend/vue_shared/components')
11 files changed, 1202 insertions, 89 deletions
diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap new file mode 100644 index 00000000000..95296de5a5d --- /dev/null +++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SplitButton renders actionItems 1`] = ` +<gldropdown-stub + menu-class="dropdown-menu-selectable " + split="true" + text="professor" +> + <gldropdownitem-stub + active="true" + active-class="is-active" + > + <strong> + professor + </strong> + + <div> + very symphonic + </div> + </gldropdownitem-stub> + + <gldropdowndivider-stub /> + <gldropdownitem-stub + active-class="is-active" + > + <strong> + captain + </strong> + + <div> + warp drive + </div> + </gldropdownitem-stub> + + <!----> +</gldropdown-stub> +`; diff --git a/spec/frontend/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js new file mode 100644 index 00000000000..77d8e00cf00 --- /dev/null +++ b/spec/frontend/vue_shared/components/commit_spec.js @@ -0,0 +1,227 @@ +import { shallowMount } from '@vue/test-utils'; +import CommitComponent from '~/vue_shared/components/commit.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; + +describe('Commit component', () => { + let props; + let wrapper; + + const findUserAvatar = () => wrapper.find(UserAvatarLink); + + const createComponent = propsData => { + wrapper = shallowMount(CommitComponent, { + propsData, + sync: false, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('should render a fork icon if it does not represent a tag', () => { + createComponent({ + tag: false, + commitRef: { + name: 'master', + ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', + }, + commitUrl: + 'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', + title: 'Commit message', + author: { + avatar_url: 'https://gitlab.com/uploads/-/system/user/avatar/300478/avatar.png', + web_url: 'https://gitlab.com/jschatz1', + path: '/jschatz1', + username: 'jschatz1', + }, + }); + + expect( + wrapper + .find('.icon-container') + .find(Icon) + .exists(), + ).toBe(true); + }); + + describe('Given all the props', () => { + beforeEach(() => { + props = { + tag: true, + commitRef: { + name: 'master', + ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', + }, + commitUrl: + 'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', + title: 'Commit message', + author: { + avatar_url: 'https://gitlab.com/uploads/-/system/user/avatar/300478/avatar.png', + web_url: 'https://gitlab.com/jschatz1', + path: '/jschatz1', + username: 'jschatz1', + }, + }; + createComponent(props); + }); + + it('should render a tag icon if it represents a tag', () => { + expect(wrapper.find('icon-stub[name="tag"]').exists()).toBe(true); + }); + + it('should render a link to the ref url', () => { + expect(wrapper.find('.ref-name').attributes('href')).toBe(props.commitRef.ref_url); + }); + + it('should render the ref name', () => { + expect(wrapper.find('.ref-name').text()).toContain(props.commitRef.name); + }); + + it('should render the commit short sha with a link to the commit url', () => { + expect(wrapper.find('.commit-sha').attributes('href')).toEqual(props.commitUrl); + + expect(wrapper.find('.commit-sha').text()).toContain(props.shortSha); + }); + + it('should render icon for commit', () => { + expect(wrapper.find('icon-stub[name="commit"]').exists()).toBe(true); + }); + + describe('Given commit title and author props', () => { + it('should render a link to the author profile', () => { + const userAvatar = findUserAvatar(); + + expect(userAvatar.props('linkHref')).toBe(props.author.path); + }); + + it('Should render the author avatar with title and alt attributes', () => { + const userAvatar = findUserAvatar(); + + expect(userAvatar.exists()).toBe(true); + + expect(userAvatar.props('imgAlt')).toBe(`${props.author.username}'s avatar`); + }); + }); + + it('should render the commit title', () => { + expect(wrapper.find('.commit-row-message').attributes('href')).toEqual(props.commitUrl); + + expect(wrapper.find('.commit-row-message').text()).toContain(props.title); + }); + }); + + describe('When commit title is not provided', () => { + it('should render default message', () => { + props = { + tag: false, + commitRef: { + name: 'master', + ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', + }, + commitUrl: + 'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', + title: null, + author: {}, + }; + + createComponent(props); + + expect(wrapper.find('.commit-title span').text()).toContain( + "Can't find HEAD commit for this branch", + ); + }); + }); + + describe('When commit ref is provided, but merge ref is not', () => { + it('should render the commit ref', () => { + props = { + tag: false, + commitRef: { + name: 'master', + ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', + }, + commitUrl: + 'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', + title: null, + author: {}, + }; + + createComponent(props); + const refEl = wrapper.find('.ref-name'); + + expect(refEl.text()).toContain('master'); + + expect(refEl.attributes('href')).toBe(props.commitRef.ref_url); + + expect(refEl.attributes('data-original-title')).toBe(props.commitRef.name); + + expect(wrapper.find('icon-stub[name="branch"]').exists()).toBe(true); + }); + }); + + describe('When both commit and merge ref are provided', () => { + it('should render the merge ref', () => { + props = { + tag: false, + commitRef: { + name: 'master', + ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', + }, + commitUrl: + 'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067', + mergeRequestRef: { + iid: 1234, + path: 'https://example.com/path/to/mr', + title: 'Test MR', + }, + shortSha: 'b7836edd', + title: null, + author: {}, + }; + + createComponent(props); + const refEl = wrapper.find('.ref-name'); + + expect(refEl.text()).toContain('1234'); + + expect(refEl.attributes('href')).toBe(props.mergeRequestRef.path); + + expect(refEl.attributes('data-original-title')).toBe(props.mergeRequestRef.title); + + expect(wrapper.find('icon-stub[name="git-merge"]').exists()).toBe(true); + }); + }); + + describe('When showRefInfo === false', () => { + it('should not render any ref info', () => { + props = { + tag: false, + commitRef: { + name: 'master', + ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', + }, + commitUrl: + 'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067', + mergeRequestRef: { + iid: 1234, + path: '/path/to/mr', + title: 'Test MR', + }, + shortSha: 'b7836edd', + title: null, + author: {}, + showRefInfo: false, + }; + + createComponent(props); + + expect(wrapper.find('.ref-name').exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js new file mode 100644 index 00000000000..3ad8f3aec7c --- /dev/null +++ b/spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js @@ -0,0 +1,45 @@ +import { shallowMount } from '@vue/test-utils'; + +import ImageViewer from '~/vue_shared/components/content_viewer/viewers/image_viewer.vue'; +import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants'; + +describe('Image Viewer', () => { + const requiredProps = { + path: GREEN_BOX_IMAGE_URL, + renderInfo: true, + }; + let wrapper; + let imageInfo; + + function createElement({ props, includeRequired = true } = {}) { + const data = includeRequired ? { ...requiredProps, ...props } : { ...props }; + + wrapper = shallowMount(ImageViewer, { + propsData: data, + }); + imageInfo = wrapper.find('.image-info'); + } + + describe('file sizes', () => { + it('should show the humanized file size when `renderInfo` is true and there is size info', () => { + createElement({ props: { fileSize: 1024 } }); + + expect(imageInfo.text()).toContain('1.00 KiB'); + }); + + it('should not show the humanized file size when `renderInfo` is true and there is no size', () => { + const FILESIZE_RE = /\d+(\.\d+)?\s*([KMGTP]i)*B/; + + createElement({ props: { fileSize: 0 } }); + + // It shouldn't show any filesize info + expect(imageInfo.text()).not.toMatch(FILESIZE_RE); + }); + + it('should not show any image information when `renderInfo` is false', () => { + createElement({ props: { renderInfo: false } }); + + expect(imageInfo.exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js index d1de98f4a15..9e6b5286899 100644 --- a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js +++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js @@ -1,114 +1,129 @@ -import Vue from 'vue'; - +import { shallowMount } from '@vue/test-utils'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; - -import mountComponent from 'helpers/vue_mount_component_helper'; import { mockAssigneesList } from '../../../../javascripts/boards/mock_data'; -const createComponent = (assignees = mockAssigneesList, cssClass = '') => { - const Component = Vue.extend(IssueAssignees); - - return mountComponent(Component, { - assignees, - cssClass, - }); -}; +const TEST_CSS_CLASSES = 'test-classes'; +const TEST_MAX_VISIBLE = 4; +const TEST_ICON_SIZE = 16; describe('IssueAssigneesComponent', () => { + let wrapper; let vm; - beforeEach(() => { - vm = createComponent(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('data', () => { - it('returns default data props', () => { - expect(vm.maxVisibleAssignees).toBe(2); - expect(vm.maxAssigneeAvatars).toBe(3); - expect(vm.maxAssignees).toBe(99); + const factory = props => { + wrapper = shallowMount(IssueAssignees, { + propsData: { + assignees: mockAssigneesList, + ...props, + }, + sync: false, }); + vm = wrapper.vm; // eslint-disable-line + }; + + const findTooltipText = () => wrapper.find('.js-assignee-tooltip').text(); + const findAvatars = () => wrapper.findAll(UserAvatarLink); + const findOverflowCounter = () => wrapper.find('.avatar-counter'); + + it('returns default data props', () => { + factory({ assignees: mockAssigneesList }); + expect(vm.iconSize).toBe(24); + expect(vm.maxVisible).toBe(3); + expect(vm.maxAssignees).toBe(99); }); - describe('computed', () => { - describe('countOverLimit', () => { - it('should return difference between assignees count and maxVisibleAssignees', () => { - expect(vm.countOverLimit).toBe(mockAssigneesList.length - vm.maxVisibleAssignees); - }); - }); - - describe('assigneesToShow', () => { - it('should return assignees containing only 2 items when count more than maxAssigneeAvatars', () => { - expect(vm.assigneesToShow.length).toBe(2); - }); - - it('should return all assignees as it is when count less than maxAssigneeAvatars', () => { - vm.assignees = mockAssigneesList.slice(0, 3); // Set 3 Assignees - - expect(vm.assigneesToShow.length).toBe(3); - }); - }); - - describe('assigneesCounterTooltip', () => { - it('should return string containing count of remaining assignees when count more than maxAssigneeAvatars', () => { - expect(vm.assigneesCounterTooltip).toBe('3 more assignees'); - }); - }); - - describe('shouldRenderAssigneesCounter', () => { - it('should return `false` when assignees count less than maxAssigneeAvatars', () => { - vm.assignees = mockAssigneesList.slice(0, 3); // Set 3 Assignees - - expect(vm.shouldRenderAssigneesCounter).toBe(false); - }); - - it('should return `true` when assignees count more than maxAssigneeAvatars', () => { - expect(vm.shouldRenderAssigneesCounter).toBe(true); + describe.each` + numAssignees | maxVisible | expectedShown | expectedHidden + ${0} | ${3} | ${0} | ${''} + ${1} | ${3} | ${1} | ${''} + ${2} | ${3} | ${2} | ${''} + ${3} | ${3} | ${3} | ${''} + ${4} | ${3} | ${2} | ${'+2'} + ${5} | ${2} | ${1} | ${'+4'} + ${1000} | ${5} | ${4} | ${'99+'} + `( + 'with assignees ($numAssignees) and maxVisible ($maxVisible)', + ({ numAssignees, maxVisible, expectedShown, expectedHidden }) => { + beforeEach(() => { + factory({ assignees: Array(numAssignees).fill({}), maxVisible }); }); - }); - describe('assigneeCounterLabel', () => { - it('should return count of additional assignees total assignees count more than maxAssigneeAvatars', () => { - expect(vm.assigneeCounterLabel).toBe('+3'); + if (expectedShown) { + it('shows assignee avatars', () => { + expect(findAvatars().length).toEqual(expectedShown); + }); + } else { + it('does not show assignee avatars', () => { + expect(findAvatars().length).toEqual(0); + }); + } + + if (expectedHidden) { + it('shows overflow counter', () => { + const hiddenCount = numAssignees - expectedShown; + + expect(findOverflowCounter().exists()).toBe(true); + expect(findOverflowCounter().text()).toEqual(expectedHidden.toString()); + expect(findOverflowCounter().attributes('data-original-title')).toEqual( + `${hiddenCount} more assignees`, + ); + }); + } else { + it('does not show overflow counter', () => { + expect(findOverflowCounter().exists()).toBe(false); + }); + } + }, + ); + + describe('when mounted', () => { + beforeEach(() => { + factory({ + imgCssClasses: TEST_CSS_CLASSES, + maxVisible: TEST_MAX_VISIBLE, + iconSize: TEST_ICON_SIZE, }); }); - }); - describe('methods', () => { - describe('avatarUrlTitle', () => { - it('returns string containing alt text for assignee avatar', () => { - expect(vm.avatarUrlTitle(mockAssigneesList[0])).toBe('Avatar for Terrell Graham'); - }); + it('computes alt text for assignee avatar', () => { + expect(vm.avatarUrlTitle(mockAssigneesList[0])).toBe('Avatar for Terrell Graham'); }); - }); - describe('template', () => { it('renders component root element with class `issue-assignees`', () => { - expect(vm.$el.classList.contains('issue-assignees')).toBe(true); + expect(wrapper.element.classList.contains('issue-assignees')).toBe(true); }); - it('renders assignee avatars', () => { - expect(vm.$el.querySelectorAll('.user-avatar-link').length).toBe(2); + it('renders assignee', () => { + const data = findAvatars().wrappers.map(x => ({ + ...x.props(), + })); + + const expected = mockAssigneesList.slice(0, TEST_MAX_VISIBLE - 1).map(x => + expect.objectContaining({ + linkHref: x.web_url, + imgAlt: `Avatar for ${x.name}`, + imgCssClasses: TEST_CSS_CLASSES, + imgSrc: x.avatar_url, + imgSize: TEST_ICON_SIZE, + }), + ); + + expect(data).toEqual(expected); }); - it('renders assignee tooltips', () => { - const tooltipText = vm.$el - .querySelectorAll('.user-avatar-link')[0] - .querySelector('.js-assignee-tooltip').innerText; - - expect(tooltipText).toContain('Assignee'); - expect(tooltipText).toContain('Terrell Graham'); - expect(tooltipText).toContain('@monserrate.gleichner'); - }); + describe('assignee tooltips', () => { + it('renders "Assignee" header', () => { + expect(findTooltipText()).toContain('Assignee'); + }); - it('renders additional assignees count', () => { - const avatarCounterEl = vm.$el.querySelector('.avatar-counter'); + it('renders assignee name', () => { + expect(findTooltipText()).toContain('Terrell Graham'); + }); - expect(avatarCounterEl.innerText.trim()).toBe('+3'); - expect(avatarCounterEl.getAttribute('data-original-title')).toBe('3 more assignees'); + it('renders assignee @username', () => { + expect(findTooltipText()).toContain('@monserrate.gleichner'); + }); }); }); }); diff --git a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js index eafff7f681e..45f131194ca 100644 --- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js +++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; import createStore from '~/notes/stores'; -import { userDataMock } from '../../../../javascripts/notes/mock_data'; +import { userDataMock } from '../../../notes/mock_data'; describe('issue placeholder system note component', () => { let store; diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js index a65e3eb294a..c2e8359f78d 100644 --- a/spec/frontend/vue_shared/components/notes/system_note_spec.js +++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js @@ -57,7 +57,7 @@ describe('system note component', () => { // we need to strip them because they break layout of commit lists in system notes: // https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png it('removes wrapping paragraph from note HTML', () => { - expect(vm.$el.querySelector('.system-note-message').innerHTML).toEqual('<span>closed</span>'); + expect(vm.$el.querySelector('.system-note-message').innerHTML).toContain('<span>closed</span>'); }); it('should initMRPopovers onMount', () => { diff --git a/spec/frontend/vue_shared/components/slot_switch_spec.js b/spec/frontend/vue_shared/components/slot_switch_spec.js new file mode 100644 index 00000000000..cff955c05b2 --- /dev/null +++ b/spec/frontend/vue_shared/components/slot_switch_spec.js @@ -0,0 +1,56 @@ +import { shallowMount } from '@vue/test-utils'; + +import SlotSwitch from '~/vue_shared/components/slot_switch'; + +describe('SlotSwitch', () => { + const slots = { + first: '<a>AGP</a>', + second: '<p>PCI</p>', + }; + + let wrapper; + + const createComponent = propsData => { + wrapper = shallowMount(SlotSwitch, { + propsData, + slots, + sync: false, + }); + }; + + const getChildrenHtml = () => wrapper.findAll('* *').wrappers.map(c => c.html()); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + it('throws an error if activeSlotNames is missing', () => { + expect(createComponent).toThrow('[Vue warn]: Missing required prop: "activeSlotNames"'); + }); + + it('renders no slots if activeSlotNames is empty', () => { + createComponent({ + activeSlotNames: [], + }); + + expect(getChildrenHtml().length).toBe(0); + }); + + it('renders one slot if activeSlotNames contains single slot name', () => { + createComponent({ + activeSlotNames: ['first'], + }); + + expect(getChildrenHtml()).toEqual([slots.first]); + }); + + it('renders multiple slots if activeSlotNames contains multiple slot names', () => { + createComponent({ + activeSlotNames: Object.keys(slots), + }); + + expect(getChildrenHtml()).toEqual(Object.values(slots)); + }); +}); diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js new file mode 100644 index 00000000000..520abb02cf7 --- /dev/null +++ b/spec/frontend/vue_shared/components/split_button_spec.js @@ -0,0 +1,104 @@ +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; + +import SplitButton from '~/vue_shared/components/split_button.vue'; + +const mockActionItems = [ + { + eventName: 'concert', + title: 'professor', + description: 'very symphonic', + }, + { + eventName: 'apocalypse', + title: 'captain', + description: 'warp drive', + }, +]; + +describe('SplitButton', () => { + let wrapper; + + const createComponent = propsData => { + wrapper = shallowMount(SplitButton, { + propsData, + sync: false, + }); + }; + + const findDropdown = () => wrapper.find(GlDropdown); + const findDropdownItem = (index = 0) => + findDropdown() + .findAll(GlDropdownItem) + .at(index); + const selectItem = index => { + findDropdownItem(index).vm.$emit('click'); + + return wrapper.vm.$nextTick(); + }; + const clickToggleButton = () => { + findDropdown().vm.$emit('click'); + + return wrapper.vm.$nextTick(); + }; + + it('fails for empty actionItems', () => { + const actionItems = []; + expect(() => createComponent({ actionItems })).toThrow(); + }); + + it('fails for single actionItems', () => { + const actionItems = [mockActionItems[0]]; + expect(() => createComponent({ actionItems })).toThrow(); + }); + + it('renders actionItems', () => { + createComponent({ actionItems: mockActionItems }); + + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('toggle button text', () => { + beforeEach(() => { + createComponent({ actionItems: mockActionItems }); + }); + + it('defaults to first actionItems title', () => { + expect(findDropdown().props().text).toBe(mockActionItems[0].title); + }); + + it('changes to selected actionItems title', () => + selectItem(1).then(() => { + expect(findDropdown().props().text).toBe(mockActionItems[1].title); + })); + }); + + describe('emitted event', () => { + let eventHandler; + + beforeEach(() => { + createComponent({ actionItems: mockActionItems }); + }); + + const addEventHandler = ({ eventName }) => { + eventHandler = jest.fn(); + wrapper.vm.$once(eventName, () => eventHandler()); + }; + + it('defaults to first actionItems event', () => { + addEventHandler(mockActionItems[0]); + + return clickToggleButton().then(() => { + expect(eventHandler).toHaveBeenCalled(); + }); + }); + + it('changes to selected actionItems event', () => + selectItem(1) + .then(() => addEventHandler(mockActionItems[1])) + .then(clickToggleButton) + .then(() => { + expect(eventHandler).toHaveBeenCalled(); + })); + }); +}); diff --git a/spec/frontend/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js new file mode 100644 index 00000000000..0a9ff36b2fb --- /dev/null +++ b/spec/frontend/vue_shared/components/table_pagination_spec.js @@ -0,0 +1,335 @@ +import { shallowMount } from '@vue/test-utils'; +import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; + +describe('Pagination component', () => { + let wrapper; + let spy; + + const mountComponent = props => { + wrapper = shallowMount(TablePagination, { + sync: false, + propsData: props, + }); + }; + + const findFirstButtonLink = () => wrapper.find('.js-first-button .page-link'); + const findPreviousButton = () => wrapper.find('.js-previous-button'); + const findPreviousButtonLink = () => wrapper.find('.js-previous-button .page-link'); + const findNextButton = () => wrapper.find('.js-next-button'); + const findNextButtonLink = () => wrapper.find('.js-next-button .page-link'); + const findLastButtonLink = () => wrapper.find('.js-last-button .page-link'); + const findPages = () => wrapper.findAll('.page'); + const findSeparator = () => wrapper.find('.separator'); + + beforeEach(() => { + spy = jest.fn(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('render', () => { + it('should not render anything', () => { + mountComponent({ + pageInfo: { + nextPage: NaN, + page: 1, + perPage: 20, + previousPage: NaN, + total: 15, + totalPages: 1, + }, + change: spy, + }); + + expect(wrapper.isEmpty()).toBe(true); + }); + + describe('prev button', () => { + it('should be disabled and non clickable', () => { + mountComponent({ + pageInfo: { + nextPage: 2, + page: 1, + perPage: 20, + previousPage: NaN, + total: 84, + totalPages: 5, + }, + change: spy, + }); + + expect(findPreviousButton().classes()).toContain('disabled'); + findPreviousButtonLink().trigger('click'); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should be disabled and non clickable when total and totalPages are NaN', () => { + mountComponent({ + pageInfo: { + nextPage: 2, + page: 1, + perPage: 20, + previousPage: NaN, + total: NaN, + totalPages: NaN, + }, + change: spy, + }); + expect(findPreviousButton().classes()).toContain('disabled'); + findPreviousButtonLink().trigger('click'); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should be enabled and clickable', () => { + mountComponent({ + pageInfo: { + nextPage: 3, + page: 2, + perPage: 20, + previousPage: 1, + total: 84, + totalPages: 5, + }, + change: spy, + }); + findPreviousButtonLink().trigger('click'); + expect(spy).toHaveBeenCalledWith(1); + }); + + it('should be enabled and clickable when total and totalPages are NaN', () => { + mountComponent({ + pageInfo: { + nextPage: 3, + page: 2, + perPage: 20, + previousPage: 1, + total: NaN, + totalPages: NaN, + }, + change: spy, + }); + findPreviousButtonLink().trigger('click'); + expect(spy).toHaveBeenCalledWith(1); + }); + }); + + describe('first button', () => { + it('should call the change callback with the first page', () => { + mountComponent({ + pageInfo: { + nextPage: 3, + page: 2, + perPage: 20, + previousPage: 1, + total: 84, + totalPages: 5, + }, + change: spy, + }); + const button = findFirstButtonLink(); + expect(button.text().trim()).toEqual('« First'); + button.trigger('click'); + expect(spy).toHaveBeenCalledWith(1); + }); + + it('should call the change callback with the first page when total and totalPages are NaN', () => { + mountComponent({ + pageInfo: { + nextPage: 3, + page: 2, + perPage: 20, + previousPage: 1, + total: NaN, + totalPages: NaN, + }, + change: spy, + }); + const button = findFirstButtonLink(); + expect(button.text().trim()).toEqual('« First'); + button.trigger('click'); + expect(spy).toHaveBeenCalledWith(1); + }); + }); + + describe('last button', () => { + it('should call the change callback with the last page', () => { + mountComponent({ + pageInfo: { + nextPage: 3, + page: 2, + perPage: 20, + previousPage: 1, + total: 84, + totalPages: 5, + }, + change: spy, + }); + const button = findLastButtonLink(); + expect(button.text().trim()).toEqual('Last »'); + button.trigger('click'); + expect(spy).toHaveBeenCalledWith(5); + }); + + it('should not render', () => { + mountComponent({ + pageInfo: { + nextPage: 3, + page: 2, + perPage: 20, + previousPage: 1, + total: NaN, + totalPages: NaN, + }, + change: spy, + }); + expect(findLastButtonLink().exists()).toBe(false); + }); + }); + + describe('next button', () => { + it('should be disabled and non clickable', () => { + mountComponent({ + pageInfo: { + nextPage: NaN, + page: 5, + perPage: 20, + previousPage: 4, + total: 84, + totalPages: 5, + }, + change: spy, + }); + expect( + findNextButton() + .text() + .trim(), + ).toEqual('Next ›'); + findNextButtonLink().trigger('click'); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should be disabled and non clickable when total and totalPages are NaN', () => { + mountComponent({ + pageInfo: { + nextPage: NaN, + page: 5, + perPage: 20, + previousPage: 4, + total: NaN, + totalPages: NaN, + }, + change: spy, + }); + expect( + findNextButton() + .text() + .trim(), + ).toEqual('Next ›'); + findNextButtonLink().trigger('click'); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should be enabled and clickable', () => { + mountComponent({ + pageInfo: { + nextPage: 4, + page: 3, + perPage: 20, + previousPage: 2, + total: 84, + totalPages: 5, + }, + change: spy, + }); + findNextButtonLink().trigger('click'); + expect(spy).toHaveBeenCalledWith(4); + }); + + it('should be enabled and clickable when total and totalPages are NaN', () => { + mountComponent({ + pageInfo: { + nextPage: 4, + page: 3, + perPage: 20, + previousPage: 2, + total: NaN, + totalPages: NaN, + }, + change: spy, + }); + findNextButtonLink().trigger('click'); + expect(spy).toHaveBeenCalledWith(4); + }); + }); + + describe('numbered buttons', () => { + it('should render 5 pages', () => { + mountComponent({ + pageInfo: { + nextPage: 4, + page: 3, + perPage: 20, + previousPage: 2, + total: 84, + totalPages: 5, + }, + change: spy, + }); + expect(findPages().length).toEqual(5); + }); + + it('should not render any page', () => { + mountComponent({ + pageInfo: { + nextPage: 4, + page: 3, + perPage: 20, + previousPage: 2, + total: NaN, + totalPages: NaN, + }, + change: spy, + }); + expect(findPages().length).toEqual(0); + }); + }); + + describe('spread operator', () => { + it('should render', () => { + mountComponent({ + pageInfo: { + nextPage: 4, + page: 3, + perPage: 20, + previousPage: 2, + total: 84, + totalPages: 10, + }, + change: spy, + }); + expect( + findSeparator() + .text() + .trim(), + ).toEqual('...'); + }); + + it('should not render', () => { + mountComponent({ + pageInfo: { + nextPage: 4, + page: 3, + perPage: 20, + previousPage: 2, + total: NaN, + totalPages: NaN, + }, + change: spy, + }); + expect(findSeparator().exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js new file mode 100644 index 00000000000..2f87359a4a6 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js @@ -0,0 +1,108 @@ +import { shallowMount } from '@vue/test-utils'; +import { placeholderImage } from '~/lazy_loader'; +import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import defaultAvatarUrl from 'images/no_avatar.png'; + +jest.mock('images/no_avatar.png', () => 'default-avatar-url'); + +const DEFAULT_PROPS = { + size: 99, + imgSrc: 'myavatarurl.com', + imgAlt: 'mydisplayname', + cssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', +}; + +describe('User Avatar Image Component', () => { + let wrapper; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Initialization', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...DEFAULT_PROPS, + }, + sync: false, + }); + }); + + it('should have <img> as a child element', () => { + const imageElement = wrapper.find('img'); + + expect(imageElement.exists()).toBe(true); + expect(imageElement.attributes('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + expect(imageElement.attributes('alt')).toBe(DEFAULT_PROPS.imgAlt); + }); + + it('should properly render img css', () => { + const classes = wrapper.find('img').classes(); + expect(classes).toEqual(expect.arrayContaining(['avatar', 's99', DEFAULT_PROPS.cssClasses])); + expect(classes).not.toContain('lazy'); + }); + }); + + describe('Initialization when lazy', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...DEFAULT_PROPS, + lazy: true, + }, + sync: false, + }); + }); + + it('should add lazy attributes', () => { + const imageElement = wrapper.find('img'); + + expect(imageElement.classes()).toContain('lazy'); + expect(imageElement.attributes('src')).toBe(placeholderImage); + expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + }); + }); + + describe('Initialization without src', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { sync: false }); + }); + + it('should have default avatar image', () => { + const imageElement = wrapper.find('img'); + + expect(imageElement.attributes('src')).toBe(`${defaultAvatarUrl}?width=20`); + }); + }); + + describe('dynamic tooltip content', () => { + const props = DEFAULT_PROPS; + const slots = { + default: ['Action!'], + }; + + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { propsData: { props }, slots, sync: false }); + }); + + it('renders the tooltip slot', () => { + expect(wrapper.find('.js-user-avatar-image-toolip').exists()).toBe(true); + }); + + it('renders the tooltip content', () => { + expect(wrapper.find('.js-user-avatar-image-toolip').text()).toContain(slots.default[0]); + }); + + it('does not render tooltip data attributes for on avatar image', () => { + const avatarImg = wrapper.find('img'); + + expect(avatarImg.attributes('data-original-title')).toBeFalsy(); + expect(avatarImg.attributes('data-placement')).not.toBeDefined(); + expect(avatarImg.attributes('data-container')).not.toBeDefined(); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js new file mode 100644 index 00000000000..fc2eb6329b0 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js @@ -0,0 +1,186 @@ +import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue'; +import { mount } from '@vue/test-utils'; + +const DEFAULT_PROPS = { + loaded: true, + user: { + username: 'root', + name: 'Administrator', + location: 'Vienna', + bio: null, + organization: null, + status: null, + }, +}; + +describe('User Popover Component', () => { + const fixtureTemplate = 'merge_requests/diff_comment.html'; + preloadFixtures(fixtureTemplate); + + let wrapper; + + beforeEach(() => { + loadFixtures(fixtureTemplate); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Empty', () => { + beforeEach(() => { + wrapper = mount(UserPopover, { + propsData: { + target: document.querySelector('.js-user-link'), + user: { + name: null, + username: null, + location: null, + bio: null, + organization: null, + status: null, + }, + }, + sync: false, + }); + }); + + it('should return skeleton loaders', () => { + expect(wrapper.findAll('.animation-container').length).toBe(4); + }); + }); + + describe('basic data', () => { + it('should show basic fields', () => { + wrapper = mount(UserPopover, { + propsData: { + ...DEFAULT_PROPS, + target: document.querySelector('.js-user-link'), + }, + sync: false, + }); + + expect(wrapper.text()).toContain(DEFAULT_PROPS.user.name); + expect(wrapper.text()).toContain(DEFAULT_PROPS.user.username); + expect(wrapper.text()).toContain(DEFAULT_PROPS.user.location); + }); + + it('shows icon for location', () => { + const iconEl = wrapper.find('.js-location svg'); + + expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('location'); + }); + }); + + describe('job data', () => { + it('should show only bio if no organization is available', () => { + const testProps = Object.assign({}, DEFAULT_PROPS); + testProps.user.bio = 'Engineer'; + + wrapper = mount(UserPopover, { + propsData: { + ...testProps, + target: document.querySelector('.js-user-link'), + }, + sync: false, + }); + + expect(wrapper.text()).toContain('Engineer'); + }); + + it('should show only organization if no bio is available', () => { + const testProps = Object.assign({}, DEFAULT_PROPS); + testProps.user.organization = 'GitLab'; + + wrapper = mount(UserPopover, { + propsData: { + ...testProps, + target: document.querySelector('.js-user-link'), + }, + sync: false, + }); + + expect(wrapper.text()).toContain('GitLab'); + }); + + it('should display bio and organization in separate lines', () => { + const testProps = Object.assign({}, DEFAULT_PROPS); + testProps.user.bio = 'Engineer'; + testProps.user.organization = 'GitLab'; + + wrapper = mount(UserPopover, { + propsData: { + ...DEFAULT_PROPS, + target: document.querySelector('.js-user-link'), + }, + sync: false, + }); + + expect(wrapper.find('.js-bio').text()).toContain('Engineer'); + expect(wrapper.find('.js-organization').text()).toContain('GitLab'); + }); + + it('should not encode special characters in bio and organization', () => { + const testProps = Object.assign({}, DEFAULT_PROPS); + testProps.user.bio = 'Manager & Team Lead'; + testProps.user.organization = 'Me & my <funky> Company'; + + wrapper = mount(UserPopover, { + propsData: { + ...DEFAULT_PROPS, + target: document.querySelector('.js-user-link'), + }, + sync: false, + }); + + expect(wrapper.find('.js-bio').text()).toContain('Manager & Team Lead'); + expect(wrapper.find('.js-organization').text()).toContain('Me & my <funky> Company'); + }); + + it('shows icon for bio', () => { + const iconEl = wrapper.find('.js-bio svg'); + + expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('profile'); + }); + + it('shows icon for organization', () => { + const iconEl = wrapper.find('.js-organization svg'); + + expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('work'); + }); + }); + + describe('status data', () => { + it('should show only message', () => { + const testProps = Object.assign({}, DEFAULT_PROPS); + testProps.user.status = { message_html: 'Hello World' }; + + wrapper = mount(UserPopover, { + propsData: { + ...DEFAULT_PROPS, + target: document.querySelector('.js-user-link'), + }, + sync: false, + }); + + expect(wrapper.text()).toContain('Hello World'); + }); + + it('should show message and emoji', () => { + const testProps = Object.assign({}, DEFAULT_PROPS); + testProps.user.status = { emoji: 'basketball_player', message_html: 'Hello World' }; + + wrapper = mount(UserPopover, { + propsData: { + ...DEFAULT_PROPS, + target: document.querySelector('.js-user-link'), + status: { emoji: 'basketball_player', message_html: 'Hello World' }, + }, + sync: false, + }); + + expect(wrapper.text()).toContain('Hello World'); + expect(wrapper.html()).toContain('<gl-emoji data-name="basketball_player"'); + }); + }); +}); |