summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared/components
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-11-19 22:11:55 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-11-19 22:11:55 +0000
commit5a8431feceba47fd8e1804d9aa1b1730606b71d5 (patch)
treee5df8e0ceee60f4af8093f5c4c2f934b8abced05 /spec/frontend/vue_shared/components
parent4d477238500c347c6553d335d920bedfc5a46869 (diff)
downloadgitlab-ce-5a8431feceba47fd8e1804d9aa1b1730606b71d5.tar.gz
Add latest changes from gitlab-org/gitlab@12-5-stable-ee
Diffstat (limited to 'spec/frontend/vue_shared/components')
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap37
-rw-r--r--spec/frontend/vue_shared/components/commit_spec.js227
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js45
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js189
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/slot_switch_spec.js56
-rw-r--r--spec/frontend/vue_shared/components/split_button_spec.js104
-rw-r--r--spec/frontend/vue_shared/components/table_pagination_spec.js335
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js108
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js186
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"');
+ });
+ });
+});