summaryrefslogtreecommitdiff
path: root/spec/frontend/sidebar
diff options
context:
space:
mode:
authorSamantha Ming <sming@gitlab.com>2019-07-25 19:30:50 -0700
committerPaul Slaughter <pslaughter@gitlab.com>2019-08-20 15:15:18 -0500
commitf1f34baf6f4e0866feb3f4c7523e3dccf5784b0b (patch)
tree0922f96df924d9e9bf99ccc3a047683b7586b9c7 /spec/frontend/sidebar
parentfd589be32f02c160812bbce0f5fb2178fb7142d3 (diff)
downloadgitlab-ce-f1f34baf6f4e0866feb3f4c7523e3dccf5784b0b.tar.gz
Improve UX multi assigness in MR
Add merge warning on avatar in: - open view assigness - collapsed view assigness - dropdown (search) view assigness Add can_merge option to MR sidebar entity
Diffstat (limited to 'spec/frontend/sidebar')
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js84
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js78
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js201
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js34
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js135
-rw-r--r--spec/frontend/sidebar/user_data_mock.js9
6 files changed, 541 insertions, 0 deletions
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
new file mode 100644
index 00000000000..9e50eefc228
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -0,0 +1,84 @@
+import { shallowMount } from '@vue/test-utils';
+import { joinPaths } from '~/lib/utils/url_utility';
+import userDataMock from '../../user_data_mock';
+import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
+
+const TOOLTIP_PLACEMENT = 'bottom';
+const { name: USER_NAME } = userDataMock();
+
+describe('AssigneeAvatarLink component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ user: userDataMock(),
+ showLess: true,
+ rootPath: 'http://localhost:3000/',
+ tooltipPlacement: TOOLTIP_PLACEMENT,
+ singleUser: false,
+ issuableType: 'merge_request',
+ ...props,
+ };
+
+ wrapper = shallowMount(AssigneeAvatarLink, {
+ propsData,
+ sync: false,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findTooltipText = () => wrapper.attributes('data-original-title');
+
+ it('user who cannot merge has "cannot merge" in tooltip', () => {
+ createComponent({
+ user: {
+ can_merge: false,
+ },
+ });
+
+ expect(findTooltipText().includes('cannot merge')).toBe(true);
+ });
+
+ it('has the root url present in the assigneeUrl method', () => {
+ createComponent();
+ const assigneeUrl = joinPaths(
+ `${wrapper.props('rootPath')}`,
+ `${wrapper.props('user').username}`,
+ );
+
+ expect(wrapper.attributes().href).toEqual(assigneeUrl);
+ });
+
+ describe.each`
+ issuableType | tooltipHasName | canMerge | expected
+ ${'merge_request'} | ${true} | ${true} | ${USER_NAME}
+ ${'merge_request'} | ${true} | ${false} | ${`${USER_NAME} (cannot merge)`}
+ ${'merge_request'} | ${false} | ${true} | ${''}
+ ${'merge_request'} | ${false} | ${false} | ${'Cannot merge'}
+ ${'issue'} | ${true} | ${true} | ${USER_NAME}
+ ${'issue'} | ${true} | ${false} | ${USER_NAME}
+ ${'issue'} | ${false} | ${true} | ${''}
+ ${'issue'} | ${false} | ${false} | ${''}
+ `(
+ 'with $issuableType and tooltipHasName=$tooltipHasName and canMerge=$canMerge',
+ ({ issuableType, tooltipHasName, canMerge, expected }) => {
+ beforeEach(() => {
+ createComponent({
+ issuableType,
+ tooltipHasName,
+ user: {
+ ...userDataMock(),
+ can_merge: canMerge,
+ },
+ });
+ });
+
+ it('sets tooltip', () => {
+ expect(findTooltipText()).toBe(expected);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js
new file mode 100644
index 00000000000..53e5dbea1df
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js
@@ -0,0 +1,78 @@
+import { shallowMount } from '@vue/test-utils';
+import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
+import { TEST_HOST } from 'helpers/test_constants';
+import userDataMock from '../../user_data_mock';
+
+const TEST_AVATAR = `${TEST_HOST}/avatar.png`;
+const TEST_DEFAULT_AVATAR_URL = `${TEST_HOST}/default/avatar/url.png`;
+
+describe('AssigneeAvatar', () => {
+ let origGon;
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ user: userDataMock(),
+ imgSize: 24,
+ issuableType: 'merge_request',
+ ...props,
+ };
+
+ wrapper = shallowMount(AssigneeAvatar, {
+ propsData,
+ sync: false,
+ });
+ }
+
+ beforeEach(() => {
+ origGon = window.gon;
+ window.gon = { default_avatar_url: TEST_DEFAULT_AVATAR_URL };
+ });
+
+ afterEach(() => {
+ window.gon = origGon;
+ wrapper.destroy();
+ });
+
+ const findImg = () => wrapper.find('img');
+
+ it('does not show warning icon if assignee can merge', () => {
+ createComponent();
+
+ expect(wrapper.element.querySelector('.merge-icon')).toBeNull();
+ });
+
+ it('shows warning icon if assignee cannot merge', () => {
+ createComponent({
+ user: {
+ can_merge: false,
+ },
+ });
+
+ expect(wrapper.element.querySelector('.merge-icon')).not.toBeNull();
+ });
+
+ it('does not show warning icon for issuableType = "issue"', () => {
+ createComponent({
+ issuableType: 'issue',
+ });
+
+ expect(wrapper.element.querySelector('.merge-icon')).toBeNull();
+ });
+
+ it.each`
+ avatar | avatar_url | expected | desc
+ ${TEST_AVATAR} | ${null} | ${TEST_AVATAR} | ${'with avatar'}
+ ${null} | ${TEST_AVATAR} | ${TEST_AVATAR} | ${'with avatar_url'}
+ ${null} | ${null} | ${TEST_DEFAULT_AVATAR_URL} | ${'with no avatar'}
+ `('$desc', ({ avatar, avatar_url, expected }) => {
+ createComponent({
+ user: {
+ avatar,
+ avatar_url,
+ },
+ });
+
+ expect(findImg().attributes('src')).toEqual(expected);
+ });
+});
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
new file mode 100644
index 00000000000..377c0e1d211
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
@@ -0,0 +1,201 @@
+import { shallowMount } from '@vue/test-utils';
+import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
+import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
+import UsersMockHelper from 'helpers/user_mock_data_helper';
+
+const DEFAULT_MAX_COUNTER = 99;
+
+describe('CollapsedAssigneeList component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ users: [],
+ issuableType: 'merge_request',
+ ...props,
+ };
+
+ wrapper = shallowMount(CollapsedAssigneeList, {
+ propsData,
+ sync: false,
+ });
+ }
+
+ const findNoUsersIcon = () => wrapper.find('i[aria-label=None]');
+ const findAvatarCounter = () => wrapper.find('.avatar-counter');
+ const findAssignees = () => wrapper.findAll(CollapsedAssignee);
+ const getTooltipTitle = () => wrapper.attributes('data-original-title');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('No assignees/users', () => {
+ beforeEach(() => {
+ createComponent({
+ users: [],
+ });
+ });
+
+ it('has no users', () => {
+ expect(findNoUsersIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('One assignee/user', () => {
+ let users;
+
+ beforeEach(() => {
+ users = UsersMockHelper.createNumberRandomUsers(1);
+ });
+
+ it('should not show no users icon', () => {
+ createComponent({ users });
+
+ expect(findNoUsersIcon().exists()).toBe(false);
+ });
+
+ it('has correct "cannot merge" tooltip when user cannot merge', () => {
+ users[0].can_merge = false;
+
+ createComponent({ users });
+
+ expect(getTooltipTitle()).toContain('cannot merge');
+ });
+
+ it('does not have "merge" word in tooltip if user can merge', () => {
+ users[0].can_merge = true;
+
+ createComponent({ users });
+
+ expect(getTooltipTitle()).not.toContain('merge');
+ });
+ });
+
+ describe('More than one assignees/users', () => {
+ let users;
+
+ beforeEach(() => {
+ users = UsersMockHelper.createNumberRandomUsers(2);
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent({ users });
+ });
+
+ it('has multiple-users class', () => {
+ expect(wrapper.classes('multiple-users')).toBe(true);
+ });
+
+ it('does not display an avatar count', () => {
+ expect(findAvatarCounter().exists()).toBe(false);
+ });
+
+ it('returns just two collapsed users', () => {
+ expect(findAssignees().length).toBe(2);
+ });
+ });
+
+ it('has correct "cannot merge" tooltip when no user can merge', () => {
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+
+ createComponent({
+ users,
+ });
+
+ expect(getTooltipTitle()).toEqual(`${users[0].name}, ${users[1].name} (no one can merge)`);
+ });
+
+ it('does not have "merge" word in tooltip if everyone can merge', () => {
+ users[0].can_merge = true;
+ users[1].can_merge = true;
+
+ createComponent({
+ users,
+ });
+
+ expect(getTooltipTitle()).toEqual(`${users[0].name}, ${users[1].name}`);
+ });
+ });
+
+ describe('More than two assignees/users', () => {
+ let users;
+
+ beforeEach(() => {
+ users = UsersMockHelper.createNumberRandomUsers(3);
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent({ users });
+ });
+
+ it('does display an avatar count', () => {
+ expect(findAvatarCounter().exists()).toBe(true);
+ expect(findAvatarCounter().text()).toEqual('+2');
+ });
+
+ it('returns one collapsed users', () => {
+ expect(findAssignees().length).toBe(1);
+ });
+ });
+
+ it('has correct "cannot merge" tooltip when one user can merge', () => {
+ users[0].can_merge = true;
+ users[1].can_merge = false;
+ users[2].can_merge = false;
+
+ createComponent({
+ users,
+ });
+
+ expect(getTooltipTitle()).toContain('1/3 can merge');
+ });
+
+ it('has correct "cannot merge" tooltip when more than one user can merge', () => {
+ users[0].can_merge = false;
+ users[1].can_merge = true;
+ users[2].can_merge = true;
+
+ createComponent({
+ users,
+ });
+
+ expect(getTooltipTitle()).toContain('2/3 can merge');
+ });
+
+ it('does not have "merge" in tooltip if everyone can merge', () => {
+ users[0].can_merge = true;
+ users[1].can_merge = true;
+ users[2].can_merge = true;
+
+ createComponent({
+ users,
+ });
+
+ expect(getTooltipTitle()).not.toContain('merge');
+ });
+
+ it('displays the correct avatar count via a computed property if less than default max counter', () => {
+ users = UsersMockHelper.createNumberRandomUsers(5);
+
+ createComponent({
+ users,
+ });
+
+ expect(findAvatarCounter().text()).toEqual(`+${users.length - 1}`);
+ });
+
+ it('displays the correct avatar count via a computed property if more than default max counter', () => {
+ users = UsersMockHelper.createNumberRandomUsers(100);
+
+ createComponent({
+ users,
+ });
+
+ expect(findAvatarCounter().text()).toEqual(`${DEFAULT_MAX_COUNTER}+`);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
new file mode 100644
index 00000000000..03b61daa2f8
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
+import userDataMock from '../../user_data_mock';
+
+describe('CollapsedAssignee assignee component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ user: userDataMock(),
+ ...props,
+ };
+
+ wrapper = shallowMount(CollapsedAssignee, {
+ propsData,
+ sync: false,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has author name', () => {
+ createComponent();
+
+ expect(
+ wrapper
+ .find('.author')
+ .text()
+ .trim(),
+ ).toEqual(wrapper.vm.user.name);
+ });
+});
diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
new file mode 100644
index 00000000000..64170a53a7f
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -0,0 +1,135 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
+import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
+import { TEST_HOST } from 'helpers/test_constants';
+import userDataMock from '../../user_data_mock';
+import UsersMockHelper from '../../../helpers/user_mock_data_helper';
+
+const DEFAULT_RENDER_COUNT = 5;
+
+describe('UncollapsedAssigneeList component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ users: [],
+ rootPath: TEST_HOST,
+ ...props,
+ };
+
+ wrapper = mount(UncollapsedAssigneeList, {
+ sync: false,
+ propsData,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findMoreButton = () => wrapper.find('.user-list-more button');
+
+ describe('One assignee/user', () => {
+ let user;
+
+ beforeEach(() => {
+ user = userDataMock();
+
+ createComponent({
+ users: [user],
+ });
+ });
+
+ it('only has one user', () => {
+ expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(1);
+ });
+
+ it('calls the AssigneeAvatarLink with the proper props', () => {
+ expect(wrapper.find(AssigneeAvatarLink).exists()).toBe(true);
+ expect(wrapper.find(AssigneeAvatarLink).props().tooltipPlacement).toEqual('left');
+ });
+
+ it('Shows one user with avatar, username and author name', () => {
+ expect(wrapper.text()).toContain(user.name);
+ expect(wrapper.text()).toContain(`@${user.username}`);
+ });
+ });
+
+ describe('Two or more assignees/users', () => {
+ beforeEach(() => {
+ createComponent({
+ users: UsersMockHelper.createNumberRandomUsers(3),
+ });
+ });
+
+ it('more than one user', () => {
+ expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(3);
+ });
+
+ it('shows the "show-less" assignees label', done => {
+ const users = UsersMockHelper.createNumberRandomUsers(6);
+
+ createComponent({
+ users,
+ });
+
+ expect(wrapper.vm.$el.querySelectorAll('.user-item').length).toEqual(DEFAULT_RENDER_COUNT);
+
+ expect(wrapper.vm.$el.querySelector('.user-list-more')).not.toBe(null);
+ const usersLabelExpectation = users.length - DEFAULT_RENDER_COUNT;
+
+ expect(wrapper.vm.$el.querySelector('.user-list-more .btn-link').innerText.trim()).not.toBe(
+ `+${usersLabelExpectation} more`,
+ );
+ wrapper.vm.toggleShowLess();
+ Vue.nextTick(() => {
+ expect(wrapper.vm.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
+ '- show less',
+ );
+ done();
+ });
+ });
+
+ it('shows the "show-less" when "n+ more " label is clicked', done => {
+ createComponent({
+ users: UsersMockHelper.createNumberRandomUsers(6),
+ });
+
+ wrapper.vm.$el.querySelector('.user-list-more .btn-link').click();
+ Vue.nextTick(() => {
+ expect(wrapper.vm.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
+ '- show less',
+ );
+ done();
+ });
+ });
+
+ it('does not show n+ more label when less than render count', () => {
+ expect(findMoreButton().exists()).toBe(false);
+ });
+ });
+
+ describe('n+ more label', () => {
+ beforeEach(() => {
+ createComponent({
+ users: UsersMockHelper.createNumberRandomUsers(DEFAULT_RENDER_COUNT + 1),
+ });
+ });
+
+ it('shows "+1 more" label', () => {
+ expect(findMoreButton().text()).toBe('+ 1 more');
+ expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT);
+ });
+
+ it('shows "show less" label', done => {
+ findMoreButton().trigger('click');
+
+ Vue.nextTick(() => {
+ expect(findMoreButton().text()).toBe('- show less');
+ expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT + 1);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/user_data_mock.js b/spec/frontend/sidebar/user_data_mock.js
new file mode 100644
index 00000000000..8ad70bb3499
--- /dev/null
+++ b/spec/frontend/sidebar/user_data_mock.js
@@ -0,0 +1,9 @@
+export default () => ({
+ avatar_url: 'mock_path',
+ id: 1,
+ name: 'Root',
+ state: 'active',
+ username: 'root',
+ web_url: '',
+ can_merge: true,
+});